Merge commit '921d0574a0' as 'engine'
This commit is contained in:
commit
add768b79b
14224 changed files with 7489260 additions and 0 deletions
183
engine/platform/android/java/lib/build.gradle
Normal file
183
engine/platform/android/java/lib/build.gradle
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
ext {
|
||||
DEBUG_PUBLISH_ARTIFACT_ID = 'godot-debug'
|
||||
PUBLISH_ARTIFACT_ID = 'godot'
|
||||
TOOLS_PUBLISH_ARTIFACT_ID = 'godot-tools'
|
||||
}
|
||||
|
||||
apply from: "../scripts/publish-module.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation "androidx.documentfile:documentfile:$versions.documentfileVersion"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
def pathToRootDir = "../../../../"
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
|
||||
manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersionName()]
|
||||
}
|
||||
|
||||
namespace = "org.godotengine.godot"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.javaVersion
|
||||
targetCompatibility versions.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
flavorDimensions = ["products"]
|
||||
productFlavors {
|
||||
editor {}
|
||||
template {}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
disable 'MissingTranslation', 'UnusedResources'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
|
||||
// Debug symbols are kept for development within Android Studio.
|
||||
if (shouldNotStrip()) {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/*.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
debug.jniLibs.srcDirs = ['libs/debug']
|
||||
release.jniLibs.srcDirs = ['libs/release']
|
||||
|
||||
// Editor jni library
|
||||
editorRelease.jniLibs.srcDirs = ['libs/tools/release']
|
||||
editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
|
||||
}
|
||||
|
||||
libraryVariants.all { variant ->
|
||||
def flavorName = variant.getFlavorName()
|
||||
if (flavorName == null || flavorName == "") {
|
||||
throw new GradleException("Invalid product flavor: $flavorName")
|
||||
}
|
||||
|
||||
def buildType = variant.buildType.name
|
||||
if (buildType == null || buildType == "" || !supportedFlavorsBuildTypes[flavorName].contains(buildType)) {
|
||||
throw new GradleException("Invalid build type: $buildType")
|
||||
}
|
||||
|
||||
boolean debugBuild = buildType == "debug"
|
||||
boolean debugSymbols = debugBuild
|
||||
boolean runTests = debugBuild
|
||||
boolean storeRelease = buildType == "release"
|
||||
boolean productionBuild = storeRelease
|
||||
|
||||
def sconsTarget = flavorName
|
||||
if (sconsTarget == "template") {
|
||||
// Tests are not supported on template builds
|
||||
runTests = false
|
||||
|
||||
//noinspection GroovyFallthrough
|
||||
switch (buildType) {
|
||||
case "release":
|
||||
sconsTarget += "_release"
|
||||
break
|
||||
case "debug":
|
||||
default:
|
||||
sconsTarget += "_debug"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update the name of the generated library
|
||||
def outputSuffix = "${sconsTarget}"
|
||||
variant.outputs.all { output ->
|
||||
output.outputFileName = "godot-lib.${outputSuffix}.aar"
|
||||
}
|
||||
|
||||
// Find scons' executable path
|
||||
File sconsExecutableFile = null
|
||||
def sconsName = "scons"
|
||||
def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows()
|
||||
? [".bat", ".cmd", ".ps1", ".exe"]
|
||||
: [""])
|
||||
logger.debug("Looking for $sconsName executable path")
|
||||
for (ext in sconsExts) {
|
||||
String sconsNameExt = sconsName + ext
|
||||
logger.debug("Checking $sconsNameExt")
|
||||
sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt)
|
||||
if (sconsExecutableFile != null) {
|
||||
// We're done!
|
||||
break
|
||||
}
|
||||
// Check all the options in path
|
||||
List<File> allOptions = org.gradle.internal.os.OperatingSystem.current().findAllInPath(sconsNameExt)
|
||||
if (!allOptions.isEmpty()) {
|
||||
// Pick the first option and we're done!
|
||||
sconsExecutableFile = allOptions.get(0)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (sconsExecutableFile == null) {
|
||||
throw new GradleException("Unable to find executable path for the '$sconsName' command.")
|
||||
} else {
|
||||
logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}")
|
||||
}
|
||||
|
||||
for (String selectedAbi : selectedAbis) {
|
||||
if (!supportedAbis.contains(selectedAbi)) {
|
||||
throw new GradleException("Invalid selected abi: $selectedAbi")
|
||||
}
|
||||
|
||||
// Creating gradle task to generate the native libraries for the selected abi.
|
||||
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
|
||||
tasks.create(name: taskName, type: Exec) {
|
||||
executable sconsExecutableFile.absolutePath
|
||||
args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${debugBuild}", "dev_build=${debugBuild}", "debug_symbols=${debugSymbols}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
|
||||
}
|
||||
|
||||
// Schedule the tasks so the generated libs are present before the aar file is packaged.
|
||||
tasks["merge${flavorName.capitalize()}${buildType.capitalize()}JniLibFolders"].dependsOn taskName
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
singleVariant("templateDebug") {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
singleVariant("templateRelease") {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
singleVariant("editorRelease") {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
|
||||
index ad6ea0de6..452c7d148 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
|
||||
@@ -32,6 +32,9 @@ import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
+// -- GODOT start --
|
||||
+import java.lang.ref.WeakReference;
|
||||
+// -- GODOT end --
|
||||
|
||||
|
||||
/**
|
||||
@@ -118,29 +121,46 @@ public class DownloaderClientMarshaller {
|
||||
/**
|
||||
* Target we publish for clients to send messages to IncomingHandler.
|
||||
*/
|
||||
- final Messenger mMessenger = new Messenger(new Handler() {
|
||||
+ // -- GODOT start --
|
||||
+ private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
|
||||
+ final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||
+
|
||||
+ private static class MessengerHandlerClient extends Handler {
|
||||
+ private final WeakReference<Stub> mDownloader;
|
||||
+ public MessengerHandlerClient(Stub downloader) {
|
||||
+ mDownloader = new WeakReference<>(downloader);
|
||||
+ }
|
||||
+
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
- switch (msg.what) {
|
||||
- case MSG_ONDOWNLOADPROGRESS:
|
||||
- Bundle bun = msg.getData();
|
||||
- if ( null != mContext ) {
|
||||
- bun.setClassLoader(mContext.getClassLoader());
|
||||
- DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
|
||||
- .getParcelable(PARAM_PROGRESS);
|
||||
- mItf.onDownloadProgress(dpi);
|
||||
- }
|
||||
- break;
|
||||
- case MSG_ONDOWNLOADSTATE_CHANGED:
|
||||
- mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
|
||||
- break;
|
||||
- case MSG_ONSERVICECONNECTED:
|
||||
- mItf.onServiceConnected(
|
||||
- (Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
|
||||
- break;
|
||||
+ Stub downloader = mDownloader.get();
|
||||
+ if (downloader != null) {
|
||||
+ downloader.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
- });
|
||||
+ }
|
||||
+
|
||||
+ private void handleMessage(Message msg) {
|
||||
+ switch (msg.what) {
|
||||
+ case MSG_ONDOWNLOADPROGRESS:
|
||||
+ Bundle bun = msg.getData();
|
||||
+ if (null != mContext) {
|
||||
+ bun.setClassLoader(mContext.getClassLoader());
|
||||
+ DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
|
||||
+ .getParcelable(PARAM_PROGRESS);
|
||||
+ mItf.onDownloadProgress(dpi);
|
||||
+ }
|
||||
+ break;
|
||||
+ case MSG_ONDOWNLOADSTATE_CHANGED:
|
||||
+ mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
|
||||
+ break;
|
||||
+ case MSG_ONSERVICECONNECTED:
|
||||
+ mItf.onServiceConnected(
|
||||
+ (Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ // -- GODOT end --
|
||||
|
||||
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
|
||||
mItf = itf;
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
|
||||
index 979352299..3771d19c9 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
|
||||
@@ -25,6 +25,9 @@ import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
|
||||
+// -- GODOT start --
|
||||
+import java.lang.ref.WeakReference;
|
||||
+// -- GODOT end --
|
||||
|
||||
|
||||
/**
|
||||
@@ -108,32 +111,49 @@ public class DownloaderServiceMarshaller {
|
||||
|
||||
private static class Stub implements IStub {
|
||||
private IDownloaderService mItf = null;
|
||||
- final Messenger mMessenger = new Messenger(new Handler() {
|
||||
+ // -- GODOT start --
|
||||
+ private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
|
||||
+ final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||
+
|
||||
+ private static class MessengerHandlerServer extends Handler {
|
||||
+ private final WeakReference<Stub> mDownloader;
|
||||
+ public MessengerHandlerServer(Stub downloader) {
|
||||
+ mDownloader = new WeakReference<>(downloader);
|
||||
+ }
|
||||
+
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
- switch (msg.what) {
|
||||
- case MSG_REQUEST_ABORT_DOWNLOAD:
|
||||
- mItf.requestAbortDownload();
|
||||
- break;
|
||||
- case MSG_REQUEST_CONTINUE_DOWNLOAD:
|
||||
- mItf.requestContinueDownload();
|
||||
- break;
|
||||
- case MSG_REQUEST_PAUSE_DOWNLOAD:
|
||||
- mItf.requestPauseDownload();
|
||||
- break;
|
||||
- case MSG_SET_DOWNLOAD_FLAGS:
|
||||
- mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
|
||||
- break;
|
||||
- case MSG_REQUEST_DOWNLOAD_STATE:
|
||||
- mItf.requestDownloadStatus();
|
||||
- break;
|
||||
- case MSG_REQUEST_CLIENT_UPDATE:
|
||||
- mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
|
||||
- PARAM_MESSENGER));
|
||||
- break;
|
||||
+ Stub downloader = mDownloader.get();
|
||||
+ if (downloader != null) {
|
||||
+ downloader.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
- });
|
||||
+ }
|
||||
+
|
||||
+ private void handleMessage(Message msg) {
|
||||
+ switch (msg.what) {
|
||||
+ case MSG_REQUEST_ABORT_DOWNLOAD:
|
||||
+ mItf.requestAbortDownload();
|
||||
+ break;
|
||||
+ case MSG_REQUEST_CONTINUE_DOWNLOAD:
|
||||
+ mItf.requestContinueDownload();
|
||||
+ break;
|
||||
+ case MSG_REQUEST_PAUSE_DOWNLOAD:
|
||||
+ mItf.requestPauseDownload();
|
||||
+ break;
|
||||
+ case MSG_SET_DOWNLOAD_FLAGS:
|
||||
+ mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
|
||||
+ break;
|
||||
+ case MSG_REQUEST_DOWNLOAD_STATE:
|
||||
+ mItf.requestDownloadStatus();
|
||||
+ break;
|
||||
+ case MSG_REQUEST_CLIENT_UPDATE:
|
||||
+ mItf.onClientUpdated((Messenger)msg.getData().getParcelable(
|
||||
+ PARAM_MESSENGER));
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ // -- GODOT end --
|
||||
|
||||
public Stub(IDownloaderService itf) {
|
||||
mItf = itf;
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
|
||||
index e4b1b0f1c..36cd6aacf 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
|
||||
@@ -24,7 +24,10 @@ import android.os.StatFs;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
-import com.android.vending.expansion.downloader.R;
|
||||
+// -- GODOT start --
|
||||
+//import com.android.vending.expansion.downloader.R;
|
||||
+import org.godotengine.godot.R;
|
||||
+// -- GODOT end --
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -146,12 +149,14 @@ public class Helpers {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
- return String.format("%.2f",
|
||||
+ // -- GODOT start --
|
||||
+ return String.format(Locale.ENGLISH, "%.2f",
|
||||
(float) overallProgress / (1024.0f * 1024.0f))
|
||||
+ "MB /" +
|
||||
- String.format("%.2f", (float) overallTotal /
|
||||
+ String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
|
||||
(1024.0f * 1024.0f))
|
||||
+ "MB";
|
||||
+ // -- GODOT end --
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +189,9 @@ public class Helpers {
|
||||
}
|
||||
|
||||
public static String getSpeedString(float bytesPerMillisecond) {
|
||||
- return String.format("%.2f", bytesPerMillisecond * 1000 / 1024);
|
||||
+ // -- GODOT start --
|
||||
+ return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
|
||||
+ // -- GODOT end --
|
||||
}
|
||||
|
||||
public static String getTimeRemaining(long durationInMilliseconds) {
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
|
||||
index 12edd97ab..a0e1165cc 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
|
||||
@@ -26,6 +26,10 @@ import android.net.NetworkInfo;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
+// -- GODOT start --
|
||||
+import android.annotation.SuppressLint;
|
||||
+// -- GODOT end --
|
||||
+
|
||||
/**
|
||||
* Contains useful helper functions, typically tied to the application context.
|
||||
*/
|
||||
@@ -51,6 +55,7 @@ class SystemFacade {
|
||||
return null;
|
||||
}
|
||||
|
||||
+ @SuppressLint("MissingPermission")
|
||||
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
|
||||
if (activeInfo == null) {
|
||||
if (Constants.LOGVV) {
|
||||
@@ -69,6 +74,7 @@ class SystemFacade {
|
||||
return false;
|
||||
}
|
||||
|
||||
+ @SuppressLint("MissingPermission")
|
||||
NetworkInfo info = connectivity.getActiveNetworkInfo();
|
||||
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
|
||||
TelephonyManager tm = (TelephonyManager) mContext
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
|
||||
index f1536e80e..4b214b22d 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
-import com.android.vending.expansion.downloader.R;
|
||||
+// -- GODOT start --
|
||||
+//import com.android.vending.expansion.downloader.R;
|
||||
+import org.godotengine.godot.R;
|
||||
+// -- GODOT end --
|
||||
+
|
||||
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
|
||||
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
||||
import com.google.android.vending.expansion.downloader.Helpers;
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
|
||||
index b2e0e7af0..c114b8a64 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
|
||||
@@ -146,8 +146,12 @@ public class DownloadThread {
|
||||
|
||||
try {
|
||||
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
|
||||
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
|
||||
- wakeLock.acquire();
|
||||
+ // -- GODOT start --
|
||||
+ //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
|
||||
+ //wakeLock.acquire();
|
||||
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
|
||||
+ wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
|
||||
+ // -- GODOT end --
|
||||
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
|
||||
index 4babe476f..8d41a7690 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
|
||||
@@ -50,6 +50,10 @@ import android.provider.Settings.Secure;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
+// -- GODOT start --
|
||||
+import android.annotation.SuppressLint;
|
||||
+// -- GODOT end --
|
||||
+
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
@@ -578,6 +582,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
|
||||
Log.w(Constants.TAG,
|
||||
"couldn't get connectivity manager to poll network state");
|
||||
} else {
|
||||
+ @SuppressLint("MissingPermission")
|
||||
NetworkInfo activeInfo = mConnectivityManager
|
||||
.getActiveNetworkInfo();
|
||||
updateNetworkState(activeInfo);
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
|
||||
index 7c42bfc28..feb579af0 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
|
||||
@@ -45,6 +45,9 @@ public class PreferenceObfuscator {
|
||||
public void putString(String key, String value) {
|
||||
if (mEditor == null) {
|
||||
mEditor = mPreferences.edit();
|
||||
+ // -- GODOT start --
|
||||
+ mEditor.apply();
|
||||
+ // -- GODOT end --
|
||||
}
|
||||
String obfuscatedValue = mObfuscator.obfuscate(value, key);
|
||||
mEditor.putString(key, obfuscatedValue);
|
||||
diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
|
||||
index a0d2779af..a8bf65f9c 100644
|
||||
--- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
|
||||
+++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
|
||||
@@ -31,6 +31,10 @@ package com.google.android.vending.licensing.util;
|
||||
* @version 1.3
|
||||
*/
|
||||
|
||||
+// -- GODOT start --
|
||||
import org.godotengine.godot.BuildConfig;
|
||||
+// -- GODOT end --
|
||||
+
|
||||
/**
|
||||
* Base64 converter class. This code is not a full-blown MIME encoder;
|
||||
* it simply converts binary data to base64 data and back.
|
||||
@@ -341,7 +345,11 @@ public class Base64 {
|
||||
e += 4;
|
||||
}
|
||||
|
||||
- assert (e == outBuff.length);
|
||||
+ // -- GODOT start --
|
||||
+ //assert (e == outBuff.length);
|
||||
+ if (BuildConfig.DEBUG && e != outBuff.length)
|
||||
+ throw new RuntimeException();
|
||||
+ // -- GODOT end --
|
||||
return outBuff;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Records the version of the Godot library -->
|
||||
<meta-data
|
||||
android:name="org.godotengine.library.version"
|
||||
android:value="${godotLibraryVersion}" />
|
||||
|
||||
<service android:name=".GodotDownloaderService" />
|
||||
|
||||
<activity
|
||||
android:name=".utils.ProcessPhoenix"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:process=":phoenix"
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/godot_provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.licensing;
|
||||
|
||||
oneway interface ILicenseResultListener {
|
||||
void verifyLicense(int responseCode, String signedData, String signature);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.licensing;
|
||||
|
||||
import com.android.vending.licensing.ILicenseResultListener;
|
||||
|
||||
oneway interface ILicensingService {
|
||||
void checkLicense(long nonce, String packageName, in ILicenseResultListener listener);
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
/**
|
||||
* Contains the internal constants that are used in the download manager.
|
||||
* As a general rule, modifying these constants should be done with care.
|
||||
*/
|
||||
public class Constants {
|
||||
/** Tag used for debugging/logging */
|
||||
public static final String TAG = "LVLDL";
|
||||
|
||||
/**
|
||||
* Expansion path where we store obb files
|
||||
*/
|
||||
public static final String EXP_PATH = File.separator + "Android"
|
||||
+ File.separator + "obb" + File.separator;
|
||||
|
||||
/** The intent that gets sent when the service must wake up for a retry */
|
||||
public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
|
||||
|
||||
/** the intent that gets sent when clicking a successful download */
|
||||
public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
|
||||
|
||||
/** the intent that gets sent when clicking an incomplete/failed download */
|
||||
public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
|
||||
|
||||
/** the intent that gets sent when deleting the notification of a completed download */
|
||||
public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
|
||||
|
||||
/**
|
||||
* When a number has to be appended to the filename, this string is used to separate the
|
||||
* base filename from the sequence number
|
||||
*/
|
||||
public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
|
||||
|
||||
/** The default user agent used for downloads */
|
||||
public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
|
||||
|
||||
/** The buffer size used to stream the data */
|
||||
public static final int BUFFER_SIZE = 4096;
|
||||
|
||||
/** The minimum amount of progress that has to be done before the progress bar gets updated */
|
||||
public static final int MIN_PROGRESS_STEP = 4096;
|
||||
|
||||
/** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
|
||||
public static final long MIN_PROGRESS_TIME = 1000;
|
||||
|
||||
/** The maximum number of rows in the database (FIFO) */
|
||||
public static final int MAX_DOWNLOADS = 1000;
|
||||
|
||||
/**
|
||||
* The number of times that the download manager will retry its network
|
||||
* operations when no progress is happening before it gives up.
|
||||
*/
|
||||
public static final int MAX_RETRIES = 5;
|
||||
|
||||
/**
|
||||
* The minimum amount of time that the download manager accepts for
|
||||
* a Retry-After response header with a parameter in delta-seconds.
|
||||
*/
|
||||
public static final int MIN_RETRY_AFTER = 30; // 30s
|
||||
|
||||
/**
|
||||
* The maximum amount of time that the download manager accepts for
|
||||
* a Retry-After response header with a parameter in delta-seconds.
|
||||
*/
|
||||
public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
|
||||
|
||||
/**
|
||||
* The maximum number of redirects.
|
||||
*/
|
||||
public static final int MAX_REDIRECTS = 5; // can't be more than 7.
|
||||
|
||||
/**
|
||||
* The time between a failure and the first retry after an IOException.
|
||||
* Each subsequent retry grows exponentially, doubling each time.
|
||||
* The time is in seconds.
|
||||
*/
|
||||
public static final int RETRY_FIRST_DELAY = 30;
|
||||
|
||||
/** Enable separate connectivity logging */
|
||||
public static final boolean LOGX = true;
|
||||
|
||||
/** Enable verbose logging */
|
||||
public static final boolean LOGV = false;
|
||||
|
||||
/** Enable super-verbose logging */
|
||||
private static final boolean LOCAL_LOGVV = false;
|
||||
public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
|
||||
|
||||
/**
|
||||
* This download has successfully completed.
|
||||
* Warning: there might be other status values that indicate success
|
||||
* in the future.
|
||||
* Use isSucccess() to capture the entire category.
|
||||
*/
|
||||
public static final int STATUS_SUCCESS = 200;
|
||||
|
||||
/**
|
||||
* This request couldn't be parsed. This is also used when processing
|
||||
* requests with unknown/unsupported URI schemes.
|
||||
*/
|
||||
public static final int STATUS_BAD_REQUEST = 400;
|
||||
|
||||
/**
|
||||
* This download can't be performed because the content type cannot be
|
||||
* handled.
|
||||
*/
|
||||
public static final int STATUS_NOT_ACCEPTABLE = 406;
|
||||
|
||||
/**
|
||||
* This download cannot be performed because the length cannot be
|
||||
* determined accurately. This is the code for the HTTP error "Length
|
||||
* Required", which is typically used when making requests that require
|
||||
* a content length but don't have one, and it is also used in the
|
||||
* client when a response is received whose length cannot be determined
|
||||
* accurately (therefore making it impossible to know when a download
|
||||
* completes).
|
||||
*/
|
||||
public static final int STATUS_LENGTH_REQUIRED = 411;
|
||||
|
||||
/**
|
||||
* This download was interrupted and cannot be resumed.
|
||||
* This is the code for the HTTP error "Precondition Failed", and it is
|
||||
* also used in situations where the client doesn't have an ETag at all.
|
||||
*/
|
||||
public static final int STATUS_PRECONDITION_FAILED = 412;
|
||||
|
||||
/**
|
||||
* The lowest-valued error status that is not an actual HTTP status code.
|
||||
*/
|
||||
public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
|
||||
|
||||
/**
|
||||
* The requested destination file already exists.
|
||||
*/
|
||||
public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
|
||||
|
||||
/**
|
||||
* Some possibly transient error occurred, but we can't resume the download.
|
||||
*/
|
||||
public static final int STATUS_CANNOT_RESUME = 489;
|
||||
|
||||
/**
|
||||
* This download was canceled
|
||||
*/
|
||||
public static final int STATUS_CANCELED = 490;
|
||||
|
||||
/**
|
||||
* This download has completed with an error.
|
||||
* Warning: there will be other status values that indicate errors in
|
||||
* the future. Use isStatusError() to capture the entire category.
|
||||
*/
|
||||
public static final int STATUS_UNKNOWN_ERROR = 491;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because of a storage issue.
|
||||
* Typically, that's because the filesystem is missing or full.
|
||||
* Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
|
||||
* and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
|
||||
*/
|
||||
public static final int STATUS_FILE_ERROR = 492;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because of an HTTP
|
||||
* redirect response that the download manager couldn't
|
||||
* handle.
|
||||
*/
|
||||
public static final int STATUS_UNHANDLED_REDIRECT = 493;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because of an
|
||||
* unspecified unhandled HTTP code.
|
||||
*/
|
||||
public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because of an
|
||||
* error receiving or processing data at the HTTP level.
|
||||
*/
|
||||
public static final int STATUS_HTTP_DATA_ERROR = 495;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because of an
|
||||
* HttpException while setting up the request.
|
||||
*/
|
||||
public static final int STATUS_HTTP_EXCEPTION = 496;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because there were
|
||||
* too many redirects.
|
||||
*/
|
||||
public static final int STATUS_TOO_MANY_REDIRECTS = 497;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed due to insufficient storage
|
||||
* space. Typically, this is because the SD card is full.
|
||||
*/
|
||||
public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
|
||||
|
||||
/**
|
||||
* This download couldn't be completed because no external storage
|
||||
* device was found. Typically, this is because the SD card is not
|
||||
* mounted.
|
||||
*/
|
||||
public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
|
||||
|
||||
/**
|
||||
* The wake duration to check to see if a download is possible.
|
||||
*/
|
||||
public static final long WATCHDOG_WAKE_TIMER = 60*1000;
|
||||
|
||||
/**
|
||||
* The wake duration to check to see if the process was killed.
|
||||
*/
|
||||
public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
|
||||
/**
|
||||
* This class contains progress information about the active download(s).
|
||||
*
|
||||
* When you build the Activity that initiates a download and tracks the
|
||||
* progress by implementing the {@link IDownloaderClient} interface, you'll
|
||||
* receive a DownloadProgressInfo object in each call to the {@link
|
||||
* IDownloaderClient#onDownloadProgress} method. This allows you to update
|
||||
* your activity's UI with information about the download progress, such
|
||||
* as the progress so far, time remaining and current speed.
|
||||
*/
|
||||
public class DownloadProgressInfo implements Parcelable {
|
||||
public long mOverallTotal;
|
||||
public long mOverallProgress;
|
||||
public long mTimeRemaining; // time remaining
|
||||
public float mCurrentSpeed; // speed in KB/S
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel p, int i) {
|
||||
p.writeLong(mOverallTotal);
|
||||
p.writeLong(mOverallProgress);
|
||||
p.writeLong(mTimeRemaining);
|
||||
p.writeFloat(mCurrentSpeed);
|
||||
}
|
||||
|
||||
public DownloadProgressInfo(Parcel p) {
|
||||
mOverallTotal = p.readLong();
|
||||
mOverallProgress = p.readLong();
|
||||
mTimeRemaining = p.readLong();
|
||||
mCurrentSpeed = p.readFloat();
|
||||
}
|
||||
|
||||
public DownloadProgressInfo(long overallTotal, long overallProgress,
|
||||
long timeRemaining,
|
||||
float currentSpeed) {
|
||||
this.mOverallTotal = overallTotal;
|
||||
this.mOverallProgress = overallProgress;
|
||||
this.mTimeRemaining = timeRemaining;
|
||||
this.mCurrentSpeed = currentSpeed;
|
||||
}
|
||||
|
||||
public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
|
||||
@Override
|
||||
public DownloadProgressInfo createFromParcel(Parcel parcel) {
|
||||
return new DownloadProgressInfo(parcel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadProgressInfo[] newArray(int i) {
|
||||
return new DownloadProgressInfo[i];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
// -- GODOT start --
|
||||
import java.lang.ref.WeakReference;
|
||||
// -- GODOT end --
|
||||
|
||||
|
||||
/**
|
||||
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
|
||||
* which is used to call functions in your client as well as the Stub, which is used to call functions
|
||||
* in the client implementation of IDownloaderClient.
|
||||
*
|
||||
* <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method
|
||||
* should be called whenever the client wants to bind to the service. It opens up a service connection
|
||||
* that ends up calling the onServiceConnected client API that passes the service messenger
|
||||
* in. If the client wants to be notified by the service, it is responsible for then passing its
|
||||
* messenger to the service in a separate call.
|
||||
*
|
||||
* <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}.
|
||||
*
|
||||
* <p>When your application first starts, you should first check whether your app's expansion files are
|
||||
* already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which
|
||||
* starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method
|
||||
* returns a value indicating whether download is required or not.
|
||||
*
|
||||
* <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through
|
||||
* the specified service and you should then call {@link #CreateStub} to instantiate a member {@link
|
||||
* IStub} object that you need in order to receive calls through your {@link IDownloaderClient}
|
||||
* interface.
|
||||
*/
|
||||
public class DownloaderClientMarshaller {
|
||||
public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
|
||||
public static final int MSG_ONDOWNLOADPROGRESS = 11;
|
||||
public static final int MSG_ONSERVICECONNECTED = 12;
|
||||
|
||||
public static final String PARAM_NEW_STATE = "newState";
|
||||
public static final String PARAM_PROGRESS = "progress";
|
||||
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
|
||||
|
||||
public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
|
||||
public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
|
||||
public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
|
||||
|
||||
private static class Proxy implements IDownloaderClient {
|
||||
private Messenger mServiceMessenger;
|
||||
|
||||
@Override
|
||||
public void onDownloadStateChanged(int newState) {
|
||||
Bundle params = new Bundle(1);
|
||||
params.putInt(PARAM_NEW_STATE, newState);
|
||||
send(MSG_ONDOWNLOADSTATE_CHANGED, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(DownloadProgressInfo progress) {
|
||||
Bundle params = new Bundle(1);
|
||||
params.putParcelable(PARAM_PROGRESS, progress);
|
||||
send(MSG_ONDOWNLOADPROGRESS, params);
|
||||
}
|
||||
|
||||
private void send(int method, Bundle params) {
|
||||
Message m = Message.obtain(null, method);
|
||||
m.setData(params);
|
||||
try {
|
||||
mServiceMessenger.send(m);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Proxy(Messenger msg) {
|
||||
mServiceMessenger = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(Messenger m) {
|
||||
/**
|
||||
* This is never called through the proxy.
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
private static class Stub implements IStub {
|
||||
private IDownloaderClient mItf = null;
|
||||
private Class<?> mDownloaderServiceClass;
|
||||
private boolean mBound;
|
||||
private Messenger mServiceMessenger;
|
||||
private Context mContext;
|
||||
/**
|
||||
* Target we publish for clients to send messages to IncomingHandler.
|
||||
*/
|
||||
// -- GODOT start --
|
||||
private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
|
||||
final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||
|
||||
private static class MessengerHandlerClient extends Handler {
|
||||
private final WeakReference<Stub> mDownloader;
|
||||
public MessengerHandlerClient(Stub downloader) {
|
||||
mDownloader = new WeakReference<>(downloader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Stub downloader = mDownloader.get();
|
||||
if (downloader != null) {
|
||||
downloader.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ONDOWNLOADPROGRESS:
|
||||
Bundle bun = msg.getData();
|
||||
if (null != mContext) {
|
||||
bun.setClassLoader(mContext.getClassLoader());
|
||||
DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
|
||||
.getParcelable(PARAM_PROGRESS);
|
||||
mItf.onDownloadProgress(dpi);
|
||||
}
|
||||
break;
|
||||
case MSG_ONDOWNLOADSTATE_CHANGED:
|
||||
mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
|
||||
break;
|
||||
case MSG_ONSERVICECONNECTED:
|
||||
mItf.onServiceConnected(
|
||||
(Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// -- GODOT end --
|
||||
|
||||
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
|
||||
mItf = itf;
|
||||
mDownloaderServiceClass = downloaderService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for interacting with the main interface of the service.
|
||||
*/
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
// This is called when the connection with the service has been
|
||||
// established, giving us the object we can use to
|
||||
// interact with the service. We are communicating with the
|
||||
// service using a Messenger, so here we get a client-side
|
||||
// representation of that from the raw IBinder object.
|
||||
mServiceMessenger = new Messenger(service);
|
||||
mItf.onServiceConnected(
|
||||
mServiceMessenger);
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
// This is called when the connection with the service has been
|
||||
// unexpectedly disconnected -- that is, its process crashed.
|
||||
mServiceMessenger = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void connect(Context c) {
|
||||
mContext = c;
|
||||
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
|
||||
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
|
||||
if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
|
||||
if ( Constants.LOGVV ) {
|
||||
Log.d(Constants.TAG, "Service Unbound");
|
||||
}
|
||||
} else {
|
||||
mBound = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(Context c) {
|
||||
if (mBound) {
|
||||
c.unbindService(mConnection);
|
||||
mBound = false;
|
||||
}
|
||||
mContext = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Messenger getMessenger() {
|
||||
return mMessenger;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a proxy that will marshal calls to IDownloaderClient methods
|
||||
*
|
||||
* @param msg
|
||||
* @return
|
||||
*/
|
||||
public static IDownloaderClient CreateProxy(Messenger msg) {
|
||||
return new Proxy(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stub object that, when connected, will listen for marshaled
|
||||
* {@link IDownloaderClient} methods and translate them into calls to the supplied
|
||||
* interface.
|
||||
*
|
||||
* @param itf An implementation of IDownloaderClient that will be called
|
||||
* when remote method calls are unmarshaled.
|
||||
* @param downloaderService The class for your implementation of {@link
|
||||
* impl.DownloaderService}.
|
||||
* @return The {@link IStub} that allows you to connect to the service such that
|
||||
* your {@link IDownloaderClient} receives status updates.
|
||||
*/
|
||||
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
|
||||
return new Stub(itf, downloaderService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the download if necessary. This function starts a flow that does `
|
||||
* many things. 1) Checks to see if the APK version has been checked and
|
||||
* the metadata database updated 2) If the APK version does not match,
|
||||
* checks the new LVL status to see if a new download is required 3) If the
|
||||
* APK version does match, then checks to see if the download(s) have been
|
||||
* completed 4) If the downloads have been completed, returns
|
||||
* NO_DOWNLOAD_REQUIRED The idea is that this can be called during the
|
||||
* startup of an application to quickly ascertain if the application needs
|
||||
* to wait to hear about any updated APK expansion files. Note that this does
|
||||
* mean that the application MUST be run for the first time with a network
|
||||
* connection, even if Market delivers all of the files.
|
||||
*
|
||||
* @param context Your application Context.
|
||||
* @param notificationClient A PendingIntent to start the Activity in your application
|
||||
* that shows the download progress and which will also start the application when download
|
||||
* completes.
|
||||
* @param serviceClass the class of your {@link imp.DownloaderService} implementation
|
||||
* @return whether the service was started and the reason for starting the service.
|
||||
* Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link
|
||||
* #DOWNLOAD_REQUIRED}.
|
||||
* @throws NameNotFoundException
|
||||
*/
|
||||
public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
|
||||
Class<?> serviceClass)
|
||||
throws NameNotFoundException {
|
||||
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
||||
serviceClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* This version assumes that the intent contains the pending intent as a parameter. This
|
||||
* is used for responding to alarms.
|
||||
* <p>The pending intent must be in an extra with the key {@link
|
||||
* impl.DownloaderService#EXTRA_PENDING_INTENT}.
|
||||
*
|
||||
* @param context
|
||||
* @param notificationClient
|
||||
* @param serviceClass the class of the service to start
|
||||
* @return
|
||||
* @throws NameNotFoundException
|
||||
*/
|
||||
public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
|
||||
Class<?> serviceClass)
|
||||
throws NameNotFoundException {
|
||||
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
||||
serviceClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
|
||||
// -- GODOT start --
|
||||
import java.lang.ref.WeakReference;
|
||||
// -- GODOT end --
|
||||
|
||||
|
||||
/**
|
||||
* This class is used by the client activity to proxy requests to the Downloader
|
||||
* Service.
|
||||
*
|
||||
* Most importantly, you must call {@link #CreateProxy} during the {@link
|
||||
* IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate
|
||||
* an {@link IDownloaderService} object that you can then use to issue commands to the {@link
|
||||
* DownloaderService} (such as to pause and resume downloads).
|
||||
*/
|
||||
public class DownloaderServiceMarshaller {
|
||||
|
||||
public static final int MSG_REQUEST_ABORT_DOWNLOAD =
|
||||
1;
|
||||
public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
|
||||
2;
|
||||
public static final int MSG_SET_DOWNLOAD_FLAGS =
|
||||
3;
|
||||
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
|
||||
4;
|
||||
public static final int MSG_REQUEST_DOWNLOAD_STATE =
|
||||
5;
|
||||
public static final int MSG_REQUEST_CLIENT_UPDATE =
|
||||
6;
|
||||
|
||||
public static final String PARAMS_FLAGS = "flags";
|
||||
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
|
||||
|
||||
private static class Proxy implements IDownloaderService {
|
||||
private Messenger mMsg;
|
||||
|
||||
private void send(int method, Bundle params) {
|
||||
Message m = Message.obtain(null, method);
|
||||
m.setData(params);
|
||||
try {
|
||||
mMsg.send(m);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Proxy(Messenger msg) {
|
||||
mMsg = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestAbortDownload() {
|
||||
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPauseDownload() {
|
||||
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDownloadFlags(int flags) {
|
||||
Bundle params = new Bundle();
|
||||
params.putInt(PARAMS_FLAGS, flags);
|
||||
send(MSG_SET_DOWNLOAD_FLAGS, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestContinueDownload() {
|
||||
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDownloadStatus() {
|
||||
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientUpdated(Messenger clientMessenger) {
|
||||
Bundle bundle = new Bundle(1);
|
||||
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
|
||||
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Stub implements IStub {
|
||||
private IDownloaderService mItf = null;
|
||||
// -- GODOT start --
|
||||
private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
|
||||
final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||
|
||||
private static class MessengerHandlerServer extends Handler {
|
||||
private final WeakReference<Stub> mDownloader;
|
||||
public MessengerHandlerServer(Stub downloader) {
|
||||
mDownloader = new WeakReference<>(downloader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Stub downloader = mDownloader.get();
|
||||
if (downloader != null) {
|
||||
downloader.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_REQUEST_ABORT_DOWNLOAD:
|
||||
mItf.requestAbortDownload();
|
||||
break;
|
||||
case MSG_REQUEST_CONTINUE_DOWNLOAD:
|
||||
mItf.requestContinueDownload();
|
||||
break;
|
||||
case MSG_REQUEST_PAUSE_DOWNLOAD:
|
||||
mItf.requestPauseDownload();
|
||||
break;
|
||||
case MSG_SET_DOWNLOAD_FLAGS:
|
||||
mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
|
||||
break;
|
||||
case MSG_REQUEST_DOWNLOAD_STATE:
|
||||
mItf.requestDownloadStatus();
|
||||
break;
|
||||
case MSG_REQUEST_CLIENT_UPDATE:
|
||||
mItf.onClientUpdated((Messenger)msg.getData().getParcelable(
|
||||
PARAM_MESSENGER));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// -- GODOT end --
|
||||
|
||||
public Stub(IDownloaderService itf) {
|
||||
mItf = itf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Messenger getMessenger() {
|
||||
return mMessenger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Context c) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(Context c) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a proxy that will marshall calls to IDownloaderService methods
|
||||
*
|
||||
* @param ctx
|
||||
* @return
|
||||
*/
|
||||
public static IDownloaderService CreateProxy(Messenger msg) {
|
||||
return new Proxy(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stub object that, when connected, will listen for marshalled
|
||||
* IDownloaderService methods and translate them into calls to the supplied
|
||||
* interface.
|
||||
*
|
||||
* @param itf An implementation of IDownloaderService that will be called
|
||||
* when remote method calls are unmarshalled.
|
||||
* @return
|
||||
*/
|
||||
public static IStub CreateStub(IDownloaderService itf) {
|
||||
return new Stub(itf);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.StatFs;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
// -- GODOT start --
|
||||
//import com.android.vending.expansion.downloader.R;
|
||||
import org.godotengine.godot.R;
|
||||
// -- GODOT end --
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Some helper functions for the download manager
|
||||
*/
|
||||
public class Helpers {
|
||||
|
||||
public static Random sRandom = new Random(SystemClock.uptimeMillis());
|
||||
|
||||
/** Regex used to parse content-disposition headers */
|
||||
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
|
||||
.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
|
||||
|
||||
private Helpers() {
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse the Content-Disposition HTTP Header. The format of the header is defined here:
|
||||
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
|
||||
* content that is going to be downloaded to the file system. We only support the attachment
|
||||
* type.
|
||||
*/
|
||||
static String parseContentDisposition(String contentDisposition) {
|
||||
try {
|
||||
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
} catch (IllegalStateException ex) {
|
||||
// This function is defined as returning null when it can't parse
|
||||
// the header
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the root of the filesystem containing the given path
|
||||
*/
|
||||
public static File getFilesystemRoot(String path) {
|
||||
File cache = Environment.getDownloadCacheDirectory();
|
||||
if (path.startsWith(cache.getPath())) {
|
||||
return cache;
|
||||
}
|
||||
File external = Environment.getExternalStorageDirectory();
|
||||
if (path.startsWith(external.getPath())) {
|
||||
return external;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot determine filesystem root for " + path);
|
||||
}
|
||||
|
||||
public static boolean isExternalMediaMounted() {
|
||||
if (!Environment.getExternalStorageState().equals(
|
||||
Environment.MEDIA_MOUNTED)) {
|
||||
// No SD card found.
|
||||
if (Constants.LOGVV) {
|
||||
Log.d(Constants.TAG, "no external storage");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of bytes available on the filesystem rooted at the given File
|
||||
*/
|
||||
public static long getAvailableBytes(File root) {
|
||||
StatFs stat = new StatFs(root.getPath());
|
||||
// put a bit of margin (in case creating the file grows the system by a
|
||||
// few blocks)
|
||||
long availableBlocks = (long) stat.getAvailableBlocks() - 4;
|
||||
return stat.getBlockSize() * availableBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the filename looks legitimate
|
||||
*/
|
||||
public static boolean isFilenameValid(String filename) {
|
||||
filename = filename.replaceFirst("/+", "/"); // normalize leading
|
||||
// slashes
|
||||
return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
|
||||
|| filename.startsWith(Environment.getExternalStorageDirectory().toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete the given file from device
|
||||
*/
|
||||
/* package */static void deleteFile(String path) {
|
||||
try {
|
||||
File file = new File(path);
|
||||
file.delete();
|
||||
} catch (Exception e) {
|
||||
Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total
|
||||
* file size, but given what we know about the expected ranges of file sizes for APK expansion
|
||||
* files, it's probably not necessary.
|
||||
*
|
||||
* @param overallProgress
|
||||
* @param overallTotal
|
||||
* @return
|
||||
*/
|
||||
|
||||
static public String getDownloadProgressString(long overallProgress, long overallTotal) {
|
||||
if (overallTotal == 0) {
|
||||
if (Constants.LOGVV) {
|
||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// -- GODOT start --
|
||||
return String.format(Locale.ENGLISH, "%.2f",
|
||||
(float) overallProgress / (1024.0f * 1024.0f))
|
||||
+ "MB /" +
|
||||
String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
|
||||
(1024.0f * 1024.0f))
|
||||
+ "MB";
|
||||
// -- GODOT end --
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a percentile to getDownloadProgressString.
|
||||
*
|
||||
* @param overallProgress
|
||||
* @param overallTotal
|
||||
* @return
|
||||
*/
|
||||
static public String getDownloadProgressStringNotification(long overallProgress,
|
||||
long overallTotal) {
|
||||
if (overallTotal == 0) {
|
||||
if (Constants.LOGVV) {
|
||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return getDownloadProgressString(overallProgress, overallTotal) + " (" +
|
||||
getDownloadProgressPercent(overallProgress, overallTotal) + ")";
|
||||
}
|
||||
|
||||
public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
|
||||
if (overallTotal == 0) {
|
||||
if (Constants.LOGVV) {
|
||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return Long.toString(overallProgress * 100 / overallTotal) + "%";
|
||||
}
|
||||
|
||||
public static String getSpeedString(float bytesPerMillisecond) {
|
||||
// -- GODOT start --
|
||||
return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
|
||||
// -- GODOT end --
|
||||
}
|
||||
|
||||
public static String getTimeRemaining(long durationInMilliseconds) {
|
||||
SimpleDateFormat sdf;
|
||||
if (durationInMilliseconds > 1000 * 60 * 60) {
|
||||
sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||
} else {
|
||||
sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
|
||||
}
|
||||
return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name (without full path) for an Expansion APK file from the given context.
|
||||
*
|
||||
* @param c the context
|
||||
* @param mainFile true for main file, false for patch file
|
||||
* @param versionCode the version of the file
|
||||
* @return String the file name of the expansion file
|
||||
*/
|
||||
public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
|
||||
return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename (where the file should be saved) from info about a download
|
||||
*/
|
||||
static public String generateSaveFileName(Context c, String fileName) {
|
||||
String path = getSaveFilePath(c)
|
||||
+ File.separator + fileName;
|
||||
return path;
|
||||
}
|
||||
|
||||
static public String getSaveFilePath(Context c) {
|
||||
// This technically existed since Honeycomb, but it is critical
|
||||
// on KitKat and greater versions since it will create the
|
||||
// directory if needed
|
||||
return c.getObbDir().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to ascertain the existence of a file and return true/false appropriately
|
||||
*
|
||||
* @param c the app/activity/service context
|
||||
* @param fileName the name (sans path) of the file to query
|
||||
* @param fileSize the size that the file must match
|
||||
* @param deleteFileOnMismatch if the file sizes do not match, delete the file
|
||||
* @return true if it does exist, false otherwise
|
||||
*/
|
||||
static public boolean doesFileExist(Context c, String fileName, long fileSize,
|
||||
boolean deleteFileOnMismatch) {
|
||||
// the file may have been delivered by Play --- let's make sure
|
||||
// it's the size we expect
|
||||
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
|
||||
if (fileForNewFile.exists()) {
|
||||
if (fileForNewFile.length() == fileSize) {
|
||||
return true;
|
||||
}
|
||||
if (deleteFileOnMismatch) {
|
||||
// delete the file --- we won't be able to resume
|
||||
// because we cannot confirm the integrity of the file
|
||||
fileForNewFile.delete();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final int FS_READABLE = 0;
|
||||
public static final int FS_DOES_NOT_EXIST = 1;
|
||||
public static final int FS_CANNOT_READ = 2;
|
||||
|
||||
/**
|
||||
* Helper function to ascertain whether a file can be read.
|
||||
*
|
||||
* @param c the app/activity/service context
|
||||
* @param fileName the name (sans path) of the file to query
|
||||
* @return true if it does exist, false otherwise
|
||||
*/
|
||||
static public int getFileStatus(Context c, String fileName) {
|
||||
// the file may have been delivered by Play --- let's make sure
|
||||
// it's the size we expect
|
||||
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
|
||||
int returnValue;
|
||||
if (fileForNewFile.exists()) {
|
||||
if (fileForNewFile.canRead()) {
|
||||
returnValue = FS_READABLE;
|
||||
} else {
|
||||
returnValue = FS_CANNOT_READ;
|
||||
}
|
||||
} else {
|
||||
returnValue = FS_DOES_NOT_EXIST;
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to ascertain whether the application has the correct access to the OBB
|
||||
* directory to allow an OBB file to be written.
|
||||
*
|
||||
* @param c the app/activity/service context
|
||||
* @return true if the application can write an OBB file, false otherwise
|
||||
*/
|
||||
static public boolean canWriteOBBFile(Context c) {
|
||||
String path = getSaveFilePath(c);
|
||||
File fileForNewFile = new File(path);
|
||||
boolean canWrite;
|
||||
if (fileForNewFile.exists()) {
|
||||
canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
|
||||
} else {
|
||||
canWrite = fileForNewFile.mkdirs();
|
||||
}
|
||||
return canWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts download states that are returned by the
|
||||
* {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful
|
||||
* if using the state strings built into the library to display user messages.
|
||||
*
|
||||
* @param state One of the STATE_* constants from {@link IDownloaderClient}.
|
||||
* @return string resource ID for the corresponding string.
|
||||
*/
|
||||
static public int getDownloaderStringResourceIDFromState(int state) {
|
||||
switch (state) {
|
||||
case IDownloaderClient.STATE_IDLE:
|
||||
return R.string.state_idle;
|
||||
case IDownloaderClient.STATE_FETCHING_URL:
|
||||
return R.string.state_fetching_url;
|
||||
case IDownloaderClient.STATE_CONNECTING:
|
||||
return R.string.state_connecting;
|
||||
case IDownloaderClient.STATE_DOWNLOADING:
|
||||
return R.string.state_downloading;
|
||||
case IDownloaderClient.STATE_COMPLETED:
|
||||
return R.string.state_completed;
|
||||
case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
|
||||
return R.string.state_paused_network_unavailable;
|
||||
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
||||
return R.string.state_paused_by_request;
|
||||
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
|
||||
return R.string.state_paused_wifi_disabled;
|
||||
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
|
||||
return R.string.state_paused_wifi_unavailable;
|
||||
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
|
||||
return R.string.state_paused_wifi_disabled;
|
||||
case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
|
||||
return R.string.state_paused_wifi_unavailable;
|
||||
case IDownloaderClient.STATE_PAUSED_ROAMING:
|
||||
return R.string.state_paused_roaming;
|
||||
case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
|
||||
return R.string.state_paused_network_setup_failure;
|
||||
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
|
||||
return R.string.state_paused_sdcard_unavailable;
|
||||
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
||||
return R.string.state_failed_unlicensed;
|
||||
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
||||
return R.string.state_failed_fetching_url;
|
||||
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
||||
return R.string.state_failed_sdcard_full;
|
||||
case IDownloaderClient.STATE_FAILED_CANCELED:
|
||||
return R.string.state_failed_cancelled;
|
||||
default:
|
||||
return R.string.state_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import android.os.Messenger;
|
||||
|
||||
/**
|
||||
* This interface should be implemented by the client activity for the
|
||||
* downloader. It is used to pass status from the service to the client.
|
||||
*/
|
||||
public interface IDownloaderClient {
|
||||
static final int STATE_IDLE = 1;
|
||||
static final int STATE_FETCHING_URL = 2;
|
||||
static final int STATE_CONNECTING = 3;
|
||||
static final int STATE_DOWNLOADING = 4;
|
||||
static final int STATE_COMPLETED = 5;
|
||||
|
||||
static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
|
||||
static final int STATE_PAUSED_BY_REQUEST = 7;
|
||||
|
||||
/**
|
||||
* Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
|
||||
* STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
|
||||
* cellular permission will restart the service. Wi-Fi disabled means that
|
||||
* the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the
|
||||
* other case Wi-Fi is enabled but not available.
|
||||
*/
|
||||
static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
|
||||
static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
|
||||
|
||||
/**
|
||||
* Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
|
||||
* Wi-Fi is unavailable and cellular permission will NOT restart the
|
||||
* service. Wi-Fi disabled means that the Wi-Fi manager is returning that
|
||||
* Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not
|
||||
* available.
|
||||
* <p>
|
||||
* The service does not return these values. We recommend that app
|
||||
* developers with very large payloads do not allow these payloads to be
|
||||
* downloaded over cellular connections.
|
||||
*/
|
||||
static final int STATE_PAUSED_WIFI_DISABLED = 10;
|
||||
static final int STATE_PAUSED_NEED_WIFI = 11;
|
||||
|
||||
static final int STATE_PAUSED_ROAMING = 12;
|
||||
|
||||
/**
|
||||
* Scary case. We were on a network that redirected us to another website
|
||||
* that delivered us the wrong file.
|
||||
*/
|
||||
static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13;
|
||||
|
||||
static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14;
|
||||
|
||||
static final int STATE_FAILED_UNLICENSED = 15;
|
||||
static final int STATE_FAILED_FETCHING_URL = 16;
|
||||
static final int STATE_FAILED_SDCARD_FULL = 17;
|
||||
static final int STATE_FAILED_CANCELED = 18;
|
||||
|
||||
static final int STATE_FAILED = 19;
|
||||
|
||||
/**
|
||||
* Called internally by the stub when the service is bound to the client.
|
||||
* <p>
|
||||
* Critical implementation detail. In onServiceConnected we create the
|
||||
* remote service and marshaler. This is how we pass the client information
|
||||
* back to the service so the client can be properly notified of changes. We
|
||||
* must do this every time we reconnect to the service.
|
||||
* <p>
|
||||
* That is, when you receive this callback, you should call
|
||||
* {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member
|
||||
* instance of {@link IDownloaderService}, then call
|
||||
* {@link IDownloaderService#onClientUpdated} with the Messenger retrieved
|
||||
* from your {@link IStub} proxy object.
|
||||
*
|
||||
* @param m the service Messenger. This Messenger is used to call the
|
||||
* service API from the client.
|
||||
*/
|
||||
void onServiceConnected(Messenger m);
|
||||
|
||||
/**
|
||||
* Called when the download state changes. Depending on the state, there may
|
||||
* be user requests. The service is free to change the download state in the
|
||||
* middle of a user request, so the client should be able to handle this.
|
||||
* <p>
|
||||
* The Downloader Library includes a collection of string resources that
|
||||
* correspond to each of the states, which you can use to provide users a
|
||||
* useful message based on the state provided in this callback. To fetch the
|
||||
* appropriate string for a state, call
|
||||
* {@link Helpers#getDownloaderStringResourceIDFromState}.
|
||||
* <p>
|
||||
* What this means to the developer: The application has gotten a message
|
||||
* that the download has paused due to lack of WiFi. The developer should
|
||||
* then show UI asking the user if they want to enable downloading over
|
||||
* cellular connections with appropriate warnings. If the application
|
||||
* suddenly starts downloading, the application should revert to showing the
|
||||
* progress again, rather than leaving up the download over cellular UI up.
|
||||
*
|
||||
* @param newState one of the STATE_* values defined in IDownloaderClient
|
||||
*/
|
||||
void onDownloadStateChanged(int newState);
|
||||
|
||||
/**
|
||||
* Shows the download progress. This is intended to be used to fill out a
|
||||
* client UI. This progress should only be shown in a few states such as
|
||||
* STATE_DOWNLOADING.
|
||||
*
|
||||
* @param progress the DownloadProgressInfo object containing the current
|
||||
* progress of all downloads.
|
||||
*/
|
||||
void onDownloadProgress(DownloadProgressInfo progress);
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
|
||||
import android.os.Messenger;
|
||||
|
||||
/**
|
||||
* This interface is implemented by the DownloaderService and by the
|
||||
* DownloaderServiceMarshaller. It contains functions to control the service.
|
||||
* When a client binds to the service, it must call the onClientUpdated
|
||||
* function.
|
||||
* <p>
|
||||
* You can acquire a proxy that implements this interface for your service by
|
||||
* calling {@link DownloaderServiceMarshaller#CreateProxy} during the
|
||||
* {@link IDownloaderClient#onServiceConnected} callback. At which point, you
|
||||
* should immediately call {@link #onClientUpdated}.
|
||||
*/
|
||||
public interface IDownloaderService {
|
||||
/**
|
||||
* Set this flag in response to the
|
||||
* IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
|
||||
* call RequestContinueDownload to resume a download
|
||||
*/
|
||||
public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1;
|
||||
|
||||
/**
|
||||
* Request that the service abort the current download. The service should
|
||||
* respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}.
|
||||
*/
|
||||
void requestAbortDownload();
|
||||
|
||||
/**
|
||||
* Request that the service pause the current download. The service should
|
||||
* respond by changing the state to
|
||||
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
||||
*/
|
||||
void requestPauseDownload();
|
||||
|
||||
/**
|
||||
* Request that the service continue a paused download, when in any paused
|
||||
* or failed state, including
|
||||
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
||||
*/
|
||||
void requestContinueDownload();
|
||||
|
||||
/**
|
||||
* Set the flags for this download (e.g.
|
||||
* {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
|
||||
*
|
||||
* @param flags
|
||||
*/
|
||||
void setDownloadFlags(int flags);
|
||||
|
||||
/**
|
||||
* Requests that the download status be sent to the client.
|
||||
*/
|
||||
void requestDownloadStatus();
|
||||
|
||||
/**
|
||||
* Call this when you get {@link
|
||||
* IDownloaderClient.onServiceConnected(Messenger m)} from the
|
||||
* DownloaderClient to register the client with the service. It will
|
||||
* automatically send the current status to the client.
|
||||
*
|
||||
* @param clientMessenger
|
||||
*/
|
||||
void onClientUpdated(Messenger clientMessenger);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Messenger;
|
||||
|
||||
/**
|
||||
* This is the interface that is used to connect/disconnect from the downloader
|
||||
* service.
|
||||
* <p>
|
||||
* You should get a proxy object that implements this interface by calling
|
||||
* {@link DownloaderClientMarshaller#CreateStub} in your activity when the
|
||||
* downloader service starts. Then, call {@link #connect} during your activity's
|
||||
* onResume() and call {@link #disconnect} during onStop().
|
||||
* <p>
|
||||
* Then during the {@link IDownloaderClient#onServiceConnected} callback, you
|
||||
* should call {@link #getMessenger} to pass the stub's Messenger object to
|
||||
* {@link IDownloaderService#onClientUpdated}.
|
||||
*/
|
||||
public interface IStub {
|
||||
Messenger getMessenger();
|
||||
|
||||
void connect(Context c);
|
||||
|
||||
void disconnect(Context c);
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
// -- GODOT start --
|
||||
import android.annotation.SuppressLint;
|
||||
// -- GODOT end --
|
||||
|
||||
/**
|
||||
* Contains useful helper functions, typically tied to the application context.
|
||||
*/
|
||||
class SystemFacade {
|
||||
private Context mContext;
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
public SystemFacade(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = (NotificationManager)
|
||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
public long currentTimeMillis() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public Integer getActiveNetworkType() {
|
||||
ConnectivityManager connectivity =
|
||||
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivity == null) {
|
||||
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
|
||||
if (activeInfo == null) {
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG, "network is not available");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return activeInfo.getType();
|
||||
}
|
||||
|
||||
public boolean isNetworkRoaming() {
|
||||
ConnectivityManager connectivity =
|
||||
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivity == null) {
|
||||
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
NetworkInfo info = connectivity.getActiveNetworkInfo();
|
||||
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
|
||||
TelephonyManager tm = (TelephonyManager) mContext
|
||||
.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (null == tm) {
|
||||
Log.w(Constants.TAG, "couldn't get telephony manager");
|
||||
return false;
|
||||
}
|
||||
boolean isRoaming = isMobile && tm.isNetworkRoaming();
|
||||
if (Constants.LOGVV && isRoaming) {
|
||||
Log.v(Constants.TAG, "network is roaming");
|
||||
}
|
||||
return isRoaming;
|
||||
}
|
||||
|
||||
public Long getMaxBytesOverMobile() {
|
||||
return (long) Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
public Long getRecommendedMaxBytesOverMobile() {
|
||||
return 2097152L;
|
||||
}
|
||||
|
||||
public void sendBroadcast(Intent intent) {
|
||||
mContext.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
|
||||
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
|
||||
}
|
||||
|
||||
public void postNotification(long id, Notification notification) {
|
||||
/**
|
||||
* TODO: The system notification manager takes ints, not longs, as IDs,
|
||||
* but the download manager uses IDs take straight from the database,
|
||||
* which are longs. This will have to be dealt with at some point.
|
||||
*/
|
||||
mNotificationManager.notify((int) id, notification);
|
||||
}
|
||||
|
||||
public void cancelNotification(long id) {
|
||||
mNotificationManager.cancel((int) id);
|
||||
}
|
||||
|
||||
public void cancelAllNotifications() {
|
||||
mNotificationManager.cancelAll();
|
||||
}
|
||||
|
||||
public void startThread(Thread thread) {
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This service differs from IntentService in a few minor ways/ It will not
|
||||
* auto-stop itself after the intent is handled unless the target returns "true"
|
||||
* in should stop. Since the goal of this service is to handle a single kind of
|
||||
* intent, it does not queue up batches of intents of the same type.
|
||||
*/
|
||||
public abstract class CustomIntentService extends Service {
|
||||
private String mName;
|
||||
private boolean mRedelivery;
|
||||
private volatile ServiceHandler mServiceHandler;
|
||||
private volatile Looper mServiceLooper;
|
||||
private static final String LOG_TAG = "CustomIntentService";
|
||||
private static final int WHAT_MESSAGE = -10;
|
||||
|
||||
public CustomIntentService(String paramString) {
|
||||
this.mName = paramString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent paramIntent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
HandlerThread localHandlerThread = new HandlerThread("IntentService["
|
||||
+ this.mName + "]");
|
||||
localHandlerThread.start();
|
||||
this.mServiceLooper = localHandlerThread.getLooper();
|
||||
this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Thread localThread = this.mServiceLooper.getThread();
|
||||
if ((localThread != null) && (localThread.isAlive())) {
|
||||
localThread.interrupt();
|
||||
}
|
||||
this.mServiceLooper.quit();
|
||||
Log.d(LOG_TAG, "onDestroy");
|
||||
}
|
||||
|
||||
protected abstract void onHandleIntent(Intent paramIntent);
|
||||
|
||||
protected abstract boolean shouldStop();
|
||||
|
||||
@Override
|
||||
public void onStart(Intent paramIntent, int startId) {
|
||||
if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
|
||||
Message localMessage = this.mServiceHandler.obtainMessage();
|
||||
localMessage.arg1 = startId;
|
||||
localMessage.obj = paramIntent;
|
||||
localMessage.what = WHAT_MESSAGE;
|
||||
this.mServiceHandler.sendMessage(localMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent paramIntent, int flags, int startId) {
|
||||
onStart(paramIntent, startId);
|
||||
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
|
||||
}
|
||||
|
||||
public void setIntentRedelivery(boolean enabled) {
|
||||
this.mRedelivery = enabled;
|
||||
}
|
||||
|
||||
private final class ServiceHandler extends Handler {
|
||||
public ServiceHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message paramMessage) {
|
||||
CustomIntentService.this
|
||||
.onHandleIntent((Intent) paramMessage.obj);
|
||||
if (shouldStop()) {
|
||||
Log.d(LOG_TAG, "stopSelf");
|
||||
CustomIntentService.this.stopSelf(paramMessage.arg1);
|
||||
Log.d(LOG_TAG, "afterStopSelf");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.Constants;
|
||||
import com.google.android.vending.expansion.downloader.Helpers;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Representation of information about an individual download from the database.
|
||||
*/
|
||||
public class DownloadInfo {
|
||||
public String mUri;
|
||||
public final int mIndex;
|
||||
public final String mFileName;
|
||||
public String mETag;
|
||||
public long mTotalBytes;
|
||||
public long mCurrentBytes;
|
||||
public long mLastMod;
|
||||
public int mStatus;
|
||||
public int mControl;
|
||||
public int mNumFailed;
|
||||
public int mRetryAfter;
|
||||
public int mRedirectCount;
|
||||
|
||||
boolean mInitialized;
|
||||
|
||||
public int mFuzz;
|
||||
|
||||
public DownloadInfo(int index, String fileName, String pkg) {
|
||||
mFuzz = Helpers.sRandom.nextInt(1001);
|
||||
mFileName = fileName;
|
||||
mIndex = index;
|
||||
}
|
||||
|
||||
public void resetDownload() {
|
||||
mCurrentBytes = 0;
|
||||
mETag = "";
|
||||
mLastMod = 0;
|
||||
mStatus = 0;
|
||||
mControl = 0;
|
||||
mNumFailed = 0;
|
||||
mRetryAfter = 0;
|
||||
mRedirectCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when a download should be restarted.
|
||||
*/
|
||||
public long restartTime(long now) {
|
||||
if (mNumFailed == 0) {
|
||||
return now;
|
||||
}
|
||||
if (mRetryAfter > 0) {
|
||||
return mLastMod + mRetryAfter;
|
||||
}
|
||||
return mLastMod +
|
||||
Constants.RETRY_FIRST_DELAY *
|
||||
(1000 + mFuzz) * (1 << (mNumFailed - 1));
|
||||
}
|
||||
|
||||
public void logVerboseInfo() {
|
||||
Log.v(Constants.TAG, "Service adding new entry");
|
||||
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
||||
Log.v(Constants.TAG, "URI : " + mUri);
|
||||
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
||||
Log.v(Constants.TAG, "CONTROL : " + mControl);
|
||||
Log.v(Constants.TAG, "STATUS : " + mStatus);
|
||||
Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
|
||||
Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
|
||||
Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
|
||||
Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
|
||||
Log.v(Constants.TAG, "TOTAL : " + mTotalBytes);
|
||||
Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
|
||||
Log.v(Constants.TAG, "ETAG : " + mETag);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
// -- GODOT start --
|
||||
//import com.android.vending.expansion.downloader.R;
|
||||
import org.godotengine.godot.R;
|
||||
// -- GODOT end --
|
||||
|
||||
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
|
||||
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
||||
import com.google.android.vending.expansion.downloader.Helpers;
|
||||
import com.google.android.vending.expansion.downloader.IDownloaderClient;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Messenger;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
/**
|
||||
* This class handles displaying the notification associated with the download
|
||||
* queue going on in the download manager. It handles multiple status types;
|
||||
* Some require user interaction and some do not. Some of the user interactions
|
||||
* may be transient. (for example: the user is queried to continue the download
|
||||
* on 3G when it started on WiFi, but then the phone locks onto WiFi again so
|
||||
* the prompt automatically goes away)
|
||||
* <p/>
|
||||
* The application interface for the downloader also needs to understand and
|
||||
* handle these transient states.
|
||||
*/
|
||||
public class DownloadNotification implements IDownloaderClient {
|
||||
|
||||
private int mState;
|
||||
private final Context mContext;
|
||||
private final NotificationManager mNotificationManager;
|
||||
private CharSequence mCurrentTitle;
|
||||
|
||||
private IDownloaderClient mClientProxy;
|
||||
private NotificationCompat.Builder mActiveDownloadBuilder;
|
||||
private NotificationCompat.Builder mBuilder;
|
||||
private NotificationCompat.Builder mCurrentBuilder;
|
||||
private CharSequence mLabel;
|
||||
private String mCurrentText;
|
||||
private DownloadProgressInfo mProgressInfo;
|
||||
private PendingIntent mContentIntent;
|
||||
|
||||
static final String LOGTAG = "DownloadNotification";
|
||||
static final int NOTIFICATION_ID = LOGTAG.hashCode();
|
||||
|
||||
public PendingIntent getClientIntent() {
|
||||
return mContentIntent;
|
||||
}
|
||||
|
||||
public void setClientIntent(PendingIntent clientIntent) {
|
||||
this.mBuilder.setContentIntent(clientIntent);
|
||||
this.mActiveDownloadBuilder.setContentIntent(clientIntent);
|
||||
this.mContentIntent = clientIntent;
|
||||
}
|
||||
|
||||
public void resendState() {
|
||||
if (null != mClientProxy) {
|
||||
mClientProxy.onDownloadStateChanged(mState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadStateChanged(int newState) {
|
||||
if (null != mClientProxy) {
|
||||
mClientProxy.onDownloadStateChanged(newState);
|
||||
}
|
||||
if (newState != mState) {
|
||||
mState = newState;
|
||||
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
|
||||
return;
|
||||
}
|
||||
int stringDownloadID;
|
||||
int iconResource;
|
||||
boolean ongoingEvent;
|
||||
|
||||
// get the new title string and paused text
|
||||
switch (newState) {
|
||||
case 0:
|
||||
iconResource = android.R.drawable.stat_sys_warning;
|
||||
stringDownloadID = R.string.state_unknown;
|
||||
ongoingEvent = false;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_DOWNLOADING:
|
||||
iconResource = android.R.drawable.stat_sys_download;
|
||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||
ongoingEvent = true;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_FETCHING_URL:
|
||||
case IDownloaderClient.STATE_CONNECTING:
|
||||
iconResource = android.R.drawable.stat_sys_download_done;
|
||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||
ongoingEvent = true;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_COMPLETED:
|
||||
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
||||
iconResource = android.R.drawable.stat_sys_download_done;
|
||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||
ongoingEvent = false;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_FAILED:
|
||||
case IDownloaderClient.STATE_FAILED_CANCELED:
|
||||
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
||||
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
||||
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
||||
iconResource = android.R.drawable.stat_sys_warning;
|
||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||
ongoingEvent = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
iconResource = android.R.drawable.stat_sys_warning;
|
||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||
ongoingEvent = true;
|
||||
break;
|
||||
}
|
||||
|
||||
mCurrentText = mContext.getString(stringDownloadID);
|
||||
mCurrentTitle = mLabel;
|
||||
mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
|
||||
mCurrentBuilder.setSmallIcon(iconResource);
|
||||
mCurrentBuilder.setContentTitle(mCurrentTitle);
|
||||
mCurrentBuilder.setContentText(mCurrentText);
|
||||
if (ongoingEvent) {
|
||||
mCurrentBuilder.setOngoing(true);
|
||||
} else {
|
||||
mCurrentBuilder.setOngoing(false);
|
||||
mCurrentBuilder.setAutoCancel(true);
|
||||
}
|
||||
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(DownloadProgressInfo progress) {
|
||||
mProgressInfo = progress;
|
||||
if (null != mClientProxy) {
|
||||
mClientProxy.onDownloadProgress(progress);
|
||||
}
|
||||
if (progress.mOverallTotal <= 0) {
|
||||
// we just show the text
|
||||
mBuilder.setTicker(mCurrentTitle);
|
||||
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
mBuilder.setContentTitle(mCurrentTitle);
|
||||
mBuilder.setContentText(mCurrentText);
|
||||
mCurrentBuilder = mBuilder;
|
||||
} else {
|
||||
mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false);
|
||||
mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
|
||||
mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
|
||||
mActiveDownloadBuilder.setContentTitle(mLabel);
|
||||
mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification,
|
||||
Helpers.getTimeRemaining(progress.mTimeRemaining)));
|
||||
mCurrentBuilder = mActiveDownloadBuilder;
|
||||
}
|
||||
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in response to onClientUpdated. Creates a new proxy and notifies
|
||||
* it of the current state.
|
||||
*
|
||||
* @param msg the client Messenger to notify
|
||||
*/
|
||||
public void setMessenger(Messenger msg) {
|
||||
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
|
||||
if (null != mProgressInfo) {
|
||||
mClientProxy.onDownloadProgress(mProgressInfo);
|
||||
}
|
||||
if (mState != -1) {
|
||||
mClientProxy.onDownloadStateChanged(mState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param ctx The context to use to obtain access to the Notification
|
||||
* Service
|
||||
*/
|
||||
DownloadNotification(Context ctx, CharSequence applicationLabel) {
|
||||
mState = -1;
|
||||
mContext = ctx;
|
||||
mLabel = applicationLabel;
|
||||
mNotificationManager = (NotificationManager)
|
||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
|
||||
mBuilder = new NotificationCompat.Builder(ctx);
|
||||
|
||||
// Set Notification category and priorities to something that makes sense for a long
|
||||
// lived background task.
|
||||
mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
|
||||
|
||||
mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
|
||||
|
||||
mCurrentBuilder = mBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(Messenger m) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,852 @@
|
|||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.Constants;
|
||||
import com.google.android.vending.expansion.downloader.Helpers;
|
||||
import com.google.android.vending.expansion.downloader.IDownloaderClient;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.SyncFailedException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Runs an actual download
|
||||
*/
|
||||
public class DownloadThread {
|
||||
|
||||
private Context mContext;
|
||||
private DownloadInfo mInfo;
|
||||
private DownloaderService mService;
|
||||
private final DownloadsDB mDB;
|
||||
private final DownloadNotification mNotification;
|
||||
private String mUserAgent;
|
||||
|
||||
public DownloadThread(DownloadInfo info, DownloaderService service,
|
||||
DownloadNotification notification) {
|
||||
mContext = service;
|
||||
mInfo = info;
|
||||
mService = service;
|
||||
mNotification = notification;
|
||||
mDB = DownloadsDB.getDB(service);
|
||||
mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";"
|
||||
+ Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/"
|
||||
+ android.os.Build.ID + ")" +
|
||||
service.getPackageName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default user agent
|
||||
*/
|
||||
private String userAgent() {
|
||||
return mUserAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the entire run() method.
|
||||
*/
|
||||
private static class State {
|
||||
public String mFilename;
|
||||
public FileOutputStream mStream;
|
||||
public boolean mCountRetry = false;
|
||||
public int mRetryAfter = 0;
|
||||
public int mRedirectCount = 0;
|
||||
public String mNewUri;
|
||||
public boolean mGotData = false;
|
||||
public String mRequestUri;
|
||||
|
||||
public State(DownloadInfo info, DownloaderService service) {
|
||||
mRedirectCount = info.mRedirectCount;
|
||||
mRequestUri = info.mUri;
|
||||
mFilename = service.generateTempSaveFileName(info.mFileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State within executeDownload()
|
||||
*/
|
||||
private static class InnerState {
|
||||
public int mBytesSoFar = 0;
|
||||
public int mBytesThisSession = 0;
|
||||
public String mHeaderETag;
|
||||
public boolean mContinuingDownload = false;
|
||||
public String mHeaderContentLength;
|
||||
public String mHeaderContentDisposition;
|
||||
public String mHeaderContentLocation;
|
||||
public int mBytesNotified = 0;
|
||||
public long mTimeLastNotification = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raised from methods called by run() to indicate that the current request
|
||||
* should be stopped immediately. Note the message passed to this exception
|
||||
* will be logged and therefore must be guaranteed not to contain any PII,
|
||||
* meaning it generally can't include any information about the request URI,
|
||||
* headers, or destination filename.
|
||||
*/
|
||||
private class StopRequest extends Throwable {
|
||||
|
||||
private static final long serialVersionUID = 6338592678988347973L;
|
||||
public int mFinalStatus;
|
||||
|
||||
public StopRequest(int finalStatus, String message) {
|
||||
super(message);
|
||||
mFinalStatus = finalStatus;
|
||||
}
|
||||
|
||||
public StopRequest(int finalStatus, String message, Throwable throwable) {
|
||||
super(message, throwable);
|
||||
mFinalStatus = finalStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raised from methods called by executeDownload() to indicate that the
|
||||
* download should be retried immediately.
|
||||
*/
|
||||
private class RetryDownload extends Throwable {
|
||||
|
||||
private static final long serialVersionUID = 6196036036517540229L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the download in a separate thread
|
||||
*/
|
||||
public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
|
||||
State state = new State(mInfo, mService);
|
||||
PowerManager.WakeLock wakeLock = null;
|
||||
int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
|
||||
|
||||
try {
|
||||
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
|
||||
// -- GODOT start --
|
||||
//wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
|
||||
//wakeLock.acquire();
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
|
||||
wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
|
||||
// -- GODOT end --
|
||||
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
|
||||
Log.v(Constants.TAG, " at " + mInfo.mUri);
|
||||
}
|
||||
|
||||
boolean finished = false;
|
||||
while (!finished) {
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
|
||||
Log.v(Constants.TAG, " at " + mInfo.mUri);
|
||||
}
|
||||
// Set or unset proxy, which may have changed since last GET
|
||||
// request.
|
||||
// setDefaultProxy() supports null as proxy parameter.
|
||||
URL url = new URL(state.mRequestUri);
|
||||
HttpURLConnection request = (HttpURLConnection)url.openConnection();
|
||||
request.setRequestProperty("User-Agent", userAgent());
|
||||
try {
|
||||
executeDownload(state, request);
|
||||
finished = true;
|
||||
} catch (RetryDownload exc) {
|
||||
// fall through
|
||||
} finally {
|
||||
request.disconnect();
|
||||
request = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
|
||||
Log.v(Constants.TAG, " at " + mInfo.mUri);
|
||||
}
|
||||
finalizeDestinationFile(state);
|
||||
finalStatus = DownloaderService.STATUS_SUCCESS;
|
||||
} catch (StopRequest error) {
|
||||
// remove the cause before printing, in case it contains PII
|
||||
Log.w(Constants.TAG,
|
||||
"Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
|
||||
error.printStackTrace();
|
||||
finalStatus = error.mFinalStatus;
|
||||
// fall through to finally block
|
||||
} catch (Throwable ex) { // sometimes the socket code throws unchecked
|
||||
// exceptions
|
||||
Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
|
||||
finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
|
||||
// falls through to the code that reports an error
|
||||
} finally {
|
||||
if (wakeLock != null) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
cleanupDestination(state, finalStatus);
|
||||
notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
|
||||
state.mRedirectCount, state.mGotData, state.mFilename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully execute a single download request - setup and send the request,
|
||||
* handle the response, and transfer the data to the destination file.
|
||||
*/
|
||||
private void executeDownload(State state, HttpURLConnection request)
|
||||
throws StopRequest, RetryDownload {
|
||||
InnerState innerState = new InnerState();
|
||||
byte data[] = new byte[Constants.BUFFER_SIZE];
|
||||
|
||||
checkPausedOrCanceled(state);
|
||||
|
||||
setupDestinationFile(state, innerState);
|
||||
addRequestHeaders(innerState, request);
|
||||
|
||||
// check just before sending the request to avoid using an invalid
|
||||
// connection at all
|
||||
checkConnectivity(state);
|
||||
|
||||
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
|
||||
int responseCode = sendRequest(state, request);
|
||||
handleExceptionalStatus(state, innerState, request, responseCode);
|
||||
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "received response for " + mInfo.mUri);
|
||||
}
|
||||
|
||||
processResponseHeaders(state, innerState, request);
|
||||
InputStream entityStream = openResponseEntity(state, request);
|
||||
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
|
||||
transferData(state, innerState, data, entityStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current connectivity is valid for this request.
|
||||
*/
|
||||
private void checkConnectivity(State state) throws StopRequest {
|
||||
switch (mService.getNetworkAvailabilityState(mDB)) {
|
||||
case DownloaderService.NETWORK_OK:
|
||||
return;
|
||||
case DownloaderService.NETWORK_NO_CONNECTION:
|
||||
throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
|
||||
"waiting for network to return");
|
||||
case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
|
||||
throw new StopRequest(
|
||||
DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
|
||||
"waiting for wifi or for download over cellular to be authorized");
|
||||
case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
|
||||
throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
|
||||
"roaming is not allowed");
|
||||
case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
|
||||
throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer as much data as possible from the HTTP response to the
|
||||
* destination file.
|
||||
*
|
||||
* @param data buffer to use to read data
|
||||
* @param entityStream stream for reading the HTTP response entity
|
||||
*/
|
||||
private void transferData(State state, InnerState innerState, byte[] data,
|
||||
InputStream entityStream) throws StopRequest {
|
||||
for (;;) {
|
||||
int bytesRead = readFromResponse(state, innerState, data, entityStream);
|
||||
if (bytesRead == -1) { // success, end of stream already reached
|
||||
handleEndOfStream(state, innerState);
|
||||
return;
|
||||
}
|
||||
|
||||
state.mGotData = true;
|
||||
writeDataToDestination(state, data, bytesRead);
|
||||
innerState.mBytesSoFar += bytesRead;
|
||||
innerState.mBytesThisSession += bytesRead;
|
||||
reportProgress(state, innerState);
|
||||
|
||||
checkPausedOrCanceled(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a successful completion to take any necessary action on the
|
||||
* downloaded file.
|
||||
*/
|
||||
private void finalizeDestinationFile(State state) throws StopRequest {
|
||||
syncDestination(state);
|
||||
String tempFilename = state.mFilename;
|
||||
String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
|
||||
if (!state.mFilename.equals(finalFilename)) {
|
||||
File startFile = new File(tempFilename);
|
||||
File destFile = new File(finalFilename);
|
||||
if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
|
||||
if (!startFile.renameTo(destFile)) {
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
|
||||
"unable to finalize destination file");
|
||||
}
|
||||
} else {
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
|
||||
"file delivered with incorrect size. probably due to network not browser configured");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called just before the thread finishes, regardless of status, to take any
|
||||
* necessary action on the downloaded file.
|
||||
*/
|
||||
private void cleanupDestination(State state, int finalStatus) {
|
||||
closeDestination(state);
|
||||
if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
|
||||
new File(state.mFilename).delete();
|
||||
state.mFilename = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the destination file to storage.
|
||||
*/
|
||||
private void syncDestination(State state) {
|
||||
FileOutputStream downloadedFileStream = null;
|
||||
try {
|
||||
downloadedFileStream = new FileOutputStream(state.mFilename, true);
|
||||
downloadedFileStream.getFD().sync();
|
||||
} catch (FileNotFoundException ex) {
|
||||
Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
|
||||
} catch (SyncFailedException ex) {
|
||||
Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
|
||||
} catch (IOException ex) {
|
||||
Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
|
||||
} catch (RuntimeException ex) {
|
||||
Log.w(Constants.TAG, "exception while syncing file: ", ex);
|
||||
} finally {
|
||||
if (downloadedFileStream != null) {
|
||||
try {
|
||||
downloadedFileStream.close();
|
||||
} catch (IOException ex) {
|
||||
Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
|
||||
} catch (RuntimeException ex) {
|
||||
Log.w(Constants.TAG, "exception while closing file: ", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the destination output stream.
|
||||
*/
|
||||
private void closeDestination(State state) {
|
||||
try {
|
||||
// close the file
|
||||
if (state.mStream != null) {
|
||||
state.mStream.close();
|
||||
state.mStream = null;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
|
||||
}
|
||||
// nothing can really be done if the file can't be closed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the download has been paused or canceled, stopping the request
|
||||
* appropriately if it has been.
|
||||
*/
|
||||
private void checkPausedOrCanceled(State state) throws StopRequest {
|
||||
if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
|
||||
int status = mService.getStatus();
|
||||
switch (status) {
|
||||
case DownloaderService.STATUS_PAUSED_BY_APP:
|
||||
throw new StopRequest(mService.getStatus(),
|
||||
"download paused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report download progress through the database if necessary.
|
||||
*/
|
||||
private void reportProgress(State state, InnerState innerState) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (innerState.mBytesSoFar - innerState.mBytesNotified
|
||||
> Constants.MIN_PROGRESS_STEP
|
||||
&& now - innerState.mTimeLastNotification
|
||||
> Constants.MIN_PROGRESS_TIME) {
|
||||
// we store progress updates to the database here
|
||||
mInfo.mCurrentBytes = innerState.mBytesSoFar;
|
||||
mDB.updateDownloadCurrentBytes(mInfo);
|
||||
|
||||
innerState.mBytesNotified = innerState.mBytesSoFar;
|
||||
innerState.mTimeLastNotification = now;
|
||||
|
||||
long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
|
||||
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of "
|
||||
+ mInfo.mTotalBytes);
|
||||
Log.v(Constants.TAG, " total " + totalBytesSoFar + " out of "
|
||||
+ mService.mTotalLength);
|
||||
}
|
||||
|
||||
mService.notifyUpdateBytes(totalBytesSoFar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a data buffer to the destination file.
|
||||
*
|
||||
* @param data buffer containing the data to write
|
||||
* @param bytesRead how many bytes to write from the buffer
|
||||
*/
|
||||
private void writeDataToDestination(State state, byte[] data, int bytesRead)
|
||||
throws StopRequest {
|
||||
for (;;) {
|
||||
try {
|
||||
if (state.mStream == null) {
|
||||
state.mStream = new FileOutputStream(state.mFilename, true);
|
||||
}
|
||||
state.mStream.write(data, 0, bytesRead);
|
||||
// we close after every write --- this may be too inefficient
|
||||
closeDestination(state);
|
||||
return;
|
||||
} catch (IOException ex) {
|
||||
if (!Helpers.isExternalMediaMounted()) {
|
||||
throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
|
||||
"external media not mounted while writing destination file");
|
||||
}
|
||||
|
||||
long availableBytes =
|
||||
Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
|
||||
if (availableBytes < bytesRead) {
|
||||
throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
|
||||
"insufficient space while writing destination file", ex);
|
||||
}
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
|
||||
"while writing destination file: " + ex.toString(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we've reached the end of the HTTP response stream, to update
|
||||
* the database and check for consistency.
|
||||
*/
|
||||
private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
|
||||
mInfo.mCurrentBytes = innerState.mBytesSoFar;
|
||||
// this should always be set from the market
|
||||
// if ( innerState.mHeaderContentLength == null ) {
|
||||
// mInfo.mTotalBytes = innerState.mBytesSoFar;
|
||||
// }
|
||||
mDB.updateDownload(mInfo);
|
||||
|
||||
boolean lengthMismatched = (innerState.mHeaderContentLength != null)
|
||||
&& (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
|
||||
if (lengthMismatched) {
|
||||
if (cannotResume(innerState)) {
|
||||
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
|
||||
"mismatched content length");
|
||||
} else {
|
||||
throw new StopRequest(getFinalStatusForHttpError(state),
|
||||
"closed socket before end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean cannotResume(InnerState innerState) {
|
||||
return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read some data from the HTTP response stream, handling I/O errors.
|
||||
*
|
||||
* @param data buffer to use to read data
|
||||
* @param entityStream stream for reading the HTTP response entity
|
||||
* @return the number of bytes actually read or -1 if the end of the stream
|
||||
* has been reached
|
||||
*/
|
||||
private int readFromResponse(State state, InnerState innerState, byte[] data,
|
||||
InputStream entityStream) throws StopRequest {
|
||||
try {
|
||||
return entityStream.read(data);
|
||||
} catch (IOException ex) {
|
||||
logNetworkState();
|
||||
mInfo.mCurrentBytes = innerState.mBytesSoFar;
|
||||
mDB.updateDownload(mInfo);
|
||||
if (cannotResume(innerState)) {
|
||||
String message = "while reading response: " + ex.toString()
|
||||
+ ", can't resume interrupted download with no ETag";
|
||||
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
|
||||
message, ex);
|
||||
} else {
|
||||
throw new StopRequest(getFinalStatusForHttpError(state),
|
||||
"while reading response: " + ex.toString(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a stream for the HTTP response entity, handling I/O errors.
|
||||
*
|
||||
* @return an InputStream to read the response entity
|
||||
*/
|
||||
private InputStream openResponseEntity(State state, HttpURLConnection response)
|
||||
throws StopRequest {
|
||||
try {
|
||||
return response.getInputStream();
|
||||
} catch (IOException ex) {
|
||||
logNetworkState();
|
||||
throw new StopRequest(getFinalStatusForHttpError(state),
|
||||
"while getting entity: " + ex.toString(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void logNetworkState() {
|
||||
if (Constants.LOGX) {
|
||||
Log.i(Constants.TAG,
|
||||
"Net "
|
||||
+ (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up"
|
||||
: "Down"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read HTTP response headers and take appropriate action, including setting
|
||||
* up the destination file and updating the database.
|
||||
*/
|
||||
private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
|
||||
throws StopRequest {
|
||||
if (innerState.mContinuingDownload) {
|
||||
// ignore response headers on resume requests
|
||||
return;
|
||||
}
|
||||
|
||||
readResponseHeaders(state, innerState, response);
|
||||
|
||||
try {
|
||||
state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
|
||||
} catch (DownloaderService.GenerateSaveFileError exc) {
|
||||
throw new StopRequest(exc.mStatus, exc.mMessage);
|
||||
}
|
||||
try {
|
||||
state.mStream = new FileOutputStream(state.mFilename);
|
||||
} catch (FileNotFoundException exc) {
|
||||
// make sure the directory exists
|
||||
File pathFile = new File(Helpers.getSaveFilePath(mService));
|
||||
try {
|
||||
if (pathFile.mkdirs()) {
|
||||
state.mStream = new FileOutputStream(state.mFilename);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
|
||||
"while opening destination file: " + exc.toString(), exc);
|
||||
}
|
||||
}
|
||||
if (Constants.LOGV) {
|
||||
Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
|
||||
}
|
||||
|
||||
updateDatabaseFromHeaders(state, innerState);
|
||||
// check connectivity again now that we know the total size
|
||||
checkConnectivity(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update necessary database fields based on values of HTTP response headers
|
||||
* that have been read.
|
||||
*/
|
||||
private void updateDatabaseFromHeaders(State state, InnerState innerState) {
|
||||
mInfo.mETag = innerState.mHeaderETag;
|
||||
mDB.updateDownload(mInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read headers from the HTTP response and store them into local state.
|
||||
*/
|
||||
private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
|
||||
throws StopRequest {
|
||||
String value = response.getHeaderField("Content-Disposition");
|
||||
if (value != null) {
|
||||
innerState.mHeaderContentDisposition = value;
|
||||
}
|
||||
value = response.getHeaderField("Content-Location");
|
||||
if (value != null) {
|
||||
innerState.mHeaderContentLocation = value;
|
||||
}
|
||||
value = response.getHeaderField("ETag");
|
||||
if (value != null) {
|
||||
innerState.mHeaderETag = value;
|
||||
}
|
||||
String headerTransferEncoding = null;
|
||||
value = response.getHeaderField("Transfer-Encoding");
|
||||
if (value != null) {
|
||||
headerTransferEncoding = value;
|
||||
}
|
||||
String headerContentType = null;
|
||||
value = response.getHeaderField("Content-Type");
|
||||
if (value != null) {
|
||||
headerContentType = value;
|
||||
if (!headerContentType.equals("application/vnd.android.obb")) {
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
|
||||
"file delivered with incorrect Mime type");
|
||||
}
|
||||
}
|
||||
|
||||
if (headerTransferEncoding == null) {
|
||||
long contentLength = response.getContentLength();
|
||||
if (value != null) {
|
||||
// this is always set from Market
|
||||
if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
|
||||
// we're most likely on a bad wifi connection -- we should
|
||||
// probably
|
||||
// also look at the mime type --- but the size mismatch is
|
||||
// enough
|
||||
// to tell us that something is wrong here
|
||||
Log.e(Constants.TAG, "Incorrect file size delivered.");
|
||||
} else {
|
||||
innerState.mHeaderContentLength = Long.toString(contentLength);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ignore content-length with transfer-encoding - 2616 4.4 3
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG,
|
||||
"ignoring content-length because of xfer-encoding");
|
||||
}
|
||||
}
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG, "Content-Disposition: " +
|
||||
innerState.mHeaderContentDisposition);
|
||||
Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
|
||||
Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
|
||||
Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
|
||||
Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
|
||||
}
|
||||
|
||||
boolean noSizeInfo = innerState.mHeaderContentLength == null
|
||||
&& (headerTransferEncoding == null
|
||||
|| !headerTransferEncoding.equalsIgnoreCase("chunked"));
|
||||
if (noSizeInfo) {
|
||||
throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
|
||||
"can't know size of download, giving up");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the HTTP response status and handle anything unusual (e.g. not
|
||||
* 200/206).
|
||||
*/
|
||||
private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode)
|
||||
throws StopRequest, RetryDownload {
|
||||
if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
|
||||
handleServiceUnavailable(state, connection);
|
||||
}
|
||||
int expectedStatus = innerState.mContinuingDownload ? 206
|
||||
: DownloaderService.STATUS_SUCCESS;
|
||||
if (responseCode != expectedStatus) {
|
||||
handleOtherStatus(state, innerState, responseCode);
|
||||
} else {
|
||||
// no longer redirected
|
||||
state.mRedirectCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a status that we don't know how to deal with properly.
|
||||
*/
|
||||
private void handleOtherStatus(State state, InnerState innerState, int statusCode)
|
||||
throws StopRequest {
|
||||
int finalStatus;
|
||||
if (DownloaderService.isStatusError(statusCode)) {
|
||||
finalStatus = statusCode;
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
|
||||
} else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
|
||||
finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
|
||||
} else {
|
||||
finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
|
||||
}
|
||||
throw new StopRequest(finalStatus, "http error " + statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for this download to the HTTP request to allow for resume.
|
||||
*/
|
||||
private void addRequestHeaders(InnerState innerState, HttpURLConnection request) {
|
||||
if (innerState.mContinuingDownload) {
|
||||
if (innerState.mHeaderETag != null) {
|
||||
request.setRequestProperty("If-Match", innerState.mHeaderETag);
|
||||
}
|
||||
request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a 503 Service Unavailable status by processing the Retry-After
|
||||
* header.
|
||||
*/
|
||||
private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest {
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG, "got HTTP response code 503");
|
||||
}
|
||||
state.mCountRetry = true;
|
||||
String retryAfterValue = connection.getHeaderField("Retry-After");
|
||||
if (retryAfterValue != null) {
|
||||
try {
|
||||
if (Constants.LOGVV) {
|
||||
Log.v(Constants.TAG, "Retry-After :" + retryAfterValue);
|
||||
}
|
||||
state.mRetryAfter = Integer.parseInt(retryAfterValue);
|
||||
if (state.mRetryAfter < 0) {
|
||||
state.mRetryAfter = 0;
|
||||
} else {
|
||||
if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
|
||||
state.mRetryAfter = Constants.MIN_RETRY_AFTER;
|
||||
} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
|
||||
state.mRetryAfter = Constants.MAX_RETRY_AFTER;
|
||||
}
|
||||
state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
|
||||
state.mRetryAfter *= 1000;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
// ignored - retryAfter stays 0 in this case.
|
||||
}
|
||||
}
|
||||
throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
|
||||
"got 503 Service Unavailable, will retry later");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the request to the server, handling any I/O exceptions.
|
||||
*/
|
||||
private int sendRequest(State state, HttpURLConnection request)
|
||||
throws StopRequest {
|
||||
try {
|
||||
return request.getResponseCode();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
|
||||
"while trying to execute request: " + ex.toString(), ex);
|
||||
} catch (IOException ex) {
|
||||
logNetworkState();
|
||||
throw new StopRequest(getFinalStatusForHttpError(state),
|
||||
"while trying to execute request: " + ex.toString(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int getFinalStatusForHttpError(State state) {
|
||||
if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
|
||||
return DownloaderService.STATUS_WAITING_FOR_NETWORK;
|
||||
} else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
|
||||
state.mCountRetry = true;
|
||||
return DownloaderService.STATUS_WAITING_TO_RETRY;
|
||||
} else {
|
||||
Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
|
||||
return DownloaderService.STATUS_HTTP_DATA_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the destination file to receive data. If the file already exists,
|
||||
* we'll set up appropriately for resumption.
|
||||
*/
|
||||
private void setupDestinationFile(State state, InnerState innerState)
|
||||
throws StopRequest {
|
||||
if (state.mFilename != null) { // only true if we've already run a
|
||||
// thread for this download
|
||||
if (!Helpers.isFilenameValid(state.mFilename)) {
|
||||
// this should never happen
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
|
||||
"found invalid internal destination filename");
|
||||
}
|
||||
// We're resuming a download that got interrupted
|
||||
File f = new File(state.mFilename);
|
||||
if (f.exists()) {
|
||||
long fileLength = f.length();
|
||||
if (fileLength == 0) {
|
||||
// The download hadn't actually started, we can restart from
|
||||
// scratch
|
||||
f.delete();
|
||||
state.mFilename = null;
|
||||
} else if (mInfo.mETag == null) {
|
||||
// This should've been caught upon failure
|
||||
f.delete();
|
||||
throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
|
||||
"Trying to resume a download that can't be resumed");
|
||||
} else {
|
||||
// All right, we'll be able to resume this download
|
||||
try {
|
||||
state.mStream = new FileOutputStream(state.mFilename, true);
|
||||
} catch (FileNotFoundException exc) {
|
||||
throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
|
||||
"while opening destination for resuming: " + exc.toString(), exc);
|
||||
}
|
||||
innerState.mBytesSoFar = (int) fileLength;
|
||||
if (mInfo.mTotalBytes != -1) {
|
||||
innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
|
||||
}
|
||||
innerState.mHeaderETag = mInfo.mETag;
|
||||
innerState.mContinuingDownload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mStream != null) {
|
||||
closeDestination(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information about the completed download, and notifies the
|
||||
* initiating application.
|
||||
*/
|
||||
private void notifyDownloadCompleted(
|
||||
int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
|
||||
String filename) {
|
||||
updateDownloadDatabase(
|
||||
status, countRetry, retryAfter, redirectCount, gotData, filename);
|
||||
if (DownloaderService.isStatusCompleted(status)) {
|
||||
// TBD: send status update?
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDownloadDatabase(
|
||||
int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
|
||||
String filename) {
|
||||
mInfo.mStatus = status;
|
||||
mInfo.mRetryAfter = retryAfter;
|
||||
mInfo.mRedirectCount = redirectCount;
|
||||
mInfo.mLastMod = System.currentTimeMillis();
|
||||
if (!countRetry) {
|
||||
mInfo.mNumFailed = 0;
|
||||
} else if (gotData) {
|
||||
mInfo.mNumFailed = 1;
|
||||
} else {
|
||||
mInfo.mNumFailed++;
|
||||
}
|
||||
mDB.updateDownload(mInfo);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,510 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.provider.BaseColumns;
|
||||
import android.util.Log;
|
||||
|
||||
public class DownloadsDB {
|
||||
private static final String DATABASE_NAME = "DownloadsDB";
|
||||
private static final int DATABASE_VERSION = 7;
|
||||
public static final String LOG_TAG = DownloadsDB.class.getName();
|
||||
final SQLiteOpenHelper mHelper;
|
||||
SQLiteStatement mGetDownloadByIndex;
|
||||
SQLiteStatement mUpdateCurrentBytes;
|
||||
private static DownloadsDB mDownloadsDB;
|
||||
long mMetadataRowID = -1;
|
||||
int mVersionCode = -1;
|
||||
int mStatus = -1;
|
||||
int mFlags;
|
||||
|
||||
static public synchronized DownloadsDB getDB(Context paramContext) {
|
||||
if (null == mDownloadsDB) {
|
||||
return new DownloadsDB(paramContext);
|
||||
}
|
||||
return mDownloadsDB;
|
||||
}
|
||||
|
||||
private SQLiteStatement getDownloadByIndexStatement() {
|
||||
if (null == mGetDownloadByIndex) {
|
||||
mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
|
||||
"SELECT " + BaseColumns._ID + " FROM "
|
||||
+ DownloadColumns.TABLE_NAME + " WHERE "
|
||||
+ DownloadColumns.INDEX + " = ?");
|
||||
}
|
||||
return mGetDownloadByIndex;
|
||||
}
|
||||
|
||||
private SQLiteStatement getUpdateCurrentBytesStatement() {
|
||||
if (null == mUpdateCurrentBytes) {
|
||||
mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
|
||||
"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES
|
||||
+ " = ?" +
|
||||
" WHERE " + DownloadColumns.INDEX + " = ?");
|
||||
}
|
||||
return mUpdateCurrentBytes;
|
||||
}
|
||||
|
||||
private DownloadsDB(Context paramContext) {
|
||||
this.mHelper = new DownloadsContentDBHelper(paramContext);
|
||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||
// Query for the version code, the row ID of the metadata (for future
|
||||
// updating) the status and the flags
|
||||
Cursor cur = sqldb.rawQuery("SELECT " +
|
||||
MetadataColumns.APKVERSION + "," +
|
||||
BaseColumns._ID + "," +
|
||||
MetadataColumns.DOWNLOAD_STATUS + "," +
|
||||
MetadataColumns.FLAGS +
|
||||
" FROM "
|
||||
+ MetadataColumns.TABLE_NAME + " LIMIT 1", null);
|
||||
if (null != cur && cur.moveToFirst()) {
|
||||
mVersionCode = cur.getInt(0);
|
||||
mMetadataRowID = cur.getLong(1);
|
||||
mStatus = cur.getInt(2);
|
||||
mFlags = cur.getInt(3);
|
||||
cur.close();
|
||||
}
|
||||
mDownloadsDB = this;
|
||||
}
|
||||
|
||||
protected DownloadInfo getDownloadInfoByFileName(String fileName) {
|
||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||
Cursor itemcur = null;
|
||||
try {
|
||||
itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
||||
DownloadColumns.FILENAME + " = ?",
|
||||
new String[] {
|
||||
fileName
|
||||
}, null, null, null);
|
||||
if (null != itemcur && itemcur.moveToFirst()) {
|
||||
return getDownloadInfoFromCursor(itemcur);
|
||||
}
|
||||
} finally {
|
||||
if (null != itemcur)
|
||||
itemcur.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public long getIDForDownloadInfo(final DownloadInfo di) {
|
||||
return getIDByIndex(di.mIndex);
|
||||
}
|
||||
|
||||
public long getIDByIndex(int index) {
|
||||
SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
|
||||
downloadByIndex.clearBindings();
|
||||
downloadByIndex.bindLong(1, index);
|
||||
try {
|
||||
return downloadByIndex.simpleQueryForLong();
|
||||
} catch (SQLiteDoneException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDownloadCurrentBytes(final DownloadInfo di) {
|
||||
SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
|
||||
downloadCurrentBytes.clearBindings();
|
||||
downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
|
||||
downloadCurrentBytes.bindLong(2, di.mIndex);
|
||||
downloadCurrentBytes.execute();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.mHelper.close();
|
||||
}
|
||||
|
||||
protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
|
||||
DownloadsContentDBHelper(Context paramContext) {
|
||||
super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
private String createTableQueryFromArray(String paramString,
|
||||
String[][] paramArrayOfString) {
|
||||
StringBuilder localStringBuilder = new StringBuilder();
|
||||
localStringBuilder.append("CREATE TABLE ");
|
||||
localStringBuilder.append(paramString);
|
||||
localStringBuilder.append(" (");
|
||||
int i = paramArrayOfString.length;
|
||||
for (int j = 0;; j++) {
|
||||
if (j >= i) {
|
||||
localStringBuilder
|
||||
.setLength(localStringBuilder.length() - 1);
|
||||
localStringBuilder.append(");");
|
||||
return localStringBuilder.toString();
|
||||
}
|
||||
String[] arrayOfString = paramArrayOfString[j];
|
||||
localStringBuilder.append(' ');
|
||||
localStringBuilder.append(arrayOfString[0]);
|
||||
localStringBuilder.append(' ');
|
||||
localStringBuilder.append(arrayOfString[1]);
|
||||
localStringBuilder.append(',');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These two arrays must match and have the same order. For every Schema
|
||||
* there must be a corresponding table name.
|
||||
*/
|
||||
static final private String[][][] sSchemas = {
|
||||
DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
|
||||
};
|
||||
|
||||
static final private String[] sTables = {
|
||||
DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
|
||||
};
|
||||
|
||||
/**
|
||||
* Goes through all of the tables in sTables and drops each table if it
|
||||
* exists. Altered to no longer make use of reflection.
|
||||
*/
|
||||
private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
|
||||
for (String table : sTables) {
|
||||
try {
|
||||
paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
|
||||
} catch (Exception localException) {
|
||||
localException.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through all of the tables in sTables and creates a database with
|
||||
* the corresponding schema described in sSchemas. Altered to no longer
|
||||
* make use of reflection.
|
||||
*/
|
||||
public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
|
||||
int numSchemas = sSchemas.length;
|
||||
for (int i = 0; i < numSchemas; i++) {
|
||||
try {
|
||||
String[][] schema = (String[][]) sSchemas[i];
|
||||
paramSQLiteDatabase.execSQL(createTableQueryFromArray(
|
||||
sTables[i], schema));
|
||||
} catch (Exception localException) {
|
||||
while (true)
|
||||
localException.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
|
||||
int paramInt1, int paramInt2) {
|
||||
Log.w(DownloadsContentDBHelper.class.getName(),
|
||||
"Upgrading database from version " + paramInt1 + " to "
|
||||
+ paramInt2 + ", which will destroy all old data");
|
||||
dropTables(paramSQLiteDatabase);
|
||||
onCreate(paramSQLiteDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MetadataColumns implements BaseColumns {
|
||||
public static final String APKVERSION = "APKVERSION";
|
||||
public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
|
||||
public static final String FLAGS = "DOWNLOADFLAGS";
|
||||
|
||||
public static final String[][] SCHEMA = {
|
||||
{
|
||||
BaseColumns._ID, "INTEGER PRIMARY KEY"
|
||||
},
|
||||
{
|
||||
APKVERSION, "INTEGER"
|
||||
}, {
|
||||
DOWNLOAD_STATUS, "INTEGER"
|
||||
},
|
||||
{
|
||||
FLAGS, "INTEGER"
|
||||
}
|
||||
};
|
||||
public static final String TABLE_NAME = "MetadataColumns";
|
||||
public static final String _ID = "MetadataColumns._id";
|
||||
}
|
||||
|
||||
public static class DownloadColumns implements BaseColumns {
|
||||
public static final String INDEX = "FILEIDX";
|
||||
public static final String URI = "URI";
|
||||
public static final String FILENAME = "FN";
|
||||
public static final String ETAG = "ETAG";
|
||||
|
||||
public static final String TOTALBYTES = "TOTALBYTES";
|
||||
public static final String CURRENTBYTES = "CURRENTBYTES";
|
||||
public static final String LASTMOD = "LASTMOD";
|
||||
|
||||
public static final String STATUS = "STATUS";
|
||||
public static final String CONTROL = "CONTROL";
|
||||
public static final String NUM_FAILED = "FAILCOUNT";
|
||||
public static final String RETRY_AFTER = "RETRYAFTER";
|
||||
public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
|
||||
|
||||
public static final String[][] SCHEMA = {
|
||||
{
|
||||
BaseColumns._ID, "INTEGER PRIMARY KEY"
|
||||
},
|
||||
{
|
||||
INDEX, "INTEGER UNIQUE"
|
||||
}, {
|
||||
URI, "TEXT"
|
||||
},
|
||||
{
|
||||
FILENAME, "TEXT UNIQUE"
|
||||
}, {
|
||||
ETAG, "TEXT"
|
||||
},
|
||||
{
|
||||
TOTALBYTES, "INTEGER"
|
||||
}, {
|
||||
CURRENTBYTES, "INTEGER"
|
||||
},
|
||||
{
|
||||
LASTMOD, "INTEGER"
|
||||
}, {
|
||||
STATUS, "INTEGER"
|
||||
},
|
||||
{
|
||||
CONTROL, "INTEGER"
|
||||
}, {
|
||||
NUM_FAILED, "INTEGER"
|
||||
},
|
||||
{
|
||||
RETRY_AFTER, "INTEGER"
|
||||
}, {
|
||||
REDIRECT_COUNT, "INTEGER"
|
||||
}
|
||||
};
|
||||
public static final String TABLE_NAME = "DownloadColumns";
|
||||
public static final String _ID = "DownloadColumns._id";
|
||||
}
|
||||
|
||||
private static final String[] DC_PROJECTION = {
|
||||
DownloadColumns.FILENAME,
|
||||
DownloadColumns.URI, DownloadColumns.ETAG,
|
||||
DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
|
||||
DownloadColumns.LASTMOD, DownloadColumns.STATUS,
|
||||
DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
|
||||
DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
|
||||
DownloadColumns.INDEX
|
||||
};
|
||||
|
||||
private static final int FILENAME_IDX = 0;
|
||||
private static final int URI_IDX = 1;
|
||||
private static final int ETAG_IDX = 2;
|
||||
private static final int TOTALBYTES_IDX = 3;
|
||||
private static final int CURRENTBYTES_IDX = 4;
|
||||
private static final int LASTMOD_IDX = 5;
|
||||
private static final int STATUS_IDX = 6;
|
||||
private static final int CONTROL_IDX = 7;
|
||||
private static final int NUM_FAILED_IDX = 8;
|
||||
private static final int RETRY_AFTER_IDX = 9;
|
||||
private static final int REDIRECT_COUNT_IDX = 10;
|
||||
private static final int INDEX_IDX = 11;
|
||||
|
||||
/**
|
||||
* This function will add a new file to the database if it does not exist.
|
||||
*
|
||||
* @param di DownloadInfo that we wish to store
|
||||
* @return the row id of the record to be updated/inserted, or -1
|
||||
*/
|
||||
public boolean updateDownload(DownloadInfo di) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(DownloadColumns.INDEX, di.mIndex);
|
||||
cv.put(DownloadColumns.FILENAME, di.mFileName);
|
||||
cv.put(DownloadColumns.URI, di.mUri);
|
||||
cv.put(DownloadColumns.ETAG, di.mETag);
|
||||
cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
|
||||
cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
|
||||
cv.put(DownloadColumns.LASTMOD, di.mLastMod);
|
||||
cv.put(DownloadColumns.STATUS, di.mStatus);
|
||||
cv.put(DownloadColumns.CONTROL, di.mControl);
|
||||
cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
|
||||
cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
|
||||
cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
|
||||
return updateDownload(di, cv);
|
||||
}
|
||||
|
||||
public boolean updateDownload(DownloadInfo di, ContentValues cv) {
|
||||
long id = di == null ? -1 : getIDForDownloadInfo(di);
|
||||
try {
|
||||
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
||||
if (id != -1) {
|
||||
if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
|
||||
cv, DownloadColumns._ID + " = " + id, null)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
|
||||
DownloadColumns.URI, cv);
|
||||
}
|
||||
} catch (android.database.sqlite.SQLiteException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getLastCheckedVersionCode() {
|
||||
return mVersionCode;
|
||||
}
|
||||
|
||||
public boolean isDownloadRequired() {
|
||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||
Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM "
|
||||
+ DownloadColumns.TABLE_NAME + " WHERE "
|
||||
+ DownloadColumns.STATUS + " <> 0", null);
|
||||
try {
|
||||
if (null != cur && cur.moveToFirst()) {
|
||||
return 0 == cur.getInt(0);
|
||||
}
|
||||
} finally {
|
||||
if (null != cur)
|
||||
cur.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getFlags() {
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
public boolean updateFlags(int flags) {
|
||||
if (mFlags != flags) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(MetadataColumns.FLAGS, flags);
|
||||
if (updateMetadata(cv)) {
|
||||
mFlags = flags;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public boolean updateStatus(int status) {
|
||||
if (mStatus != status) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
|
||||
if (updateMetadata(cv)) {
|
||||
mStatus = status;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public boolean updateMetadata(ContentValues cv) {
|
||||
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
||||
if (-1 == this.mMetadataRowID) {
|
||||
long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
|
||||
MetadataColumns.APKVERSION, cv);
|
||||
if (-1 == newID)
|
||||
return false;
|
||||
mMetadataRowID = newID;
|
||||
} else {
|
||||
if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
|
||||
BaseColumns._ID + " = " + mMetadataRowID, null))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean updateMetadata(int apkVersion, int downloadStatus) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(MetadataColumns.APKVERSION, apkVersion);
|
||||
cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
|
||||
if (updateMetadata(cv)) {
|
||||
mVersionCode = apkVersion;
|
||||
mStatus = downloadStatus;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
public boolean updateFromDb(DownloadInfo di) {
|
||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||
Cursor cur = null;
|
||||
try {
|
||||
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
||||
DownloadColumns.FILENAME + "= ?",
|
||||
new String[] {
|
||||
di.mFileName
|
||||
}, null, null, null);
|
||||
if (null != cur && cur.moveToFirst()) {
|
||||
setDownloadInfoFromCursor(di, cur);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (null != cur) {
|
||||
cur.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
|
||||
di.mUri = cur.getString(URI_IDX);
|
||||
di.mETag = cur.getString(ETAG_IDX);
|
||||
di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
|
||||
di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
|
||||
di.mLastMod = cur.getLong(LASTMOD_IDX);
|
||||
di.mStatus = cur.getInt(STATUS_IDX);
|
||||
di.mControl = cur.getInt(CONTROL_IDX);
|
||||
di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
|
||||
di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
|
||||
di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
|
||||
}
|
||||
|
||||
public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
|
||||
DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
|
||||
cur.getString(FILENAME_IDX), this.getClass().getPackage()
|
||||
.getName());
|
||||
setDownloadInfoFromCursor(di, cur);
|
||||
return di;
|
||||
}
|
||||
|
||||
public DownloadInfo[] getDownloads() {
|
||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||
Cursor cur = null;
|
||||
try {
|
||||
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
|
||||
null, null, null, null);
|
||||
if (null != cur && cur.moveToFirst()) {
|
||||
DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
|
||||
int idx = 0;
|
||||
do {
|
||||
DownloadInfo di = getDownloadInfoFromCursor(cur);
|
||||
retInfos[idx++] = di;
|
||||
} while (cur.moveToNext());
|
||||
return retInfos;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (null != cur) {
|
||||
cur.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.expansion.downloader.impl;
|
||||
|
||||
import android.text.format.Time;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Helper for parsing an HTTP date.
|
||||
*/
|
||||
public final class HttpDateTime {
|
||||
|
||||
/*
|
||||
* Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
|
||||
* RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850,
|
||||
* obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format
|
||||
* with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon
|
||||
* YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS
|
||||
* GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon
|
||||
* (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first
|
||||
* digit is zero. Mon can be the full name of the month.
|
||||
*/
|
||||
private static final String HTTP_DATE_RFC_REGEXP =
|
||||
"([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
|
||||
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
|
||||
|
||||
private static final String HTTP_DATE_ANSIC_REGEXP =
|
||||
"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
|
||||
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
|
||||
|
||||
/**
|
||||
* The compiled version of the HTTP-date regular expressions.
|
||||
*/
|
||||
private static final Pattern HTTP_DATE_RFC_PATTERN =
|
||||
Pattern.compile(HTTP_DATE_RFC_REGEXP);
|
||||
private static final Pattern HTTP_DATE_ANSIC_PATTERN =
|
||||
Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
|
||||
|
||||
private static class TimeOfDay {
|
||||
TimeOfDay(int h, int m, int s) {
|
||||
this.hour = h;
|
||||
this.minute = m;
|
||||
this.second = s;
|
||||
}
|
||||
|
||||
int hour;
|
||||
int minute;
|
||||
int second;
|
||||
}
|
||||
|
||||
public static long parse(String timeString)
|
||||
throws IllegalArgumentException {
|
||||
|
||||
int date = 1;
|
||||
int month = Calendar.JANUARY;
|
||||
int year = 1970;
|
||||
TimeOfDay timeOfDay;
|
||||
|
||||
Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
|
||||
if (rfcMatcher.find()) {
|
||||
date = getDate(rfcMatcher.group(1));
|
||||
month = getMonth(rfcMatcher.group(2));
|
||||
year = getYear(rfcMatcher.group(3));
|
||||
timeOfDay = getTime(rfcMatcher.group(4));
|
||||
} else {
|
||||
Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
|
||||
if (ansicMatcher.find()) {
|
||||
month = getMonth(ansicMatcher.group(1));
|
||||
date = getDate(ansicMatcher.group(2));
|
||||
timeOfDay = getTime(ansicMatcher.group(3));
|
||||
year = getYear(ansicMatcher.group(4));
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Y2038 BUG!
|
||||
if (year >= 2038) {
|
||||
year = 2038;
|
||||
month = Calendar.JANUARY;
|
||||
date = 1;
|
||||
}
|
||||
|
||||
Time time = new Time(Time.TIMEZONE_UTC);
|
||||
time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
|
||||
month, year);
|
||||
return time.toMillis(false /* use isDst */);
|
||||
}
|
||||
|
||||
private static int getDate(String dateString) {
|
||||
if (dateString.length() == 2) {
|
||||
return (dateString.charAt(0) - '0') * 10
|
||||
+ (dateString.charAt(1) - '0');
|
||||
} else {
|
||||
return (dateString.charAt(0) - '0');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0
|
||||
* + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20
|
||||
* + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19
|
||||
* = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
|
||||
*/
|
||||
private static int getMonth(String monthString) {
|
||||
int hash = Character.toLowerCase(monthString.charAt(0)) +
|
||||
Character.toLowerCase(monthString.charAt(1)) +
|
||||
Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
|
||||
switch (hash) {
|
||||
case 22:
|
||||
return Calendar.JANUARY;
|
||||
case 10:
|
||||
return Calendar.FEBRUARY;
|
||||
case 29:
|
||||
return Calendar.MARCH;
|
||||
case 32:
|
||||
return Calendar.APRIL;
|
||||
case 36:
|
||||
return Calendar.MAY;
|
||||
case 42:
|
||||
return Calendar.JUNE;
|
||||
case 40:
|
||||
return Calendar.JULY;
|
||||
case 26:
|
||||
return Calendar.AUGUST;
|
||||
case 37:
|
||||
return Calendar.SEPTEMBER;
|
||||
case 35:
|
||||
return Calendar.OCTOBER;
|
||||
case 48:
|
||||
return Calendar.NOVEMBER;
|
||||
case 9:
|
||||
return Calendar.DECEMBER;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private static int getYear(String yearString) {
|
||||
if (yearString.length() == 2) {
|
||||
int year = (yearString.charAt(0) - '0') * 10
|
||||
+ (yearString.charAt(1) - '0');
|
||||
if (year >= 70) {
|
||||
return year + 1900;
|
||||
} else {
|
||||
return year + 2000;
|
||||
}
|
||||
} else if (yearString.length() == 3) {
|
||||
// According to RFC 2822, three digit years should be added to 1900.
|
||||
int year = (yearString.charAt(0) - '0') * 100
|
||||
+ (yearString.charAt(1) - '0') * 10
|
||||
+ (yearString.charAt(2) - '0');
|
||||
return year + 1900;
|
||||
} else if (yearString.length() == 4) {
|
||||
return (yearString.charAt(0) - '0') * 1000
|
||||
+ (yearString.charAt(1) - '0') * 100
|
||||
+ (yearString.charAt(2) - '0') * 10
|
||||
+ (yearString.charAt(3) - '0');
|
||||
} else {
|
||||
return 1970;
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeOfDay getTime(String timeString) {
|
||||
// HH might be H
|
||||
int i = 0;
|
||||
int hour = timeString.charAt(i++) - '0';
|
||||
if (timeString.charAt(i) != ':')
|
||||
hour = hour * 10 + (timeString.charAt(i++) - '0');
|
||||
// Skip ':'
|
||||
i++;
|
||||
|
||||
int minute = (timeString.charAt(i++) - '0') * 10
|
||||
+ (timeString.charAt(i++) - '0');
|
||||
// Skip ':'
|
||||
i++;
|
||||
|
||||
int second = (timeString.charAt(i++) - '0') * 10
|
||||
+ (timeString.charAt(i++) - '0');
|
||||
|
||||
return new TimeOfDay(hour, minute, second);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import com.google.android.vending.licensing.util.Base64;
|
||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.spec.KeySpec;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* An Obfuscator that uses AES to encrypt data.
|
||||
*/
|
||||
public class AESObfuscator implements Obfuscator {
|
||||
private static final String UTF8 = "UTF-8";
|
||||
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
|
||||
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
private static final byte[] IV =
|
||||
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
|
||||
private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|";
|
||||
|
||||
private Cipher mEncryptor;
|
||||
private Cipher mDecryptor;
|
||||
|
||||
/**
|
||||
* @param salt an array of random bytes to use for each (un)obfuscation
|
||||
* @param applicationId application identifier, e.g. the package name
|
||||
* @param deviceId device identifier. Use as many sources as possible to
|
||||
* create this unique identifier.
|
||||
*/
|
||||
public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
|
||||
KeySpec keySpec =
|
||||
new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
|
||||
SecretKey tmp = factory.generateSecret(keySpec);
|
||||
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
|
||||
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
|
||||
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
|
||||
} catch (GeneralSecurityException e) {
|
||||
// This can't happen on a compatible Android device.
|
||||
throw new RuntimeException("Invalid environment", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String obfuscate(String original, String key) {
|
||||
if (original == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Header is appended as an integrity check
|
||||
return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("Invalid environment", e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("Invalid environment", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String unobfuscate(String obfuscated, String key) throws ValidationException {
|
||||
if (obfuscated == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
|
||||
// Check for presence of header. This serves as a final integrity check, for cases
|
||||
// where the block size is correct during decryption.
|
||||
int headerIndex = result.indexOf(header+key);
|
||||
if (headerIndex != 0) {
|
||||
throw new ValidationException("Header not found (invalid data or key)" + ":" +
|
||||
obfuscated);
|
||||
}
|
||||
return result.substring(header.length()+key.length(), result.length());
|
||||
} catch (Base64DecoderException e) {
|
||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("Invalid environment", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.vending.licensing.util.URIQueryDecoder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Default policy. All policy decisions are based off of response data received
|
||||
* from the licensing service. Specifically, the licensing server sends the
|
||||
* following information: response validity period, error retry period,
|
||||
* error retry count and a URL for restoring app access in unlicensed cases.
|
||||
* <p>
|
||||
* These values will vary based on the the way the application is configured in
|
||||
* the Google Play publishing console, such as whether the application is
|
||||
* marked as free or is within its refund period, as well as how often an
|
||||
* application is checking with the licensing service.
|
||||
* <p>
|
||||
* Developers who need more fine grained control over their application's
|
||||
* licensing policy should implement a custom Policy.
|
||||
*/
|
||||
public class APKExpansionPolicy implements Policy {
|
||||
|
||||
private static final String TAG = "APKExpansionPolicy";
|
||||
private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy";
|
||||
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
||||
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
||||
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
||||
private static final String PREF_MAX_RETRIES = "maxRetries";
|
||||
private static final String PREF_RETRY_COUNT = "retryCount";
|
||||
private static final String PREF_LICENSING_URL = "licensingUrl";
|
||||
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
||||
private static final String DEFAULT_RETRY_UNTIL = "0";
|
||||
private static final String DEFAULT_MAX_RETRIES = "0";
|
||||
private static final String DEFAULT_RETRY_COUNT = "0";
|
||||
|
||||
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
||||
|
||||
private long mValidityTimestamp;
|
||||
private long mRetryUntil;
|
||||
private long mMaxRetries;
|
||||
private long mRetryCount;
|
||||
private long mLastResponseTime = 0;
|
||||
private int mLastResponse;
|
||||
private String mLicensingUrl;
|
||||
private PreferenceObfuscator mPreferences;
|
||||
private Vector<String> mExpansionURLs = new Vector<String>();
|
||||
private Vector<String> mExpansionFileNames = new Vector<String>();
|
||||
private Vector<Long> mExpansionFileSizes = new Vector<Long>();
|
||||
|
||||
/**
|
||||
* The design of the protocol supports n files. Currently the market can
|
||||
* only deliver two files. To accommodate this, we have these two constants,
|
||||
* but the order is the only relevant thing here.
|
||||
*/
|
||||
public static final int MAIN_FILE_URL_INDEX = 0;
|
||||
public static final int PATCH_FILE_URL_INDEX = 1;
|
||||
|
||||
/**
|
||||
* @param context The context for the current application
|
||||
* @param obfuscator An obfuscator to be used with preferences.
|
||||
*/
|
||||
public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
|
||||
// Import old values
|
||||
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
||||
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
||||
mLastResponse = Integer.parseInt(
|
||||
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
||||
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
||||
DEFAULT_VALIDITY_TIMESTAMP));
|
||||
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
||||
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
||||
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
||||
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* We call this to guarantee that we fetch a fresh policy from the server.
|
||||
* This is to be used if the URL is invalid.
|
||||
*/
|
||||
public void resetPolicy() {
|
||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
|
||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||
setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
|
||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||
mPreferences.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a new response from the license server.
|
||||
* <p>
|
||||
* This data will be used for computing future policy decisions. The
|
||||
* following parameters are processed:
|
||||
* <ul>
|
||||
* <li>VT: the timestamp that the client should consider the response valid
|
||||
* until
|
||||
* <li>GT: the timestamp that the client should ignore retry errors until
|
||||
* <li>GR: the number of retry errors that the client should ignore
|
||||
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
|
||||
* buy app on the Play Store)
|
||||
* </ul>
|
||||
*
|
||||
* @param response the result from validating the server response
|
||||
* @param rawData the raw server response data
|
||||
*/
|
||||
public void processServerResponse(int response,
|
||||
com.google.android.vending.licensing.ResponseData rawData) {
|
||||
|
||||
// Update retry counter
|
||||
if (response != Policy.RETRY) {
|
||||
setRetryCount(0);
|
||||
} else {
|
||||
setRetryCount(mRetryCount + 1);
|
||||
}
|
||||
|
||||
// Update server policy data
|
||||
Map<String, String> extras = decodeExtras(rawData);
|
||||
if (response == Policy.LICENSED) {
|
||||
mLastResponse = response;
|
||||
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
|
||||
setLicensingUrl(null);
|
||||
setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
|
||||
Set<String> keys = extras.keySet();
|
||||
for (String key : keys) {
|
||||
if (key.equals("VT")) {
|
||||
setValidityTimestamp(extras.get(key));
|
||||
} else if (key.equals("GT")) {
|
||||
setRetryUntil(extras.get(key));
|
||||
} else if (key.equals("GR")) {
|
||||
setMaxRetries(extras.get(key));
|
||||
} else if (key.startsWith("FILE_URL")) {
|
||||
int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
|
||||
setExpansionURL(index, extras.get(key));
|
||||
} else if (key.startsWith("FILE_NAME")) {
|
||||
int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
|
||||
setExpansionFileName(index, extras.get(key));
|
||||
} else if (key.startsWith("FILE_SIZE")) {
|
||||
int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
|
||||
setExpansionFileSize(index, Long.parseLong(extras.get(key)));
|
||||
}
|
||||
}
|
||||
} else if (response == Policy.NOT_LICENSED) {
|
||||
// Clear out stale retry params
|
||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||
// Update the licensing URL
|
||||
setLicensingUrl(extras.get("LU"));
|
||||
}
|
||||
|
||||
setLastResponse(response);
|
||||
mPreferences.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last license response received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param l the response
|
||||
*/
|
||||
private void setLastResponse(int l) {
|
||||
mLastResponseTime = System.currentTimeMillis();
|
||||
mLastResponse = l;
|
||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current retry count and add to preferences. You must manually
|
||||
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||
*
|
||||
* @param c the new retry count
|
||||
*/
|
||||
private void setRetryCount(long c) {
|
||||
mRetryCount = c;
|
||||
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
||||
}
|
||||
|
||||
public long getRetryCount() {
|
||||
return mRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last validity timestamp (VT) received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param validityTimestamp the VT string received
|
||||
*/
|
||||
private void setValidityTimestamp(String validityTimestamp) {
|
||||
Long lValidityTimestamp;
|
||||
try {
|
||||
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parseable, expire in one minute.
|
||||
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
||||
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
||||
validityTimestamp = Long.toString(lValidityTimestamp);
|
||||
}
|
||||
|
||||
mValidityTimestamp = lValidityTimestamp;
|
||||
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
||||
}
|
||||
|
||||
public long getValidityTimestamp() {
|
||||
return mValidityTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the retry until timestamp (GT) received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param retryUntil the GT string received
|
||||
*/
|
||||
private void setRetryUntil(String retryUntil) {
|
||||
Long lRetryUntil;
|
||||
try {
|
||||
lRetryUntil = Long.parseLong(retryUntil);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parseable, expire immediately
|
||||
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
||||
retryUntil = "0";
|
||||
lRetryUntil = 0l;
|
||||
}
|
||||
|
||||
mRetryUntil = lRetryUntil;
|
||||
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
||||
}
|
||||
|
||||
public long getRetryUntil() {
|
||||
return mRetryUntil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max retries value (GR) as received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param maxRetries the GR string received
|
||||
*/
|
||||
private void setMaxRetries(String maxRetries) {
|
||||
Long lMaxRetries;
|
||||
try {
|
||||
lMaxRetries = Long.parseLong(maxRetries);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parseable, expire immediately
|
||||
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
||||
maxRetries = "0";
|
||||
lMaxRetries = 0l;
|
||||
}
|
||||
|
||||
mMaxRetries = lMaxRetries;
|
||||
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
||||
}
|
||||
|
||||
public long getMaxRetries() {
|
||||
return mMaxRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the licensing URL that displays a Play Store UI for the user to regain app access.
|
||||
*
|
||||
* @param url the LU string received
|
||||
*/
|
||||
private void setLicensingUrl(String url) {
|
||||
mLicensingUrl = url;
|
||||
mPreferences.putString(PREF_LICENSING_URL, url);
|
||||
}
|
||||
|
||||
public String getLicensingUrl() {
|
||||
return mLicensingUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of expansion URLs. Since expansionURLs are not committed
|
||||
* to preferences, this will return zero if there has been no LVL fetch
|
||||
* in the current session.
|
||||
*
|
||||
* @return the number of expansion URLs. (0,1,2)
|
||||
*/
|
||||
public int getExpansionURLCount() {
|
||||
return mExpansionURLs.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expansion URL. Since these URLs are not committed to
|
||||
* preferences, this will always return null if there has not been an LVL
|
||||
* fetch in the current session.
|
||||
*
|
||||
* @param index the index of the URL to fetch. This value will be either
|
||||
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
||||
*/
|
||||
public String getExpansionURL(int index) {
|
||||
if (index < mExpansionURLs.size()) {
|
||||
return mExpansionURLs.elementAt(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expansion URL. Expansion URL's are not committed to preferences,
|
||||
* but are instead intended to be stored when the license response is
|
||||
* processed by the front-end.
|
||||
*
|
||||
* @param index the index of the expansion URL. This value will be either
|
||||
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
||||
* @param URL the URL to set
|
||||
*/
|
||||
public void setExpansionURL(int index, String URL) {
|
||||
if (index >= mExpansionURLs.size()) {
|
||||
mExpansionURLs.setSize(index + 1);
|
||||
}
|
||||
mExpansionURLs.set(index, URL);
|
||||
}
|
||||
|
||||
public String getExpansionFileName(int index) {
|
||||
if (index < mExpansionFileNames.size()) {
|
||||
return mExpansionFileNames.elementAt(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setExpansionFileName(int index, String name) {
|
||||
if (index >= mExpansionFileNames.size()) {
|
||||
mExpansionFileNames.setSize(index + 1);
|
||||
}
|
||||
mExpansionFileNames.set(index, name);
|
||||
}
|
||||
|
||||
public long getExpansionFileSize(int index) {
|
||||
if (index < mExpansionFileSizes.size()) {
|
||||
return mExpansionFileSizes.elementAt(index);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void setExpansionFileSize(int index, long size) {
|
||||
if (index >= mExpansionFileSizes.size()) {
|
||||
mExpansionFileSizes.setSize(index + 1);
|
||||
}
|
||||
mExpansionFileSizes.set(index, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc} This implementation allows access if either:<br>
|
||||
* <ol>
|
||||
* <li>a LICENSED response was received within the validity period
|
||||
* <li>a RETRY response was received in the last minute, and we are under
|
||||
* the RETRY count or in the RETRY period.
|
||||
* </ol>
|
||||
*/
|
||||
public boolean allowAccess() {
|
||||
long ts = System.currentTimeMillis();
|
||||
if (mLastResponse == Policy.LICENSED) {
|
||||
// Check if the LICENSED response occurred within the validity
|
||||
// timeout.
|
||||
if (ts <= mValidityTimestamp) {
|
||||
// Cached LICENSED response is still valid.
|
||||
return true;
|
||||
}
|
||||
} else if (mLastResponse == Policy.RETRY &&
|
||||
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
||||
// Only allow access if we are within the retry period or we haven't
|
||||
// used up our
|
||||
// max retries.
|
||||
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, String> decodeExtras(
|
||||
com.google.android.vending.licensing.ResponseData rawData) {
|
||||
Map<String, String> results = new HashMap<String, String>();
|
||||
if (rawData == null) {
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
URI rawExtras = new URI("?" + rawData.extra);
|
||||
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||
} catch (URISyntaxException e) {
|
||||
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Allows the developer to limit the number of devices using a single license.
|
||||
* <p>
|
||||
* The LICENSED response from the server contains a user identifier unique to
|
||||
* the <application, user> pair. The developer can send this identifier
|
||||
* to their own server along with some device identifier (a random number
|
||||
* generated and stored once per application installation,
|
||||
* {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
|
||||
* {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
|
||||
* The more sources used to identify the device, the harder it will be for an
|
||||
* attacker to spoof.
|
||||
* <p>
|
||||
* The server can look at the <application, user, device id> tuple and
|
||||
* restrict a user's application license to run on at most 10 different devices
|
||||
* in a week (for example). We recommend not being too restrictive because a
|
||||
* user might legitimately have multiple devices or be in the process of
|
||||
* changing phones. This will catch egregious violations of multiple people
|
||||
* sharing one license.
|
||||
*/
|
||||
public interface DeviceLimiter {
|
||||
|
||||
/**
|
||||
* Checks if this device is allowed to use the given user's license.
|
||||
*
|
||||
* @param userId the user whose license the server responded with
|
||||
* @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
|
||||
*/
|
||||
int isDeviceAllowed(String userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.Settings.Secure;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.vending.licensing.ILicenseResultListener;
|
||||
import com.android.vending.licensing.ILicensingService;
|
||||
import com.google.android.vending.licensing.util.Base64;
|
||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Client library for Google Play license verifications.
|
||||
* <p>
|
||||
* The LicenseChecker is configured via a {@link Policy} which contains the logic to determine
|
||||
* whether a user should have access to the application. For example, the Policy can define a
|
||||
* threshold for allowable number of server or client failures before the library reports the user
|
||||
* as not having access.
|
||||
* <p>
|
||||
* Must also provide the Base64-encoded RSA public key associated with your developer account. The
|
||||
* public key is obtainable from the publisher site.
|
||||
*/
|
||||
public class LicenseChecker implements ServiceConnection {
|
||||
private static final String TAG = "LicenseChecker";
|
||||
|
||||
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
||||
|
||||
// Timeout value (in milliseconds) for calls to service.
|
||||
private static final int TIMEOUT_MS = 10 * 1000;
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final boolean DEBUG_LICENSE_ERROR = false;
|
||||
|
||||
private ILicensingService mService;
|
||||
|
||||
private PublicKey mPublicKey;
|
||||
private final Context mContext;
|
||||
private final Policy mPolicy;
|
||||
/**
|
||||
* A handler for running tasks on a background thread. We don't want license processing to block
|
||||
* the UI thread.
|
||||
*/
|
||||
private Handler mHandler;
|
||||
private final String mPackageName;
|
||||
private final String mVersionCode;
|
||||
private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
|
||||
private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
|
||||
|
||||
/**
|
||||
* @param context a Context
|
||||
* @param policy implementation of Policy
|
||||
* @param encodedPublicKey Base64-encoded RSA public key
|
||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
||||
*/
|
||||
public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
|
||||
mContext = context;
|
||||
mPolicy = policy;
|
||||
mPublicKey = generatePublicKey(encodedPublicKey);
|
||||
mPackageName = mContext.getPackageName();
|
||||
mVersionCode = getVersionCode(context, mPackageName);
|
||||
HandlerThread handlerThread = new HandlerThread("background thread");
|
||||
handlerThread.start();
|
||||
mHandler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PublicKey instance from a string containing the Base64-encoded public key.
|
||||
*
|
||||
* @param encodedPublicKey Base64-encoded public key
|
||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
||||
*/
|
||||
private static PublicKey generatePublicKey(String encodedPublicKey) {
|
||||
try {
|
||||
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
||||
|
||||
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// This won't happen in an Android-compatible environment.
|
||||
throw new RuntimeException(e);
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Could not decode from Base64.");
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.e(TAG, "Invalid key specification.");
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user should have access to the app. Binds the service if necessary.
|
||||
* <p>
|
||||
* NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we
|
||||
* recommend obfuscating the string that is passed into bindService using another method of your
|
||||
* own devising.
|
||||
* <p>
|
||||
* source string: "com.android.vending.licensing.ILicensingService"
|
||||
* <p>
|
||||
*
|
||||
* @param callback
|
||||
*/
|
||||
public synchronized void checkAccess(LicenseCheckerCallback callback) {
|
||||
// If we have a valid recent LICENSED response, we can skip asking
|
||||
// Market.
|
||||
if (mPolicy.allowAccess()) {
|
||||
Log.i(TAG, "Using cached license response");
|
||||
callback.allow(Policy.LICENSED);
|
||||
} else {
|
||||
LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
|
||||
callback, generateNonce(), mPackageName, mVersionCode);
|
||||
|
||||
if (mService == null) {
|
||||
Log.i(TAG, "Binding to licensing service.");
|
||||
try {
|
||||
boolean bindResult = mContext
|
||||
.bindService(
|
||||
new Intent(
|
||||
new String(
|
||||
// Base64 encoded -
|
||||
// com.android.vending.licensing.ILicensingService
|
||||
// Consider encoding this in another way in your
|
||||
// code to improve security
|
||||
Base64.decode(
|
||||
"Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
|
||||
// As of Android 5.0, implicit
|
||||
// Service Intents are no longer
|
||||
// allowed because it's not
|
||||
// possible for the user to
|
||||
// participate in disambiguating
|
||||
// them. This does mean we break
|
||||
// compatibility with Android
|
||||
// Cupcake devices with this
|
||||
// release, since setPackage was
|
||||
// added in Donut.
|
||||
.setPackage(
|
||||
new String(
|
||||
// Base64
|
||||
// encoded -
|
||||
// com.android.vending
|
||||
Base64.decode(
|
||||
"Y29tLmFuZHJvaWQudmVuZGluZw=="))),
|
||||
this, // ServiceConnection.
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (bindResult) {
|
||||
mPendingChecks.offer(validator);
|
||||
} else {
|
||||
Log.e(TAG, "Could not bind to service.");
|
||||
handleServiceConnectionError(validator);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
|
||||
} catch (Base64DecoderException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
mPendingChecks.offer(validator);
|
||||
runChecks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the last deep link licensing URL returned from the server, which redirects users to a
|
||||
* page which enables them to gain access to the app. If no such URL is returned by the server, it
|
||||
* will go to the details page of the app in the Play Store.
|
||||
*/
|
||||
public void followLastLicensingUrl(Context context) {
|
||||
String licensingUrl = mPolicy.getLicensingUrl();
|
||||
if (licensingUrl == null) {
|
||||
licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
|
||||
}
|
||||
Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
|
||||
context.startActivity(marketIntent);
|
||||
}
|
||||
|
||||
private void runChecks() {
|
||||
LicenseValidator validator;
|
||||
while ((validator = mPendingChecks.poll()) != null) {
|
||||
try {
|
||||
Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
|
||||
mService.checkLicense(
|
||||
validator.getNonce(), validator.getPackageName(),
|
||||
new ResultListener(validator));
|
||||
mChecksInProgress.add(validator);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "RemoteException in checkLicense call.", e);
|
||||
handleServiceConnectionError(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void finishCheck(LicenseValidator validator) {
|
||||
mChecksInProgress.remove(validator);
|
||||
if (mChecksInProgress.isEmpty()) {
|
||||
cleanupService();
|
||||
}
|
||||
}
|
||||
|
||||
private class ResultListener extends ILicenseResultListener.Stub {
|
||||
private final LicenseValidator mValidator;
|
||||
private Runnable mOnTimeout;
|
||||
|
||||
public ResultListener(LicenseValidator validator) {
|
||||
mValidator = validator;
|
||||
mOnTimeout = new Runnable() {
|
||||
public void run() {
|
||||
Log.i(TAG, "Check timed out.");
|
||||
handleServiceConnectionError(mValidator);
|
||||
finishCheck(mValidator);
|
||||
}
|
||||
};
|
||||
startTimeout();
|
||||
}
|
||||
|
||||
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
||||
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
||||
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
||||
|
||||
// Runs in IPC thread pool. Post it to the Handler, so we can guarantee
|
||||
// either this or the timeout runs.
|
||||
public void verifyLicense(final int responseCode, final String signedData,
|
||||
final String signature) {
|
||||
mHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
Log.i(TAG, "Received response.");
|
||||
// Make sure it hasn't already timed out.
|
||||
if (mChecksInProgress.contains(mValidator)) {
|
||||
clearTimeout();
|
||||
mValidator.verify(mPublicKey, responseCode, signedData, signature);
|
||||
finishCheck(mValidator);
|
||||
}
|
||||
if (DEBUG_LICENSE_ERROR) {
|
||||
boolean logResponse;
|
||||
String stringError = null;
|
||||
switch (responseCode) {
|
||||
case ERROR_CONTACTING_SERVER:
|
||||
logResponse = true;
|
||||
stringError = "ERROR_CONTACTING_SERVER";
|
||||
break;
|
||||
case ERROR_INVALID_PACKAGE_NAME:
|
||||
logResponse = true;
|
||||
stringError = "ERROR_INVALID_PACKAGE_NAME";
|
||||
break;
|
||||
case ERROR_NON_MATCHING_UID:
|
||||
logResponse = true;
|
||||
stringError = "ERROR_NON_MATCHING_UID";
|
||||
break;
|
||||
default:
|
||||
logResponse = false;
|
||||
}
|
||||
|
||||
if (logResponse) {
|
||||
String android_id = Secure.getString(mContext.getContentResolver(),
|
||||
Secure.ANDROID_ID);
|
||||
Date date = new Date();
|
||||
Log.d(TAG, "Server Failure: " + stringError);
|
||||
Log.d(TAG, "Android ID: " + android_id);
|
||||
Log.d(TAG, "Time: " + date.toGMTString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startTimeout() {
|
||||
Log.i(TAG, "Start monitoring timeout.");
|
||||
mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private void clearTimeout() {
|
||||
Log.i(TAG, "Clearing timeout.");
|
||||
mHandler.removeCallbacks(mOnTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mService = ILicensingService.Stub.asInterface(service);
|
||||
runChecks();
|
||||
}
|
||||
|
||||
public synchronized void onServiceDisconnected(ComponentName name) {
|
||||
// Called when the connection with the service has been
|
||||
// unexpectedly disconnected. That is, Market crashed.
|
||||
// If there are any checks in progress, the timeouts will handle them.
|
||||
Log.w(TAG, "Service unexpectedly disconnected.");
|
||||
mService = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates policy response for service connection errors, as a result of disconnections or
|
||||
* timeouts.
|
||||
*/
|
||||
private synchronized void handleServiceConnectionError(LicenseValidator validator) {
|
||||
mPolicy.processServerResponse(Policy.RETRY, null);
|
||||
|
||||
if (mPolicy.allowAccess()) {
|
||||
validator.getCallback().allow(Policy.RETRY);
|
||||
} else {
|
||||
validator.getCallback().dontAllow(Policy.RETRY);
|
||||
}
|
||||
}
|
||||
|
||||
/** Unbinds service if necessary and removes reference to it. */
|
||||
private void cleanupService() {
|
||||
if (mService != null) {
|
||||
try {
|
||||
mContext.unbindService(this);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Somehow we've already been unbound. This is a non-fatal
|
||||
// error.
|
||||
Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
|
||||
}
|
||||
mService = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform the library that the context is about to be destroyed, so that any open connections
|
||||
* can be cleaned up.
|
||||
* <p>
|
||||
* Failure to call this method can result in a crash under certain circumstances, such as during
|
||||
* screen rotation if an Activity requests the license check or when the user exits the
|
||||
* application.
|
||||
*/
|
||||
public synchronized void onDestroy() {
|
||||
cleanupService();
|
||||
mHandler.getLooper().quit();
|
||||
}
|
||||
|
||||
/** Generates a nonce (number used once). */
|
||||
private int generateNonce() {
|
||||
return RANDOM.nextInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version code for the application package name.
|
||||
*
|
||||
* @param context
|
||||
* @param packageName application package name
|
||||
* @return the version code or empty string if package not found
|
||||
*/
|
||||
private static String getVersionCode(Context context, String packageName) {
|
||||
try {
|
||||
return String.valueOf(
|
||||
context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(TAG, "Package not found. could not get version code.");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Callback for the license checker library.
|
||||
* <p>
|
||||
* Upon checking with the Market server and conferring with the {@link Policy},
|
||||
* the library calls the appropriate callback method to communicate the result.
|
||||
* <p>
|
||||
* <b>The callback does not occur in the original checking thread.</b> Your
|
||||
* application should post to the appropriate handling thread or lock
|
||||
* accordingly.
|
||||
* <p>
|
||||
* The reason that is passed back with allow/dontAllow is the base status handed
|
||||
* to the policy for allowed/disallowing the license. Policy.RETRY will call
|
||||
* allow or dontAllow depending on other statistics associated with the policy,
|
||||
* while in most cases Policy.NOT_LICENSED will call dontAllow and
|
||||
* Policy.LICENSED will Allow.
|
||||
*/
|
||||
public interface LicenseCheckerCallback {
|
||||
|
||||
/**
|
||||
* Allow use. App should proceed as normal.
|
||||
*
|
||||
* @param reason Policy.LICENSED or Policy.RETRY typically. (although in
|
||||
* theory the policy can return Policy.NOT_LICENSED here as well)
|
||||
*/
|
||||
public void allow(int reason);
|
||||
|
||||
/**
|
||||
* Don't allow use. App should inform user and take appropriate action.
|
||||
*
|
||||
* @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
|
||||
* the policy can return Policy.LICENSED here as well ---
|
||||
* perhaps the call to the LVL took too long, for example)
|
||||
*/
|
||||
public void dontAllow(int reason);
|
||||
|
||||
/** Application error codes. */
|
||||
public static final int ERROR_INVALID_PACKAGE_NAME = 1;
|
||||
public static final int ERROR_NON_MATCHING_UID = 2;
|
||||
public static final int ERROR_NOT_MARKET_MANAGED = 3;
|
||||
public static final int ERROR_CHECK_IN_PROGRESS = 4;
|
||||
public static final int ERROR_INVALID_PUBLIC_KEY = 5;
|
||||
public static final int ERROR_MISSING_PERMISSION = 6;
|
||||
|
||||
/**
|
||||
* Error in application code. Caller did not call or set up license checker
|
||||
* correctly. Should be considered fatal.
|
||||
*/
|
||||
public void applicationError(int errorCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import com.google.android.vending.licensing.util.Base64;
|
||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
|
||||
/**
|
||||
* Contains data related to a licensing request and methods to verify
|
||||
* and process the response.
|
||||
*/
|
||||
class LicenseValidator {
|
||||
private static final String TAG = "LicenseValidator";
|
||||
|
||||
// Server response codes.
|
||||
private static final int LICENSED = 0x0;
|
||||
private static final int NOT_LICENSED = 0x1;
|
||||
private static final int LICENSED_OLD_KEY = 0x2;
|
||||
private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
|
||||
private static final int ERROR_SERVER_FAILURE = 0x4;
|
||||
private static final int ERROR_OVER_QUOTA = 0x5;
|
||||
|
||||
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
||||
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
||||
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
||||
|
||||
private final Policy mPolicy;
|
||||
private final LicenseCheckerCallback mCallback;
|
||||
private final int mNonce;
|
||||
private final String mPackageName;
|
||||
private final String mVersionCode;
|
||||
private final DeviceLimiter mDeviceLimiter;
|
||||
|
||||
LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
|
||||
int nonce, String packageName, String versionCode) {
|
||||
mPolicy = policy;
|
||||
mDeviceLimiter = deviceLimiter;
|
||||
mCallback = callback;
|
||||
mNonce = nonce;
|
||||
mPackageName = packageName;
|
||||
mVersionCode = versionCode;
|
||||
}
|
||||
|
||||
public LicenseCheckerCallback getCallback() {
|
||||
return mCallback;
|
||||
}
|
||||
|
||||
public int getNonce() {
|
||||
return mNonce;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
||||
|
||||
/**
|
||||
* Verifies the response from server and calls appropriate callback method.
|
||||
*
|
||||
* @param publicKey public key associated with the developer account
|
||||
* @param responseCode server response code
|
||||
* @param signedData signed data from server
|
||||
* @param signature server signature
|
||||
*/
|
||||
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
|
||||
String userId = null;
|
||||
// Skip signature check for unsuccessful requests
|
||||
ResponseData data = null;
|
||||
if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
|
||||
responseCode == LICENSED_OLD_KEY) {
|
||||
// Verify signature.
|
||||
try {
|
||||
if (TextUtils.isEmpty(signedData)) {
|
||||
Log.e(TAG, "Signature verification failed: signedData is empty. " +
|
||||
"(Device not signed-in to any Google accounts?)");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(signedData.getBytes());
|
||||
|
||||
if (!sig.verify(Base64.decode(signature))) {
|
||||
Log.e(TAG, "Signature verification failed.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// This can't happen on an Android compatible device.
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
|
||||
return;
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Base64DecoderException e) {
|
||||
Log.e(TAG, "Could not Base64-decode signature.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and validate response.
|
||||
try {
|
||||
data = ResponseData.parse(signedData);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "Could not parse response.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.responseCode != responseCode) {
|
||||
Log.e(TAG, "Response codes don't match.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.nonce != mNonce) {
|
||||
Log.e(TAG, "Nonce doesn't match.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.packageName.equals(mPackageName)) {
|
||||
Log.e(TAG, "Package name doesn't match.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.versionCode.equals(mVersionCode)) {
|
||||
Log.e(TAG, "Version codes don't match.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
// Application-specific user identifier.
|
||||
userId = data.userId;
|
||||
if (TextUtils.isEmpty(userId)) {
|
||||
Log.e(TAG, "User identifier is empty.");
|
||||
handleInvalidResponse();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (responseCode) {
|
||||
case LICENSED:
|
||||
case LICENSED_OLD_KEY:
|
||||
int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
|
||||
handleResponse(limiterResponse, data);
|
||||
break;
|
||||
case NOT_LICENSED:
|
||||
handleResponse(Policy.NOT_LICENSED, data);
|
||||
break;
|
||||
case ERROR_CONTACTING_SERVER:
|
||||
Log.w(TAG, "Error contacting licensing server.");
|
||||
handleResponse(Policy.RETRY, data);
|
||||
break;
|
||||
case ERROR_SERVER_FAILURE:
|
||||
Log.w(TAG, "An error has occurred on the licensing server.");
|
||||
handleResponse(Policy.RETRY, data);
|
||||
break;
|
||||
case ERROR_OVER_QUOTA:
|
||||
Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
|
||||
handleResponse(Policy.RETRY, data);
|
||||
break;
|
||||
case ERROR_INVALID_PACKAGE_NAME:
|
||||
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
|
||||
break;
|
||||
case ERROR_NON_MATCHING_UID:
|
||||
handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
|
||||
break;
|
||||
case ERROR_NOT_MARKET_MANAGED:
|
||||
handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unknown response code for license check.");
|
||||
handleInvalidResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confers with policy and calls appropriate callback method.
|
||||
*
|
||||
* @param response
|
||||
* @param rawData
|
||||
*/
|
||||
private void handleResponse(int response, ResponseData rawData) {
|
||||
// Update policy data and increment retry counter (if needed)
|
||||
mPolicy.processServerResponse(response, rawData);
|
||||
|
||||
// Given everything we know, including cached data, ask the policy if we should grant
|
||||
// access.
|
||||
if (mPolicy.allowAccess()) {
|
||||
mCallback.allow(response);
|
||||
} else {
|
||||
mCallback.dontAllow(response);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleApplicationError(int code) {
|
||||
mCallback.applicationError(code);
|
||||
}
|
||||
|
||||
private void handleInvalidResponse() {
|
||||
mCallback.dontAllow(Policy.NOT_LICENSED);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* A DeviceLimiter that doesn't limit the number of devices that can use a
|
||||
* given user's license.
|
||||
* <p>
|
||||
* Unless you have reason to believe that your application is being pirated
|
||||
* by multiple users using the same license (signing in to Market as the same
|
||||
* user), we recommend you use this implementation.
|
||||
*/
|
||||
public class NullDeviceLimiter implements DeviceLimiter {
|
||||
|
||||
public int isDeviceAllowed(String userId) {
|
||||
return Policy.LICENSED;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Interface used as part of a {@link Policy} to allow application authors to obfuscate
|
||||
* licensing data that will be stored into a SharedPreferences file.
|
||||
* <p>
|
||||
* Any transformation scheme must be reversible. Implementing classes may optionally implement an
|
||||
* integrity check to further prevent modification to preference data. Implementing classes
|
||||
* should use device-specific information as a key in the obfuscation algorithm to prevent
|
||||
* obfuscated preferences from being shared among devices.
|
||||
*/
|
||||
public interface Obfuscator {
|
||||
|
||||
/**
|
||||
* Obfuscate a string that is being stored into shared preferences.
|
||||
*
|
||||
* @param original The data that is to be obfuscated.
|
||||
* @param key The key for the data that is to be obfuscated.
|
||||
* @return A transformed version of the original data.
|
||||
*/
|
||||
String obfuscate(String original, String key);
|
||||
|
||||
/**
|
||||
* Undo the transformation applied to data by the obfuscate() method.
|
||||
*
|
||||
* @param obfuscated The data that is to be un-obfuscated.
|
||||
* @param key The key for the data that is to be un-obfuscated.
|
||||
* @return The original data transformed by the obfuscate() method.
|
||||
* @throws ValidationException Optionally thrown if a data integrity check fails.
|
||||
*/
|
||||
String unobfuscate(String obfuscated, String key) throws ValidationException;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Policy used by {@link LicenseChecker} to determine whether a user should have
|
||||
* access to the application.
|
||||
*/
|
||||
public interface Policy {
|
||||
|
||||
/**
|
||||
* Change these values to make it more difficult for tools to automatically
|
||||
* strip LVL protection from your APK.
|
||||
*/
|
||||
|
||||
/**
|
||||
* LICENSED means that the server returned back a valid license response
|
||||
*/
|
||||
public static final int LICENSED = 0x0100;
|
||||
/**
|
||||
* NOT_LICENSED means that the server returned back a valid license response
|
||||
* that indicated that the user definitively is not licensed
|
||||
*/
|
||||
public static final int NOT_LICENSED = 0x0231;
|
||||
/**
|
||||
* RETRY means that the license response was unable to be determined ---
|
||||
* perhaps as a result of faulty networking
|
||||
*/
|
||||
public static final int RETRY = 0x0123;
|
||||
|
||||
/**
|
||||
* Provide results from contact with the license server. Retry counts are
|
||||
* incremented if the current value of response is RETRY. Results will be
|
||||
* used for any future policy decisions.
|
||||
*
|
||||
* @param response the result from validating the server response
|
||||
* @param rawData the raw server response data, can be null for RETRY
|
||||
*/
|
||||
void processServerResponse(int response, ResponseData rawData);
|
||||
|
||||
/**
|
||||
* Check if the user should be allowed access to the application.
|
||||
*/
|
||||
boolean allowAccess();
|
||||
|
||||
/**
|
||||
* Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g.
|
||||
* buy app on the Play Store).
|
||||
*/
|
||||
String getLicensingUrl();
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* An wrapper for SharedPreferences that transparently performs data obfuscation.
|
||||
*/
|
||||
public class PreferenceObfuscator {
|
||||
|
||||
private static final String TAG = "PreferenceObfuscator";
|
||||
|
||||
private final SharedPreferences mPreferences;
|
||||
private final Obfuscator mObfuscator;
|
||||
private SharedPreferences.Editor mEditor;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param sp A SharedPreferences instance provided by the system.
|
||||
* @param o The Obfuscator to use when reading or writing data.
|
||||
*/
|
||||
public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
|
||||
mPreferences = sp;
|
||||
mObfuscator = o;
|
||||
mEditor = null;
|
||||
}
|
||||
|
||||
public void putString(String key, String value) {
|
||||
if (mEditor == null) {
|
||||
mEditor = mPreferences.edit();
|
||||
// -- GODOT start --
|
||||
mEditor.apply();
|
||||
// -- GODOT end --
|
||||
}
|
||||
String obfuscatedValue = mObfuscator.obfuscate(value, key);
|
||||
mEditor.putString(key, obfuscatedValue);
|
||||
}
|
||||
|
||||
public String getString(String key, String defValue) {
|
||||
String result;
|
||||
String value = mPreferences.getString(key, null);
|
||||
if (value != null) {
|
||||
try {
|
||||
result = mObfuscator.unobfuscate(value, key);
|
||||
} catch (ValidationException e) {
|
||||
// Unable to unobfuscate, data corrupt or tampered
|
||||
Log.w(TAG, "Validation error while reading preference: " + key);
|
||||
result = defValue;
|
||||
}
|
||||
} else {
|
||||
// Preference not found
|
||||
result = defValue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void commit() {
|
||||
if (mEditor != null) {
|
||||
mEditor.commit();
|
||||
mEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* ResponseData from licensing server.
|
||||
*/
|
||||
public class ResponseData {
|
||||
|
||||
public int responseCode;
|
||||
public int nonce;
|
||||
public String packageName;
|
||||
public String versionCode;
|
||||
public String userId;
|
||||
public long timestamp;
|
||||
/** Response-specific data. */
|
||||
public String extra;
|
||||
|
||||
/**
|
||||
* Parses response string into ResponseData.
|
||||
*
|
||||
* @param responseData response data string
|
||||
* @throws IllegalArgumentException upon parsing error
|
||||
* @return ResponseData object
|
||||
*/
|
||||
public static ResponseData parse(String responseData) {
|
||||
// Must parse out main response data and response-specific data.
|
||||
int index = responseData.indexOf(':');
|
||||
String mainData, extraData;
|
||||
if (-1 == index) {
|
||||
mainData = responseData;
|
||||
extraData = "";
|
||||
} else {
|
||||
mainData = responseData.substring(0, index);
|
||||
extraData = index >= responseData.length() ? "" : responseData.substring(index + 1);
|
||||
}
|
||||
|
||||
String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
|
||||
if (fields.length < 6) {
|
||||
throw new IllegalArgumentException("Wrong number of fields.");
|
||||
}
|
||||
|
||||
ResponseData data = new ResponseData();
|
||||
data.extra = extraData;
|
||||
data.responseCode = Integer.parseInt(fields[0]);
|
||||
data.nonce = Integer.parseInt(fields[1]);
|
||||
data.packageName = fields[2];
|
||||
data.versionCode = fields[3];
|
||||
// Application-specific user identifier.
|
||||
data.userId = fields[4];
|
||||
data.timestamp = Long.parseLong(fields[5]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return TextUtils.join("|", new Object[] {
|
||||
responseCode, nonce, packageName, versionCode,
|
||||
userId, timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.vending.licensing.util.URIQueryDecoder;
|
||||
|
||||
/**
|
||||
* Default policy. All policy decisions are based off of response data received
|
||||
* from the licensing service. Specifically, the licensing server sends the
|
||||
* following information: response validity period, error retry period,
|
||||
* error retry count and a URL for restoring app access in unlicensed cases.
|
||||
* <p>
|
||||
* These values will vary based on the the way the application is configured in
|
||||
* the Google Play publishing console, such as whether the application is
|
||||
* marked as free or is within its refund period, as well as how often an
|
||||
* application is checking with the licensing service.
|
||||
* <p>
|
||||
* Developers who need more fine grained control over their application's
|
||||
* licensing policy should implement a custom Policy.
|
||||
*/
|
||||
public class ServerManagedPolicy implements Policy {
|
||||
|
||||
private static final String TAG = "ServerManagedPolicy";
|
||||
private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy";
|
||||
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
||||
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
||||
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
||||
private static final String PREF_MAX_RETRIES = "maxRetries";
|
||||
private static final String PREF_RETRY_COUNT = "retryCount";
|
||||
private static final String PREF_LICENSING_URL = "licensingUrl";
|
||||
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
||||
private static final String DEFAULT_RETRY_UNTIL = "0";
|
||||
private static final String DEFAULT_MAX_RETRIES = "0";
|
||||
private static final String DEFAULT_RETRY_COUNT = "0";
|
||||
|
||||
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
||||
|
||||
private long mValidityTimestamp;
|
||||
private long mRetryUntil;
|
||||
private long mMaxRetries;
|
||||
private long mRetryCount;
|
||||
private long mLastResponseTime = 0;
|
||||
private int mLastResponse;
|
||||
private String mLicensingUrl;
|
||||
private PreferenceObfuscator mPreferences;
|
||||
|
||||
/**
|
||||
* @param context The context for the current application
|
||||
* @param obfuscator An obfuscator to be used with preferences.
|
||||
*/
|
||||
public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
|
||||
// Import old values
|
||||
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
||||
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
||||
mLastResponse = Integer.parseInt(
|
||||
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
||||
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
||||
DEFAULT_VALIDITY_TIMESTAMP));
|
||||
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
||||
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
||||
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
||||
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a new response from the license server.
|
||||
* <p>
|
||||
* This data will be used for computing future policy decisions. The
|
||||
* following parameters are processed:
|
||||
* <ul>
|
||||
* <li>VT: the timestamp that the client should consider the response valid
|
||||
* until
|
||||
* <li>GT: the timestamp that the client should ignore retry errors until
|
||||
* <li>GR: the number of retry errors that the client should ignore
|
||||
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
|
||||
* buy app on the Play Store)
|
||||
* </ul>
|
||||
*
|
||||
* @param response the result from validating the server response
|
||||
* @param rawData the raw server response data
|
||||
*/
|
||||
public void processServerResponse(int response, ResponseData rawData) {
|
||||
|
||||
// Update retry counter
|
||||
if (response != Policy.RETRY) {
|
||||
setRetryCount(0);
|
||||
} else {
|
||||
setRetryCount(mRetryCount + 1);
|
||||
}
|
||||
|
||||
// Update server policy data
|
||||
Map<String, String> extras = decodeExtras(rawData);
|
||||
if (response == Policy.LICENSED) {
|
||||
mLastResponse = response;
|
||||
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
|
||||
setLicensingUrl(null);
|
||||
setValidityTimestamp(extras.get("VT"));
|
||||
setRetryUntil(extras.get("GT"));
|
||||
setMaxRetries(extras.get("GR"));
|
||||
} else if (response == Policy.NOT_LICENSED) {
|
||||
// Clear out stale retry params
|
||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||
// Update the licensing URL
|
||||
setLicensingUrl(extras.get("LU"));
|
||||
}
|
||||
|
||||
setLastResponse(response);
|
||||
mPreferences.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last license response received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param l the response
|
||||
*/
|
||||
private void setLastResponse(int l) {
|
||||
mLastResponseTime = System.currentTimeMillis();
|
||||
mLastResponse = l;
|
||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current retry count and add to preferences. You must manually
|
||||
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||
*
|
||||
* @param c the new retry count
|
||||
*/
|
||||
private void setRetryCount(long c) {
|
||||
mRetryCount = c;
|
||||
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
||||
}
|
||||
|
||||
public long getRetryCount() {
|
||||
return mRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last validity timestamp (VT) received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param validityTimestamp the VT string received
|
||||
*/
|
||||
private void setValidityTimestamp(String validityTimestamp) {
|
||||
Long lValidityTimestamp;
|
||||
try {
|
||||
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parsable, expire in one minute.
|
||||
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
||||
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
||||
validityTimestamp = Long.toString(lValidityTimestamp);
|
||||
}
|
||||
|
||||
mValidityTimestamp = lValidityTimestamp;
|
||||
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
||||
}
|
||||
|
||||
public long getValidityTimestamp() {
|
||||
return mValidityTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the retry until timestamp (GT) received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param retryUntil the GT string received
|
||||
*/
|
||||
private void setRetryUntil(String retryUntil) {
|
||||
Long lRetryUntil;
|
||||
try {
|
||||
lRetryUntil = Long.parseLong(retryUntil);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parsable, expire immediately
|
||||
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
||||
retryUntil = "0";
|
||||
lRetryUntil = 0l;
|
||||
}
|
||||
|
||||
mRetryUntil = lRetryUntil;
|
||||
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
||||
}
|
||||
|
||||
public long getRetryUntil() {
|
||||
return mRetryUntil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max retries value (GR) as received from the server and add to
|
||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||
* commit these changes to disk.
|
||||
*
|
||||
* @param maxRetries the GR string received
|
||||
*/
|
||||
private void setMaxRetries(String maxRetries) {
|
||||
Long lMaxRetries;
|
||||
try {
|
||||
lMaxRetries = Long.parseLong(maxRetries);
|
||||
} catch (NumberFormatException e) {
|
||||
// No response or not parsable, expire immediately
|
||||
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
||||
maxRetries = "0";
|
||||
lMaxRetries = 0l;
|
||||
}
|
||||
|
||||
mMaxRetries = lMaxRetries;
|
||||
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
||||
}
|
||||
|
||||
public long getMaxRetries() {
|
||||
return mMaxRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the license URL value (LU) as received from the server and add to preferences. You must
|
||||
* manually call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||
*
|
||||
* @param url the LU string received
|
||||
*/
|
||||
private void setLicensingUrl(String url) {
|
||||
mLicensingUrl = url;
|
||||
mPreferences.putString(PREF_LICENSING_URL, url);
|
||||
}
|
||||
|
||||
public String getLicensingUrl() {
|
||||
return mLicensingUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* This implementation allows access if either:<br>
|
||||
* <ol>
|
||||
* <li>a LICENSED response was received within the validity period
|
||||
* <li>a RETRY response was received in the last minute, and we are under
|
||||
* the RETRY count or in the RETRY period.
|
||||
* </ol>
|
||||
*/
|
||||
public boolean allowAccess() {
|
||||
long ts = System.currentTimeMillis();
|
||||
if (mLastResponse == Policy.LICENSED) {
|
||||
// Check if the LICENSED response occurred within the validity timeout.
|
||||
if (ts <= mValidityTimestamp) {
|
||||
// Cached LICENSED response is still valid.
|
||||
return true;
|
||||
}
|
||||
} else if (mLastResponse == Policy.RETRY &&
|
||||
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
||||
// Only allow access if we are within the retry period or we haven't used up our
|
||||
// max retries.
|
||||
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, String> decodeExtras(
|
||||
com.google.android.vending.licensing.ResponseData rawData) {
|
||||
Map<String, String> results = new HashMap<String, String>();
|
||||
if (rawData == null) {
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
URI rawExtras = new URI("?" + rawData.extra);
|
||||
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||
} catch (URISyntaxException e) {
|
||||
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.vending.licensing.util.URIQueryDecoder;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Non-caching policy. All requests will be sent to the licensing service,
|
||||
* and no local caching is performed.
|
||||
* <p>
|
||||
* Using a non-caching policy ensures that there is no local preference data
|
||||
* for malicious users to tamper with. As a side effect, applications
|
||||
* will not be permitted to run while offline. Developers should carefully
|
||||
* weigh the risks of using this Policy over one which implements caching,
|
||||
* such as ServerManagedPolicy.
|
||||
* <p>
|
||||
* Access to the application is only allowed if a LICENSED response is.
|
||||
* received. All other responses (including RETRY) will deny access.
|
||||
*/
|
||||
public class StrictPolicy implements Policy {
|
||||
|
||||
private static final String TAG = "StrictPolicy";
|
||||
|
||||
private int mLastResponse;
|
||||
private String mLicensingUrl;
|
||||
|
||||
public StrictPolicy() {
|
||||
// Set default policy. This will force the application to check the policy on launch.
|
||||
mLastResponse = Policy.RETRY;
|
||||
mLicensingUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a new response from the license server. Since we aren't
|
||||
* performing any caching, this equates to reading the LicenseResponse.
|
||||
* Any cache-related ResponseData is ignored, but the licensing URL
|
||||
* extra is still extracted in cases where the app is unlicensed.
|
||||
*
|
||||
* @param response the result from validating the server response
|
||||
* @param rawData the raw server response data
|
||||
*/
|
||||
public void processServerResponse(int response, ResponseData rawData) {
|
||||
mLastResponse = response;
|
||||
|
||||
if (response == Policy.NOT_LICENSED) {
|
||||
Map<String, String> extras = decodeExtras(rawData);
|
||||
mLicensingUrl = extras.get("LU");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* This implementation allows access if and only if a LICENSED response
|
||||
* was received the last time the server was contacted.
|
||||
*/
|
||||
public boolean allowAccess() {
|
||||
return (mLastResponse == Policy.LICENSED);
|
||||
}
|
||||
|
||||
public String getLicensingUrl() {
|
||||
return mLicensingUrl;
|
||||
}
|
||||
|
||||
private Map<String, String> decodeExtras(
|
||||
com.google.android.vending.licensing.ResponseData rawData) {
|
||||
Map<String, String> results = new HashMap<String, String>();
|
||||
if (rawData == null) {
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
URI rawExtras = new URI("?" + rawData.extra);
|
||||
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||
} catch (URISyntaxException e) {
|
||||
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while validating the integrity of data managed by an
|
||||
* {@link Obfuscator}.}
|
||||
*/
|
||||
public class ValidationException extends Exception {
|
||||
public ValidationException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ValidationException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
// Portions copyright 2002, Google, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.android.vending.licensing.util;
|
||||
|
||||
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
||||
// Lots of extraneous features were removed.
|
||||
/* The original code said:
|
||||
* <p>
|
||||
* I am placing this code in the Public Domain. Do with it as you will.
|
||||
* This software comes with no guarantees or warranties but with
|
||||
* plenty of well-wishing instead!
|
||||
* Please visit
|
||||
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
||||
* periodically to check for updates or to contribute improvements.
|
||||
* </p>
|
||||
*
|
||||
* @author Robert Harder
|
||||
* @author rharder@usa.net
|
||||
* @version 1.3
|
||||
*/
|
||||
|
||||
// -- GODOT start --
|
||||
import org.godotengine.godot.BuildConfig;
|
||||
// -- GODOT end --
|
||||
|
||||
/**
|
||||
* Base64 converter class. This code is not a full-blown MIME encoder;
|
||||
* it simply converts binary data to base64 data and back.
|
||||
*
|
||||
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
||||
* class.
|
||||
*/
|
||||
public class Base64 {
|
||||
/** Specify encoding (value is {@code true}). */
|
||||
public final static boolean ENCODE = true;
|
||||
|
||||
/** Specify decoding (value is {@code false}). */
|
||||
public final static boolean DECODE = false;
|
||||
|
||||
/** The equals sign (=) as a byte. */
|
||||
private final static byte EQUALS_SIGN = (byte) '=';
|
||||
|
||||
/** The new line character (\n) as a byte. */
|
||||
private final static byte NEW_LINE = (byte) '\n';
|
||||
|
||||
/**
|
||||
* The 64 valid Base64 values.
|
||||
*/
|
||||
private final static byte[] ALPHABET =
|
||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||
(byte) '9', (byte) '+', (byte) '/'};
|
||||
|
||||
/**
|
||||
* The 64 valid web safe Base64 values.
|
||||
*/
|
||||
private final static byte[] WEBSAFE_ALPHABET =
|
||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
||||
(byte) '9', (byte) '-', (byte) '_'};
|
||||
|
||||
/**
|
||||
* Translates a Base64 value to either its 6-bit reconstruction value
|
||||
* or a negative number indicating some other meaning.
|
||||
**/
|
||||
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||
-5, -5, // Whitespace: Tab and Linefeed
|
||||
-9, -9, // Decimal 11 - 12
|
||||
-5, // Whitespace: Carriage Return
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||
-5, // Whitespace: Space
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
||||
62, // Plus sign at decimal 43
|
||||
-9, -9, -9, // Decimal 44 - 46
|
||||
63, // Slash at decimal 47
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||
-9, -9, -9, // Decimal 58 - 60
|
||||
-1, // Equals sign at decimal 61
|
||||
-9, -9, -9, // Decimal 62 - 64
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||
};
|
||||
|
||||
/** The web safe decodabet */
|
||||
private final static byte[] WEBSAFE_DECODABET =
|
||||
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||
-5, -5, // Whitespace: Tab and Linefeed
|
||||
-9, -9, // Decimal 11 - 12
|
||||
-5, // Whitespace: Carriage Return
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||
-5, // Whitespace: Space
|
||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
||||
62, // Dash '-' sign at decimal 45
|
||||
-9, -9, // Decimal 46-47
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||
-9, -9, -9, // Decimal 58 - 60
|
||||
-1, // Equals sign at decimal 61
|
||||
-9, -9, -9, // Decimal 62 - 64
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||
-9, -9, -9, -9, // Decimal 91-94
|
||||
63, // Underscore '_' at decimal 95
|
||||
-9, // Decimal 96
|
||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||
};
|
||||
|
||||
// Indicates white space in encoding
|
||||
private final static byte WHITE_SPACE_ENC = -5;
|
||||
// Indicates equals sign in encoding
|
||||
private final static byte EQUALS_SIGN_ENC = -1;
|
||||
|
||||
/** Defeats instantiation. */
|
||||
private Base64() {
|
||||
}
|
||||
|
||||
/* ******** E N C O D I N G M E T H O D S ******** */
|
||||
|
||||
/**
|
||||
* Encodes up to three bytes of the array <var>source</var>
|
||||
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
||||
* The source and destination arrays can be manipulated
|
||||
* anywhere along their length by specifying
|
||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||
* This method does not check to make sure your arrays
|
||||
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
||||
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
||||
* the <var>destination</var> array.
|
||||
* The actual number of significant bytes in your array is
|
||||
* given by <var>numSigBytes</var>.
|
||||
*
|
||||
* @param source the array to convert
|
||||
* @param srcOffset the index where conversion begins
|
||||
* @param numSigBytes the number of significant bytes in your array
|
||||
* @param destination the array to hold the conversion
|
||||
* @param destOffset the index where output will be put
|
||||
* @param alphabet is the encoding alphabet
|
||||
* @return the <var>destination</var> array
|
||||
* @since 1.3
|
||||
*/
|
||||
private static byte[] encode3to4(byte[] source, int srcOffset,
|
||||
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
||||
// 1 2 3
|
||||
// 01234567890123456789012345678901 Bit position
|
||||
// --------000000001111111122222222 Array position from threeBytes
|
||||
// --------| || || || | Six bit groups to index alphabet
|
||||
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
||||
// 0x3f 0x3f 0x3f Additional AND
|
||||
|
||||
// Create buffer with zero-padding if there are only one or two
|
||||
// significant bytes passed in the array.
|
||||
// We have to shift left 24 in order to flush out the 1's that appear
|
||||
// when Java treats a value as negative that is cast from a byte to an int.
|
||||
int inBuff =
|
||||
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
||||
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
||||
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
||||
|
||||
switch (numSigBytes) {
|
||||
case 3:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
||||
return destination;
|
||||
case 2:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
destination[destOffset + 3] = EQUALS_SIGN;
|
||||
return destination;
|
||||
case 1:
|
||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
destination[destOffset + 2] = EQUALS_SIGN;
|
||||
destination[destOffset + 3] = EQUALS_SIGN;
|
||||
return destination;
|
||||
default:
|
||||
return destination;
|
||||
} // end switch
|
||||
} // end encode3to4
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
* Equivalent to calling
|
||||
* {@code encodeBytes(source, 0, source.length)}
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @since 1.4
|
||||
*/
|
||||
public static String encode(byte[] source) {
|
||||
return encode(source, 0, source.length, ALPHABET, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into web safe Base64 notation.
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @param doPadding is {@code true} to pad result with '=' chars
|
||||
* if it does not fall on 3 byte boundaries
|
||||
*/
|
||||
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
||||
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @param off Offset in array where conversion should begin
|
||||
* @param len Length of data to convert
|
||||
* @param alphabet is the encoding alphabet
|
||||
* @param doPadding is {@code true} to pad result with '=' chars
|
||||
* if it does not fall on 3 byte boundaries
|
||||
* @since 1.4
|
||||
*/
|
||||
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
||||
boolean doPadding) {
|
||||
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
||||
int outLen = outBuff.length;
|
||||
|
||||
// If doPadding is false, set length to truncate '='
|
||||
// padding characters
|
||||
while (doPadding == false && outLen > 0) {
|
||||
if (outBuff[outLen - 1] != '=') {
|
||||
break;
|
||||
}
|
||||
outLen -= 1;
|
||||
}
|
||||
|
||||
return new String(outBuff, 0, outLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte array into Base64 notation.
|
||||
*
|
||||
* @param source The data to convert
|
||||
* @param off Offset in array where conversion should begin
|
||||
* @param len Length of data to convert
|
||||
* @param alphabet is the encoding alphabet
|
||||
* @param maxLineLength maximum length of one line.
|
||||
* @return the BASE64-encoded byte array
|
||||
*/
|
||||
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
||||
int maxLineLength) {
|
||||
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
||||
int len43 = lenDiv3 * 4;
|
||||
byte[] outBuff = new byte[len43 // Main 4:3
|
||||
+ (len43 / maxLineLength)]; // New lines
|
||||
|
||||
int d = 0;
|
||||
int e = 0;
|
||||
int len2 = len - 2;
|
||||
int lineLength = 0;
|
||||
for (; d < len2; d += 3, e += 4) {
|
||||
|
||||
// The following block of code is the same as
|
||||
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
||||
// but inlined for faster encoding (~20% improvement)
|
||||
int inBuff =
|
||||
((source[d + off] << 24) >>> 8)
|
||||
| ((source[d + 1 + off] << 24) >>> 16)
|
||||
| ((source[d + 2 + off] << 24) >>> 24);
|
||||
outBuff[e] = alphabet[(inBuff >>> 18)];
|
||||
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
||||
|
||||
lineLength += 4;
|
||||
if (lineLength == maxLineLength) {
|
||||
outBuff[e + 4] = NEW_LINE;
|
||||
e++;
|
||||
lineLength = 0;
|
||||
} // end if: end of line
|
||||
} // end for: each piece of array
|
||||
|
||||
if (d < len) {
|
||||
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
||||
|
||||
lineLength += 4;
|
||||
if (lineLength == maxLineLength) {
|
||||
// Add a last newline
|
||||
outBuff[e + 4] = NEW_LINE;
|
||||
e++;
|
||||
}
|
||||
e += 4;
|
||||
}
|
||||
|
||||
// -- GODOT start --
|
||||
//assert (e == outBuff.length);
|
||||
if (BuildConfig.DEBUG && e != outBuff.length)
|
||||
throw new RuntimeException();
|
||||
// -- GODOT end --
|
||||
return outBuff;
|
||||
}
|
||||
|
||||
|
||||
/* ******** D E C O D I N G M E T H O D S ******** */
|
||||
|
||||
|
||||
/**
|
||||
* Decodes four bytes from array <var>source</var>
|
||||
* and writes the resulting bytes (up to three of them)
|
||||
* to <var>destination</var>.
|
||||
* The source and destination arrays can be manipulated
|
||||
* anywhere along their length by specifying
|
||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||
* This method does not check to make sure your arrays
|
||||
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
||||
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
||||
* the <var>destination</var> array.
|
||||
* This method returns the actual number of bytes that
|
||||
* were converted from the Base64 encoding.
|
||||
*
|
||||
*
|
||||
* @param source the array to convert
|
||||
* @param srcOffset the index where conversion begins
|
||||
* @param destination the array to hold the conversion
|
||||
* @param destOffset the index where output will be put
|
||||
* @param decodabet the decodabet for decoding Base64 content
|
||||
* @return the number of decoded bytes converted
|
||||
* @since 1.3
|
||||
*/
|
||||
private static int decode4to3(byte[] source, int srcOffset,
|
||||
byte[] destination, int destOffset, byte[] decodabet) {
|
||||
// Example: Dk==
|
||||
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||
return 1;
|
||||
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
||||
// Example: DkL=
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
||||
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
||||
return 2;
|
||||
} else {
|
||||
// Example: DkLE
|
||||
int outBuff =
|
||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
||||
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
||||
|
||||
destination[destOffset] = (byte) (outBuff >> 16);
|
||||
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
||||
destination[destOffset + 2] = (byte) (outBuff);
|
||||
return 3;
|
||||
}
|
||||
} // end decodeToBytes
|
||||
|
||||
|
||||
/**
|
||||
* Decodes data from Base64 notation.
|
||||
*
|
||||
* @param s the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] decode(String s) throws Base64DecoderException {
|
||||
byte[] bytes = s.getBytes();
|
||||
return decode(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes data from web safe Base64 notation.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param s the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
||||
byte[] bytes = s.getBytes();
|
||||
return decodeWebSafe(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source The Base64 encoded data
|
||||
* @return decoded data
|
||||
* @since 1.3
|
||||
* @throws Base64DecoderException
|
||||
*/
|
||||
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
||||
return decode(source, 0, source.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes web safe Base64 content in byte array format and returns
|
||||
* the decoded data.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param source the string to decode (decoded in default encoding)
|
||||
* @return the decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(byte[] source)
|
||||
throws Base64DecoderException {
|
||||
return decodeWebSafe(source, 0, source.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source The Base64 encoded data
|
||||
* @param off The offset of where to begin decoding
|
||||
* @param len The length of characters to decode
|
||||
* @return decoded data
|
||||
* @since 1.3
|
||||
* @throws Base64DecoderException
|
||||
*/
|
||||
public static byte[] decode(byte[] source, int off, int len)
|
||||
throws Base64DecoderException {
|
||||
return decode(source, off, len, DECODABET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes web safe Base64 content in byte array format and returns
|
||||
* the decoded byte array.
|
||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||
*
|
||||
* @param source The Base64 encoded data
|
||||
* @param off The offset of where to begin decoding
|
||||
* @param len The length of characters to decode
|
||||
* @return decoded data
|
||||
*/
|
||||
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
||||
throws Base64DecoderException {
|
||||
return decode(source, off, len, WEBSAFE_DECODABET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Base64 content using the supplied decodabet and returns
|
||||
* the decoded byte array.
|
||||
*
|
||||
* @param source The Base64 encoded data
|
||||
* @param off The offset of where to begin decoding
|
||||
* @param len The length of characters to decode
|
||||
* @param decodabet the decodabet for decoding Base64 content
|
||||
* @return decoded data
|
||||
*/
|
||||
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
||||
throws Base64DecoderException {
|
||||
int len34 = len * 3 / 4;
|
||||
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
||||
int outBuffPosn = 0;
|
||||
|
||||
byte[] b4 = new byte[4];
|
||||
int b4Posn = 0;
|
||||
int i = 0;
|
||||
byte sbiCrop = 0;
|
||||
byte sbiDecode = 0;
|
||||
for (i = 0; i < len; i++) {
|
||||
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
||||
sbiDecode = decodabet[sbiCrop];
|
||||
|
||||
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
||||
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
||||
// An equals sign (for padding) must not occur at position 0 or 1
|
||||
// and must be the last byte[s] in the encoded value
|
||||
if (sbiCrop == EQUALS_SIGN) {
|
||||
int bytesLeft = len - i;
|
||||
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
||||
if (b4Posn == 0 || b4Posn == 1) {
|
||||
throw new Base64DecoderException(
|
||||
"invalid padding byte '=' at byte offset " + i);
|
||||
} else if ((b4Posn == 3 && bytesLeft > 2)
|
||||
|| (b4Posn == 4 && bytesLeft > 1)) {
|
||||
throw new Base64DecoderException(
|
||||
"padding byte '=' falsely signals end of encoded value "
|
||||
+ "at offset " + i);
|
||||
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
||||
throw new Base64DecoderException(
|
||||
"encoded value has invalid trailing byte");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
b4[b4Posn++] = sbiCrop;
|
||||
if (b4Posn == 4) {
|
||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||
b4Posn = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Base64DecoderException("Bad Base64 input character at " + i
|
||||
+ ": " + source[i + off] + "(decimal)");
|
||||
}
|
||||
}
|
||||
|
||||
// Because web safe encoding allows non padding base64 encodes, we
|
||||
// need to pad the rest of the b4 buffer with equal signs when
|
||||
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
||||
// four characters, so the b4 buffer must have two or three
|
||||
// characters. This also catches the case where the input is
|
||||
// padded with EQUALS_SIGN
|
||||
if (b4Posn != 0) {
|
||||
if (b4Posn == 1) {
|
||||
throw new Base64DecoderException("single trailing character at offset "
|
||||
+ (len - 1));
|
||||
}
|
||||
b4[b4Posn++] = EQUALS_SIGN;
|
||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||
}
|
||||
|
||||
byte[] out = new byte[outBuffPosn];
|
||||
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2002, Google, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.android.vending.licensing.util;
|
||||
|
||||
/**
|
||||
* Exception thrown when encountering an invalid Base64 input character.
|
||||
*
|
||||
* @author nelson
|
||||
*/
|
||||
public class Base64DecoderException extends Exception {
|
||||
public Base64DecoderException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Base64DecoderException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class URIQueryDecoder {
|
||||
private static final String TAG = "URIQueryDecoder";
|
||||
|
||||
/**
|
||||
* Decodes the query portion of the passed-in URI.
|
||||
*
|
||||
* @param encodedURI the URI containing the query to decode
|
||||
* @param results a map containing all query parameters. Query parameters that do not have a
|
||||
* value will map to a null string
|
||||
*/
|
||||
static public void DecodeQuery(URI encodedURI, Map<String, String> results) {
|
||||
Scanner scanner = new Scanner(encodedURI.getRawQuery());
|
||||
scanner.useDelimiter("&");
|
||||
try {
|
||||
while (scanner.hasNext()) {
|
||||
String param = scanner.next();
|
||||
String[] valuePair = param.split("=");
|
||||
String name, value;
|
||||
if (valuePair.length == 1) {
|
||||
value = null;
|
||||
} else if (valuePair.length == 2) {
|
||||
value = URLDecoder.decode(valuePair[1], "UTF-8");
|
||||
} else {
|
||||
throw new IllegalArgumentException("query parameter invalid");
|
||||
}
|
||||
name = URLDecoder.decode(valuePair[0], "UTF-8");
|
||||
results.put(name, value);
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// This should never happen.
|
||||
Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/**************************************************************************/
|
||||
/* BuildProvider.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.variant.Callable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Provides an environment for executing build commands.
|
||||
*/
|
||||
public interface BuildProvider {
|
||||
/**
|
||||
* Connects to the build environment.
|
||||
*
|
||||
* @param callback The callback to call when connected
|
||||
* @return Whether or not connecting is possible
|
||||
*/
|
||||
boolean buildEnvConnect(@NonNull Callable callback);
|
||||
|
||||
/**
|
||||
* Disconnects from the build environment.
|
||||
*/
|
||||
void buildEnvDisconnect();
|
||||
|
||||
/**
|
||||
* Executes a command via the build environment.
|
||||
*
|
||||
* @param buildTool The build tool to execute (for example, "gradle")
|
||||
* @param arguments The argument for the command
|
||||
* @param projectPath The working directory to use when executing the command
|
||||
* @param buildDir The build directory within the project
|
||||
* @param outputCallback The callback to call for each line of output from the command
|
||||
* @param resultCallback The callback to call when the command is finished running
|
||||
* @return A positive job id, if successful; otherwise, a negative number
|
||||
*/
|
||||
int buildEnvExecute(String buildTool, @NonNull String[] arguments, @NonNull String projectPath, @NonNull String buildDir, @NonNull Callable outputCallback, @NonNull Callable resultCallback);
|
||||
|
||||
/**
|
||||
* Cancels a command executed via the build environment.
|
||||
*
|
||||
* @param jobId The job id returned from buildEnvExecute()
|
||||
*/
|
||||
void buildEnvCancel(int jobId);
|
||||
|
||||
/**
|
||||
* Requests that a project be cleaned up via the build environment.
|
||||
*
|
||||
* @param projectPath The working directory to use when executing the command
|
||||
* @param buildDir The build directory within the project
|
||||
*/
|
||||
void buildEnvCleanProject(@NonNull String projectPath, @NonNull String buildDir, @NonNull Callable callback);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/**************************************************************************/
|
||||
/* Dictionary.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class Dictionary extends HashMap<String, Object> {
|
||||
private String[] keys_cache;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link java.util.HashMap#keySet()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public String[] get_keys() {
|
||||
String[] ret = new String[size()];
|
||||
int i = 0;
|
||||
Set<String> keys = keySet();
|
||||
for (String key : keys) {
|
||||
ret[i] = key;
|
||||
i++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link java.util.HashMap#values()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Object[] get_values() {
|
||||
Object[] ret = new Object[size()];
|
||||
int i = 0;
|
||||
Set<String> keys = keySet();
|
||||
for (String key : keys) {
|
||||
ret[i] = get(key);
|
||||
i++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link java.util.HashMap#putAll(Map)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void set_keys(String[] keys) {
|
||||
keys_cache = keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link java.util.HashMap#putAll(Map)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void set_values(Object[] vals) {
|
||||
int i = 0;
|
||||
for (String key : keys_cache) {
|
||||
put(key, vals[i]);
|
||||
i++;
|
||||
}
|
||||
keys_cache = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**************************************************************************/
|
||||
/* FullScreenGodotApp.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
/**
|
||||
* Base abstract activity for Android apps intending to use Godot as the primary screen.
|
||||
*
|
||||
* @deprecated Use {@link GodotActivity}
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class FullScreenGodotApp extends GodotActivity {}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,359 @@
|
|||
/**************************************************************************/
|
||||
/* GodotActivity.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.utils.CommandLineFileParser
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Base abstract activity for Android apps intending to use Godot as the primary screen.
|
||||
*
|
||||
* Also a reference implementation for how to setup and use the [GodotFragment] fragment
|
||||
* within an Android app.
|
||||
*/
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPictureProvider {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotActivity::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
|
||||
|
||||
@JvmStatic
|
||||
protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
|
||||
|
||||
// This window must not match those in BaseGodotEditor.RUN_GAME_INFO etc
|
||||
@JvmStatic
|
||||
private final val DEFAULT_WINDOW_ID = 664;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if the activity should automatically enter picture-in-picture when put in the background.
|
||||
*/
|
||||
private val pipAspectRatio = AtomicReference<Rational>()
|
||||
private val autoEnterPiP = AtomicBoolean(false)
|
||||
private val gameViewSourceRectHint = Rect()
|
||||
private val commandLineParams = ArrayList<String>()
|
||||
/**
|
||||
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
|
||||
*/
|
||||
protected var godotFragment: GodotFragment? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Strip out the command line parameters from intent targeting exported activities.
|
||||
*/
|
||||
protected fun sanitizeLaunchIntent(launchIntent: Intent = intent): Intent {
|
||||
val targetComponent = launchIntent.component ?: componentName
|
||||
val activityInfo = packageManager.getActivityInfo(targetComponent, 0)
|
||||
if (activityInfo.exported) {
|
||||
launchIntent.removeExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
}
|
||||
|
||||
return launchIntent
|
||||
}
|
||||
|
||||
/**
|
||||
* Only retrieve the command line parameters from the intent from non-exported activities.
|
||||
* This ensures only internal components can configure how the engine is run.
|
||||
*/
|
||||
protected fun retrieveCommandLineParamsFromLaunchIntent(launchIntent: Intent = intent): Array<String> {
|
||||
val targetComponent = launchIntent.component ?: componentName
|
||||
val activityInfo = packageManager.getActivityInfo(targetComponent, 0)
|
||||
if (!activityInfo.exported) {
|
||||
val params = launchIntent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
return params ?: emptyArray()
|
||||
}
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = sanitizeLaunchIntent(intent)
|
||||
|
||||
val assetsCommandLine = try {
|
||||
CommandLineFileParser.parseCommandLine(assets.open("_cl_"))
|
||||
} catch (_: Exception) {
|
||||
mutableListOf()
|
||||
}
|
||||
Log.d(TAG, "Project command line parameters: $assetsCommandLine")
|
||||
commandLineParams.addAll(assetsCommandLine)
|
||||
|
||||
val intentCommandLine = retrieveCommandLineParamsFromLaunchIntent()
|
||||
Log.d(TAG, "Launch intent $intent with parameters ${intentCommandLine.contentToString()}")
|
||||
commandLineParams.addAll(intentCommandLine)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(getGodotAppLayout())
|
||||
|
||||
handleStartIntent(intent, true)
|
||||
|
||||
val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container)
|
||||
if (currentFragment is GodotFragment) {
|
||||
Log.v(TAG, "Reusing existing Godot fragment instance.")
|
||||
godotFragment = currentFragment
|
||||
} else {
|
||||
Log.v(TAG, "Creating new Godot fragment instance.")
|
||||
godotFragment = initGodotInstance()
|
||||
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
if (currentFragment != null) {
|
||||
Log.v(TAG, "Removing existing fragment before replacement.")
|
||||
transaction.remove(currentFragment)
|
||||
}
|
||||
|
||||
transaction.replace(R.id.godot_fragment_container, godotFragment!!)
|
||||
.setPrimaryNavigationFragment(godotFragment)
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val gameView = findViewById<View>(R.id.godot_fragment_container)
|
||||
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
|
||||
Log.d(TAG, "Restarting with parameters ${args.contentToString()}")
|
||||
val intent = Intent()
|
||||
.setComponent(ComponentName(this, javaClass.name))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
|
||||
triggerRebirth(null, intent)
|
||||
// fake 'process' id returned by create_instance() etc
|
||||
return DEFAULT_WINDOW_ID
|
||||
}
|
||||
|
||||
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
||||
// Launch a new activity
|
||||
Godot.getInstance(applicationContext).destroyAndKillProcess {
|
||||
ProcessPhoenix.triggerRebirth(this, bundle, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
protected open fun getGodotAppLayout() = R.layout.godot_app_layout
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.v(TAG, "Destroying GodotActivity $this...")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (isInPictureInPictureMode && !isFinishing) {
|
||||
// We get in this state when PiP is closed, so we terminate the activity.
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotForceQuit(instance: Godot) {
|
||||
runOnUiThread { terminateGodotInstance(instance) }
|
||||
}
|
||||
|
||||
private fun terminateGodotInstance(instance: Godot) {
|
||||
godotFragment?.let {
|
||||
if (instance === it.godot) {
|
||||
Log.v(TAG, "Force quitting Godot instance")
|
||||
ProcessPhoenix.forceQuit(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotRestartRequested(instance: Godot) {
|
||||
runOnUiThread {
|
||||
godotFragment?.let {
|
||||
if (instance === it.godot) {
|
||||
// It's very hard to properly de-initialize Godot on Android to restart the game
|
||||
// from scratch. Therefore, we need to kill the whole app process and relaunch it.
|
||||
//
|
||||
// Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
|
||||
// releasing and reloading native libs or resetting their state somehow and clearing static data).
|
||||
Log.v(TAG, "Restarting Godot instance...")
|
||||
ProcessPhoenix.triggerRebirth(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotSetupCompleted() {
|
||||
super.onGodotSetupCompleted()
|
||||
|
||||
if (isPiPEnabled()) {
|
||||
try {
|
||||
// Update the aspect ratio for picture-in-picture mode.
|
||||
val viewportWidth = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_width"))
|
||||
val viewportHeight = Integer.parseInt(GodotLib.getGlobal("display/window/size/viewport_height"))
|
||||
pipAspectRatio.set(Rational(viewportWidth, viewportHeight))
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.w(TAG, "Unable to parse viewport dimensions.", e)
|
||||
}
|
||||
|
||||
runOnHostThread { updatePiPParams() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(newIntent: Intent) {
|
||||
intent = sanitizeLaunchIntent(newIntent)
|
||||
super.onNewIntent(intent)
|
||||
handleStartIntent(intent, false)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
||||
if (!newLaunch) {
|
||||
val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
|
||||
if (newLaunchRequested) {
|
||||
Log.d(TAG, "New launch requested, restarting..")
|
||||
val restartIntent = Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false)
|
||||
ProcessPhoenix.triggerRebirth(this, restartIntent)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
godotFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
// Logging the result of permission requests
|
||||
if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE || requestCode == PermissionsUtil.REQUEST_SINGLE_PERMISSION_REQ_CODE) {
|
||||
Log.d(TAG, "Received permissions request result..")
|
||||
for (i in permissions.indices) {
|
||||
val permissionGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED
|
||||
Log.d(TAG, "Permission ${permissions[i]} ${if (permissionGranted) { "granted"} else { "denied" }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
godotFragment?.onBackPressed() ?: super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun getActivity(): Activity? {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getGodot(): Godot? {
|
||||
return godotFragment?.godot
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to initialize the Godot fragment instance in [onCreate].
|
||||
*/
|
||||
protected open fun initGodotInstance(): GodotFragment {
|
||||
return GodotFragment()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun getCommandLine(): MutableList<String> = commandLineParams
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
godot?.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if picture-in-picture (PiP) mode is supported.
|
||||
*/
|
||||
override fun isPiPModeSupported() = isPiPEnabled() && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
|
||||
/**
|
||||
* Returns true if the current activity has enabled picture-in-picture in its manifest declaration using
|
||||
* 'android:supportsPictureInPicture="true"'
|
||||
*/
|
||||
protected open fun isPiPEnabled() = false
|
||||
|
||||
internal fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) {
|
||||
if (isPiPModeSupported()) {
|
||||
autoEnterPiP.set(enableAutoEnter)
|
||||
pipAspectRatio.set(aspectRatio)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setSourceRectHint(gameViewSourceRectHint)
|
||||
.setAspectRatio(aspectRatio)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setSeamlessResizeEnabled(false)
|
||||
.setAutoEnterEnabled(enableAutoEnter)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
builder.setExpandedAspectRatio(aspectRatio)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enterPiPMode() {
|
||||
if (isPiPModeSupported()) {
|
||||
updatePiPParams()
|
||||
|
||||
Log.v(TAG, "Entering PiP mode")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (autoEnterPiP.get()) {
|
||||
enterPiPMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**************************************************************************/
|
||||
/* GodotDownloaderAlarmReceiver.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
||||
|
||||
/**
|
||||
* You should start your derived downloader class when this receiver gets the message
|
||||
* from the alarm service using the provided service helper function within the
|
||||
* DownloaderClientMarshaller. This class must be then registered in your AndroidManifest.xml
|
||||
* file with a section like this:
|
||||
* <receiver android:name=".GodotDownloaderAlarmReceiver"/>
|
||||
*/
|
||||
public class GodotDownloaderAlarmReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d("GODOT", "Alarma recivida");
|
||||
try {
|
||||
DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, GodotDownloaderService.class);
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.d("GODOT", "Exception: " + e.getClass().getName() + ":" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/**************************************************************************/
|
||||
/* GodotDownloaderService.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
|
||||
|
||||
/**
|
||||
* This class demonstrates the minimal client implementation of the
|
||||
* DownloaderService from the Downloader library.
|
||||
*/
|
||||
public class GodotDownloaderService extends DownloaderService {
|
||||
// stuff for LVL -- MODIFY FOR YOUR APPLICATION!
|
||||
private static final String BASE64_PUBLIC_KEY = "REPLACE THIS WITH YOUR PUBLIC KEY";
|
||||
// used by the preference obfuscater
|
||||
private static final byte[] SALT = new byte[] {
|
||||
1, 43, -12, -1, 54, 98,
|
||||
-100, -12, 43, 2, -8, -4, 9, 5, -106, -108, -33, 45, -1, 84
|
||||
};
|
||||
|
||||
/**
|
||||
* This public key comes from your Android Market publisher account, and it
|
||||
* used by the LVL to validate responses from Market on your behalf.
|
||||
*/
|
||||
@Override
|
||||
public String getPublicKey() {
|
||||
SharedPreferences prefs = getApplicationContext().getSharedPreferences("app_data_keys", Context.MODE_PRIVATE);
|
||||
Log.d("GODOT", "getting public key:" + prefs.getString("store_public_key", null));
|
||||
return prefs.getString("store_public_key", null);
|
||||
|
||||
//return BASE64_PUBLIC_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used by the preference obfuscater to make sure that your
|
||||
* obfuscated preferences are different than the ones used by other
|
||||
* applications.
|
||||
*/
|
||||
@Override
|
||||
public byte[] getSALT() {
|
||||
return SALT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill this in with the class name for your alarm receiver. We do this
|
||||
* because receivers must be unique across all of Android (it's a good idea
|
||||
* to make sure that your receiver is in your unique package)
|
||||
*/
|
||||
@Override
|
||||
public String getAlarmReceiverClassName() {
|
||||
Log.d("GODOT", "getAlarmReceiverClassName()");
|
||||
return GodotDownloaderAlarmReceiver.class.getName();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
/**************************************************************************/
|
||||
/* GodotFragment.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.plugin.GodotPlugin;
|
||||
import org.godotengine.godot.utils.BenchmarkUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.Messenger;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
|
||||
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
||||
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
|
||||
import com.google.android.vending.expansion.downloader.Helpers;
|
||||
import com.google.android.vending.expansion.downloader.IDownloaderClient;
|
||||
import com.google.android.vending.expansion.downloader.IDownloaderService;
|
||||
import com.google.android.vending.expansion.downloader.IStub;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base fragment for Android apps intending to use Godot for part of the app's UI.
|
||||
*/
|
||||
public class GodotFragment extends Fragment implements IDownloaderClient, GodotHost {
|
||||
private static final String TAG = GodotFragment.class.getSimpleName();
|
||||
|
||||
private IStub mDownloaderClientStub;
|
||||
private TextView mStatusText;
|
||||
private TextView mProgressFraction;
|
||||
private TextView mProgressPercent;
|
||||
private TextView mAverageSpeed;
|
||||
private TextView mTimeRemaining;
|
||||
private ProgressBar mPB;
|
||||
|
||||
private View mDashboard;
|
||||
private View mCellMessage;
|
||||
|
||||
private Button mPauseButton;
|
||||
|
||||
private FrameLayout godotContainerLayout;
|
||||
private int mState;
|
||||
|
||||
@Nullable
|
||||
private GodotHost parentHost;
|
||||
private Godot godot;
|
||||
|
||||
private void setState(int newState) {
|
||||
if (mState != newState) {
|
||||
mState = newState;
|
||||
mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
|
||||
}
|
||||
}
|
||||
|
||||
private void setButtonPausedState(boolean paused) {
|
||||
int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
|
||||
mPauseButton.setText(stringResourceID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Godot getGodot() {
|
||||
return godot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (getParentFragment() instanceof GodotHost) {
|
||||
parentHost = (GodotHost)getParentFragment();
|
||||
} else if (getActivity() instanceof GodotHost) {
|
||||
parentHost = (GodotHost)getActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
if (godotContainerLayout != null && godotContainerLayout.getParent() != null) {
|
||||
Log.d(TAG, "Cleaning up Godot container layout during detach.");
|
||||
((ViewGroup)godotContainerLayout.getParent()).removeView(godotContainerLayout);
|
||||
}
|
||||
|
||||
super.onDetach();
|
||||
parentHost = null;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
godot.onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
godot.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
godot.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(Messenger m) {
|
||||
IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m);
|
||||
remoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
||||
super.onCreate(icicle);
|
||||
|
||||
if (parentHost != null) {
|
||||
godot = parentHost.getGodot();
|
||||
}
|
||||
if (godot == null) {
|
||||
godot = Godot.getInstance(requireContext());
|
||||
}
|
||||
performEngineInitialization();
|
||||
BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
|
||||
}
|
||||
|
||||
private void performEngineInitialization() {
|
||||
try {
|
||||
if (!godot.initEngine(this, getCommandLine(), getHostPlugins(godot))) {
|
||||
throw new IllegalStateException("Unable to initialize Godot engine");
|
||||
}
|
||||
|
||||
godotContainerLayout = godot.onInitRenderView(this);
|
||||
if (godotContainerLayout == null) {
|
||||
throw new IllegalStateException("Unable to initialize engine render view");
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "Engine initialization failed", e);
|
||||
final String errorMessage = TextUtils.isEmpty(e.getMessage())
|
||||
? getString(R.string.error_engine_setup_message)
|
||||
: e.getMessage();
|
||||
godot.alert(errorMessage, getString(R.string.text_error_title), godot::destroyAndKillProcess);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
final Activity activity = getActivity();
|
||||
Intent notifierIntent = new Intent(activity, activity.getClass());
|
||||
notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0,
|
||||
notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
int startResult;
|
||||
try {
|
||||
startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(getContext(), pendingIntent, GodotDownloaderService.class);
|
||||
|
||||
if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
|
||||
// This is where you do set up to display the download
|
||||
// progress (next step in onCreateView)
|
||||
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, GodotDownloaderService.class);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restart engine initialization
|
||||
performEngineInitialization();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to start download service", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle icicle) {
|
||||
if (mDownloaderClientStub != null) {
|
||||
View downloadingExpansionView =
|
||||
inflater.inflate(R.layout.downloading_expansion, container, false);
|
||||
mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar);
|
||||
mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText);
|
||||
mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction);
|
||||
mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage);
|
||||
mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed);
|
||||
mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining);
|
||||
mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
|
||||
mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
|
||||
mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
|
||||
|
||||
return downloadingExpansionView;
|
||||
}
|
||||
|
||||
if (godotContainerLayout != null && godotContainerLayout.getParent() != null) {
|
||||
Log.w(TAG, "Godot container layout already has a parent, removing it.");
|
||||
((ViewGroup)godotContainerLayout.getParent()).removeView(godotContainerLayout);
|
||||
}
|
||||
|
||||
return godotContainerLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (godotContainerLayout != null && godotContainerLayout.getParent() != null) {
|
||||
Log.w(TAG, "Removing Godot container layout from parent during destruction.");
|
||||
((ViewGroup)godotContainerLayout.getParent()).removeView(godotContainerLayout);
|
||||
}
|
||||
|
||||
godot.onDestroy(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (!godot.isInitialized()) {
|
||||
if (null != mDownloaderClientStub) {
|
||||
mDownloaderClientStub.disconnect(getActivity());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
godot.onPause(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (!godot.isInitialized()) {
|
||||
if (null != mDownloaderClientStub) {
|
||||
mDownloaderClientStub.disconnect(getActivity());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
godot.onStop(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (!godot.isInitialized()) {
|
||||
if (null != mDownloaderClientStub) {
|
||||
mDownloaderClientStub.connect(getActivity());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
godot.onStart(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!godot.isInitialized()) {
|
||||
if (null != mDownloaderClientStub) {
|
||||
mDownloaderClientStub.connect(getActivity());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
godot.onResume(this);
|
||||
}
|
||||
|
||||
public void onBackPressed() {
|
||||
godot.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* The download state should trigger changes in the UI --- it may be useful
|
||||
* to show the state as being indeterminate at times. This sample can be
|
||||
* considered a guideline.
|
||||
*/
|
||||
@Override
|
||||
public void onDownloadStateChanged(int newState) {
|
||||
setState(newState);
|
||||
boolean showDashboard = true;
|
||||
boolean showCellMessage = false;
|
||||
boolean paused;
|
||||
boolean indeterminate;
|
||||
switch (newState) {
|
||||
case IDownloaderClient.STATE_IDLE:
|
||||
// STATE_IDLE means the service is listening, so it's
|
||||
// safe to start making remote service calls.
|
||||
paused = false;
|
||||
indeterminate = true;
|
||||
break;
|
||||
case IDownloaderClient.STATE_CONNECTING:
|
||||
case IDownloaderClient.STATE_FETCHING_URL:
|
||||
showDashboard = true;
|
||||
paused = false;
|
||||
indeterminate = true;
|
||||
break;
|
||||
case IDownloaderClient.STATE_DOWNLOADING:
|
||||
paused = false;
|
||||
showDashboard = true;
|
||||
indeterminate = false;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_FAILED_CANCELED:
|
||||
case IDownloaderClient.STATE_FAILED:
|
||||
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
||||
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
||||
paused = true;
|
||||
showDashboard = false;
|
||||
indeterminate = false;
|
||||
break;
|
||||
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
|
||||
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
|
||||
showDashboard = false;
|
||||
paused = true;
|
||||
indeterminate = false;
|
||||
showCellMessage = true;
|
||||
break;
|
||||
|
||||
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
||||
paused = true;
|
||||
indeterminate = false;
|
||||
break;
|
||||
case IDownloaderClient.STATE_PAUSED_ROAMING:
|
||||
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
|
||||
paused = true;
|
||||
indeterminate = false;
|
||||
break;
|
||||
case IDownloaderClient.STATE_COMPLETED:
|
||||
showDashboard = false;
|
||||
paused = false;
|
||||
indeterminate = false;
|
||||
performEngineInitialization();
|
||||
return;
|
||||
default:
|
||||
paused = true;
|
||||
indeterminate = true;
|
||||
showDashboard = true;
|
||||
}
|
||||
int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
|
||||
if (mDashboard.getVisibility() != newDashboardVisibility) {
|
||||
mDashboard.setVisibility(newDashboardVisibility);
|
||||
}
|
||||
int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
|
||||
if (mCellMessage.getVisibility() != cellMessageVisibility) {
|
||||
mCellMessage.setVisibility(cellMessageVisibility);
|
||||
}
|
||||
|
||||
mPB.setIndeterminate(indeterminate);
|
||||
setButtonPausedState(paused);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(DownloadProgressInfo progress) {
|
||||
mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
|
||||
Helpers.getSpeedString(progress.mCurrentSpeed)));
|
||||
mTimeRemaining.setText(getString(R.string.time_remaining,
|
||||
Helpers.getTimeRemaining(progress.mTimeRemaining)));
|
||||
|
||||
mPB.setMax((int)(progress.mOverallTotal >> 8));
|
||||
mPB.setProgress((int)(progress.mOverallProgress >> 8));
|
||||
mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal));
|
||||
mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
|
||||
progress.mOverallTotal));
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public List<String> getCommandLine() {
|
||||
return parentHost != null ? parentHost.getCommandLine() : Collections.emptyList();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onGodotSetupCompleted() {
|
||||
if (parentHost != null) {
|
||||
parentHost.onGodotSetupCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onGodotMainLoopStarted() {
|
||||
if (parentHost != null) {
|
||||
parentHost.onGodotMainLoopStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGodotForceQuit(Godot instance) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onGodotForceQuit(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGodotForceQuit(int godotInstanceId) {
|
||||
return parentHost != null && parentHost.onGodotForceQuit(godotInstanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGodotRestartRequested(Godot instance) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onGodotRestartRequested(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onNewGodotInstanceRequested(String[] args) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.onNewGodotInstanceRequested(args);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public Set<GodotPlugin> getHostPlugins(Godot engine) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.getHostPlugins(engine);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword);
|
||||
}
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Error verifyApk(@NonNull String apkPath) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.verifyApk(apkPath);
|
||||
}
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFeature(String featureTag) {
|
||||
if (parentHost != null) {
|
||||
return parentHost.supportsFeature(featureTag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorWorkspaceSelected(String workspace) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onEditorWorkspaceSelected(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDistractionFreeModeChanged(Boolean enabled) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onDistractionFreeModeChanged(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BuildProvider getBuildProvider() {
|
||||
if (parentHost != null) {
|
||||
return parentHost.getBuildProvider();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
/**************************************************************************/
|
||||
/* GodotGLRenderView.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.gl.GLSurfaceView;
|
||||
import org.godotengine.godot.gl.GodotRenderer;
|
||||
import org.godotengine.godot.input.GodotInputHandler;
|
||||
import org.godotengine.godot.xr.XRMode;
|
||||
import org.godotengine.godot.xr.ovr.OvrConfigChooser;
|
||||
import org.godotengine.godot.xr.ovr.OvrContextFactory;
|
||||
import org.godotengine.godot.xr.ovr.OvrWindowSurfaceFactory;
|
||||
import org.godotengine.godot.xr.regular.RegularConfigChooser;
|
||||
import org.godotengine.godot.xr.regular.RegularContextFactory;
|
||||
import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseArray;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* A simple GLSurfaceView sub-class that demonstrate how to perform
|
||||
* OpenGL ES 2.0 rendering into a GL Surface. Note the following important
|
||||
* details:
|
||||
*
|
||||
* - The class must use a custom context factory to enable 2.0 rendering.
|
||||
* See ContextFactory class definition below.
|
||||
*
|
||||
* - The class must use a custom EGLConfigChooser to be able to select
|
||||
* an EGLConfig that supports 3.0. This is done by providing a config
|
||||
* specification to eglChooseConfig() that has the attribute
|
||||
* EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag
|
||||
* set. See ConfigChooser class definition below.
|
||||
*
|
||||
* - The class must select the surface's format, then choose an EGLConfig
|
||||
* that matches it exactly (with regards to red/green/blue/alpha channels
|
||||
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
|
||||
*/
|
||||
class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
|
||||
private final Godot godot;
|
||||
private final GodotInputHandler inputHandler;
|
||||
private final GodotRenderer godotRenderer;
|
||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||
|
||||
public GodotGLRenderView(Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl, boolean shouldBeTranslucent) {
|
||||
super(godot.getContext());
|
||||
|
||||
this.godot = godot;
|
||||
this.inputHandler = inputHandler;
|
||||
this.godotRenderer = new GodotRenderer();
|
||||
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
|
||||
init(xrMode, shouldBeTranslucent, useDebugOpengl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SurfaceView getView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueOnRenderThread(Runnable event) {
|
||||
queueEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused() {
|
||||
queueEvent(() -> {
|
||||
GodotLib.focusout();
|
||||
// Pause the renderer
|
||||
godotRenderer.onActivityPaused();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped() {
|
||||
pauseGLThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed() {
|
||||
queueEvent(() -> {
|
||||
// Resume the renderer
|
||||
godotRenderer.onActivityResumed();
|
||||
GodotLib.focusin();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted() {
|
||||
resumeGLThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockingExitRenderer(long blockingTimeInMs) {
|
||||
return requestRenderThreadExitAndWait(blockingTimeInMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GodotInputHandler getInputHandler() {
|
||||
return inputHandler;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
super.onTouchEvent(event);
|
||||
return inputHandler.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(final int keyCode, KeyEvent event) {
|
||||
return inputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, KeyEvent event) {
|
||||
return inputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
return inputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCapturedPointerEvent(MotionEvent event) {
|
||||
return inputHandler.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointerCaptureChange(boolean hasCapture) {
|
||||
super.onPointerCaptureChange(hasCapture);
|
||||
inputHandler.onPointerCaptureChange(hasCapture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCapturePointer() {
|
||||
// Pointer capture is not supported on XR devices.
|
||||
return !godot.isXrRuntime() && inputHandler.canCapturePointer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestPointerCapture() {
|
||||
if (canCapturePointer()) {
|
||||
super.requestPointerCapture();
|
||||
inputHandler.onPointerCaptureChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePointerCapture() {
|
||||
super.releasePointerCapture();
|
||||
inputHandler.onPointerCaptureChange(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to configure the PointerIcon for the given type.
|
||||
*
|
||||
* Called from JNI
|
||||
*/
|
||||
@Keep
|
||||
@Override
|
||||
public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) {
|
||||
try {
|
||||
Bitmap bitmap = null;
|
||||
if (!TextUtils.isEmpty(imagePath)) {
|
||||
if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) {
|
||||
// Try to load the bitmap from the file system
|
||||
bitmap = BitmapFactory.decodeFile(imagePath);
|
||||
} else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) {
|
||||
// Try to load the bitmap from the assets directory
|
||||
AssetManager am = getContext().getAssets();
|
||||
InputStream imageInputStream = am.open(imagePath);
|
||||
bitmap = BitmapFactory.decodeStream(imageInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY);
|
||||
customPointerIcons.put(pointerType, customPointerIcon);
|
||||
} catch (Exception e) {
|
||||
// Reset the custom pointer icon
|
||||
customPointerIcons.delete(pointerType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called from JNI to change pointer icon
|
||||
*/
|
||||
@Keep
|
||||
@Override
|
||||
public void setPointerIcon(int pointerType) {
|
||||
PointerIcon pointerIcon = customPointerIcons.get(pointerType);
|
||||
if (pointerIcon == null) {
|
||||
pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType);
|
||||
}
|
||||
setPointerIcon(pointerIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) {
|
||||
return getPointerIcon();
|
||||
}
|
||||
|
||||
private void init(XRMode xrMode, boolean translucent, boolean useDebugOpengl) {
|
||||
setPreserveEGLContextOnPause(true);
|
||||
setFocusableInTouchMode(true);
|
||||
switch (xrMode) {
|
||||
case OPENXR:
|
||||
// Replace the default egl config chooser.
|
||||
setEGLConfigChooser(new OvrConfigChooser());
|
||||
|
||||
// Replace the default context factory.
|
||||
setEGLContextFactory(new OvrContextFactory());
|
||||
|
||||
// Replace the default window surface factory.
|
||||
setEGLWindowSurfaceFactory(new OvrWindowSurfaceFactory());
|
||||
break;
|
||||
|
||||
case REGULAR:
|
||||
default:
|
||||
/* By default, GLSurfaceView() creates a RGB_565 opaque surface.
|
||||
* If we want a translucent one, we should change the surface's
|
||||
* format here, using PixelFormat.TRANSLUCENT for GL Surfaces
|
||||
* is interpreted as any 32-bit surface with alpha by SurfaceFlinger.
|
||||
*/
|
||||
if (translucent) {
|
||||
this.getHolder().setFormat(PixelFormat.TRANSLUCENT);
|
||||
}
|
||||
|
||||
/* Setup the context factory for 2.0 rendering.
|
||||
* See ContextFactory class definition below
|
||||
*/
|
||||
setEGLContextFactory(new RegularContextFactory(useDebugOpengl));
|
||||
|
||||
/* We need to choose an EGLConfig that matches the format of
|
||||
* our surface exactly. This is going to be done in our
|
||||
* custom config chooser. See ConfigChooser class definition
|
||||
* below.
|
||||
*/
|
||||
|
||||
setEGLConfigChooser(
|
||||
new RegularFallbackConfigChooser(8, 8, 8, 8, 24, 0,
|
||||
new RegularConfigChooser(8, 8, 8, 8, 16, 0)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRenderer() {
|
||||
/* Set the renderer responsible for frame rendering */
|
||||
setRenderer(godotRenderer);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
/**************************************************************************/
|
||||
/* GodotHost.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.plugin.GodotPlugin;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} engine.
|
||||
*/
|
||||
public interface GodotHost {
|
||||
/**
|
||||
* Provides a set of command line parameters to setup the {@link Godot} engine.
|
||||
*/
|
||||
default List<String> getCommandLine() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when setup of the {@link Godot} engine is complete.
|
||||
*/
|
||||
default void onGodotSetupCompleted() {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the {@link Godot} engine main loop has started.
|
||||
*/
|
||||
default void onGodotMainLoopStarted() {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread to terminate the given {@link Godot} engine instance.
|
||||
*/
|
||||
default void onGodotForceQuit(Godot instance) {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread to terminate the {@link Godot} engine instance with the given id.
|
||||
* @param godotInstanceId id of the Godot instance to terminate. See {@code onNewGodotInstanceRequested}
|
||||
*
|
||||
* @return true if successful, false otherwise.
|
||||
*/
|
||||
default boolean onGodotForceQuit(int godotInstanceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot instance wants to be restarted. It's up to the host
|
||||
* to perform the appropriate action(s).
|
||||
*/
|
||||
default void onGodotRestartRequested(Godot instance) {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when a new Godot instance is requested. It's up to the host to
|
||||
* perform the appropriate action(s).
|
||||
*
|
||||
* @param args Arguments used to initialize the new instance.
|
||||
*
|
||||
* @return the id of the new instance. See {@code onGodotForceQuit}
|
||||
*/
|
||||
default int onNewGodotInstanceRequested(String[] args) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide access to the Activity hosting the {@link Godot} engine if any.
|
||||
*/
|
||||
@Nullable
|
||||
Activity getActivity();
|
||||
|
||||
/**
|
||||
* Provide access to the hosted {@link Godot} engine.
|
||||
*/
|
||||
Godot getGodot();
|
||||
|
||||
/**
|
||||
* Returns a set of {@link GodotPlugin} to be registered with the hosted {@link Godot} engine.
|
||||
*/
|
||||
default Set<GodotPlugin> getHostPlugins(Godot engine) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the given Android apk
|
||||
*
|
||||
* @param inputPath Path to the apk that should be signed
|
||||
* @param outputPath Path for the signed output apk; can be the same as inputPath
|
||||
* @param keystorePath Path to the keystore to use for signing the apk
|
||||
* @param keystoreUser Keystore user credential
|
||||
* @param keystorePassword Keystore password credential
|
||||
*
|
||||
* @return {@link Error#OK} if signing is successful
|
||||
*/
|
||||
default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given Android apk is signed
|
||||
*
|
||||
* @param apkPath Path to the apk that should be verified
|
||||
* @return {@link Error#OK} if verification was successful
|
||||
*/
|
||||
default Error verifyApk(@NonNull String apkPath) {
|
||||
return Error.ERR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given feature tag is supported.
|
||||
*
|
||||
* @see <a href="https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html">Feature tags</a>
|
||||
*/
|
||||
default boolean supportsFeature(String featureTag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when an editor workspace has been selected.
|
||||
*/
|
||||
default void onEditorWorkspaceSelected(String workspace) {}
|
||||
|
||||
/**
|
||||
* Triggered when the editor's distraction-free mode changes.
|
||||
*/
|
||||
default void onDistractionFreeModeChanged(Boolean enabled) {}
|
||||
|
||||
/**
|
||||
* Runs the specified action on a host provided thread.
|
||||
*/
|
||||
default void runOnHostThread(Runnable action) {
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) {
|
||||
activity.runOnUiThread(action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the build provider, if available.
|
||||
*
|
||||
* @return the build provider, if available; otherwise, null.
|
||||
*/
|
||||
default @Nullable BuildProvider getBuildProvider() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/**************************************************************************/
|
||||
/* GodotIO.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.error.Error;
|
||||
import org.godotengine.godot.input.GodotEditText;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
// Wrapper for native library
|
||||
|
||||
public class GodotIO {
|
||||
private static final String TAG = GodotIO.class.getSimpleName();
|
||||
|
||||
private final Godot godot;
|
||||
|
||||
private final String uniqueId;
|
||||
GodotEditText edit;
|
||||
|
||||
final int SCREEN_LANDSCAPE = 0;
|
||||
final int SCREEN_PORTRAIT = 1;
|
||||
final int SCREEN_REVERSE_LANDSCAPE = 2;
|
||||
final int SCREEN_REVERSE_PORTRAIT = 3;
|
||||
final int SCREEN_SENSOR_LANDSCAPE = 4;
|
||||
final int SCREEN_SENSOR_PORTRAIT = 5;
|
||||
final int SCREEN_SENSOR = 6;
|
||||
|
||||
GodotIO(Godot godot) {
|
||||
this.godot = godot;
|
||||
String androidId = Settings.Secure.getString(godot.getContext().getContentResolver(),
|
||||
Settings.Secure.ANDROID_ID);
|
||||
if (androidId == null) {
|
||||
androidId = "";
|
||||
}
|
||||
|
||||
uniqueId = androidId;
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = godot.getActivity();
|
||||
if (context == null) {
|
||||
context = godot.getContext();
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// MISCELLANEOUS OS IO
|
||||
/////////////////////////
|
||||
|
||||
public int openURI(String uriString) {
|
||||
try {
|
||||
Context context = getContext();
|
||||
|
||||
Uri dataUri;
|
||||
String dataType = "";
|
||||
boolean grantReadUriPermission = false;
|
||||
|
||||
if (uriString.startsWith("/") || uriString.startsWith("file://")) {
|
||||
String filePath = uriString;
|
||||
// File uris needs to be provided via the FileProvider
|
||||
grantReadUriPermission = true;
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.replace("file://", "");
|
||||
}
|
||||
|
||||
File targetFile = new File(filePath);
|
||||
dataUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", targetFile);
|
||||
dataType = context.getContentResolver().getType(dataUri);
|
||||
} else {
|
||||
dataUri = Uri.parse(uriString);
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if (TextUtils.isEmpty(dataType)) {
|
||||
intent.setData(dataUri);
|
||||
} else {
|
||||
intent.setDataAndType(dataUri, dataType);
|
||||
}
|
||||
if (grantReadUriPermission) {
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
return Error.OK.toNativeValue();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable to open uri " + uriString, e);
|
||||
return Error.FAILED.toNativeValue();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCacheDir() {
|
||||
return getContext().getCacheDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getTempDir() {
|
||||
File tempDir = new File(getCacheDir() + "/tmp");
|
||||
|
||||
if (!tempDir.exists()) {
|
||||
if (!tempDir.mkdirs()) {
|
||||
Log.e(TAG, "Unable to create temp dir");
|
||||
}
|
||||
}
|
||||
|
||||
return tempDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getDataDir() {
|
||||
return getContext().getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return Locale.getDefault().toString();
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return Build.MODEL;
|
||||
}
|
||||
|
||||
public int getScreenDPI() {
|
||||
return getContext().getResources().getDisplayMetrics().densityDpi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bucketized density values.
|
||||
*/
|
||||
public float getScaledDensity() {
|
||||
int densityDpi = getContext().getResources().getDisplayMetrics().densityDpi;
|
||||
float selectedScaledDensity;
|
||||
if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
|
||||
selectedScaledDensity = 4.0f;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
|
||||
selectedScaledDensity = 3.0f;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
|
||||
selectedScaledDensity = 2.0f;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
|
||||
selectedScaledDensity = 1.5f;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_MEDIUM) {
|
||||
selectedScaledDensity = 1.0f;
|
||||
} else {
|
||||
selectedScaledDensity = 0.75f;
|
||||
}
|
||||
return selectedScaledDensity;
|
||||
}
|
||||
|
||||
public double getScreenRefreshRate(double fallback) {
|
||||
Activity activity = godot.getActivity();
|
||||
|
||||
Display display = null;
|
||||
if (activity != null) {
|
||||
display = activity.getWindowManager().getDefaultDisplay();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
display = godot.getContext().getDisplay();
|
||||
}
|
||||
|
||||
if (display != null) {
|
||||
return display.getRefreshRate();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public int[] getDisplaySafeArea() {
|
||||
Rect rect = new Rect();
|
||||
int[] result = new int[4];
|
||||
|
||||
View topView = null;
|
||||
if (godot.getActivity() != null) {
|
||||
topView = godot.getActivity().getWindow().getDecorView();
|
||||
} else if (godot.getRenderView() != null) {
|
||||
topView = godot.getRenderView().getView();
|
||||
}
|
||||
|
||||
if (topView != null) {
|
||||
int insetTypes;
|
||||
if (godot.isInImmersiveMode()) {
|
||||
insetTypes = WindowInsetsCompat.Type.displayCutout();
|
||||
} else {
|
||||
insetTypes = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout();
|
||||
}
|
||||
|
||||
if (topView.getRootWindowInsets() != null) {
|
||||
WindowInsetsCompat insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(topView.getRootWindowInsets(), topView);
|
||||
Insets insets = insetsCompat.getInsets(insetTypes);
|
||||
|
||||
if (godot.isInEdgeToEdgeMode() || godot.isInImmersiveMode()) {
|
||||
result[0] = insets.left;
|
||||
result[1] = insets.top;
|
||||
} else {
|
||||
// The top and left padding (if required) is already applied.
|
||||
result[0] = 0;
|
||||
result[1] = 0;
|
||||
}
|
||||
result[2] = topView.getWidth() - insets.right - insets.left;
|
||||
result[3] = topView.getHeight() - insets.bottom - insets.top;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] getDisplayCutouts() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
View topView = null;
|
||||
if (godot.getActivity() != null) {
|
||||
topView = godot.getActivity().getWindow().getDecorView();
|
||||
} else if (godot.getRenderView() != null) {
|
||||
topView = godot.getRenderView().getView();
|
||||
}
|
||||
|
||||
if (topView == null) {
|
||||
return new int[0];
|
||||
}
|
||||
DisplayCutout cutout = topView.getRootWindowInsets().getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
return new int[0];
|
||||
}
|
||||
List<Rect> rects = cutout.getBoundingRects();
|
||||
int cutouts = rects.size();
|
||||
int[] result = new int[cutouts * 4];
|
||||
int index = 0;
|
||||
for (Rect rect : rects) {
|
||||
result[index++] = rect.left;
|
||||
result[index++] = rect.top;
|
||||
result[index++] = rect.width();
|
||||
result[index++] = rect.height();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean hasHardwareKeyboard() {
|
||||
if (edit != null) {
|
||||
return edit.hasHardwareKeyboard();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void showKeyboard(String p_existing_text, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
|
||||
if (edit != null) {
|
||||
edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideKeyboard() {
|
||||
if (edit != null)
|
||||
edit.hideKeyboard();
|
||||
}
|
||||
|
||||
public void setScreenOrientation(int p_orientation) {
|
||||
final Activity activity = godot.getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (p_orientation) {
|
||||
case SCREEN_LANDSCAPE: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} break;
|
||||
case SCREEN_PORTRAIT: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
} break;
|
||||
case SCREEN_REVERSE_LANDSCAPE: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
|
||||
} break;
|
||||
case SCREEN_REVERSE_PORTRAIT: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
|
||||
} break;
|
||||
case SCREEN_SENSOR_LANDSCAPE: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
|
||||
} break;
|
||||
case SCREEN_SENSOR_PORTRAIT: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
|
||||
} break;
|
||||
case SCREEN_SENSOR: {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
public int getScreenOrientation() {
|
||||
final Activity activity = godot.getActivity();
|
||||
if (activity == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int orientation = activity.getRequestedOrientation();
|
||||
switch (orientation) {
|
||||
case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
|
||||
return SCREEN_LANDSCAPE;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
|
||||
return SCREEN_PORTRAIT;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
|
||||
return SCREEN_REVERSE_LANDSCAPE;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT:
|
||||
return SCREEN_REVERSE_PORTRAIT;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE:
|
||||
return SCREEN_SENSOR_LANDSCAPE;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT:
|
||||
return SCREEN_SENSOR_PORTRAIT;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_SENSOR:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_FULL_USER:
|
||||
return SCREEN_SENSOR;
|
||||
case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_USER:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_BEHIND:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_NOSENSOR:
|
||||
case ActivityInfo.SCREEN_ORIENTATION_LOCKED:
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int getDisplayRotation() {
|
||||
Activity activity = godot.getActivity();
|
||||
|
||||
Display display = null;
|
||||
if (activity != null) {
|
||||
display = activity.getWindowManager().getDefaultDisplay();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
display = godot.getContext().getDisplay();
|
||||
}
|
||||
|
||||
if (display != null) {
|
||||
int rotation = display.getRotation();
|
||||
if (rotation == Surface.ROTATION_90) {
|
||||
return 90;
|
||||
} else if (rotation == Surface.ROTATION_180) {
|
||||
return 180;
|
||||
} else if (rotation == Surface.ROTATION_270) {
|
||||
return 270;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setEdit(GodotEditText _edit) {
|
||||
edit = _edit;
|
||||
}
|
||||
|
||||
public static final int SYSTEM_DIR_DESKTOP = 0;
|
||||
public static final int SYSTEM_DIR_DCIM = 1;
|
||||
public static final int SYSTEM_DIR_DOCUMENTS = 2;
|
||||
public static final int SYSTEM_DIR_DOWNLOADS = 3;
|
||||
public static final int SYSTEM_DIR_MOVIES = 4;
|
||||
public static final int SYSTEM_DIR_MUSIC = 5;
|
||||
public static final int SYSTEM_DIR_PICTURES = 6;
|
||||
public static final int SYSTEM_DIR_RINGTONES = 7;
|
||||
|
||||
public String getSystemDir(int idx, boolean shared_storage) {
|
||||
String what;
|
||||
switch (idx) {
|
||||
case SYSTEM_DIR_DESKTOP:
|
||||
default: {
|
||||
what = null; // This leads to the app specific external root directory.
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_DCIM: {
|
||||
what = Environment.DIRECTORY_DCIM;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_DOCUMENTS: {
|
||||
what = Environment.DIRECTORY_DOCUMENTS;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_DOWNLOADS: {
|
||||
what = Environment.DIRECTORY_DOWNLOADS;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_MOVIES: {
|
||||
what = Environment.DIRECTORY_MOVIES;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_MUSIC: {
|
||||
what = Environment.DIRECTORY_MUSIC;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_PICTURES: {
|
||||
what = Environment.DIRECTORY_PICTURES;
|
||||
} break;
|
||||
|
||||
case SYSTEM_DIR_RINGTONES: {
|
||||
what = Environment.DIRECTORY_RINGTONES;
|
||||
} break;
|
||||
}
|
||||
|
||||
if (shared_storage) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Log.w(TAG, "Shared storage access is limited on Android 10 and higher.");
|
||||
}
|
||||
if (TextUtils.isEmpty(what)) {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
} else {
|
||||
return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath();
|
||||
}
|
||||
} else {
|
||||
return getContext().getExternalFilesDir(what).getAbsolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
public String getUniqueID() {
|
||||
return uniqueId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
/**************************************************************************/
|
||||
/* GodotLib.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.gl.GodotRenderer;
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler;
|
||||
import org.godotengine.godot.io.file.FileAccessHandler;
|
||||
import org.godotengine.godot.tts.GodotTTS;
|
||||
import org.godotengine.godot.utils.GodotNetUtils;
|
||||
import org.godotengine.godot.variant.Callable;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.AssetManager;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.view.Surface;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Wrapper for native library
|
||||
*/
|
||||
public class GodotLib {
|
||||
static {
|
||||
System.loadLibrary("godot_android");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the main thread to initialize Godot native layer.
|
||||
*/
|
||||
public static native boolean initialize(
|
||||
Godot p_instance,
|
||||
AssetManager p_asset_manager,
|
||||
GodotIO godotIO,
|
||||
GodotNetUtils netUtils,
|
||||
DirectoryAccessHandler directoryAccessHandler,
|
||||
FileAccessHandler fileAccessHandler,
|
||||
boolean use_apk_expansion);
|
||||
|
||||
/**
|
||||
* Invoked on the main thread to clean up Godot native layer.
|
||||
* @see androidx.fragment.app.Fragment#onDestroy()
|
||||
*/
|
||||
public static native void ondestroy();
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread to complete setup for the Godot native layer logic.
|
||||
* @param p_cmdline Command line arguments used to configure Godot native layer components.
|
||||
*/
|
||||
public static native boolean setup(String[] p_cmdline, GodotTTS tts);
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread when the underlying Android surface has changed size.
|
||||
* @param p_surface
|
||||
* @param p_width
|
||||
* @param p_height
|
||||
* @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int)
|
||||
*/
|
||||
public static native void resize(Surface p_surface, int p_width, int p_height);
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the underlying Android surface is created or recreated.
|
||||
* @param p_surface
|
||||
*/
|
||||
public static native void newcontext(Surface p_surface);
|
||||
|
||||
/**
|
||||
* Forward {@link Activity#onBackPressed()} event.
|
||||
*/
|
||||
public static native void back();
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread to draw the current frame.
|
||||
* @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onDrawFrame(GL10)
|
||||
*/
|
||||
public static native boolean step();
|
||||
|
||||
/**
|
||||
* TTS callback.
|
||||
*/
|
||||
public static native void ttsCallback(int event, long id, int pos);
|
||||
|
||||
/**
|
||||
* Forward touch events.
|
||||
*/
|
||||
public static native void dispatchTouchEvent(int event, int pointer, int pointerCount, float[] positions, boolean doubleTap);
|
||||
|
||||
/**
|
||||
* Dispatch mouse events
|
||||
*/
|
||||
public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY);
|
||||
|
||||
public static native void magnify(float x, float y, float factor);
|
||||
|
||||
public static native void pan(float x, float y, float deltaX, float deltaY);
|
||||
|
||||
/**
|
||||
* Forward accelerometer sensor events.
|
||||
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
|
||||
*/
|
||||
public static native void accelerometer(float x, float y, float z);
|
||||
|
||||
/**
|
||||
* Forward gravity sensor events.
|
||||
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
|
||||
*/
|
||||
public static native void gravity(float x, float y, float z);
|
||||
|
||||
/**
|
||||
* Forward magnetometer sensor events.
|
||||
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
|
||||
*/
|
||||
public static native void magnetometer(float x, float y, float z);
|
||||
|
||||
/**
|
||||
* Forward gyroscope sensor events.
|
||||
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
|
||||
*/
|
||||
public static native void gyroscope(float x, float y, float z);
|
||||
|
||||
/**
|
||||
* Forward regular key events.
|
||||
*/
|
||||
public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed, boolean p_echo);
|
||||
|
||||
/**
|
||||
* Forward game device's key events.
|
||||
*/
|
||||
public static native void joybutton(int p_device, int p_but, boolean p_pressed);
|
||||
|
||||
/**
|
||||
* Forward joystick devices axis motion events.
|
||||
*/
|
||||
public static native void joyaxis(int p_device, int p_axis, float p_value);
|
||||
|
||||
/**
|
||||
* Forward joystick devices hat motion events.
|
||||
*/
|
||||
public static native void joyhat(int p_device, int p_hat_x, int p_hat_y);
|
||||
|
||||
/**
|
||||
* Fires when a joystick device is added or removed.
|
||||
*/
|
||||
public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name);
|
||||
|
||||
/**
|
||||
* Invoked when the Android app resumes.
|
||||
* @see androidx.fragment.app.Fragment#onResume()
|
||||
*/
|
||||
public static native void focusin();
|
||||
|
||||
/**
|
||||
* Invoked when the Android app pauses.
|
||||
* @see androidx.fragment.app.Fragment#onPause()
|
||||
*/
|
||||
public static native void focusout();
|
||||
|
||||
/**
|
||||
* Used to access Godot global properties.
|
||||
* @param p_key Property key
|
||||
* @return String value of the property
|
||||
*/
|
||||
public static native String getGlobal(String p_key);
|
||||
|
||||
/**
|
||||
* Used to get info about the current rendering system.
|
||||
*
|
||||
* @return A String array with three elements:
|
||||
* [0] Rendering driver name chosen for rendering.
|
||||
* [1] Rendering driver name chosen before any fallbacks were applied.
|
||||
* [2] Rendering method.
|
||||
* [3] Source where the rendering driver was chosen from.
|
||||
* [4] Source where the rendering method was chosen from.
|
||||
*/
|
||||
public static native String[] getRendererInfo(boolean p_vulkan_requirements_met);
|
||||
|
||||
/**
|
||||
* Used to access Godot's editor settings.
|
||||
* @param settingKey Setting key
|
||||
* @return String value of the setting
|
||||
*/
|
||||
public static native String getEditorSetting(String settingKey);
|
||||
|
||||
/**
|
||||
* Update the 'key' editor setting with the given data. Must be called on the render thread.
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorSetting(String key, Object data);
|
||||
|
||||
/**
|
||||
* Used to access project metadata from the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
public static native Object getEditorProjectMetadata(String section, String key, Object defaultValue);
|
||||
|
||||
/**
|
||||
* Set the project metadata to the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorProjectMetadata(String section, String key, Object data);
|
||||
|
||||
/**
|
||||
* Invoke method |p_method| on the Godot object specified by |p_id|
|
||||
* @param p_id Id of the Godot object to invoke
|
||||
* @param p_method Name of the method to invoke
|
||||
* @param p_params Parameters to use for method invocation
|
||||
*
|
||||
* @deprecated Use {@link Callable#call(long, String, Object...)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void callobject(long p_id, String p_method, Object[] p_params) {
|
||||
Callable.call(p_id, p_method, p_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke method |p_method| on the Godot object specified by |p_id| during idle time.
|
||||
* @param p_id Id of the Godot object to invoke
|
||||
* @param p_method Name of the method to invoke
|
||||
* @param p_params Parameters to use for method invocation
|
||||
*
|
||||
* @deprecated Use {@link Callable#callDeferred(long, String, Object...)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void calldeferred(long p_id, String p_method, Object[] p_params) {
|
||||
Callable.callDeferred(p_id, p_method, p_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the results from a permission request.
|
||||
* @see Activity#onRequestPermissionsResult(int, String[], int[])
|
||||
* @param p_permission Request permission
|
||||
* @param p_result True if the permission was granted, false otherwise
|
||||
*/
|
||||
public static native void requestPermissionResult(String p_permission, boolean p_result);
|
||||
|
||||
/**
|
||||
* Invoked on the theme light/dark mode change.
|
||||
*/
|
||||
public static native void onNightModeChanged();
|
||||
|
||||
/**
|
||||
* Invoked on the hardware keyboard connected/disconnected.
|
||||
*/
|
||||
public static native void hardwareKeyboardConnected(boolean connected);
|
||||
|
||||
/**
|
||||
* Invoked on the file picker closed.
|
||||
*/
|
||||
public static native void filePickerCallback(boolean p_ok, String[] p_selected_paths);
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread to configure the height of the virtual keyboard.
|
||||
*/
|
||||
public static native void setVirtualKeyboardHeight(int p_height);
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread when the {@link GodotRenderer} has been resumed.
|
||||
* @see GodotRenderer#onActivityResumed()
|
||||
*/
|
||||
public static native void onRendererResumed();
|
||||
|
||||
/**
|
||||
* Invoked on the GL thread when the {@link GodotRenderer} has been paused.
|
||||
* @see GodotRenderer#onActivityPaused()
|
||||
*/
|
||||
public static native void onRendererPaused();
|
||||
|
||||
/**
|
||||
* Invoked when the screen orientation changes.
|
||||
* @param orientation the new screen orientation
|
||||
*/
|
||||
static native void onOrientationChange(int orientation);
|
||||
|
||||
/**
|
||||
* @return true if input must be dispatched from the render thread. If false, input is
|
||||
* dispatched from the UI thread.
|
||||
*/
|
||||
public static native boolean shouldDispatchInputToRenderThread();
|
||||
|
||||
/**
|
||||
* @return the project resource directory
|
||||
*/
|
||||
public static native String getProjectResourceDir();
|
||||
|
||||
static native boolean isEditorHint();
|
||||
|
||||
static native boolean isProjectManagerHint();
|
||||
|
||||
static native boolean hasFeature(String feature);
|
||||
|
||||
static native void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**************************************************************************/
|
||||
/* GodotRenderView.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.input.GodotInputHandler;
|
||||
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public interface GodotRenderView {
|
||||
SurfaceView getView();
|
||||
|
||||
/**
|
||||
* Starts the thread that will drive Godot's rendering.
|
||||
*/
|
||||
void startRenderer();
|
||||
|
||||
/**
|
||||
* Queues a runnable to be run on the rendering thread.
|
||||
*/
|
||||
void queueOnRenderThread(Runnable event);
|
||||
|
||||
void onActivityPaused();
|
||||
|
||||
void onActivityStopped();
|
||||
|
||||
void onActivityResumed();
|
||||
|
||||
void onActivityStarted();
|
||||
|
||||
boolean blockingExitRenderer(long blockingTimeInMs);
|
||||
|
||||
GodotInputHandler getInputHandler();
|
||||
|
||||
void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY);
|
||||
|
||||
void setPointerIcon(int pointerType);
|
||||
|
||||
/**
|
||||
* @return true if pointer capture is supported.
|
||||
*/
|
||||
boolean canCapturePointer();
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
/**************************************************************************/
|
||||
/* GodotVulkanRenderView.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot;
|
||||
|
||||
import org.godotengine.godot.input.GodotInputHandler;
|
||||
import org.godotengine.godot.vulkan.VkRenderer;
|
||||
import org.godotengine.godot.vulkan.VkSurfaceView;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseArray;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
|
||||
private final Godot godot;
|
||||
private final GodotInputHandler mInputHandler;
|
||||
private final VkRenderer mRenderer;
|
||||
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
|
||||
|
||||
public GodotVulkanRenderView(Godot godot, GodotInputHandler inputHandler, boolean shouldBeTranslucent) {
|
||||
super(godot.getContext());
|
||||
|
||||
this.godot = godot;
|
||||
mInputHandler = inputHandler;
|
||||
mRenderer = new VkRenderer();
|
||||
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
|
||||
setFocusableInTouchMode(true);
|
||||
setClickable(false);
|
||||
|
||||
if (shouldBeTranslucent) {
|
||||
this.getHolder().setFormat(PixelFormat.TRANSLUCENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startRenderer() {
|
||||
startRenderer(mRenderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SurfaceView getView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueOnRenderThread(Runnable event) {
|
||||
queueOnVkThread(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused() {
|
||||
queueOnVkThread(() -> {
|
||||
GodotLib.focusout();
|
||||
// Pause the renderer
|
||||
mRenderer.onVkPause();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped() {
|
||||
pauseRenderThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted() {
|
||||
resumeRenderThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed() {
|
||||
queueOnVkThread(() -> {
|
||||
// Resume the renderer
|
||||
mRenderer.onVkResume();
|
||||
GodotLib.focusin();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockingExitRenderer(long blockingTimeInMs) {
|
||||
return requestRenderThreadExitAndWait(blockingTimeInMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GodotInputHandler getInputHandler() {
|
||||
return mInputHandler;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
super.onTouchEvent(event);
|
||||
return mInputHandler.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(final int keyCode, KeyEvent event) {
|
||||
return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, KeyEvent event) {
|
||||
return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCapturedPointerEvent(MotionEvent event) {
|
||||
return mInputHandler.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCapturePointer() {
|
||||
// Pointer capture is not supported on XR devices.
|
||||
return !godot.isXrRuntime() && mInputHandler.canCapturePointer();
|
||||
}
|
||||
@Override
|
||||
public void requestPointerCapture() {
|
||||
if (canCapturePointer()) {
|
||||
super.requestPointerCapture();
|
||||
mInputHandler.onPointerCaptureChange(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePointerCapture() {
|
||||
super.releasePointerCapture();
|
||||
mInputHandler.onPointerCaptureChange(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointerCaptureChange(boolean hasCapture) {
|
||||
super.onPointerCaptureChange(hasCapture);
|
||||
mInputHandler.onPointerCaptureChange(hasCapture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to configure the PointerIcon for the given type.
|
||||
*
|
||||
* Called from JNI
|
||||
*/
|
||||
@Keep
|
||||
@Override
|
||||
public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) {
|
||||
try {
|
||||
Bitmap bitmap = null;
|
||||
if (!TextUtils.isEmpty(imagePath)) {
|
||||
if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) {
|
||||
// Try to load the bitmap from the file system
|
||||
bitmap = BitmapFactory.decodeFile(imagePath);
|
||||
} else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) {
|
||||
// Try to load the bitmap from the assets directory
|
||||
AssetManager am = getContext().getAssets();
|
||||
InputStream imageInputStream = am.open(imagePath);
|
||||
bitmap = BitmapFactory.decodeStream(imageInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY);
|
||||
customPointerIcons.put(pointerType, customPointerIcon);
|
||||
} catch (Exception e) {
|
||||
// Reset the custom pointer icon
|
||||
customPointerIcons.delete(pointerType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called from JNI to change pointer icon
|
||||
*/
|
||||
@Keep
|
||||
@Override
|
||||
public void setPointerIcon(int pointerType) {
|
||||
PointerIcon pointerIcon = customPointerIcons.get(pointerType);
|
||||
if (pointerIcon == null) {
|
||||
pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType);
|
||||
}
|
||||
setPointerIcon(pointerIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) {
|
||||
return getPointerIcon();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/**************************************************************************/
|
||||
/* EditorUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.editor.utils
|
||||
|
||||
/**
|
||||
* Utility class for accessing and using editor specific capabilities.
|
||||
*
|
||||
* This class is only functional on editor builds.
|
||||
*/
|
||||
object EditorUtils {
|
||||
@JvmStatic
|
||||
external fun runScene(scene: String, sceneArgs: Array<String>)
|
||||
|
||||
@JvmStatic
|
||||
external fun toggleTitleBar(visible: Boolean)
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/**************************************************************************/
|
||||
/* GameMenuUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.editor.utils
|
||||
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Utility class for accessing and using game menu APIs.
|
||||
*
|
||||
* This class is only functional on editor builds.
|
||||
*/
|
||||
object GameMenuUtils {
|
||||
private val TAG = GameMenuUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Enum representing the "run/window_placement/game_embed_mode" editor settings.
|
||||
*/
|
||||
enum class GameEmbedMode(internal val nativeValue: Int) {
|
||||
DISABLED(-1), AUTO(0), ENABLED(1);
|
||||
|
||||
companion object {
|
||||
internal const val SETTING_KEY = "run/window_placement/game_embed_mode"
|
||||
|
||||
@JvmStatic
|
||||
internal fun fromNativeValue(nativeValue: Int): GameEmbedMode? {
|
||||
for (mode in GameEmbedMode.entries) {
|
||||
if (mode.nativeValue == nativeValue) {
|
||||
return mode
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun setSuspend(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun nextFrame()
|
||||
|
||||
@JvmStatic
|
||||
external fun setNodeType(type: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectionVisible(visible: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraOverride(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraManipulateMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera2DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera3DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun playMainScene()
|
||||
|
||||
@JvmStatic
|
||||
external fun setDebugMuteAudio(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun resetTimeScale()
|
||||
|
||||
@JvmStatic
|
||||
external fun setTimeScale(scale: Double)
|
||||
|
||||
|
||||
/**
|
||||
* Returns [GameEmbedMode] stored in the editor settings.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun fetchGameEmbedMode(): GameEmbedMode {
|
||||
try {
|
||||
val gameEmbedModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameEmbedMode.SETTING_KEY))
|
||||
val gameEmbedMode = GameEmbedMode.fromNativeValue(gameEmbedModeValue) ?: GameEmbedMode.AUTO
|
||||
return gameEmbedMode
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to retrieve game embed mode", e)
|
||||
return GameEmbedMode.AUTO
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 'game_embed_mode' editor setting.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun saveGameEmbedMode(gameEmbedMode: GameEmbedMode) {
|
||||
GodotLib.setEditorSetting(GameEmbedMode.SETTING_KEY, gameEmbedMode.nativeValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/**************************************************************************/
|
||||
/* Error.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.error
|
||||
|
||||
/**
|
||||
* Godot error list.
|
||||
*
|
||||
* This enum MUST match its native counterpart in 'core/error/error_list.h'
|
||||
*/
|
||||
enum class Error(private val description: String) {
|
||||
OK("OK"), // (0)
|
||||
FAILED("Failed"), ///< Generic fail error
|
||||
ERR_UNAVAILABLE("Unavailable"), ///< What is requested is unsupported/unavailable
|
||||
ERR_UNCONFIGURED("Unconfigured"), ///< The object being used hasn't been properly set up yet
|
||||
ERR_UNAUTHORIZED("Unauthorized"), ///< Missing credentials for requested resource
|
||||
ERR_PARAMETER_RANGE_ERROR("Parameter out of range"), ///< Parameter given out of range (5)
|
||||
ERR_OUT_OF_MEMORY("Out of memory"), ///< Out of memory
|
||||
ERR_FILE_NOT_FOUND("File not found"),
|
||||
ERR_FILE_BAD_DRIVE("File: Bad drive"),
|
||||
ERR_FILE_BAD_PATH("File: Bad path"),
|
||||
ERR_FILE_NO_PERMISSION("File: Permission denied"), // (10)
|
||||
ERR_FILE_ALREADY_IN_USE("File already in use"),
|
||||
ERR_FILE_CANT_OPEN("Can't open file"),
|
||||
ERR_FILE_CANT_WRITE("Can't write file"),
|
||||
ERR_FILE_CANT_READ("Can't read file"),
|
||||
ERR_FILE_UNRECOGNIZED("File unrecognized"), // (15)
|
||||
ERR_FILE_CORRUPT("File corrupt"),
|
||||
ERR_FILE_MISSING_DEPENDENCIES("Missing dependencies for file"),
|
||||
ERR_FILE_EOF("End of file"),
|
||||
ERR_CANT_OPEN("Can't open"), ///< Can't open a resource/socket/file
|
||||
ERR_CANT_CREATE("Can't create"), // (20)
|
||||
ERR_QUERY_FAILED("Query failed"),
|
||||
ERR_ALREADY_IN_USE("Already in use"),
|
||||
ERR_LOCKED("Locked"), ///< resource is locked
|
||||
ERR_TIMEOUT("Timeout"),
|
||||
ERR_CANT_CONNECT("Can't connect"), // (25)
|
||||
ERR_CANT_RESOLVE("Can't resolve"),
|
||||
ERR_CONNECTION_ERROR("Connection error"),
|
||||
ERR_CANT_ACQUIRE_RESOURCE("Can't acquire resource"),
|
||||
ERR_CANT_FORK("Can't fork"),
|
||||
ERR_INVALID_DATA("Invalid data"), ///< Data passed is invalid (30)
|
||||
ERR_INVALID_PARAMETER("Invalid parameter"), ///< Parameter passed is invalid
|
||||
ERR_ALREADY_EXISTS("Already exists"), ///< When adding, item already exists
|
||||
ERR_DOES_NOT_EXIST("Does not exist"), ///< When retrieving/erasing, if item does not exist
|
||||
ERR_DATABASE_CANT_READ("Can't read database"), ///< database is full
|
||||
ERR_DATABASE_CANT_WRITE("Can't write database"), ///< database is full (35)
|
||||
ERR_COMPILATION_FAILED("Compilation failed"),
|
||||
ERR_METHOD_NOT_FOUND("Method not found"),
|
||||
ERR_LINK_FAILED("Link failed"),
|
||||
ERR_SCRIPT_FAILED("Script failed"),
|
||||
ERR_CYCLIC_LINK("Cyclic link detected"), // (40)
|
||||
ERR_INVALID_DECLARATION("Invalid declaration"),
|
||||
ERR_DUPLICATE_SYMBOL("Duplicate symbol"),
|
||||
ERR_PARSE_ERROR("Parse error"),
|
||||
ERR_BUSY("Busy"),
|
||||
ERR_SKIP("Skip"), // (45)
|
||||
ERR_HELP("Help"), ///< user requested help!!
|
||||
ERR_BUG("Bug"), ///< a bug in the software certainly happened, due to a double check failing or unexpected behavior.
|
||||
ERR_PRINTER_ON_FIRE("Printer on fire"); /// the parallel port printer is engulfed in flames
|
||||
|
||||
companion object {
|
||||
internal fun fromNativeValue(nativeValue: Int): Error? {
|
||||
return Error.entries.getOrNull(nativeValue)
|
||||
}
|
||||
}
|
||||
|
||||
fun toNativeValue(): Int = this.ordinal
|
||||
|
||||
override fun toString(): String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/**************************************************************************/
|
||||
/* PictureInPictureProvider.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.feature
|
||||
|
||||
/**
|
||||
* Provides APIs to enable picture-in-picture.
|
||||
*/
|
||||
interface PictureInPictureProvider {
|
||||
|
||||
fun enterPiPMode()
|
||||
|
||||
fun isPiPModeSupported(): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
// clang-format off
|
||||
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.godotengine.godot.gl;
|
||||
|
||||
import android.opengl.GLDebugHelper;
|
||||
import android.opengl.GLException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGL11;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
|
||||
class EGLLogWrapper implements EGL11 {
|
||||
private EGL10 mEgl10;
|
||||
Writer mLog;
|
||||
boolean mLogArgumentNames;
|
||||
boolean mCheckError;
|
||||
private int mArgCount;
|
||||
|
||||
|
||||
public EGLLogWrapper(EGL egl, int configFlags, Writer log) {
|
||||
mEgl10 = (EGL10) egl;
|
||||
mLog = log;
|
||||
mLogArgumentNames =
|
||||
(GLDebugHelper.CONFIG_LOG_ARGUMENT_NAMES & configFlags) != 0;
|
||||
mCheckError =
|
||||
(GLDebugHelper.CONFIG_CHECK_GL_ERROR & configFlags) != 0;
|
||||
}
|
||||
|
||||
public boolean eglChooseConfig(EGLDisplay display, int[] attrib_list,
|
||||
EGLConfig[] configs, int config_size, int[] num_config) {
|
||||
begin("eglChooseConfig");
|
||||
arg("display", display);
|
||||
arg("attrib_list", attrib_list);
|
||||
arg("config_size", config_size);
|
||||
end();
|
||||
|
||||
boolean result = mEgl10.eglChooseConfig(display, attrib_list, configs,
|
||||
config_size, num_config);
|
||||
arg("configs", configs);
|
||||
arg("num_config", num_config);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglCopyBuffers(EGLDisplay display, EGLSurface surface,
|
||||
Object native_pixmap) {
|
||||
begin("eglCopyBuffers");
|
||||
arg("display", display);
|
||||
arg("surface", surface);
|
||||
arg("native_pixmap", native_pixmap);
|
||||
end();
|
||||
|
||||
boolean result = mEgl10.eglCopyBuffers(display, surface, native_pixmap);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLContext eglCreateContext(EGLDisplay display, EGLConfig config,
|
||||
EGLContext share_context, int[] attrib_list) {
|
||||
begin("eglCreateContext");
|
||||
arg("display", display);
|
||||
arg("config", config);
|
||||
arg("share_context", share_context);
|
||||
arg("attrib_list", attrib_list);
|
||||
end();
|
||||
|
||||
EGLContext result = mEgl10.eglCreateContext(display, config,
|
||||
share_context, attrib_list);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLSurface eglCreatePbufferSurface(EGLDisplay display,
|
||||
EGLConfig config, int[] attrib_list) {
|
||||
begin("eglCreatePbufferSurface");
|
||||
arg("display", display);
|
||||
arg("config", config);
|
||||
arg("attrib_list", attrib_list);
|
||||
end();
|
||||
|
||||
EGLSurface result = mEgl10.eglCreatePbufferSurface(display, config,
|
||||
attrib_list);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLSurface eglCreatePixmapSurface(EGLDisplay display,
|
||||
EGLConfig config, Object native_pixmap, int[] attrib_list) {
|
||||
begin("eglCreatePixmapSurface");
|
||||
arg("display", display);
|
||||
arg("config", config);
|
||||
arg("native_pixmap", native_pixmap);
|
||||
arg("attrib_list", attrib_list);
|
||||
end();
|
||||
|
||||
EGLSurface result = mEgl10.eglCreatePixmapSurface(display, config,
|
||||
native_pixmap, attrib_list);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLSurface eglCreateWindowSurface(EGLDisplay display,
|
||||
EGLConfig config, Object native_window, int[] attrib_list) {
|
||||
begin("eglCreateWindowSurface");
|
||||
arg("display", display);
|
||||
arg("config", config);
|
||||
arg("native_window", native_window);
|
||||
arg("attrib_list", attrib_list);
|
||||
end();
|
||||
|
||||
EGLSurface result = mEgl10.eglCreateWindowSurface(display, config,
|
||||
native_window, attrib_list);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglDestroyContext(EGLDisplay display, EGLContext context) {
|
||||
begin("eglDestroyContext");
|
||||
arg("display", display);
|
||||
arg("context", context);
|
||||
end();
|
||||
|
||||
boolean result = mEgl10.eglDestroyContext(display, context);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglDestroySurface(EGLDisplay display, EGLSurface surface) {
|
||||
begin("eglDestroySurface");
|
||||
arg("display", display);
|
||||
arg("surface", surface);
|
||||
end();
|
||||
|
||||
boolean result = mEgl10.eglDestroySurface(display, surface);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config,
|
||||
int attribute, int[] value) {
|
||||
begin("eglGetConfigAttrib");
|
||||
arg("display", display);
|
||||
arg("config", config);
|
||||
arg("attribute", attribute);
|
||||
end();
|
||||
boolean result = mEgl10.eglGetConfigAttrib(display, config, attribute,
|
||||
value);
|
||||
arg("value", value);
|
||||
returns(result);
|
||||
checkError();
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean eglGetConfigs(EGLDisplay display, EGLConfig[] configs,
|
||||
int config_size, int[] num_config) {
|
||||
begin("eglGetConfigs");
|
||||
arg("display", display);
|
||||
arg("config_size", config_size);
|
||||
end();
|
||||
|
||||
boolean result = mEgl10.eglGetConfigs(display, configs, config_size,
|
||||
num_config);
|
||||
arg("configs", configs);
|
||||
arg("num_config", num_config);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLContext eglGetCurrentContext() {
|
||||
begin("eglGetCurrentContext");
|
||||
end();
|
||||
|
||||
EGLContext result = mEgl10.eglGetCurrentContext();
|
||||
returns(result);
|
||||
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLDisplay eglGetCurrentDisplay() {
|
||||
begin("eglGetCurrentDisplay");
|
||||
end();
|
||||
|
||||
EGLDisplay result = mEgl10.eglGetCurrentDisplay();
|
||||
returns(result);
|
||||
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLSurface eglGetCurrentSurface(int readdraw) {
|
||||
begin("eglGetCurrentSurface");
|
||||
arg("readdraw", readdraw);
|
||||
end();
|
||||
|
||||
EGLSurface result = mEgl10.eglGetCurrentSurface(readdraw);
|
||||
returns(result);
|
||||
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public EGLDisplay eglGetDisplay(Object native_display) {
|
||||
begin("eglGetDisplay");
|
||||
arg("native_display", native_display);
|
||||
end();
|
||||
|
||||
EGLDisplay result = mEgl10.eglGetDisplay(native_display);
|
||||
returns(result);
|
||||
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public int eglGetError() {
|
||||
begin("eglGetError");
|
||||
end();
|
||||
|
||||
int result = mEgl10.eglGetError();
|
||||
returns(getErrorString(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglInitialize(EGLDisplay display, int[] major_minor) {
|
||||
begin("eglInitialize");
|
||||
arg("display", display);
|
||||
end();
|
||||
boolean result = mEgl10.eglInitialize(display, major_minor);
|
||||
returns(result);
|
||||
arg("major_minor", major_minor);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglMakeCurrent(EGLDisplay display, EGLSurface draw,
|
||||
EGLSurface read, EGLContext context) {
|
||||
begin("eglMakeCurrent");
|
||||
arg("display", display);
|
||||
arg("draw", draw);
|
||||
arg("read", read);
|
||||
arg("context", context);
|
||||
end();
|
||||
boolean result = mEgl10.eglMakeCurrent(display, draw, read, context);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglQueryContext(EGLDisplay display, EGLContext context,
|
||||
int attribute, int[] value) {
|
||||
begin("eglQueryContext");
|
||||
arg("display", display);
|
||||
arg("context", context);
|
||||
arg("attribute", attribute);
|
||||
end();
|
||||
boolean result = mEgl10.eglQueryContext(display, context, attribute,
|
||||
value);
|
||||
returns(value[0]);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public String eglQueryString(EGLDisplay display, int name) {
|
||||
begin("eglQueryString");
|
||||
arg("display", display);
|
||||
arg("name", name);
|
||||
end();
|
||||
String result = mEgl10.eglQueryString(display, name);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglQuerySurface(EGLDisplay display, EGLSurface surface,
|
||||
int attribute, int[] value) {
|
||||
begin("eglQuerySurface");
|
||||
arg("display", display);
|
||||
arg("surface", surface);
|
||||
arg("attribute", attribute);
|
||||
end();
|
||||
boolean result = mEgl10.eglQuerySurface(display, surface, attribute,
|
||||
value);
|
||||
returns(value[0]);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) {
|
||||
begin("eglSwapBuffers");
|
||||
arg("display", display);
|
||||
arg("surface", surface);
|
||||
end();
|
||||
boolean result = mEgl10.eglSwapBuffers(display, surface);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglTerminate(EGLDisplay display) {
|
||||
begin("eglTerminate");
|
||||
arg("display", display);
|
||||
end();
|
||||
boolean result = mEgl10.eglTerminate(display);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglWaitGL() {
|
||||
begin("eglWaitGL");
|
||||
end();
|
||||
boolean result = mEgl10.eglWaitGL();
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean eglWaitNative(int engine, Object bindTarget) {
|
||||
begin("eglWaitNative");
|
||||
arg("engine", engine);
|
||||
arg("bindTarget", bindTarget);
|
||||
end();
|
||||
boolean result = mEgl10.eglWaitNative(engine, bindTarget);
|
||||
returns(result);
|
||||
checkError();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void checkError() {
|
||||
int eglError;
|
||||
if ((eglError = mEgl10.eglGetError()) != EGL_SUCCESS) {
|
||||
String errorMessage = "eglError: " + getErrorString(eglError);
|
||||
logLine(errorMessage);
|
||||
if (mCheckError) {
|
||||
throw new GLException(eglError, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logLine(String message) {
|
||||
log(message + '\n');
|
||||
}
|
||||
|
||||
private void log(String message) {
|
||||
try {
|
||||
mLog.write(message);
|
||||
} catch (IOException e) {
|
||||
// Ignore exception, keep on trying
|
||||
}
|
||||
}
|
||||
|
||||
private void begin(String name) {
|
||||
log(name + '(');
|
||||
mArgCount = 0;
|
||||
}
|
||||
|
||||
private void arg(String name, String value) {
|
||||
if (mArgCount++ > 0) {
|
||||
log(", ");
|
||||
}
|
||||
if (mLogArgumentNames) {
|
||||
log(name + "=");
|
||||
}
|
||||
log(value);
|
||||
}
|
||||
|
||||
private void end() {
|
||||
log(");\n");
|
||||
flush();
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
try {
|
||||
mLog.flush();
|
||||
} catch (IOException e) {
|
||||
mLog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void arg(String name, int value) {
|
||||
arg(name, Integer.toString(value));
|
||||
}
|
||||
|
||||
private void arg(String name, Object object) {
|
||||
arg(name, toString(object));
|
||||
}
|
||||
|
||||
private void arg(String name, EGLDisplay object) {
|
||||
if (object == EGL10.EGL_DEFAULT_DISPLAY) {
|
||||
arg(name, "EGL10.EGL_DEFAULT_DISPLAY");
|
||||
} else if (object == EGL_NO_DISPLAY) {
|
||||
arg(name, "EGL10.EGL_NO_DISPLAY");
|
||||
} else {
|
||||
arg(name, toString(object));
|
||||
}
|
||||
}
|
||||
|
||||
private void arg(String name, EGLContext object) {
|
||||
if (object == EGL10.EGL_NO_CONTEXT) {
|
||||
arg(name, "EGL10.EGL_NO_CONTEXT");
|
||||
} else {
|
||||
arg(name, toString(object));
|
||||
}
|
||||
}
|
||||
|
||||
private void arg(String name, EGLSurface object) {
|
||||
if (object == EGL10.EGL_NO_SURFACE) {
|
||||
arg(name, "EGL10.EGL_NO_SURFACE");
|
||||
} else {
|
||||
arg(name, toString(object));
|
||||
}
|
||||
}
|
||||
|
||||
private void returns(String result) {
|
||||
log(" returns " + result + ";\n");
|
||||
flush();
|
||||
}
|
||||
|
||||
private void returns(int result) {
|
||||
returns(Integer.toString(result));
|
||||
}
|
||||
|
||||
private void returns(boolean result) {
|
||||
returns(Boolean.toString(result));
|
||||
}
|
||||
|
||||
private void returns(Object result) {
|
||||
returns(toString(result));
|
||||
}
|
||||
|
||||
private String toString(Object obj) {
|
||||
if (obj == null) {
|
||||
return "null";
|
||||
} else {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private void arg(String name, int[] arr) {
|
||||
if (arr == null) {
|
||||
arg(name, "null");
|
||||
} else {
|
||||
arg(name, toString(arr.length, arr, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private void arg(String name, Object[] arr) {
|
||||
if (arr == null) {
|
||||
arg(name, "null");
|
||||
} else {
|
||||
arg(name, toString(arr.length, arr, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private String toString(int n, int[] arr, int offset) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("{\n");
|
||||
int arrLen = arr.length;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int index = offset + i;
|
||||
buf.append(" [" + index + "] = ");
|
||||
if (index < 0 || index >= arrLen) {
|
||||
buf.append("out of bounds");
|
||||
} else {
|
||||
buf.append(arr[index]);
|
||||
}
|
||||
buf.append('\n');
|
||||
}
|
||||
buf.append("}");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private String toString(int n, Object[] arr, int offset) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append("{\n");
|
||||
int arrLen = arr.length;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int index = offset + i;
|
||||
buf.append(" [" + index + "] = ");
|
||||
if (index < 0 || index >= arrLen) {
|
||||
buf.append("out of bounds");
|
||||
} else {
|
||||
buf.append(arr[index]);
|
||||
}
|
||||
buf.append('\n');
|
||||
}
|
||||
buf.append("}");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private static String getHex(int value) {
|
||||
return "0x" + Integer.toHexString(value);
|
||||
}
|
||||
|
||||
public static String getErrorString(int error) {
|
||||
switch (error) {
|
||||
case EGL_SUCCESS:
|
||||
return "EGL_SUCCESS";
|
||||
case EGL_NOT_INITIALIZED:
|
||||
return "EGL_NOT_INITIALIZED";
|
||||
case EGL_BAD_ACCESS:
|
||||
return "EGL_BAD_ACCESS";
|
||||
case EGL_BAD_ALLOC:
|
||||
return "EGL_BAD_ALLOC";
|
||||
case EGL_BAD_ATTRIBUTE:
|
||||
return "EGL_BAD_ATTRIBUTE";
|
||||
case EGL_BAD_CONFIG:
|
||||
return "EGL_BAD_CONFIG";
|
||||
case EGL_BAD_CONTEXT:
|
||||
return "EGL_BAD_CONTEXT";
|
||||
case EGL_BAD_CURRENT_SURFACE:
|
||||
return "EGL_BAD_CURRENT_SURFACE";
|
||||
case EGL_BAD_DISPLAY:
|
||||
return "EGL_BAD_DISPLAY";
|
||||
case EGL_BAD_MATCH:
|
||||
return "EGL_BAD_MATCH";
|
||||
case EGL_BAD_NATIVE_PIXMAP:
|
||||
return "EGL_BAD_NATIVE_PIXMAP";
|
||||
case EGL_BAD_NATIVE_WINDOW:
|
||||
return "EGL_BAD_NATIVE_WINDOW";
|
||||
case EGL_BAD_PARAMETER:
|
||||
return "EGL_BAD_PARAMETER";
|
||||
case EGL_BAD_SURFACE:
|
||||
return "EGL_BAD_SURFACE";
|
||||
case EGL11.EGL_CONTEXT_LOST:
|
||||
return "EGL_CONTEXT_LOST";
|
||||
default:
|
||||
return getHex(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,98 @@
|
|||
/**************************************************************************/
|
||||
/* GodotRenderer.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.gl;
|
||||
|
||||
import org.godotengine.godot.GodotLib;
|
||||
import org.godotengine.godot.plugin.GodotPlugin;
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Godot's GL renderer implementation.
|
||||
*/
|
||||
public class GodotRenderer implements GLSurfaceView.Renderer {
|
||||
private final String TAG = GodotRenderer.class.getSimpleName();
|
||||
|
||||
private final GodotPluginRegistry pluginRegistry;
|
||||
private boolean activityJustResumed = false;
|
||||
|
||||
public GodotRenderer() {
|
||||
this.pluginRegistry = GodotPluginRegistry.getPluginRegistry();
|
||||
}
|
||||
|
||||
public boolean onDrawFrame(GL10 gl) {
|
||||
if (activityJustResumed) {
|
||||
GodotLib.onRendererResumed();
|
||||
activityJustResumed = false;
|
||||
}
|
||||
|
||||
boolean swapBuffers = GodotLib.step();
|
||||
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
|
||||
plugin.onGLDrawFrame(gl);
|
||||
}
|
||||
|
||||
return swapBuffers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderThreadExiting() {
|
||||
Log.d(TAG, "Destroying Godot Engine");
|
||||
GodotLib.ondestroy();
|
||||
}
|
||||
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
GodotLib.resize(null, width, height);
|
||||
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
|
||||
plugin.onGLSurfaceChanged(gl, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
GodotLib.newcontext(null);
|
||||
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
|
||||
plugin.onGLSurfaceCreated(gl, config);
|
||||
}
|
||||
}
|
||||
|
||||
public void onActivityResumed() {
|
||||
// We defer invoking GodotLib.onRendererResumed() until the first draw frame call.
|
||||
// This ensures we have a valid GL context and surface when we do so.
|
||||
activityJustResumed = true;
|
||||
}
|
||||
|
||||
public void onActivityPaused() {
|
||||
GodotLib.onRendererPaused();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
/**************************************************************************/
|
||||
/* GodotEditText.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input;
|
||||
|
||||
import org.godotengine.godot.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.InputFilter;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.DigitsKeyListener;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Locale;
|
||||
|
||||
public class GodotEditText extends EditText {
|
||||
// ===========================================================
|
||||
// Constants
|
||||
// ===========================================================
|
||||
private final static int HANDLER_OPEN_IME_KEYBOARD = 2;
|
||||
private final static int HANDLER_CLOSE_IME_KEYBOARD = 3;
|
||||
|
||||
// Enum must be kept up-to-date with DisplayServerEnums::VirtualKeyboardType
|
||||
public enum VirtualKeyboardType {
|
||||
KEYBOARD_TYPE_DEFAULT,
|
||||
KEYBOARD_TYPE_MULTILINE,
|
||||
KEYBOARD_TYPE_NUMBER,
|
||||
KEYBOARD_TYPE_NUMBER_DECIMAL,
|
||||
KEYBOARD_TYPE_PHONE,
|
||||
KEYBOARD_TYPE_EMAIL_ADDRESS,
|
||||
KEYBOARD_TYPE_PASSWORD,
|
||||
KEYBOARD_TYPE_URL
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Fields
|
||||
// ===========================================================
|
||||
private GodotRenderView mRenderView;
|
||||
private GodotTextInputWrapper mInputWrapper;
|
||||
private EditHandler sHandler = new EditHandler(this);
|
||||
private String mOriginText;
|
||||
private int mMaxInputLength = Integer.MAX_VALUE;
|
||||
private VirtualKeyboardType mKeyboardType = VirtualKeyboardType.KEYBOARD_TYPE_DEFAULT;
|
||||
|
||||
private static class EditHandler extends Handler {
|
||||
private final WeakReference<GodotEditText> mEdit;
|
||||
public EditHandler(GodotEditText edit) {
|
||||
mEdit = new WeakReference<>(edit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
GodotEditText edit = mEdit.get();
|
||||
if (edit != null) {
|
||||
edit.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Constructors
|
||||
// ===========================================================
|
||||
public GodotEditText(final Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
}
|
||||
|
||||
public GodotEditText(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView();
|
||||
}
|
||||
|
||||
public GodotEditText(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initView();
|
||||
}
|
||||
|
||||
protected void initView() {
|
||||
setPadding(0, 0, 0, 0);
|
||||
setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE);
|
||||
}
|
||||
|
||||
public VirtualKeyboardType getKeyboardType() {
|
||||
return mKeyboardType;
|
||||
}
|
||||
|
||||
private void handleMessage(final Message msg) {
|
||||
switch (msg.what) {
|
||||
case HANDLER_OPEN_IME_KEYBOARD: {
|
||||
GodotEditText edit = (GodotEditText)msg.obj;
|
||||
String text = edit.mOriginText;
|
||||
if (edit.requestFocus()) {
|
||||
edit.removeTextChangedListener(edit.mInputWrapper);
|
||||
setMaxInputLength(edit);
|
||||
edit.setText("");
|
||||
edit.append(text);
|
||||
if (msg.arg2 != -1) {
|
||||
int selectionStart = Math.min(msg.arg1, edit.length());
|
||||
int selectionEnd = Math.min(msg.arg2, edit.length());
|
||||
edit.setSelection(selectionStart, selectionEnd);
|
||||
edit.mInputWrapper.setSelection(true);
|
||||
} else {
|
||||
edit.mInputWrapper.setSelection(false);
|
||||
}
|
||||
|
||||
int inputType = InputType.TYPE_CLASS_TEXT;
|
||||
String acceptCharacters = null;
|
||||
switch (edit.getKeyboardType()) {
|
||||
case KEYBOARD_TYPE_DEFAULT:
|
||||
inputType = InputType.TYPE_CLASS_TEXT;
|
||||
break;
|
||||
case KEYBOARD_TYPE_MULTILINE:
|
||||
inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
|
||||
break;
|
||||
case KEYBOARD_TYPE_NUMBER:
|
||||
inputType = InputType.TYPE_CLASS_NUMBER;
|
||||
break;
|
||||
case KEYBOARD_TYPE_NUMBER_DECIMAL:
|
||||
inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL;
|
||||
acceptCharacters = "0123456789,.- ";
|
||||
break;
|
||||
case KEYBOARD_TYPE_PHONE:
|
||||
inputType = InputType.TYPE_CLASS_PHONE;
|
||||
break;
|
||||
case KEYBOARD_TYPE_EMAIL_ADDRESS:
|
||||
inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
|
||||
break;
|
||||
case KEYBOARD_TYPE_PASSWORD:
|
||||
inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
|
||||
break;
|
||||
case KEYBOARD_TYPE_URL:
|
||||
inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
|
||||
break;
|
||||
}
|
||||
edit.setInputType(inputType);
|
||||
|
||||
if (!TextUtils.isEmpty(acceptCharacters)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
edit.setKeyListener(DigitsKeyListener.getInstance(Locale.getDefault(), true, true));
|
||||
} else {
|
||||
edit.setKeyListener(DigitsKeyListener.getInstance(acceptCharacters));
|
||||
}
|
||||
}
|
||||
|
||||
edit.mInputWrapper.setOriginText(text);
|
||||
edit.addTextChangedListener(edit.mInputWrapper);
|
||||
final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(edit, 0);
|
||||
}
|
||||
} break;
|
||||
|
||||
case HANDLER_CLOSE_IME_KEYBOARD: {
|
||||
GodotEditText edit = (GodotEditText)msg.obj;
|
||||
|
||||
edit.removeTextChangedListener(mInputWrapper);
|
||||
final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
|
||||
edit.mRenderView.getView().requestFocus();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setMaxInputLength(EditText p_edit_text) {
|
||||
InputFilter[] filters = new InputFilter[1];
|
||||
filters[0] = new InputFilter.LengthFilter(this.mMaxInputLength);
|
||||
p_edit_text.setFilters(filters);
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Getter & Setter
|
||||
// ===========================================================
|
||||
public void setView(final GodotRenderView view) {
|
||||
mRenderView = view;
|
||||
if (mInputWrapper == null)
|
||||
mInputWrapper = new GodotTextInputWrapper(mRenderView, this);
|
||||
setOnEditorActionListener(mInputWrapper);
|
||||
view.getView().requestFocus();
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Methods for/from SuperClass/Interfaces
|
||||
// ===========================================================
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
|
||||
/* Let SurfaceView get focus if back key is input. */
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
// Clear focus from EditText immediately
|
||||
clearFocus();
|
||||
|
||||
// Transfer focus to render view
|
||||
mRenderView.getView().requestFocus();
|
||||
|
||||
// Forward this back key event to the render view's input handler
|
||||
// since we're no longer the focused view
|
||||
return mRenderView.getInputHandler().onKeyDown(keyCode, keyEvent);
|
||||
}
|
||||
|
||||
// When a hardware keyboard is connected, all key events come through so we can route them
|
||||
// directly to the engine.
|
||||
// This is not the case when using a soft keyboard, requiring extra processing from this class.
|
||||
if (hasHardwareKeyboard()) {
|
||||
return mRenderView.getInputHandler().onKeyDown(keyCode, keyEvent);
|
||||
}
|
||||
|
||||
// pass event to godot in special cases
|
||||
if (needHandlingInGodot(keyCode, keyEvent) && mRenderView.getInputHandler().onKeyDown(keyCode, keyEvent)) {
|
||||
return true;
|
||||
} else {
|
||||
return super.onKeyDown(keyCode, keyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
|
||||
// When a hardware keyboard is connected, all key events come through so we can route them
|
||||
// directly to the engine.
|
||||
// This is not the case when using a soft keyboard, requiring extra processing from this class.
|
||||
if (hasHardwareKeyboard()) {
|
||||
return mRenderView.getInputHandler().onKeyUp(keyCode, keyEvent);
|
||||
}
|
||||
|
||||
// If this is a BACK key and we don't have focus anymore, forward to render view
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && !hasFocus()) {
|
||||
return mRenderView.getInputHandler().onKeyUp(keyCode, keyEvent);
|
||||
}
|
||||
|
||||
if (needHandlingInGodot(keyCode, keyEvent) && mRenderView.getInputHandler().onKeyUp(keyCode, keyEvent)) {
|
||||
return true;
|
||||
} else {
|
||||
return super.onKeyUp(keyCode, keyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needHandlingInGodot(int keyCode, KeyEvent keyEvent) {
|
||||
boolean isArrowKey = keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
|
||||
keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
boolean isModifiedKey = keyEvent.isAltPressed() || keyEvent.isCtrlPressed() || keyEvent.isSymPressed() ||
|
||||
keyEvent.isFunctionPressed() || keyEvent.isMetaPressed();
|
||||
return isArrowKey || keyCode == KeyEvent.KEYCODE_TAB || KeyEvent.isModifierKey(keyCode) ||
|
||||
isModifiedKey;
|
||||
}
|
||||
|
||||
public boolean hasHardwareKeyboard() {
|
||||
return mRenderView.getInputHandler().hasHardwareKeyboard();
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Methods
|
||||
// ===========================================================
|
||||
public void showKeyboard(String p_existing_text, VirtualKeyboardType p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
|
||||
if (hasHardwareKeyboard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cursorStart = p_cursor_start;
|
||||
int cursorEnd = p_cursor_end;
|
||||
int maxInputLength = (p_max_input_length <= 0) ? Integer.MAX_VALUE : p_max_input_length;
|
||||
if (cursorStart == -1) { // cursor position not given
|
||||
this.mOriginText = p_existing_text;
|
||||
this.mMaxInputLength = maxInputLength;
|
||||
} else if (cursorEnd == -1) { // not text selection
|
||||
cursorStart = Math.min(p_existing_text.length(), cursorStart);
|
||||
this.mOriginText = p_existing_text.substring(0, cursorStart);
|
||||
this.mMaxInputLength = maxInputLength - (p_existing_text.length() - cursorStart);
|
||||
} else {
|
||||
cursorEnd = Math.min(p_existing_text.length(), cursorEnd);
|
||||
this.mOriginText = p_existing_text.substring(0, cursorEnd);
|
||||
this.mMaxInputLength = maxInputLength - (p_existing_text.length() - cursorEnd);
|
||||
}
|
||||
|
||||
this.mKeyboardType = p_type;
|
||||
|
||||
final Message msg = new Message();
|
||||
msg.what = HANDLER_OPEN_IME_KEYBOARD;
|
||||
msg.obj = this;
|
||||
msg.arg1 = cursorStart;
|
||||
msg.arg2 = cursorEnd;
|
||||
sHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
public void hideKeyboard() {
|
||||
if (hasHardwareKeyboard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Message msg = new Message();
|
||||
msg.what = HANDLER_CLOSE_IME_KEYBOARD;
|
||||
msg.obj = this;
|
||||
sHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Inner and Anonymous Classes
|
||||
// ===========================================================
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
/**************************************************************************/
|
||||
/* GodotGestureHandler.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input
|
||||
|
||||
import android.os.Build
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import android.view.InputDevice
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.ScaleGestureDetector.OnScaleGestureListener
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Handles regular and scale gesture input related events for the [GodotView] view.
|
||||
*
|
||||
* @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener
|
||||
* @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener
|
||||
*/
|
||||
internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotGestureHandler::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable pan and scale gestures
|
||||
*/
|
||||
var panningAndScalingEnabled = false
|
||||
|
||||
var scrollDeadzoneDisabled = false
|
||||
|
||||
/**
|
||||
* Enable haptic feedback on long-press right-click
|
||||
*/
|
||||
var hapticFeedbackEnabled = false
|
||||
|
||||
private var nextDownIsDoubleTap = false
|
||||
private var dragInProgress = false
|
||||
private var scaleInProgress = false
|
||||
private var contextClickInProgress = false
|
||||
private var pointerCaptureInProgress = false
|
||||
|
||||
private var lastDragX: Float = 0.0f
|
||||
private var lastDragY: Float = 0.0f
|
||||
|
||||
override fun onDown(event: MotionEvent): Boolean {
|
||||
inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap)
|
||||
nextDownIsDoubleTap = false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(event: MotionEvent): Boolean {
|
||||
inputHandler.handleMotionEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent) {
|
||||
val toolType = GodotInputHandler.getEventToolType(event)
|
||||
if (toolType != MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
if (hapticFeedbackEnabled) {
|
||||
inputHandler.performHapticFeedback()
|
||||
}
|
||||
contextClickRouter(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun contextClickRouter(event: MotionEvent) {
|
||||
if (scaleInProgress || nextDownIsDoubleTap) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel the previous down event
|
||||
inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL)
|
||||
|
||||
// Turn a context click into a single tap right mouse button click.
|
||||
inputHandler.handleMouseEvent(
|
||||
event,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.BUTTON_SECONDARY,
|
||||
false
|
||||
)
|
||||
contextClickInProgress = true
|
||||
}
|
||||
|
||||
fun onPointerCaptureChange(hasCapture: Boolean) {
|
||||
if (pointerCaptureInProgress == hasCapture) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasCapture) {
|
||||
// Dispatch a mouse relative ACTION_UP event to signal the end of the capture
|
||||
inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true)
|
||||
}
|
||||
pointerCaptureInProgress = hasCapture
|
||||
}
|
||||
|
||||
fun onMotionEvent(event: MotionEvent): Boolean {
|
||||
return when (event.actionMasked) {
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
|
||||
onActionUp(event)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
onActionMove(event)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onActionUp(event: MotionEvent): Boolean {
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL && pointerCaptureInProgress) {
|
||||
// Don't dispatch the ACTION_CANCEL while a capture is in progress
|
||||
return true
|
||||
}
|
||||
|
||||
if (pointerCaptureInProgress || dragInProgress || contextClickInProgress) {
|
||||
if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) {
|
||||
// This may be an ACTION_BUTTON_RELEASE event which we don't handle,
|
||||
// so we convert it to an ACTION_UP event.
|
||||
inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP)
|
||||
} else {
|
||||
inputHandler.handleTouchEvent(event)
|
||||
}
|
||||
pointerCaptureInProgress = false
|
||||
dragInProgress = false
|
||||
contextClickInProgress = false
|
||||
lastDragX = 0.0f
|
||||
lastDragY = 0.0f
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onActionMove(event: MotionEvent): Boolean {
|
||||
if (contextClickInProgress) {
|
||||
inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false)
|
||||
return true
|
||||
} else if (scrollDeadzoneDisabled && !scaleInProgress) {
|
||||
// The 'onScroll' event is triggered with a long delay.
|
||||
// Force the 'InputEventScreenDrag' event earlier here.
|
||||
// We don't toggle 'dragInProgress' here so that the scaling logic can override the drag operation if needed.
|
||||
// Once the 'onScroll' event kicks-in, 'dragInProgress' will be properly set.
|
||||
if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) {
|
||||
lastDragX = event.getX(0)
|
||||
lastDragY = event.getY(0)
|
||||
inputHandler.handleMotionEvent(event)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTapEvent(event: MotionEvent): Boolean {
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||
nextDownIsDoubleTap = false
|
||||
inputHandler.handleMotionEvent(event)
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) {
|
||||
inputHandler.handleMotionEvent(event)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDoubleTap(event: MotionEvent): Boolean {
|
||||
nextDownIsDoubleTap = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
originEvent: MotionEvent?,
|
||||
terminusEvent: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
if (scaleInProgress) {
|
||||
if (dragInProgress || (scrollDeadzoneDisabled && (lastDragX != 0.0f || lastDragY != 0.0f))) {
|
||||
if (originEvent != null) {
|
||||
// Cancel the drag
|
||||
inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL)
|
||||
}
|
||||
dragInProgress = false
|
||||
lastDragX = 0.0f
|
||||
lastDragY = 0.0f
|
||||
}
|
||||
}
|
||||
|
||||
val x = terminusEvent.x
|
||||
val y = terminusEvent.y
|
||||
if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) {
|
||||
inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f)
|
||||
} else if (!scaleInProgress) {
|
||||
dragInProgress = true
|
||||
lastDragX = terminusEvent.getX(0)
|
||||
lastDragY = terminusEvent.getY(0)
|
||||
inputHandler.handleMotionEvent(terminusEvent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) {
|
||||
inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) {
|
||||
return false
|
||||
}
|
||||
scaleInProgress = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
||||
scaleInProgress = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,863 @@
|
|||
/**************************************************************************/
|
||||
/* GodotInputHandler.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input;
|
||||
|
||||
import static org.godotengine.godot.utils.GLUtils.DEBUG;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
import org.godotengine.godot.GodotLib;
|
||||
import org.godotengine.godot.GodotRenderView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Handles input related events for the {@link GodotRenderView} view.
|
||||
*/
|
||||
public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener {
|
||||
private static final String TAG = GodotInputHandler.class.getSimpleName();
|
||||
|
||||
private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
|
||||
private static final int ROTARY_INPUT_HORIZONTAL_AXIS = 0;
|
||||
|
||||
private static final String[] JOYPAD_IGNORE_LIST = new String[] {
|
||||
// Ignore fingerprint scanners.
|
||||
"uinput-fpc",
|
||||
"uinput-goodix",
|
||||
"uinput-synaptics",
|
||||
"uinput-elan",
|
||||
"uinput-vfs",
|
||||
"uinput-atrus",
|
||||
};
|
||||
|
||||
private final SparseIntArray mJoystickIds = new SparseIntArray(4);
|
||||
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
|
||||
private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>();
|
||||
|
||||
private final Godot godot;
|
||||
private final InputManager mInputManager;
|
||||
private final WindowManager windowManager;
|
||||
private final GestureDetector gestureDetector;
|
||||
private final ScaleGestureDetector scaleGestureDetector;
|
||||
private final GodotGestureHandler godotGestureHandler;
|
||||
|
||||
/**
|
||||
* Used to decide whether mouse capture can be enabled.
|
||||
*/
|
||||
private AtomicInteger lastSeenToolType = new AtomicInteger(MotionEvent.TOOL_TYPE_UNKNOWN);
|
||||
|
||||
private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
|
||||
|
||||
private int cachedRotation = -1;
|
||||
private boolean overrideVolumeButtons = false;
|
||||
private boolean hasHardwareKeyboardConfig = false;
|
||||
|
||||
public GodotInputHandler(Context context, Godot godot) {
|
||||
this.godot = godot;
|
||||
mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
|
||||
mInputManager.registerInputDeviceListener(this, null);
|
||||
|
||||
windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
|
||||
|
||||
this.godotGestureHandler = new GodotGestureHandler(this);
|
||||
this.gestureDetector = new GestureDetector(context, godotGestureHandler);
|
||||
this.gestureDetector.setIsLongpressEnabled(false);
|
||||
this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler);
|
||||
this.scaleGestureDetector.setStylusScaleEnabled(true);
|
||||
Configuration config = context.getResources().getConfiguration();
|
||||
hasHardwareKeyboardConfig = config.keyboard != Configuration.KEYBOARD_NOKEYS &&
|
||||
config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable long press events. This is false by default.
|
||||
*/
|
||||
public void enableLongPress(boolean enable) {
|
||||
this.gestureDetector.setIsLongpressEnabled(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable scroll deadzone. This is false by default.
|
||||
*/
|
||||
public void disableScrollDeadzone(boolean disable) {
|
||||
this.godotGestureHandler.setScrollDeadzoneDisabled(disable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable haptic feedback (vibration) when a long-press right-click is triggered.
|
||||
*/
|
||||
public void enableHapticFeedback(boolean enable) {
|
||||
this.godotGestureHandler.setHapticFeedbackEnabled(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform haptic feedback on the render view.
|
||||
*/
|
||||
void performHapticFeedback() {
|
||||
GodotRenderView view = godot.getRenderView();
|
||||
if (view != null) {
|
||||
view.getView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable multi-fingers pan & scale gestures. This is false by default.
|
||||
* <p>
|
||||
* Note: This may interfere with multi-touch handling / support.
|
||||
*/
|
||||
public void enablePanningAndScalingGestures(boolean enable) {
|
||||
this.godotGestureHandler.setPanningAndScalingEnabled(enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if input must be dispatched from the render thread. If false, input is
|
||||
* dispatched from the UI thread.
|
||||
*/
|
||||
private boolean shouldDispatchInputToRenderThread() {
|
||||
return GodotLib.shouldDispatchInputToRenderThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default.
|
||||
*/
|
||||
public void setRotaryInputAxis(int axis) {
|
||||
rotaryInputAxis = axis;
|
||||
}
|
||||
|
||||
public void setOverrideVolumeButtons(boolean value) {
|
||||
overrideVolumeButtons = value;
|
||||
}
|
||||
|
||||
boolean hasHardwareKeyboard() {
|
||||
if (hasHardwareKeyboardConfig) {
|
||||
return true;
|
||||
}
|
||||
return !mHardwareKeyboardIds.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isKeyEventGameDevice(int source) {
|
||||
// Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD)
|
||||
if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD))
|
||||
return false;
|
||||
|
||||
return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
|
||||
}
|
||||
|
||||
public boolean canCapturePointer() {
|
||||
return lastSeenToolType.get() == MotionEvent.TOOL_TYPE_MOUSE ||
|
||||
lastSeenToolType.get() == MotionEvent.TOOL_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
public void onPointerCaptureChange(boolean hasCapture) {
|
||||
godotGestureHandler.onPointerCaptureChange(hasCapture);
|
||||
}
|
||||
|
||||
public boolean onKeyUp(final int keyCode, KeyEvent event) {
|
||||
int source = event.getSource();
|
||||
if (isKeyEventGameDevice(source)) {
|
||||
// Check if the device exists
|
||||
final int deviceId = event.getDeviceId();
|
||||
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
|
||||
final int button = getGodotButton(keyCode);
|
||||
final int godotJoyId = mJoystickIds.get(deviceId);
|
||||
handleJoystickButtonEvent(godotJoyId, button, false);
|
||||
}
|
||||
} else {
|
||||
// getKeyCode(): The physical key that was pressed.
|
||||
final int physical_keycode = event.getKeyCode();
|
||||
final int unicode = event.getUnicodeChar();
|
||||
final int key_label = event.getDisplayLabel();
|
||||
handleKeyEvent(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0);
|
||||
};
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
return overrideVolumeButtons;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onKeyDown(final int keyCode, KeyEvent event) {
|
||||
int source = event.getSource();
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
// Check if source is a game device and that the device is a registered gamepad
|
||||
if (isKeyEventGameDevice(source)) {
|
||||
if (event.getRepeatCount() > 0) // ignore key echo
|
||||
return true;
|
||||
|
||||
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
|
||||
final int button = getGodotButton(keyCode);
|
||||
final int godotJoyId = mJoystickIds.get(deviceId);
|
||||
handleJoystickButtonEvent(godotJoyId, button, true);
|
||||
}
|
||||
} else {
|
||||
final int physical_keycode = event.getKeyCode();
|
||||
final int unicode = event.getUnicodeChar();
|
||||
final int key_label = event.getDisplayLabel();
|
||||
handleKeyEvent(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0);
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
return overrideVolumeButtons;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
lastSeenToolType.set(getEventToolType(event));
|
||||
|
||||
this.scaleGestureDetector.onTouchEvent(event);
|
||||
if (this.gestureDetector.onTouchEvent(event)) {
|
||||
// The gesture detector has handled the event.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (godotGestureHandler.onMotionEvent(event)) {
|
||||
// The gesture handler has handled the event.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drag events are handled by the [GodotGestureHandler]
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isMouseEvent(event)) {
|
||||
return handleMouseEvent(event);
|
||||
}
|
||||
|
||||
return handleTouchEvent(event);
|
||||
}
|
||||
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
lastSeenToolType.set(getEventToolType(event));
|
||||
|
||||
if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getActionMasked() == MotionEvent.ACTION_MOVE) {
|
||||
// Check if the device exists
|
||||
final int deviceId = event.getDeviceId();
|
||||
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
|
||||
final int godotJoyId = mJoystickIds.get(deviceId);
|
||||
Joystick joystick = mJoysticksDevices.get(deviceId);
|
||||
if (joystick == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < joystick.axes.size(); i++) {
|
||||
final int axis = joystick.axes.get(i);
|
||||
final float value = event.getAxisValue(axis);
|
||||
/*
|
||||
As all axes are polled for each event, only fire an axis event if the value has actually changed.
|
||||
Prevents flooding Godot with repeated events.
|
||||
*/
|
||||
if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) {
|
||||
// save value to prevent repeats
|
||||
joystick.axesValues.put(axis, value);
|
||||
handleJoystickAxisEvent(godotJoyId, i, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (joystick.hasAxisHat) {
|
||||
final int hatX = Math.round(event.getAxisValue(MotionEvent.AXIS_HAT_X));
|
||||
final int hatY = Math.round(event.getAxisValue(MotionEvent.AXIS_HAT_Y));
|
||||
if (joystick.hatX != hatX || joystick.hatY != hatY) {
|
||||
joystick.hatX = hatX;
|
||||
joystick.hatY = hatY;
|
||||
handleJoystickHatEvent(godotJoyId, hatX, hatY);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gestureDetector.onGenericMotionEvent(event)) {
|
||||
// The gesture detector has handled the event.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (godotGestureHandler.onMotionEvent(event)) {
|
||||
// The gesture handler has handled the event.
|
||||
return true;
|
||||
}
|
||||
|
||||
return handleMouseEvent(event);
|
||||
}
|
||||
|
||||
public void initInputDevices() {
|
||||
/* initially add input devices*/
|
||||
int[] deviceIds = mInputManager.getInputDeviceIds();
|
||||
for (int deviceId : deviceIds) {
|
||||
InputDevice device = mInputManager.getInputDevice(deviceId);
|
||||
if (device != null) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
|
||||
}
|
||||
onInputDeviceAdded(deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int assignJoystickIdNumber(int deviceId) {
|
||||
int godotJoyId = 0;
|
||||
while (mJoystickIds.indexOfValue(godotJoyId) >= 0) {
|
||||
godotJoyId++;
|
||||
}
|
||||
mJoystickIds.put(deviceId, godotJoyId);
|
||||
return godotJoyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
// Check if the device has not been already added
|
||||
|
||||
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputDevice device = mInputManager.getInputDevice(deviceId);
|
||||
//device can be null if deviceId is not found
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Device may be an external keyboard; store the device id
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
device.supportsSource(InputDevice.SOURCE_KEYBOARD) &&
|
||||
device.isExternal() &&
|
||||
device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
mHardwareKeyboardIds.add(deviceId);
|
||||
}
|
||||
|
||||
// Device may not be a joystick or gamepad
|
||||
if (!device.supportsSource(InputDevice.SOURCE_GAMEPAD) &&
|
||||
!device.supportsSource(InputDevice.SOURCE_JOYSTICK)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String name : JOYPAD_IGNORE_LIST) {
|
||||
if (device.getName().equals(name)) {
|
||||
Log.i(TAG, "=== Input Device ignored: " + device.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign first available number. Reuse numbers where possible.
|
||||
final int id = assignJoystickIdNumber(deviceId);
|
||||
|
||||
final Joystick joystick = new Joystick();
|
||||
joystick.device_id = deviceId;
|
||||
joystick.name = device.getName();
|
||||
|
||||
//Helps with creating new joypad mappings.
|
||||
Log.i(TAG, "=== New Input Device: " + joystick.name);
|
||||
|
||||
Set<Integer> already = new HashSet<>();
|
||||
for (InputDevice.MotionRange range : device.getMotionRanges()) {
|
||||
boolean isJoystick = range.isFromSource(InputDevice.SOURCE_JOYSTICK);
|
||||
boolean isGamepad = range.isFromSource(InputDevice.SOURCE_GAMEPAD);
|
||||
if (!isJoystick && !isGamepad) {
|
||||
continue;
|
||||
}
|
||||
final int axis = range.getAxis();
|
||||
if (axis == MotionEvent.AXIS_HAT_X || axis == MotionEvent.AXIS_HAT_Y) {
|
||||
joystick.hasAxisHat = true;
|
||||
} else {
|
||||
if (!already.contains(axis)) {
|
||||
already.add(axis);
|
||||
joystick.axes.add(axis);
|
||||
} else {
|
||||
Log.w(TAG, " - DUPLICATE AXIS VALUE IN LIST: " + axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(joystick.axes);
|
||||
for (int idx = 0; idx < joystick.axes.size(); idx++) {
|
||||
//Helps with creating new joypad mappings.
|
||||
Log.i(TAG, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx);
|
||||
}
|
||||
mJoysticksDevices.put(deviceId, joystick);
|
||||
|
||||
handleJoystickConnectionChangedEvent(id, true, joystick.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
mHardwareKeyboardIds.remove(deviceId);
|
||||
|
||||
// Check if the device has not been already removed
|
||||
if (mJoystickIds.indexOfKey(deviceId) < 0) {
|
||||
return;
|
||||
}
|
||||
final int godotJoyId = mJoystickIds.get(deviceId);
|
||||
mJoystickIds.delete(deviceId);
|
||||
mJoysticksDevices.delete(deviceId);
|
||||
handleJoystickConnectionChangedEvent(godotJoyId, false, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
onInputDeviceRemoved(deviceId);
|
||||
onInputDeviceAdded(deviceId);
|
||||
}
|
||||
|
||||
public static int getGodotButton(int keyCode) {
|
||||
int button;
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B
|
||||
button = 0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
button = 1;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_X: // Android X is SNES Y
|
||||
button = 2;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
button = 3;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
button = 9;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
button = 15;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
button = 10;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
button = 16;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
button = 4;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE: // Home/Xbox Button on Xbox controllers
|
||||
button = 5;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
button = 6;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
button = 7;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
button = 8;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
button = 11;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
button = 12;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
button = 13;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
button = 14;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MEDIA_RECORD: // Share Button on Xbox controllers
|
||||
button = 15;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_C:
|
||||
button = 17;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_Z:
|
||||
button = 18;
|
||||
break;
|
||||
|
||||
default:
|
||||
button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20;
|
||||
break;
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
static int getEventToolType(MotionEvent event) {
|
||||
return event.getPointerCount() > 0 ? event.getToolType(0) : MotionEvent.TOOL_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
static boolean isMouseEvent(MotionEvent event) {
|
||||
int toolType = getEventToolType(event);
|
||||
int eventSource = event.getSource();
|
||||
|
||||
switch (toolType) {
|
||||
case MotionEvent.TOOL_TYPE_FINGER:
|
||||
return false;
|
||||
|
||||
case MotionEvent.TOOL_TYPE_MOUSE:
|
||||
case MotionEvent.TOOL_TYPE_STYLUS:
|
||||
case MotionEvent.TOOL_TYPE_ERASER:
|
||||
return true;
|
||||
|
||||
case MotionEvent.TOOL_TYPE_UNKNOWN:
|
||||
default:
|
||||
boolean mouseSource =
|
||||
((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) ||
|
||||
((eventSource & (InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_STYLUS)) == InputDevice.SOURCE_STYLUS);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mouseSource = mouseSource ||
|
||||
((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE);
|
||||
}
|
||||
return mouseSource;
|
||||
}
|
||||
}
|
||||
|
||||
boolean handleMotionEvent(final MotionEvent event) {
|
||||
return handleMotionEvent(event, event.getActionMasked());
|
||||
}
|
||||
|
||||
boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) {
|
||||
return handleMotionEvent(event, eventActionOverride, false);
|
||||
}
|
||||
|
||||
boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
|
||||
if (isMouseEvent(event)) {
|
||||
return handleMouseEvent(event, eventActionOverride, doubleTap);
|
||||
}
|
||||
return handleTouchEvent(event, eventActionOverride, doubleTap);
|
||||
}
|
||||
|
||||
static float getEventTiltX(MotionEvent event) {
|
||||
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
|
||||
final float orientation = event.getOrientation();
|
||||
|
||||
// Tilt is zero is perpendicular to the screen and pi/2 is flat on the surface.
|
||||
final float tilt = event.getAxisValue(MotionEvent.AXIS_TILT);
|
||||
|
||||
float tiltMult = (float)Math.sin(tilt);
|
||||
|
||||
// To be consistent with expected tilt.
|
||||
return (float)-Math.sin(orientation) * tiltMult;
|
||||
}
|
||||
|
||||
static float getEventTiltY(MotionEvent event) {
|
||||
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
|
||||
final float orientation = event.getOrientation();
|
||||
|
||||
// Tilt is zero is perpendicular to the screen and pi/2 is flat on the surface.
|
||||
final float tilt = event.getAxisValue(MotionEvent.AXIS_TILT);
|
||||
|
||||
float tiltMult = (float)Math.sin(tilt);
|
||||
|
||||
// To be consistent with expected tilt.
|
||||
return (float)Math.cos(orientation) * tiltMult;
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(final MotionEvent event) {
|
||||
return handleMouseEvent(event, event.getActionMasked());
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) {
|
||||
return handleMouseEvent(event, eventActionOverride, false);
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
|
||||
return handleMouseEvent(event, eventActionOverride, event.getButtonState(), doubleTap);
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) {
|
||||
final float x = event.getX();
|
||||
final float y = event.getY();
|
||||
|
||||
final float pressure = event.getPressure();
|
||||
|
||||
float verticalFactor = 0;
|
||||
float horizontalFactor = 0;
|
||||
|
||||
// If event came from RotaryEncoder (Bezel or Crown rotate event on Wear OS smart watches),
|
||||
// convert it to mouse wheel event.
|
||||
if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
|
||||
if (rotaryInputAxis == ROTARY_INPUT_HORIZONTAL_AXIS) {
|
||||
horizontalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL);
|
||||
} else {
|
||||
// If rotaryInputAxis is not ROTARY_INPUT_HORIZONTAL_AXIS then use default ROTARY_INPUT_VERTICAL_AXIS axis.
|
||||
verticalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL);
|
||||
}
|
||||
} else {
|
||||
verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
|
||||
}
|
||||
boolean sourceMouseRelative = false;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE);
|
||||
}
|
||||
return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event));
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) {
|
||||
return handleMouseEvent(eventAction, 0, 0f, 0f, 0f, 0f, false, sourceMouseRelative, 1f, 0f, 0f);
|
||||
}
|
||||
|
||||
boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix the buttonsMask
|
||||
switch (eventAction) {
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
// Zero-up the button state
|
||||
buttonsMask = 0;
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonsMask == 0) {
|
||||
buttonsMask = MotionEvent.BUTTON_PRIMARY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically
|
||||
// follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate
|
||||
// stream of events to the engine.
|
||||
switch (eventAction) {
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_SCROLL: {
|
||||
runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean handleTouchEvent(final MotionEvent event) {
|
||||
return handleTouchEvent(event, event.getActionMasked());
|
||||
}
|
||||
|
||||
boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) {
|
||||
return handleTouchEvent(event, eventActionOverride, false);
|
||||
}
|
||||
|
||||
boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
|
||||
if (event.getPointerCount() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (eventActionOverride) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
runnable.setTouchEvent(event, eventActionOverride, doubleTap);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void handleMagnifyEvent(float x, float y, float factor) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setMagnifyEvent(x, y, factor);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
void handlePanEvent(float x, float y, float deltaX, float deltaY) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setPanEvent(x, y, deltaX, deltaY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickButtonEvent(int device, int button, boolean pressed) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickButtonEvent(device, button, pressed);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickAxisEvent(int device, int axis, float value) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickAxisEvent(device, axis, value);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickHatEvent(int device, int hatX, int hatY) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickHatEvent(device, hatX, hatY);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setJoystickConnectionChangedEvent(device, connected, name);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo);
|
||||
dispatchInputEventRunnable(runnable);
|
||||
}
|
||||
|
||||
private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) {
|
||||
if (shouldDispatchInputToRenderThread()) {
|
||||
godot.runOnRenderThread(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
final float[] values = event.values;
|
||||
if (values == null || values.length != 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputEventRunnable runnable = InputEventRunnable.obtain();
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedRotation == -1) {
|
||||
updateCachedRotation();
|
||||
}
|
||||
|
||||
float rotatedValue0 = 0f;
|
||||
float rotatedValue1 = 0f;
|
||||
float rotatedValue2 = 0f;
|
||||
switch (cachedRotation) {
|
||||
case Surface.ROTATION_0:
|
||||
rotatedValue0 = values[0];
|
||||
rotatedValue1 = values[1];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_90:
|
||||
rotatedValue0 = -values[1];
|
||||
rotatedValue1 = values[0];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_180:
|
||||
rotatedValue0 = -values[0];
|
||||
rotatedValue1 = -values[1];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
|
||||
case Surface.ROTATION_270:
|
||||
rotatedValue0 = values[1];
|
||||
rotatedValue1 = -values[0];
|
||||
rotatedValue2 = values[2];
|
||||
break;
|
||||
}
|
||||
|
||||
runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2);
|
||||
godot.runOnRenderThread(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
|
||||
|
||||
private void updateCachedRotation() {
|
||||
cachedRotation = windowManager.getDefaultDisplay().getRotation();
|
||||
}
|
||||
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
updateCachedRotation();
|
||||
|
||||
boolean newHardwareKeyboardConfig = newConfig.keyboard != Configuration.KEYBOARD_NOKEYS &&
|
||||
newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
|
||||
if (hasHardwareKeyboardConfig != newHardwareKeyboardConfig) {
|
||||
hasHardwareKeyboardConfig = newHardwareKeyboardConfig;
|
||||
GodotLib.hardwareKeyboardConnected(hasHardwareKeyboard());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/**************************************************************************/
|
||||
/* GodotTextInputWrapper.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input;
|
||||
|
||||
import org.godotengine.godot.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
|
||||
public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListener {
|
||||
// ===========================================================
|
||||
// Constants
|
||||
// ===========================================================
|
||||
private static final String TAG = GodotTextInputWrapper.class.getSimpleName();
|
||||
|
||||
// ===========================================================
|
||||
// Fields
|
||||
// ===========================================================
|
||||
private final GodotRenderView mRenderView;
|
||||
private final GodotEditText mEdit;
|
||||
private String mOriginText;
|
||||
private boolean mHasSelection;
|
||||
|
||||
// ===========================================================
|
||||
// Constructors
|
||||
// ===========================================================
|
||||
|
||||
public GodotTextInputWrapper(final GodotRenderView view, final GodotEditText edit) {
|
||||
mRenderView = view;
|
||||
mEdit = edit;
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Getter & Setter
|
||||
// ===========================================================
|
||||
|
||||
private boolean isFullScreenEdit() {
|
||||
final TextView textField = mEdit;
|
||||
final InputMethodManager imm = (InputMethodManager)textField.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
return imm.isFullscreenMode();
|
||||
}
|
||||
|
||||
public void setOriginText(final String originText) {
|
||||
mOriginText = originText;
|
||||
}
|
||||
|
||||
public void setSelection(boolean selection) {
|
||||
mHasSelection = selection;
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Methods for/from SuperClass/Interfaces
|
||||
// ===========================================================
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, true, false);
|
||||
mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, false, false);
|
||||
|
||||
if (mHasSelection) {
|
||||
mHasSelection = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence pCharSequence, final int start, final int before, final int count) {
|
||||
final int[] newChars = new int[count];
|
||||
for (int i = start; i < start + count; ++i) {
|
||||
newChars[i - start] = pCharSequence.charAt(i);
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
final int character = newChars[i];
|
||||
if ((character == '\n') && !(mEdit.getKeyboardType() == GodotEditText.VirtualKeyboardType.KEYBOARD_TYPE_MULTILINE)) {
|
||||
// Return keys are handled through action events
|
||||
continue;
|
||||
}
|
||||
mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false);
|
||||
mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) {
|
||||
if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) {
|
||||
final String characters = pKeyEvent.getCharacters();
|
||||
if (characters != null) {
|
||||
for (int i = 0; i < characters.length(); i++) {
|
||||
final int character = characters.codePointAt(i);
|
||||
mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false);
|
||||
mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pActionID == EditorInfo.IME_ACTION_DONE) {
|
||||
// Enter key has been pressed
|
||||
mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, true, false);
|
||||
mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, false, false);
|
||||
mRenderView.getView().requestFocus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// Methods
|
||||
// ===========================================================
|
||||
|
||||
// ===========================================================
|
||||
// Inner and Anonymous Classes
|
||||
// ===========================================================
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
/**************************************************************************/
|
||||
/* InputEventRunnable.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input;
|
||||
|
||||
import org.godotengine.godot.GodotLib;
|
||||
|
||||
import android.hardware.Sensor;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pools;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Used to dispatch input events.
|
||||
*
|
||||
* This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of
|
||||
* objects for input events dispatching, thus avoid the creation (and garbage collection) of
|
||||
* spurious @{@link Runnable} objects.
|
||||
*/
|
||||
final class InputEventRunnable implements Runnable {
|
||||
private static final String TAG = InputEventRunnable.class.getSimpleName();
|
||||
|
||||
private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers
|
||||
|
||||
private static final Pools.Pool<InputEventRunnable> POOL = new Pools.Pool<>() {
|
||||
private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2
|
||||
|
||||
private final ArrayBlockingQueue<InputEventRunnable> queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE);
|
||||
private final AtomicInteger createdCount = new AtomicInteger();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InputEventRunnable acquire() {
|
||||
InputEventRunnable instance = queue.poll();
|
||||
if (instance == null) {
|
||||
int creationCount = createdCount.incrementAndGet();
|
||||
if (creationCount <= MAX_POOL_SIZE) {
|
||||
instance = new InputEventRunnable(creationCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release(@NonNull InputEventRunnable instance) {
|
||||
return queue.offer(instance);
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
static InputEventRunnable obtain() {
|
||||
InputEventRunnable runnable = POOL.acquire();
|
||||
if (runnable == null) {
|
||||
Log.w(TAG, "Input event pool is at capacity");
|
||||
}
|
||||
return runnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to track when this instance was created and added to the pool. Primarily used for
|
||||
* debug purposes.
|
||||
*/
|
||||
private final int creationRank;
|
||||
|
||||
private InputEventRunnable(int creationRank) {
|
||||
this.creationRank = creationRank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of supported input events.
|
||||
*/
|
||||
private enum EventType {
|
||||
MOUSE,
|
||||
TOUCH,
|
||||
MAGNIFY,
|
||||
PAN,
|
||||
JOYSTICK_BUTTON,
|
||||
JOYSTICK_AXIS,
|
||||
JOYSTICK_HAT,
|
||||
JOYSTICK_CONNECTION_CHANGED,
|
||||
KEY,
|
||||
SENSOR
|
||||
}
|
||||
|
||||
private EventType currentEventType = null;
|
||||
|
||||
// common event fields
|
||||
private float eventX;
|
||||
private float eventY;
|
||||
private float eventDeltaX;
|
||||
private float eventDeltaY;
|
||||
private boolean eventPressed;
|
||||
|
||||
// common touch / mouse fields
|
||||
private int eventAction;
|
||||
private boolean doubleTap;
|
||||
|
||||
// Mouse event fields and setter
|
||||
private int buttonsMask;
|
||||
private boolean sourceMouseRelative;
|
||||
private float pressure;
|
||||
private float tiltX;
|
||||
private float tiltY;
|
||||
void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
|
||||
this.currentEventType = EventType.MOUSE;
|
||||
this.eventAction = eventAction;
|
||||
this.buttonsMask = buttonsMask;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.eventDeltaX = deltaX;
|
||||
this.eventDeltaY = deltaY;
|
||||
this.doubleTap = doubleClick;
|
||||
this.sourceMouseRelative = sourceMouseRelative;
|
||||
this.pressure = pressure;
|
||||
this.tiltX = tiltX;
|
||||
this.tiltY = tiltY;
|
||||
}
|
||||
|
||||
// Touch event fields and setter
|
||||
private int actionPointerId;
|
||||
private int pointerCount;
|
||||
private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc...
|
||||
void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) {
|
||||
this.currentEventType = EventType.TOUCH;
|
||||
this.eventAction = eventAction;
|
||||
this.doubleTap = doubleTap;
|
||||
this.actionPointerId = event.getPointerId(event.getActionIndex());
|
||||
this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT);
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
positions[i * 6 + 0] = event.getPointerId(i);
|
||||
positions[i * 6 + 1] = event.getX(i);
|
||||
positions[i * 6 + 2] = event.getY(i);
|
||||
positions[i * 6 + 3] = event.getPressure(i);
|
||||
positions[i * 6 + 4] = GodotInputHandler.getEventTiltX(event);
|
||||
positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Magnify event fields and setter
|
||||
private float magnifyFactor;
|
||||
void setMagnifyEvent(float x, float y, float factor) {
|
||||
this.currentEventType = EventType.MAGNIFY;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.magnifyFactor = factor;
|
||||
}
|
||||
|
||||
// Pan event setter
|
||||
void setPanEvent(float x, float y, float deltaX, float deltaY) {
|
||||
this.currentEventType = EventType.PAN;
|
||||
this.eventX = x;
|
||||
this.eventY = y;
|
||||
this.eventDeltaX = deltaX;
|
||||
this.eventDeltaY = deltaY;
|
||||
}
|
||||
|
||||
// common joystick field
|
||||
private int joystickDevice;
|
||||
|
||||
// Joystick button event fields and setter
|
||||
private int button;
|
||||
void setJoystickButtonEvent(int device, int button, boolean pressed) {
|
||||
this.currentEventType = EventType.JOYSTICK_BUTTON;
|
||||
this.joystickDevice = device;
|
||||
this.button = button;
|
||||
this.eventPressed = pressed;
|
||||
}
|
||||
|
||||
// Joystick axis event fields and setter
|
||||
private int axis;
|
||||
private float value;
|
||||
void setJoystickAxisEvent(int device, int axis, float value) {
|
||||
this.currentEventType = EventType.JOYSTICK_AXIS;
|
||||
this.joystickDevice = device;
|
||||
this.axis = axis;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// Joystick hat event fields and setter
|
||||
private int hatX;
|
||||
private int hatY;
|
||||
void setJoystickHatEvent(int device, int hatX, int hatY) {
|
||||
this.currentEventType = EventType.JOYSTICK_HAT;
|
||||
this.joystickDevice = device;
|
||||
this.hatX = hatX;
|
||||
this.hatY = hatY;
|
||||
}
|
||||
|
||||
// Joystick connection changed event fields and setter
|
||||
private boolean connected;
|
||||
private String joystickName;
|
||||
void setJoystickConnectionChangedEvent(int device, boolean connected, String name) {
|
||||
this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED;
|
||||
this.joystickDevice = device;
|
||||
this.connected = connected;
|
||||
this.joystickName = name;
|
||||
}
|
||||
|
||||
// Key event fields and setter
|
||||
private int physicalKeycode;
|
||||
private int unicode;
|
||||
private int keyLabel;
|
||||
private boolean echo;
|
||||
void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
|
||||
this.currentEventType = EventType.KEY;
|
||||
this.physicalKeycode = physicalKeycode;
|
||||
this.unicode = unicode;
|
||||
this.keyLabel = keyLabel;
|
||||
this.eventPressed = pressed;
|
||||
this.echo = echo;
|
||||
}
|
||||
|
||||
// Sensor event fields and setter
|
||||
private int sensorType;
|
||||
private float rotatedValue0;
|
||||
private float rotatedValue1;
|
||||
private float rotatedValue2;
|
||||
void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) {
|
||||
this.currentEventType = EventType.SENSOR;
|
||||
this.sensorType = sensorType;
|
||||
this.rotatedValue0 = rotatedValue0;
|
||||
this.rotatedValue1 = rotatedValue1;
|
||||
this.rotatedValue2 = rotatedValue2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (currentEventType == null) {
|
||||
Log.w(TAG, "Invalid event type");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (currentEventType) {
|
||||
case MOUSE:
|
||||
GodotLib.dispatchMouseEvent(
|
||||
eventAction,
|
||||
buttonsMask,
|
||||
eventX,
|
||||
eventY,
|
||||
eventDeltaX,
|
||||
eventDeltaY,
|
||||
doubleTap,
|
||||
sourceMouseRelative,
|
||||
pressure,
|
||||
tiltX,
|
||||
tiltY);
|
||||
break;
|
||||
|
||||
case TOUCH:
|
||||
GodotLib.dispatchTouchEvent(
|
||||
eventAction,
|
||||
actionPointerId,
|
||||
pointerCount,
|
||||
positions,
|
||||
doubleTap);
|
||||
break;
|
||||
|
||||
case MAGNIFY:
|
||||
GodotLib.magnify(eventX, eventY, magnifyFactor);
|
||||
break;
|
||||
|
||||
case PAN:
|
||||
GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY);
|
||||
break;
|
||||
|
||||
case JOYSTICK_BUTTON:
|
||||
GodotLib.joybutton(joystickDevice, button, eventPressed);
|
||||
break;
|
||||
|
||||
case JOYSTICK_AXIS:
|
||||
GodotLib.joyaxis(joystickDevice, axis, value);
|
||||
break;
|
||||
|
||||
case JOYSTICK_HAT:
|
||||
GodotLib.joyhat(joystickDevice, hatX, hatY);
|
||||
break;
|
||||
|
||||
case JOYSTICK_CONNECTION_CHANGED:
|
||||
GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName);
|
||||
break;
|
||||
|
||||
case KEY:
|
||||
GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo);
|
||||
break;
|
||||
|
||||
case SENSOR:
|
||||
switch (sensorType) {
|
||||
case Sensor.TYPE_ACCELEROMETER:
|
||||
GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_GRAVITY:
|
||||
GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_MAGNETIC_FIELD:
|
||||
GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_GYROSCOPE:
|
||||
GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the current instance back to the pool
|
||||
*/
|
||||
private void recycle() {
|
||||
currentEventType = null;
|
||||
POOL.release(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/**************************************************************************/
|
||||
/* Joystick.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.input;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* POJO class to represent a Joystick input device.
|
||||
*/
|
||||
class Joystick {
|
||||
int device_id;
|
||||
String name;
|
||||
List<Integer> axes = new ArrayList<>();
|
||||
protected boolean hasAxisHat = false;
|
||||
/*
|
||||
* Keep track of values so we can prevent flooding the engine with useless events.
|
||||
*/
|
||||
protected final SparseArray<Float> axesValues = new SparseArray<>(4);
|
||||
protected int hatX;
|
||||
protected int hatY;
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
/**************************************************************************/
|
||||
/* FilePicker.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toUri
|
||||
import org.godotengine.godot.GodotLib
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Utility class for managing file selection and file picker activities.
|
||||
*
|
||||
* It provides methods to launch a file picker and handle the result, supporting various file modes,
|
||||
* including opening files, directories, and saving files.
|
||||
*/
|
||||
internal class FilePicker {
|
||||
companion object {
|
||||
private const val FILE_PICKER_REQUEST = 1000
|
||||
private val TAG = FilePicker::class.java.simpleName
|
||||
|
||||
// Constants for fileMode values
|
||||
private const val FILE_MODE_OPEN_FILE = 0
|
||||
private const val FILE_MODE_OPEN_FILES = 1
|
||||
private const val FILE_MODE_OPEN_DIR = 2
|
||||
private const val FILE_MODE_OPEN_ANY = 3
|
||||
private const val FILE_MODE_SAVE_FILE = 4
|
||||
|
||||
/**
|
||||
* Handles the result from a file picker activity and processes the selected file(s) or directory.
|
||||
*
|
||||
* @param context The context from which the file picker was launched.
|
||||
* @param requestCode The request code used when starting the file picker activity.
|
||||
* @param resultCode The result code returned by the activity.
|
||||
* @param data The intent data containing the selected file(s) or directory.
|
||||
*/
|
||||
fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == FILE_PICKER_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Log.d(TAG, "File picker canceled")
|
||||
GodotLib.filePickerCallback(false, emptyArray())
|
||||
return
|
||||
}
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val selectedFiles: MutableList<String> = mutableListOf()
|
||||
val clipData = data?.clipData
|
||||
|
||||
if (clipData != null) {
|
||||
// Handle multiple file selection.
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uri?.let {
|
||||
selectedFiles.add(it.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val uri: Uri? = data?.data
|
||||
uri?.let {
|
||||
selectedFiles.add(it.toString())
|
||||
}
|
||||
}
|
||||
if (selectedFiles.isNotEmpty()) {
|
||||
GodotLib.filePickerCallback(true, selectedFiles.toTypedArray())
|
||||
} else {
|
||||
GodotLib.filePickerCallback(false, emptyArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a file picker activity with specified settings based on the mode, initial directory,
|
||||
* file type filters, and other parameters.
|
||||
*
|
||||
* @param context The context from which to start the file picker.
|
||||
* @param activity The activity instance used to initiate the picker. Required for activity results.
|
||||
* @param currentDirectory The directory path to start the file picker in.
|
||||
* @param filename The name of the file when using save mode.
|
||||
* @param fileMode The mode to operate in, specifying open, save, or directory select.
|
||||
* @param filters Array of MIME types to filter file selection.
|
||||
*/
|
||||
fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
|
||||
val intent = when (fileMode) {
|
||||
FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
else -> Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
}
|
||||
val initialDirectory = getUriFromDirectoryPath(context, currentDirectory)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialDirectory != null) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory)
|
||||
} else {
|
||||
Log.d(TAG, "Error cannot set initial directory")
|
||||
}
|
||||
if (fileMode == FILE_MODE_OPEN_FILES) {
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES
|
||||
} else if (fileMode == FILE_MODE_SAVE_FILE) {
|
||||
intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE
|
||||
}
|
||||
// ACTION_OPEN_DOCUMENT_TREE does not support intent type
|
||||
if (fileMode != FILE_MODE_OPEN_DIR) {
|
||||
val resolvedFilters = filters.map { resolveMimeType(it) }.distinct()
|
||||
intent.type = resolvedFilters.firstOrNull { it != "application/octet-stream" } ?: "*/*"
|
||||
if (resolvedFilters.size > 1) {
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, resolvedFilters.toTypedArray())
|
||||
}
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||
activity?.startActivityForResult(intent, FILE_PICKER_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the MIME type for a given file extension.
|
||||
*
|
||||
* @param ext the extension whose MIME type is to be determined.
|
||||
* @return the MIME type as a string, or "application/octet-stream" if the type is unknown.
|
||||
*/
|
||||
private fun resolveMimeType(ext: String): String {
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
var input = ext
|
||||
|
||||
// Fix for extensions like "*.txt" or ".txt".
|
||||
if (ext.contains(".")) {
|
||||
input = ext.substring(ext.indexOf(".") + 1);
|
||||
}
|
||||
|
||||
// Check if the input is already a valid MIME type.
|
||||
if (mimeTypeMap.hasMimeType(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
val resolvedMimeType = mimeTypeMap.getMimeTypeFromExtension(input)
|
||||
if (resolvedMimeType != null) {
|
||||
return resolvedMimeType
|
||||
}
|
||||
// Check for wildcard MIME types like "image/*".
|
||||
if (input.contains("/*")) {
|
||||
val category = input.substringBefore("/*")
|
||||
return when (category) {
|
||||
"image" -> "image/*"
|
||||
"video" -> "video/*"
|
||||
"audio" -> "audio/*"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
}
|
||||
// Fallback to a generic MIME type if the input is neither a valid extension nor MIME type.
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
private fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? {
|
||||
if (directoryPath.startsWith("content://")) {
|
||||
return directoryPath.toUri()
|
||||
}
|
||||
if (!directoryExists(directoryPath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath
|
||||
if (directoryPath.startsWith(externalStorageRoot)) {
|
||||
val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/')
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("com.android.externalstorage.documents")
|
||||
.appendPath("document")
|
||||
.appendPath("primary:$relativePath")
|
||||
.build()
|
||||
return uri
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun directoryExists(path: String): Boolean {
|
||||
return try {
|
||||
val file = File(path)
|
||||
file.isDirectory && file.exists()
|
||||
} catch (e: SecurityException) {
|
||||
Log.d(TAG, "Failed to check directoryExists: $path", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
/**************************************************************************/
|
||||
/* StorageScope.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Represents the different storage scopes.
|
||||
*/
|
||||
internal enum class StorageScope {
|
||||
/**
|
||||
* Covers the 'assets' directory
|
||||
*/
|
||||
ASSETS,
|
||||
|
||||
/**
|
||||
* Covers internal and external directories accessible to the app without restrictions.
|
||||
*/
|
||||
APP,
|
||||
|
||||
/**
|
||||
* Covers shared directories (from Android 10 and higher).
|
||||
*/
|
||||
SHARED,
|
||||
|
||||
/**
|
||||
* Covers the file access using SAF
|
||||
*/
|
||||
SAF,
|
||||
|
||||
/**
|
||||
* Everything else..
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
class Identifier(context: Context) {
|
||||
|
||||
companion object {
|
||||
internal const val ASSETS_PREFIX = "assets://"
|
||||
internal const val CONTENT_PREFIX = "content://"
|
||||
}
|
||||
|
||||
private val internalAppDir: String? = context.filesDir.canonicalPath
|
||||
private val internalCacheDir: String? = context.cacheDir.canonicalPath
|
||||
private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath
|
||||
private val obbDir: String? = context.obbDir?.canonicalPath
|
||||
private val sharedDir : String? = Environment.getExternalStorageDirectory().canonicalPath
|
||||
private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
|
||||
private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
|
||||
|
||||
/**
|
||||
* Determine if the given path is accessible.
|
||||
*/
|
||||
fun canAccess(path: String?): Boolean {
|
||||
val storageScope = identifyStorageScope(path)
|
||||
return storageScope == APP || storageScope == SHARED
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which [StorageScope] the given path falls under.
|
||||
*/
|
||||
fun identifyStorageScope(path: String?): StorageScope {
|
||||
if (path == null) {
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
if (path.startsWith(ASSETS_PREFIX)) {
|
||||
return ASSETS
|
||||
}
|
||||
|
||||
// If it's either content uri for a file, or starts with a tree uri.
|
||||
if (path.startsWith(CONTENT_PREFIX)) {
|
||||
return SAF
|
||||
}
|
||||
|
||||
var pathFile = File(path)
|
||||
if (!pathFile.isAbsolute) {
|
||||
pathFile = File(GodotLib.getProjectResourceDir(), path)
|
||||
if (!pathFile.isAbsolute) {
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// If we have 'All Files Access' permission, we can access all directories without
|
||||
// restriction.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& Environment.isExternalStorageManager()) {
|
||||
return APP
|
||||
}
|
||||
|
||||
val canonicalPathFile = pathFile.canonicalPath
|
||||
|
||||
if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) {
|
||||
return APP
|
||||
}
|
||||
|
||||
if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) {
|
||||
return APP
|
||||
}
|
||||
|
||||
if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) {
|
||||
return APP
|
||||
}
|
||||
|
||||
val rootDir: String? = System.getenv("ANDROID_ROOT")
|
||||
if (rootDir != null && canonicalPathFile.startsWith(rootDir)) {
|
||||
return APP
|
||||
}
|
||||
|
||||
if (obbDir != null && canonicalPathFile.startsWith(obbDir)) {
|
||||
return APP
|
||||
}
|
||||
|
||||
if (sharedDir != null && canonicalPathFile.startsWith(sharedDir)) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
// Before R, apps had access to shared storage so long as they have the right
|
||||
// permissions (and flag on Q).
|
||||
return APP
|
||||
}
|
||||
|
||||
// Post R, access is limited based on the target destination
|
||||
// 'Downloads' and 'Documents' are still accessible
|
||||
if ((downloadsSharedDir != null && canonicalPathFile.startsWith(downloadsSharedDir))
|
||||
|| (documentsSharedDir != null && canonicalPathFile.startsWith(documentsSharedDir))) {
|
||||
return APP
|
||||
}
|
||||
|
||||
return SHARED
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/**************************************************************************/
|
||||
/* AssetsDirectoryAccess.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.directory
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
|
||||
import org.godotengine.godot.io.file.AssetData
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Handles directories access within the Android assets directory.
|
||||
*/
|
||||
internal class AssetsDirectoryAccess(private val context: Context) : DirectoryAccessHandler.DirectoryAccess {
|
||||
|
||||
companion object {
|
||||
private val TAG = AssetsDirectoryAccess::class.java.simpleName
|
||||
|
||||
internal fun getAssetsPath(originalPath: String): String {
|
||||
if (originalPath.startsWith(File.separator)) {
|
||||
return originalPath.substring(File.separator.length)
|
||||
}
|
||||
if (originalPath.startsWith(StorageScope.Identifier.ASSETS_PREFIX)) {
|
||||
return originalPath.substring(StorageScope.Identifier.ASSETS_PREFIX.length)
|
||||
}
|
||||
return originalPath
|
||||
}
|
||||
}
|
||||
|
||||
private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
|
||||
|
||||
private val assetManager = context.assets
|
||||
|
||||
private var lastDirId = STARTING_DIR_ID
|
||||
private val dirs = SparseArray<AssetDir>()
|
||||
|
||||
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
|
||||
|
||||
override fun dirOpen(path: String): Int {
|
||||
val assetsPath = getAssetsPath(path)
|
||||
try {
|
||||
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
if (files.isEmpty()) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
val ad = AssetDir(assetsPath, files)
|
||||
|
||||
dirs.put(++lastDirId, ad)
|
||||
return lastDirId
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on dirOpen", e)
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
}
|
||||
|
||||
override fun dirExists(path: String): Boolean {
|
||||
val assetsPath = getAssetsPath(path)
|
||||
try {
|
||||
val files = assetManager.list(assetsPath) ?: return false
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
return files.isNotEmpty()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on dirExists", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun fileExists(path: String) = AssetData.fileExists(context, path)
|
||||
|
||||
override fun dirIsDir(dirId: Int): Boolean {
|
||||
val ad: AssetDir = dirs[dirId]
|
||||
|
||||
var idx = ad.current
|
||||
if (idx > 0) {
|
||||
idx--
|
||||
}
|
||||
|
||||
if (idx >= ad.files.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
val fileName = ad.files[idx]
|
||||
// List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a
|
||||
// directory
|
||||
val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}"
|
||||
val fileContents = assetManager.list(filePath)
|
||||
return (fileContents?.size?: 0) > 0
|
||||
}
|
||||
|
||||
override fun isCurrentHidden(dirId: Int): Boolean {
|
||||
val ad = dirs[dirId]
|
||||
|
||||
var idx = ad.current
|
||||
if (idx > 0) {
|
||||
idx--
|
||||
}
|
||||
|
||||
if (idx >= ad.files.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
val fileName = ad.files[idx]
|
||||
return fileName.startsWith('.')
|
||||
}
|
||||
|
||||
override fun dirNext(dirId: Int): String {
|
||||
val ad: AssetDir = dirs[dirId]
|
||||
|
||||
if (ad.current >= ad.files.size) {
|
||||
ad.current++
|
||||
return ""
|
||||
}
|
||||
|
||||
return ad.files[ad.current++]
|
||||
}
|
||||
|
||||
override fun dirClose(dirId: Int) {
|
||||
dirs.remove(dirId)
|
||||
}
|
||||
|
||||
override fun getDriveCount() = 0
|
||||
|
||||
override fun getDrive(drive: Int) = ""
|
||||
|
||||
override fun makeDir(dir: String) = false
|
||||
|
||||
override fun getSpaceLeft() = 0L
|
||||
|
||||
override fun rename(from: String, to: String) = AssetData.rename(from, to)
|
||||
|
||||
override fun remove(filename: String) = AssetData.delete(filename)
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
/**************************************************************************/
|
||||
/* DirectoryAccessHandler.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.directory
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Handles files and directories access and manipulation for the Android platform
|
||||
*/
|
||||
class DirectoryAccessHandler(context: Context) {
|
||||
|
||||
companion object {
|
||||
private val TAG = DirectoryAccessHandler::class.java.simpleName
|
||||
|
||||
internal const val INVALID_DIR_ID = -1
|
||||
internal const val STARTING_DIR_ID = 1
|
||||
}
|
||||
|
||||
private enum class AccessType(val nativeValue: Int) {
|
||||
ACCESS_RESOURCES(0),
|
||||
|
||||
/**
|
||||
* Maps to [ACCESS_FILESYSTEM]
|
||||
*/
|
||||
ACCESS_USERDATA(1),
|
||||
ACCESS_FILESYSTEM(2);
|
||||
|
||||
fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue
|
||||
|
||||
companion object {
|
||||
const val DIR_ACCESS_ID_MULTIPLIER = 10
|
||||
|
||||
fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> {
|
||||
val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER
|
||||
val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER
|
||||
return Pair(fromNative(nativeValue), dirId)
|
||||
}
|
||||
|
||||
private fun fromNative(nativeAccessType: Int): AccessType? {
|
||||
for (accessType in entries) {
|
||||
if (accessType.nativeValue == nativeAccessType) {
|
||||
return accessType
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? {
|
||||
val accessType = fromNative(nativeAccessType)
|
||||
if (accessType == null) {
|
||||
Log.w(TAG, "Unsupported access type $nativeAccessType")
|
||||
return null
|
||||
}
|
||||
|
||||
// 'Resources' access type takes precedence as it is simple to handle:
|
||||
// if we receive a 'Resources' access type and this is a template build,
|
||||
// we provide a 'Resources' directory handler.
|
||||
// If this is an editor build, 'Resources' refers to the opened project resources
|
||||
// and so we provide a 'Filesystem' directory handler.
|
||||
if (accessType == ACCESS_RESOURCES) {
|
||||
return if (Godot.isEditorBuild()) {
|
||||
ACCESS_FILESYSTEM
|
||||
} else {
|
||||
ACCESS_RESOURCES
|
||||
}
|
||||
} else {
|
||||
// We've received a 'Filesystem' or 'Userdata' access type. On Android, this
|
||||
// may refer to:
|
||||
// - assets directory (path has 'assets:/' prefix)
|
||||
// - app directories
|
||||
// - device shared directories
|
||||
// As such we check the storage scope (if available) to figure what type of
|
||||
// directory handler to provide
|
||||
if (storageScope != null) {
|
||||
val accessTypeFromStorageScope = when (storageScope) {
|
||||
StorageScope.ASSETS -> ACCESS_RESOURCES
|
||||
StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM
|
||||
StorageScope.SAF, StorageScope.UNKNOWN -> null
|
||||
}
|
||||
|
||||
if (accessTypeFromStorageScope != null) {
|
||||
return accessTypeFromStorageScope
|
||||
}
|
||||
}
|
||||
// If we're not able to infer the type of directory handler from the storage
|
||||
// scope, we fall-back to the 'Filesystem' directory handler as it's the default
|
||||
// for the 'Filesystem' access type.
|
||||
// Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM
|
||||
return ACCESS_FILESYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface DirectoryAccess {
|
||||
fun dirOpen(path: String): Int
|
||||
fun dirNext(dirId: Int): String
|
||||
fun dirClose(dirId: Int)
|
||||
fun dirIsDir(dirId: Int): Boolean
|
||||
fun dirExists(path: String): Boolean
|
||||
fun fileExists(path: String): Boolean
|
||||
fun hasDirId(dirId: Int): Boolean
|
||||
fun isCurrentHidden(dirId: Int): Boolean
|
||||
fun getDriveCount() : Int
|
||||
fun getDrive(drive: Int): String
|
||||
fun makeDir(dir: String): Boolean
|
||||
fun getSpaceLeft(): Long
|
||||
fun rename(from: String, to: String): Boolean
|
||||
fun remove(filename: String): Boolean
|
||||
}
|
||||
|
||||
private val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
|
||||
private val assetsDirAccess = AssetsDirectoryAccess(context)
|
||||
private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier)
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
fun assetsFileExists(assetsPath: String) = lock.withLock { assetsDirAccess.fileExists(assetsPath) }
|
||||
fun filesystemFileExists(path: String) = lock.withLock { fileSystemDirAccess.fileExists(path) }
|
||||
|
||||
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
|
||||
else -> fileSystemDirAccess.hasDirId(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirOpen(nativeAccessType: Int, path: String?): Int = lock.withLock {
|
||||
if (path == null) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID
|
||||
|
||||
val dirId = when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
|
||||
else -> fileSystemDirAccess.dirOpen(path)
|
||||
}
|
||||
if (dirId == INVALID_DIR_ID) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
val dirAccessId = accessType.generateDirAccessId(dirId)
|
||||
return dirAccessId
|
||||
}
|
||||
|
||||
fun dirNext(dirAccessId: Int): String = lock.withLock {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
|
||||
return ""
|
||||
}
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
|
||||
else -> fileSystemDirAccess.dirNext(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirClose(dirAccessId: Int) = lock.withLock {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
|
||||
return
|
||||
}
|
||||
|
||||
when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
|
||||
else -> fileSystemDirAccess.dirClose(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirIsDir(dirAccessId: Int): Boolean = lock.withLock {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
|
||||
return false
|
||||
}
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
|
||||
else -> fileSystemDirAccess.dirIsDir(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun isCurrentHidden(dirAccessId: Int): Boolean = lock.withLock {
|
||||
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
|
||||
if (accessType == null || !hasDirId(accessType, dirId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
|
||||
else -> fileSystemDirAccess.isCurrentHidden(dirId)
|
||||
}
|
||||
}
|
||||
|
||||
fun dirExists(nativeAccessType: Int, path: String?): Boolean = lock.withLock {
|
||||
if (path == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
|
||||
else -> fileSystemDirAccess.dirExists(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun fileExists(nativeAccessType: Int, path: String?): Boolean = lock.withLock {
|
||||
if (path == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
|
||||
else -> fileSystemDirAccess.fileExists(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDriveCount(nativeAccessType: Int): Int = lock.withLock {
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
|
||||
return when(accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
|
||||
else -> fileSystemDirAccess.getDriveCount()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDrive(nativeAccessType: Int, drive: Int): String = lock.withLock {
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
|
||||
else -> fileSystemDirAccess.getDrive(drive)
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDir(nativeAccessType: Int, dir: String?): Boolean = lock.withLock {
|
||||
if (dir == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
|
||||
else -> fileSystemDirAccess.makeDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSpaceLeft(nativeAccessType: Int): Long = lock.withLock {
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
|
||||
else -> fileSystemDirAccess.getSpaceLeft()
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(nativeAccessType: Int, from: String, to: String): Boolean = lock.withLock {
|
||||
val accessType = AccessType.fromNative(nativeAccessType) ?: return false
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
|
||||
else -> fileSystemDirAccess.rename(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(nativeAccessType: Int, filename: String?): Boolean = lock.withLock {
|
||||
if (filename == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
|
||||
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
|
||||
return when (accessType) {
|
||||
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
|
||||
else -> fileSystemDirAccess.remove(filename)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
/**************************************************************************/
|
||||
/* FilesystemDirectoryAccess.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.directory
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.storage.StorageManager
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
|
||||
import org.godotengine.godot.io.file.FileAccessHandler
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Handles directories access with the internal and external filesystem.
|
||||
*/
|
||||
internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier):
|
||||
DirectoryAccessHandler.DirectoryAccess {
|
||||
|
||||
companion object {
|
||||
private val TAG = FilesystemDirectoryAccess::class.java.simpleName
|
||||
}
|
||||
|
||||
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
|
||||
|
||||
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
private var lastDirId = STARTING_DIR_ID
|
||||
private val dirs = SparseArray<DirData>()
|
||||
|
||||
private fun inScope(path: String): Boolean {
|
||||
// Directory access is available for shared storage on Android 11+
|
||||
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
|
||||
// tag is available.
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS
|
||||
}
|
||||
|
||||
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
|
||||
|
||||
override fun dirOpen(path: String): Int {
|
||||
if (!inScope(path)) {
|
||||
Log.w(TAG, "Path $path is not accessible.")
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
// Check this is a directory.
|
||||
val dirFile = File(path)
|
||||
if (!dirFile.isDirectory) {
|
||||
return INVALID_DIR_ID
|
||||
}
|
||||
|
||||
// Get the files in the directory
|
||||
val files = dirFile.listFiles()?: return INVALID_DIR_ID
|
||||
|
||||
// Create the data representing this directory
|
||||
val dirData = DirData(dirFile, files)
|
||||
|
||||
dirs.put(++lastDirId, dirData)
|
||||
return lastDirId
|
||||
}
|
||||
|
||||
override fun dirExists(path: String): Boolean {
|
||||
if (!inScope(path)) {
|
||||
Log.w(TAG, "Path $path is not accessible.")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
return File(path).isDirectory
|
||||
} catch (e: SecurityException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun fileExists(path: String) = FileAccessHandler.fileExists(context, storageScopeIdentifier, path)
|
||||
|
||||
override fun dirNext(dirId: Int): String {
|
||||
val dirData = dirs[dirId]
|
||||
if (dirData.current >= dirData.files.size) {
|
||||
dirData.current++
|
||||
return ""
|
||||
}
|
||||
|
||||
return dirData.files[dirData.current++].name
|
||||
}
|
||||
|
||||
override fun dirClose(dirId: Int) {
|
||||
dirs.remove(dirId)
|
||||
}
|
||||
|
||||
override fun dirIsDir(dirId: Int): Boolean {
|
||||
val dirData = dirs[dirId]
|
||||
|
||||
var index = dirData.current
|
||||
if (index > 0) {
|
||||
index--
|
||||
}
|
||||
|
||||
if (index >= dirData.files.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
return dirData.files[index].isDirectory
|
||||
}
|
||||
|
||||
override fun isCurrentHidden(dirId: Int): Boolean {
|
||||
val dirData = dirs[dirId]
|
||||
|
||||
var index = dirData.current
|
||||
if (index > 0) {
|
||||
index--
|
||||
}
|
||||
|
||||
if (index >= dirData.files.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
return dirData.files[index].isHidden
|
||||
}
|
||||
|
||||
override fun getDriveCount(): Int {
|
||||
return storageManager.storageVolumes.size
|
||||
}
|
||||
|
||||
override fun getDrive(drive: Int): String {
|
||||
if (drive < 0 || drive >= storageManager.storageVolumes.size) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val storageVolume = storageManager.storageVolumes[drive]
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
storageVolume.directory?.absolutePath ?: ""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
override fun makeDir(dir: String): Boolean {
|
||||
if (!inScope(dir)) {
|
||||
Log.w(TAG, "Directory $dir is not accessible.")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
val dirFile = File(dir)
|
||||
return dirFile.isDirectory || dirFile.mkdirs()
|
||||
} catch (e: SecurityException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UsableSpace")
|
||||
override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L
|
||||
|
||||
override fun rename(from: String, to: String): Boolean {
|
||||
if (!inScope(from) || !inScope(to)) {
|
||||
Log.w(TAG, "Argument filenames are not accessible:\n" +
|
||||
"from: $from\n" +
|
||||
"to: $to")
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
val fromFile = File(from)
|
||||
if (fromFile.isDirectory) {
|
||||
fromFile.renameTo(File(to))
|
||||
} else {
|
||||
FileAccessHandler.renameFile(context, storageScopeIdentifier, from, to)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(filename: String): Boolean {
|
||||
if (!inScope(filename)) {
|
||||
Log.w(TAG, "Filename $filename is not accessible.")
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
val deleteFile = File(filename)
|
||||
if (deleteFile.exists()) {
|
||||
if (deleteFile.isDirectory) {
|
||||
deleteFile.delete()
|
||||
} else {
|
||||
FileAccessHandler.removeFile(context, storageScopeIdentifier, filename)
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/**************************************************************************/
|
||||
/* AssetData.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.directory.AssetsDirectoryAccess
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
|
||||
/**
|
||||
* Implementation of the [DataAccess] which handles access and interaction with files in the
|
||||
* 'assets' directory
|
||||
*/
|
||||
internal class AssetData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess() {
|
||||
|
||||
companion object {
|
||||
private val TAG = AssetData::class.java.simpleName
|
||||
|
||||
fun fileExists(context: Context, path: String): Boolean {
|
||||
val assetsPath = AssetsDirectoryAccess.getAssetsPath(path)
|
||||
try {
|
||||
val files = context.assets.list(assetsPath) ?: return false
|
||||
// Empty directories don't get added to the 'assets' directory, so
|
||||
// if files.length > 0 ==> path is directory
|
||||
// if files.length == 0 ==> path is file
|
||||
return files.isEmpty()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception on fileExists", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastModified(path: String) = 0L
|
||||
fun fileLastAccessed(path: String) = 0L
|
||||
fun fileSize(path: String) = -1L
|
||||
|
||||
fun delete(path: String) = false
|
||||
|
||||
fun rename(from: String, to: String) = false
|
||||
}
|
||||
|
||||
private val inputStream: InputStream
|
||||
internal val readChannel: ReadableByteChannel
|
||||
|
||||
private var position = 0L
|
||||
private val length: Long
|
||||
|
||||
init {
|
||||
if (accessFlag == FileAccessFlags.WRITE) {
|
||||
throw UnsupportedOperationException("Writing to the 'assets' directory is not supported")
|
||||
}
|
||||
|
||||
val assetsPath = AssetsDirectoryAccess.getAssetsPath(filePath)
|
||||
inputStream = context.assets.open(assetsPath, AssetManager.ACCESS_BUFFER)
|
||||
readChannel = Channels.newChannel(inputStream)
|
||||
|
||||
length = inputStream.available().toLong()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
inputStream.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
Log.w(TAG, "flush() is not supported.")
|
||||
}
|
||||
|
||||
override fun seek(position: Long) {
|
||||
try {
|
||||
inputStream.reset();
|
||||
inputStream.skip(position)
|
||||
|
||||
this.position = position
|
||||
if (this.position > length) {
|
||||
this.position = length
|
||||
endOfFile = true
|
||||
} else {
|
||||
endOfFile = false
|
||||
}
|
||||
|
||||
} catch(e: IOException) {
|
||||
Log.w(TAG, "Exception when seeking file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(length: Long): Error {
|
||||
Log.w(TAG, "resize() is not supported.")
|
||||
return Error.ERR_UNAVAILABLE
|
||||
}
|
||||
|
||||
override fun position() = position
|
||||
|
||||
override fun size() = length
|
||||
|
||||
override fun read(buffer: ByteBuffer): Int {
|
||||
return try {
|
||||
val readBytes = readChannel.read(buffer)
|
||||
if (readBytes == -1) {
|
||||
endOfFile = true
|
||||
0
|
||||
} else {
|
||||
position += readBytes
|
||||
readBytes
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while reading from $filePath.", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer): Boolean {
|
||||
Log.w(TAG, "write() is not supported.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
/**************************************************************************/
|
||||
/* DataAccess.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.NonWritableChannelException
|
||||
import kotlin.jvm.Throws
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Base class for file IO operations.
|
||||
*
|
||||
* Its derived instances provide concrete implementations to handle regular file access, as well
|
||||
* as file access through the media store API on versions of Android were scoped storage is enabled.
|
||||
*/
|
||||
internal abstract class DataAccess {
|
||||
|
||||
companion object {
|
||||
private val TAG = DataAccess::class.java.simpleName
|
||||
|
||||
@Throws(java.lang.Exception::class, FileNotFoundException::class)
|
||||
fun getInputStream(storageScope: StorageScope, context: Context, filePath: String): InputStream? {
|
||||
return when(storageScope) {
|
||||
StorageScope.ASSETS -> {
|
||||
val assetData = AssetData(context, filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(assetData.readChannel)
|
||||
}
|
||||
StorageScope.APP -> {
|
||||
val fileData = FileData(filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(fileData.fileChannel)
|
||||
}
|
||||
StorageScope.SHARED -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val mediaStoreData = MediaStoreData(context, filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(mediaStoreData.fileChannel)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
StorageScope.SAF -> {
|
||||
val safData = SAFData(context, filePath, FileAccessFlags.READ)
|
||||
Channels.newInputStream(safData.fileChannel)
|
||||
}
|
||||
|
||||
StorageScope.UNKNOWN -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(java.lang.Exception::class, FileNotFoundException::class)
|
||||
fun generateDataAccess(
|
||||
storageScope: StorageScope,
|
||||
context: Context,
|
||||
filePath: String,
|
||||
accessFlag: FileAccessFlags
|
||||
): DataAccess? {
|
||||
return when (storageScope) {
|
||||
StorageScope.APP -> FileData(filePath, accessFlag)
|
||||
StorageScope.ASSETS -> AssetData(context, filePath, accessFlag)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData(context, filePath, accessFlag)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
StorageScope.SAF -> SAFData(context, filePath, accessFlag)
|
||||
|
||||
StorageScope.UNKNOWN -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileExists(path)
|
||||
StorageScope.ASSETS -> AssetData.fileExists(context, path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.fileExists(context, path)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
StorageScope.SAF -> SAFData.fileExists(context, path)
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileLastModified(path)
|
||||
StorageScope.ASSETS -> AssetData.fileLastModified(path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.fileLastModified(context, path)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
StorageScope.SAF -> SAFData.fileLastModified(context, path)
|
||||
|
||||
StorageScope.UNKNOWN -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastAccessed(storageScope: StorageScope, context: Context, path: String): Long {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileLastAccessed(path)
|
||||
StorageScope.ASSETS -> AssetData.fileLastAccessed(path)
|
||||
StorageScope.SHARED, StorageScope.SAF, StorageScope.UNKNOWN -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileSize(storageScope: StorageScope, context: Context, path: String): Long {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.fileSize(path)
|
||||
StorageScope.ASSETS -> AssetData.fileSize(path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.fileSize(context, path)
|
||||
} else {
|
||||
-1L
|
||||
}
|
||||
StorageScope.SAF -> SAFData.fileSize(context, path)
|
||||
|
||||
StorageScope.UNKNOWN -> -1L
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.delete(path)
|
||||
StorageScope.ASSETS -> AssetData.delete(path)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.delete(context, path)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
StorageScope.SAF -> SAFData.delete(context, path)
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
|
||||
return when(storageScope) {
|
||||
StorageScope.APP -> FileData.rename(from, to)
|
||||
StorageScope.ASSETS -> AssetData.rename(from, to)
|
||||
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStoreData.rename(context, from, to)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
StorageScope.SAF -> SAFData.rename(context, from, to)
|
||||
|
||||
StorageScope.UNKNOWN -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal var endOfFile = false
|
||||
abstract fun close()
|
||||
abstract fun flush()
|
||||
abstract fun seek(position: Long)
|
||||
abstract fun resize(length: Long): Error
|
||||
abstract fun position(): Long
|
||||
abstract fun size(): Long
|
||||
abstract fun read(buffer: ByteBuffer): Int
|
||||
abstract fun write(buffer: ByteBuffer): Boolean
|
||||
|
||||
fun seekFromEnd(positionFromEnd: Long) {
|
||||
val positionFromBeginning = size() + positionFromEnd
|
||||
if (positionFromBeginning >= 0) {
|
||||
seek(positionFromBeginning)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
|
||||
internal abstract val fileChannel: FileChannel
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
fileChannel.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
try {
|
||||
fileChannel.force(false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when flushing file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seek(position: Long) {
|
||||
try {
|
||||
fileChannel.position(position)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception when seeking file $filePath.", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resize(length: Long): Error {
|
||||
return try {
|
||||
fileChannel.truncate(length)
|
||||
Error.OK
|
||||
} catch (e: NonWritableChannelException) {
|
||||
Error.ERR_FILE_CANT_OPEN
|
||||
} catch (e: ClosedChannelException) {
|
||||
Error.ERR_FILE_CANT_OPEN
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Error.ERR_INVALID_PARAMETER
|
||||
} catch (e: IOException) {
|
||||
Error.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
override fun position(): Long {
|
||||
return try {
|
||||
fileChannel.position()
|
||||
} catch (e: IOException) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Exception when retrieving position for file $filePath.",
|
||||
e
|
||||
)
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun size() = try {
|
||||
fileChannel.size()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
|
||||
0L
|
||||
}
|
||||
|
||||
override fun read(buffer: ByteBuffer): Int {
|
||||
return try {
|
||||
val readBytes = fileChannel.read(buffer)
|
||||
if (readBytes == -1) {
|
||||
endOfFile = true
|
||||
0
|
||||
} else {
|
||||
readBytes
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while reading from file $filePath.", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteBuffer): Boolean {
|
||||
try {
|
||||
val writtenBytes = fileChannel.write(buffer)
|
||||
if (writtenBytes > 0) {
|
||||
endOfFile = false
|
||||
}
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception while writing to file $filePath.", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/**************************************************************************/
|
||||
/* FileAccessFlags.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
/**
|
||||
* Android representation of Godot native access flags.
|
||||
*/
|
||||
internal enum class FileAccessFlags(val nativeValue: Int) {
|
||||
/**
|
||||
* Opens the file for read operations.
|
||||
* The cursor is positioned at the beginning of the file.
|
||||
*/
|
||||
READ(1),
|
||||
|
||||
/**
|
||||
* Opens the file for write operations.
|
||||
* The file is created if it does not exist, and truncated if it does.
|
||||
*/
|
||||
WRITE(2),
|
||||
|
||||
/**
|
||||
* Opens the file for read and write operations.
|
||||
* Does not truncate the file. The cursor is positioned at the beginning of the file.
|
||||
*/
|
||||
READ_WRITE(3),
|
||||
|
||||
/**
|
||||
* Opens the file for read and write operations.
|
||||
* The file is created if it does not exist, and truncated if it does.
|
||||
* The cursor is positioned at the beginning of the file.
|
||||
*/
|
||||
WRITE_READ(7);
|
||||
|
||||
fun getMode(): String {
|
||||
return when (this) {
|
||||
READ -> "r"
|
||||
WRITE -> "w"
|
||||
READ_WRITE, WRITE_READ -> "rw"
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldTruncate(): Boolean {
|
||||
return when (this) {
|
||||
READ, READ_WRITE -> false
|
||||
WRITE, WRITE_READ -> true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
|
||||
for (flag in entries) {
|
||||
if (flag.nativeValue == modeFlag) {
|
||||
return flag
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
/**************************************************************************/
|
||||
/* FileAccessHandler.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Handles regular and media store file access and interactions.
|
||||
*/
|
||||
class FileAccessHandler(val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val TAG = FileAccessHandler::class.java.simpleName
|
||||
|
||||
private const val INVALID_FILE_ID = 0
|
||||
private const val STARTING_FILE_ID = 1
|
||||
private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID)
|
||||
|
||||
internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
return try {
|
||||
path?.let {
|
||||
DataAccess.getInputStream(storageScope, context, path)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
path?.let {
|
||||
DataAccess.fileExists(storageScope, context, it)
|
||||
} ?: false
|
||||
} catch (e: SecurityException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
path?.let {
|
||||
DataAccess.removeFile(storageScope, context, it)
|
||||
} ?: false
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(from)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
DataAccess.renameFile(storageScope, context, from, to)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val storageScopeIdentifier = StorageScope.Identifier(context)
|
||||
private val files = SparseArray<DataAccess>()
|
||||
private var lastFileId = STARTING_FILE_ID
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
private fun hasFileId(fileId: Int): Boolean {
|
||||
return files.indexOfKey(fileId) >= 0
|
||||
}
|
||||
|
||||
fun canAccess(filePath: String?): Boolean {
|
||||
return storageScopeIdentifier.canAccess(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive (> 0) file id when the operation succeeds.
|
||||
* Otherwise, returns a negative value of [Error].
|
||||
*/
|
||||
fun fileOpen(path: String?, modeFlags: Int): Int = lock.withLock {
|
||||
val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
|
||||
return if (fileError == Error.OK) {
|
||||
fileId
|
||||
} else {
|
||||
// Return the negative of the [Error#toNativeValue()] value to differentiate from the
|
||||
// positive file id.
|
||||
-fileError.toNativeValue()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> {
|
||||
if (accessFlag == null) {
|
||||
return FILE_OPEN_FAILED
|
||||
}
|
||||
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return FILE_OPEN_FAILED
|
||||
}
|
||||
|
||||
return try {
|
||||
path?.let {
|
||||
val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED
|
||||
files.put(++lastFileId, dataAccess)
|
||||
Pair(Error.OK, lastFileId)
|
||||
} ?: FILE_OPEN_FAILED
|
||||
} catch (e: FileNotFoundException) {
|
||||
Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error while opening $path", e)
|
||||
FILE_OPEN_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
fun fileGetSize(fileId: Int): Long = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return files[fileId].size()
|
||||
}
|
||||
|
||||
fun fileSeek(fileId: Int, position: Long) = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return
|
||||
}
|
||||
|
||||
files[fileId].seek(position)
|
||||
}
|
||||
|
||||
fun fileSeekFromEnd(fileId: Int, position: Long) = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return
|
||||
}
|
||||
|
||||
files[fileId].seekFromEnd(position)
|
||||
}
|
||||
|
||||
fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int = lock.withLock {
|
||||
if (!hasFileId(fileId) || byteBuffer == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return files[fileId].read(byteBuffer)
|
||||
}
|
||||
|
||||
fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?): Boolean = lock.withLock {
|
||||
if (!hasFileId(fileId) || byteBuffer == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return files[fileId].write(byteBuffer)
|
||||
}
|
||||
|
||||
fun fileFlush(fileId: Int) = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return
|
||||
}
|
||||
|
||||
files[fileId].flush()
|
||||
}
|
||||
|
||||
fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path)
|
||||
|
||||
fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to)
|
||||
|
||||
fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
|
||||
|
||||
fun fileLastModified(filepath: String?): Long {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return try {
|
||||
filepath?.let {
|
||||
DataAccess.fileLastModified(storageScope, context, it)
|
||||
} ?: 0L
|
||||
} catch (e: SecurityException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastAccessed(filepath: String?): Long {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return try {
|
||||
filepath?.let {
|
||||
DataAccess.fileLastAccessed(storageScope, context, it)
|
||||
} ?: 0L
|
||||
} catch (e: SecurityException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileResize(fileId: Int, length: Long): Int = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return Error.FAILED.toNativeValue()
|
||||
}
|
||||
|
||||
return files[fileId].resize(length).toNativeValue()
|
||||
}
|
||||
|
||||
fun fileSize(filepath: String?): Long {
|
||||
val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
|
||||
if (storageScope == StorageScope.UNKNOWN) {
|
||||
return -1L
|
||||
}
|
||||
|
||||
return try {
|
||||
filepath?.let {
|
||||
DataAccess.fileSize(storageScope, context, it)
|
||||
} ?: -1L
|
||||
} catch (e: SecurityException) {
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileGetPosition(fileId: Int): Long = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return files[fileId].position()
|
||||
}
|
||||
|
||||
fun isFileEof(fileId: Int): Boolean = lock.withLock {
|
||||
if (!hasFileId(fileId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return files[fileId].endOfFile
|
||||
}
|
||||
|
||||
fun setFileEof(fileId: Int, eof: Boolean) = lock.withLock {
|
||||
val file = files[fileId] ?: return
|
||||
file.endOfFile = eof
|
||||
}
|
||||
|
||||
fun fileClose(fileId: Int) = lock.withLock {
|
||||
if (hasFileId(fileId)) {
|
||||
files[fileId].close()
|
||||
files.remove(fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/**************************************************************************/
|
||||
/* FileData.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.os.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
|
||||
*/
|
||||
internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) {
|
||||
|
||||
companion object {
|
||||
private val TAG = FileData::class.java.simpleName
|
||||
|
||||
fun fileExists(path: String): Boolean {
|
||||
return try {
|
||||
File(path).isFile
|
||||
} catch (e: SecurityException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastModified(filepath: String): Long {
|
||||
return try {
|
||||
File(filepath).lastModified() / 1000L
|
||||
} catch (e: SecurityException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastAccessed(filepath: String): Long {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.readAttributes<BasicFileAttributes>(FileSystems.getDefault().getPath(filepath), BasicFileAttributes::class.java).lastAccessTime().to(TimeUnit.SECONDS)
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileSize(filepath: String): Long {
|
||||
return try {
|
||||
if (File(filepath).isFile) {
|
||||
File(filepath).length()
|
||||
} else {
|
||||
-1L
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(filepath: String): Boolean {
|
||||
return try {
|
||||
File(filepath).delete()
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(from: String, to: String): Boolean {
|
||||
return try {
|
||||
val fromFile = File(from)
|
||||
fromFile.renameTo(File(to))
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val fileChannel: FileChannel
|
||||
|
||||
init {
|
||||
fileChannel = if (accessFlag == FileAccessFlags.WRITE) {
|
||||
// Create parent directory is necessary
|
||||
val parentDir = File(filePath).parentFile
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
|
||||
FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
|
||||
} else {
|
||||
RandomAccessFile(filePath, accessFlag.getMode()).channel
|
||||
}
|
||||
|
||||
if (accessFlag.shouldTruncate()) {
|
||||
fileChannel.truncate(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
/**************************************************************************/
|
||||
/* MediaStoreData.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of [DataAccess] which handles access and interactions with file and data
|
||||
* under scoped storage via the MediaStore API.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
internal class MediaStoreData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) :
|
||||
DataAccess.FileChannelDataAccess(filePath) {
|
||||
|
||||
private data class DataItem(
|
||||
val id: Long,
|
||||
val uri: Uri,
|
||||
val displayName: String,
|
||||
val relativePath: String,
|
||||
val size: Int,
|
||||
val dateModified: Int,
|
||||
val mediaType: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = MediaStoreData::class.java.simpleName
|
||||
|
||||
private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
|
||||
private val PROJECTION = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.DISPLAY_NAME,
|
||||
MediaStore.Files.FileColumns.RELATIVE_PATH,
|
||||
MediaStore.Files.FileColumns.SIZE,
|
||||
MediaStore.Files.FileColumns.DATE_MODIFIED,
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
||||
)
|
||||
|
||||
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
|
||||
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
|
||||
|
||||
private fun getSelectionByPathArguments(path: String): Array<String> {
|
||||
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
|
||||
}
|
||||
|
||||
private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? "
|
||||
|
||||
private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString())
|
||||
|
||||
private fun getMediaStoreDisplayName(path: String) = File(path).name
|
||||
|
||||
private fun getMediaStoreRelativePath(path: String): String {
|
||||
val pathFile = File(path)
|
||||
val environmentDir = Environment.getExternalStorageDirectory()
|
||||
var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/')
|
||||
if (relativePath.isNotBlank()) {
|
||||
relativePath += "/"
|
||||
}
|
||||
return relativePath
|
||||
}
|
||||
|
||||
private fun queryById(context: Context, id: Long): List<DataItem> {
|
||||
val query = context.contentResolver.query(
|
||||
COLLECTION,
|
||||
PROJECTION,
|
||||
SELECTION_BY_ID,
|
||||
getSelectionByIdArgument(id),
|
||||
null
|
||||
)
|
||||
return dataItemFromCursor(query)
|
||||
}
|
||||
|
||||
private fun queryByPath(context: Context, path: String): List<DataItem> {
|
||||
val query = context.contentResolver.query(
|
||||
COLLECTION,
|
||||
PROJECTION,
|
||||
SELECTION_BY_PATH,
|
||||
getSelectionByPathArguments(path),
|
||||
null
|
||||
)
|
||||
return dataItemFromCursor(query)
|
||||
}
|
||||
|
||||
private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
|
||||
query?.use { cursor ->
|
||||
cursor.count
|
||||
if (cursor.count == 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val displayNameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
|
||||
val relativePathColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
|
||||
val dateModifiedColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
||||
val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||
|
||||
val result = ArrayList<DataItem>()
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
result.add(
|
||||
DataItem(
|
||||
id,
|
||||
ContentUris.withAppendedId(COLLECTION, id),
|
||||
cursor.getString(displayNameColumn),
|
||||
cursor.getString(relativePathColumn),
|
||||
cursor.getInt(sizeColumn),
|
||||
cursor.getInt(dateModifiedColumn),
|
||||
cursor.getInt(mediaTypeColumn)
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun addFile(context: Context, path: String): DataItem? {
|
||||
val fileDetails = ContentValues().apply {
|
||||
put(MediaStore.Files.FileColumns._ID, 0)
|
||||
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path))
|
||||
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path))
|
||||
}
|
||||
|
||||
context.contentResolver.insert(COLLECTION, fileDetails) ?: return null
|
||||
|
||||
// File was successfully added, let's retrieve its info
|
||||
val infos = queryByPath(context, path)
|
||||
if (infos.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return infos[0]
|
||||
}
|
||||
|
||||
fun delete(context: Context, path: String): Boolean {
|
||||
val itemsToDelete = queryByPath(context, path)
|
||||
if (itemsToDelete.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
var itemsDeleted = 0
|
||||
for (item in itemsToDelete) {
|
||||
itemsDeleted += resolver.delete(item.uri, null, null)
|
||||
}
|
||||
|
||||
return itemsDeleted > 0
|
||||
}
|
||||
|
||||
fun fileExists(context: Context, path: String): Boolean {
|
||||
return queryByPath(context, path).isNotEmpty()
|
||||
}
|
||||
|
||||
fun fileLastModified(context: Context, path: String): Long {
|
||||
val result = queryByPath(context, path)
|
||||
if (result.isEmpty()) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
val dataItem = result[0]
|
||||
return dataItem.dateModified.toLong() / 1000L
|
||||
}
|
||||
|
||||
fun fileSize(context: Context, path: String): Long {
|
||||
val result = queryByPath(context, path)
|
||||
if (result.isEmpty()) {
|
||||
return -1L
|
||||
}
|
||||
|
||||
val dataItem = result[0]
|
||||
return dataItem.size.toLong()
|
||||
}
|
||||
|
||||
fun rename(context: Context, from: String, to: String): Boolean {
|
||||
// Ensure the source exists.
|
||||
val sources = queryByPath(context, from)
|
||||
if (sources.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Take the first source
|
||||
val source = sources[0]
|
||||
|
||||
// Set up the updated values
|
||||
val updatedDetails = ContentValues().apply {
|
||||
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to))
|
||||
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to))
|
||||
}
|
||||
|
||||
val updated = context.contentResolver.update(
|
||||
source.uri,
|
||||
updatedDetails,
|
||||
SELECTION_BY_ID,
|
||||
getSelectionByIdArgument(source.id)
|
||||
)
|
||||
return updated > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val id: Long
|
||||
private val uri: Uri
|
||||
override val fileChannel: FileChannel
|
||||
private val parcelFileDescriptor: ParcelFileDescriptor
|
||||
|
||||
init {
|
||||
val contentResolver = context.contentResolver
|
||||
val dataItems = queryByPath(context, filePath)
|
||||
|
||||
val dataItem = when (accessFlag) {
|
||||
FileAccessFlags.READ -> {
|
||||
// The file should already exist
|
||||
if (dataItems.isEmpty()) {
|
||||
throw FileNotFoundException("Unable to access file $filePath")
|
||||
}
|
||||
|
||||
val dataItem = dataItems[0]
|
||||
dataItem
|
||||
}
|
||||
|
||||
FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> {
|
||||
// Create the file if it doesn't exist
|
||||
val dataItem = if (dataItems.isEmpty()) {
|
||||
addFile(context, filePath)
|
||||
} else {
|
||||
dataItems[0]
|
||||
}
|
||||
|
||||
if (dataItem == null) {
|
||||
throw FileNotFoundException("Unable to access file $filePath")
|
||||
}
|
||||
dataItem
|
||||
}
|
||||
}
|
||||
|
||||
id = dataItem.id
|
||||
uri = dataItem.uri
|
||||
|
||||
parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode())
|
||||
?: throw IllegalStateException("Unable to access file descriptor")
|
||||
fileChannel = if (accessFlag == FileAccessFlags.READ) {
|
||||
FileInputStream(parcelFileDescriptor.fileDescriptor).channel
|
||||
} else {
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).channel
|
||||
}
|
||||
|
||||
if (accessFlag.shouldTruncate()) {
|
||||
fileChannel.truncate(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
fileChannel.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $filePath.", e)
|
||||
} finally {
|
||||
try {
|
||||
parcelFileDescriptor.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing ParcelFileDescriptor for $filePath.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/**************************************************************************/
|
||||
/* SAFData.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.io.file
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
/**
|
||||
* Implementation of [DataAccess] which handles file access via a content URI obtained using the Android
|
||||
* Storage Access Framework (SAF).
|
||||
*/
|
||||
internal class SAFData(context: Context, private val path: String, accessFlag: FileAccessFlags) :
|
||||
DataAccess.FileChannelDataAccess(path) {
|
||||
|
||||
companion object {
|
||||
private val TAG = SAFData::class.java.simpleName
|
||||
|
||||
fun fileExists(context: Context, path: String): Boolean {
|
||||
return try {
|
||||
val uri = resolvePath(context, path, FileAccessFlags.READ)
|
||||
context.contentResolver.query(uri, arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME), null, null, null)
|
||||
?.use { cursor -> cursor.moveToFirst() } == true
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error checking file existence", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun fileLastModified(context: Context, path: String): Long {
|
||||
return try {
|
||||
val uri = resolvePath(context, path, FileAccessFlags.READ)
|
||||
val projection = arrayOf(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
if (index != -1) {
|
||||
return cursor.getLong(index) / 1000L
|
||||
}
|
||||
}
|
||||
}
|
||||
0L
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error reading last modified for", e)
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun fileSize(context: Context, path: String): Long {
|
||||
return try {
|
||||
val uri = resolvePath(context, path, FileAccessFlags.READ)
|
||||
val projection = arrayOf(DocumentsContract.Document.COLUMN_SIZE)
|
||||
context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
|
||||
if (index != -1) {
|
||||
return cursor.getLong(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1L
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error reading file size", e)
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(context: Context, path: String): Boolean {
|
||||
return try {
|
||||
val uri = resolvePath(context, path, FileAccessFlags.READ)
|
||||
DocumentsContract.deleteDocument(context.contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Error deleting file", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(context: Context, from: String, to: String): Boolean {
|
||||
// See https://github.com/godotengine/godot/pull/112215#discussion_r2479311235
|
||||
return false
|
||||
}
|
||||
|
||||
private fun resolvePath(context: Context, path: String, accessFlag: FileAccessFlags): Uri {
|
||||
val uri = path.toUri()
|
||||
val fragment = uri.fragment
|
||||
|
||||
if (fragment == null) {
|
||||
return uri
|
||||
}
|
||||
|
||||
// For directory format: content://treeUri#relative/path/to/file
|
||||
val treeUri = uri.buildUpon().fragment(null).build()
|
||||
val relativePath = fragment
|
||||
|
||||
val rootDir = DocumentFile.fromTreeUri(context, treeUri)
|
||||
?: throw IllegalStateException("Unable to resolve tree URI: $treeUri")
|
||||
|
||||
val parts = relativePath.split('/')
|
||||
val filename = parts.last()
|
||||
val folderParts = parts.dropLast(1)
|
||||
|
||||
var current: DocumentFile? = rootDir
|
||||
|
||||
val isWriteMode = when (accessFlag) {
|
||||
FileAccessFlags.WRITE,
|
||||
FileAccessFlags.READ_WRITE,
|
||||
FileAccessFlags.WRITE_READ -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
for (folder in folderParts) {
|
||||
var next = current?.findFile(folder)
|
||||
|
||||
if (next == null) {
|
||||
if (isWriteMode) {
|
||||
next = current?.createDirectory(folder)
|
||||
?: throw IllegalStateException("Failed to create directory: $folder")
|
||||
} else {
|
||||
throw IllegalStateException("Directory not found: $folder")
|
||||
}
|
||||
}
|
||||
|
||||
current = next
|
||||
}
|
||||
|
||||
var file = current?.findFile(filename)
|
||||
|
||||
if (file == null) {
|
||||
if (isWriteMode) {
|
||||
file = current?.createFile("*/*", filename)
|
||||
?: throw IllegalStateException("Failed to create file: $filename")
|
||||
} else {
|
||||
throw IllegalStateException("File does not exist: $relativePath")
|
||||
}
|
||||
}
|
||||
|
||||
return file.uri
|
||||
}
|
||||
}
|
||||
|
||||
override val fileChannel: FileChannel
|
||||
val parcelFileDescriptor: ParcelFileDescriptor
|
||||
init {
|
||||
val uri = resolvePath(context, path, accessFlag)
|
||||
parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, accessFlag.getMode())
|
||||
?: throw IllegalStateException("Unable to access file descriptor")
|
||||
fileChannel = if (accessFlag == FileAccessFlags.READ) {
|
||||
FileInputStream(parcelFileDescriptor.fileDescriptor).channel
|
||||
} else {
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).channel
|
||||
}
|
||||
|
||||
if (accessFlag.shouldTruncate()) {
|
||||
fileChannel.truncate(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
fileChannel.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing file $path.", e)
|
||||
} finally {
|
||||
try {
|
||||
parcelFileDescriptor.close()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Exception when closing ParcelFileDescriptor for $path.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/**************************************************************************/
|
||||
/* AndroidRuntimePlugin.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.plugin
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.net.toUri
|
||||
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.variant.Callable
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
/**
|
||||
* Built-in Godot Android plugin used to provide access to the Android runtime capabilities.
|
||||
*
|
||||
* @see <a href="https://docs.godotengine.org/en/latest/tutorials/platform/android/javaclasswrapper_and_androidruntimeplugin.html">Integrating with Android APIs</a>
|
||||
*/
|
||||
class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
|
||||
companion object {
|
||||
private val TAG = AndroidRuntimePlugin::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Helper method used to generate Godot Proxy instances.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Keep
|
||||
private fun generateProxyInstance(interfaces: Array<String>, invocationHandler: InvocationHandler): Any? {
|
||||
try {
|
||||
val interfaceClasses = interfaces.map { Class.forName(it) }.toTypedArray()
|
||||
|
||||
val proxy = Proxy.newProxyInstance(invocationHandler.javaClass.classLoader, interfaceClasses, invocationHandler)
|
||||
return proxy
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error generating Godot proxy for interfaces ${interfaces.joinToString(",")}", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method used to create [java.lang.reflect.Proxy] instance wrapping a given Godot [Callable].
|
||||
*
|
||||
* The [Proxy] instance is used to implement one SAM interface with the [Callable] serving as the delegate
|
||||
* implementation for the SAM interface overridden methods.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Keep
|
||||
private fun createProxyFromGodotCallable(interfaceName: String, godotCallable: Callable): Any? {
|
||||
return generateProxyInstance(arrayOf(interfaceName)) { proxy, method, args ->
|
||||
when (method.name) {
|
||||
// We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller
|
||||
// and provide consistency.
|
||||
"toString" -> "Godot Callable Proxy for $interfaceName"
|
||||
"equals" -> proxy == args[0]
|
||||
"hashCode" -> godotCallable.hashCode()
|
||||
|
||||
// Invocation for the interface single abstract method falls here and is dispatched to the
|
||||
// Godot [Callable].
|
||||
else -> godotCallable.call(*args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method used to create [java.lang.reflect.Proxy] instance wrapping a given Godot Object represented by
|
||||
* its ObjectID.
|
||||
*
|
||||
* The [Proxy] instance is used to implement one or multiple interfaces with the Object represented by
|
||||
* [godotObjectID] serving as the delegate implementation for the interface(s) overridden methods.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Keep
|
||||
private fun createProxyFromGodotObjectID(godotObjectID: Long, interfaces: Array<String>): Any? {
|
||||
return generateProxyInstance(interfaces) { proxy, method, args ->
|
||||
when (val methodName = method.name) {
|
||||
// We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller
|
||||
// and provide consistency.
|
||||
"toString" -> "Godot Object Proxy for ${interfaces.joinToString(",")}"
|
||||
"equals" -> proxy == args[0]
|
||||
"hashCode" -> godotObjectID
|
||||
|
||||
// Invocation for the remaining interface(s) methods falls here and is dispatched to the
|
||||
// Godot Object.
|
||||
else -> Callable.call(godotObjectID, methodName, *args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPluginName() = "AndroidRuntime"
|
||||
|
||||
/**
|
||||
* Provides access to the application [android.content.Context] to GDScript
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun getApplicationContext() = activity?.applicationContext
|
||||
|
||||
/**
|
||||
* Provides access to the host [android.app.Activity] to GDScript
|
||||
*/
|
||||
@UsedByGodot
|
||||
public override fun getActivity() = super.getActivity()
|
||||
|
||||
/**
|
||||
* Utility method used to create [Runnable] from Godot [Callable].
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun createRunnableFromGodotCallable(godotCallable: Callable): Runnable {
|
||||
return Runnable { godotCallable.call() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method used to create [java.util.concurrent.Callable] from Godot [Callable].
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable<Any> {
|
||||
return java.util.concurrent.Callable { godotCallable.call() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to take/release persistable URI permission.
|
||||
*/
|
||||
@UsedByGodot
|
||||
fun updatePersistableUriPermission(uriString: String, persist: Boolean): Boolean {
|
||||
try {
|
||||
val uri = uriString.toUri()
|
||||
val contentResolver = context.contentResolver
|
||||
if (persist) {
|
||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
} else {
|
||||
contentResolver.releasePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
Log.d(TAG, "Error updating persistable permission: ", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
/**************************************************************************/
|
||||
/* GodotPlugin.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.plugin;
|
||||
|
||||
import org.godotengine.godot.BuildConfig;
|
||||
import org.godotengine.godot.Godot;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Base class for Godot Android plugins.
|
||||
* <p>
|
||||
* A Godot Android plugin is an Android library with the following requirements:
|
||||
* <p>
|
||||
* - The plugin must have a dependency on the Godot Android library: `implementation "org.godotengine:godot:<godotLibVersion>"`
|
||||
* <p>
|
||||
* - The plugin must include a <meta-data> tag in its Android manifest with the following format:
|
||||
* <meta-data android:name="org.godotengine.plugin.v2.[PluginName]" android:value="[plugin.init.ClassFullName]" />
|
||||
* <p>
|
||||
* Where:
|
||||
* <p>
|
||||
* - 'PluginName' is the name of the plugin.
|
||||
* <p>
|
||||
* - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin init class
|
||||
* extending {@link GodotPlugin}.
|
||||
* <p>
|
||||
* A Godot Android plugin can also define and provide c/c++ gdextension libraries, which will be
|
||||
* automatically bundled by the aar build system.
|
||||
* GDExtension ('*.gdextension') config files must be located in the project 'assets' directory and
|
||||
* their paths specified by {@link GodotPlugin#getPluginGDExtensionLibrariesPaths()}.
|
||||
*
|
||||
* @see <a href="https://docs.godotengine.org/en/stable/tutorials/platform/android/index.html">Android plugins</a>
|
||||
*/
|
||||
public abstract class GodotPlugin {
|
||||
private static final String TAG = GodotPlugin.class.getSimpleName();
|
||||
|
||||
private final Godot godot;
|
||||
private final ConcurrentHashMap<String, SignalInfo> registeredSignals = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Base constructor passing a {@link Godot} instance through which the plugin can access Godot's
|
||||
* APIs and lifecycle events.
|
||||
*/
|
||||
public GodotPlugin(Godot godot) {
|
||||
this.godot = godot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to the Godot engine.
|
||||
*/
|
||||
protected Godot getGodot() {
|
||||
return godot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to the hosting {@link Activity}.
|
||||
*/
|
||||
@Nullable
|
||||
protected Activity getActivity() {
|
||||
return godot.getActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to the {@link Context}.
|
||||
*/
|
||||
protected Context getContext() {
|
||||
return godot.getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the plugin with Godot native code.
|
||||
* <p>
|
||||
* This method is invoked on the render thread to register the plugin on engine startup.
|
||||
*/
|
||||
public final void onRegisterPluginWithGodotNative() {
|
||||
final String pluginName = getPluginName();
|
||||
if (!nativeRegisterSingleton(pluginName, this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> pluginMethods = getPluginMethods();
|
||||
|
||||
Set<Method> filteredMethods = new HashSet<>();
|
||||
Class<?> clazz = getClass();
|
||||
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
// Check if the method is annotated with {@link UsedByGodot}.
|
||||
if (method.getAnnotation(UsedByGodot.class) != null) {
|
||||
filteredMethods.add(method);
|
||||
} else {
|
||||
// For backward compatibility, process the methods from the given <pluginMethods> argument.
|
||||
for (String methodName : pluginMethods) {
|
||||
if (methodName.equals(method.getName())) {
|
||||
filteredMethods.add(method);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Method method : filteredMethods) {
|
||||
List<String> ptr = new ArrayList<>();
|
||||
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
for (Class<?> c : paramTypes) {
|
||||
ptr.add(c.getName());
|
||||
}
|
||||
|
||||
String[] pt = new String[ptr.size()];
|
||||
ptr.toArray(pt);
|
||||
|
||||
nativeRegisterMethod(pluginName, method.getName(), method.getReturnType().getName(), pt);
|
||||
}
|
||||
|
||||
Set<SignalInfo> pluginSignals = getPluginSignals();
|
||||
|
||||
// Register the signals for this plugin.
|
||||
for (SignalInfo signalInfo : pluginSignals) {
|
||||
String signalName = signalInfo.getName();
|
||||
nativeRegisterSignal(pluginName, signalName, signalInfo.getParamTypesNames());
|
||||
registeredSignals.put(signalName, signalInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked once during the initialization process after creation of the
|
||||
* {@link org.godotengine.godot.GodotRenderView} view.
|
||||
* <p>
|
||||
* The plugin can return a non-null {@link View} layout which will be added to the Godot view
|
||||
* hierarchy.
|
||||
* <p>
|
||||
* Use {@link GodotPlugin#shouldBeOnTop()} to specify whether the plugin's {@link View} should
|
||||
* be added on top or behind the main Godot view.
|
||||
*
|
||||
* @see Activity#onCreate(Bundle)
|
||||
* @return the plugin's view to be included; null if no views should be included.
|
||||
*/
|
||||
@Nullable
|
||||
public View onMainCreate(@Nullable Activity activity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Activity#onActivityResult(int, int, Intent)
|
||||
*/
|
||||
public void onMainActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Activity#onRequestPermissionsResult(int, String[], int[])
|
||||
*/
|
||||
public void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Activity#onPause()
|
||||
*/
|
||||
public void onMainPause() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onResume()
|
||||
*/
|
||||
public void onMainResume() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onDestroy()
|
||||
*/
|
||||
public void onMainDestroy() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onBackPressed()
|
||||
*/
|
||||
public boolean onMainBackPressed() { return false; }
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when set up of the Godot engine is complete.
|
||||
* <p>
|
||||
* This is invoked before {@link GodotPlugin#onGodotMainLoopStarted()}.
|
||||
*/
|
||||
public void onGodotSetupCompleted() {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot main loop has started.
|
||||
*
|
||||
* This is invoked after {@link GodotPlugin#onGodotSetupCompleted()}.
|
||||
*/
|
||||
public void onGodotMainLoopStarted() {}
|
||||
|
||||
/**
|
||||
* When using the OpenGL renderer, this is invoked once per frame on the GL thread after the
|
||||
* frame is drawn.
|
||||
*/
|
||||
public void onGLDrawFrame(GL10 gl) {}
|
||||
|
||||
/**
|
||||
* When using the OpenGL renderer, this is called on the GL thread after the surface is created
|
||||
* and whenever the OpenGL ES surface size changes.
|
||||
*/
|
||||
public void onGLSurfaceChanged(GL10 gl, int width, int height) {}
|
||||
|
||||
/**
|
||||
* When using the OpenGL renderer, this is called on the GL thread when the surface is created
|
||||
* or recreated.
|
||||
*/
|
||||
public void onGLSurfaceCreated(GL10 gl, EGLConfig config) {}
|
||||
|
||||
/**
|
||||
* When using the Vulkan renderer, this is invoked once per frame on the Vulkan thread after
|
||||
* the frame is drawn.
|
||||
*/
|
||||
public void onVkDrawFrame() {}
|
||||
|
||||
/**
|
||||
* When using the Vulkan renderer, this is called on the Vulkan thread after the surface is
|
||||
* created and whenever the surface size changes.
|
||||
*/
|
||||
public void onVkSurfaceChanged(Surface surface, int width, int height) {}
|
||||
|
||||
/**
|
||||
* When using the Vulkan renderer, this is called on the Vulkan thread when the surface is
|
||||
* created or recreated.
|
||||
*/
|
||||
public void onVkSurfaceCreated(Surface surface) {}
|
||||
|
||||
/**
|
||||
* Returns the name of the plugin.
|
||||
* <p>
|
||||
* This value must match the one listed in the plugin '<meta-data>' manifest entry.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getPluginName();
|
||||
|
||||
/**
|
||||
* Returns the list of methods to be exposed to Godot.
|
||||
*
|
||||
* @deprecated Use the {@link UsedByGodot} annotation instead.
|
||||
*/
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public List<String> getPluginMethods() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of signals to be exposed to Godot.
|
||||
*/
|
||||
@NonNull
|
||||
public Set<SignalInfo> getPluginSignals() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths for the plugin's gdextension libraries (if any).
|
||||
* <p>
|
||||
* Each returned path must be relative to the 'assets' directory and point to a '*.gdextension' file.
|
||||
*/
|
||||
@NonNull
|
||||
public Set<String> getPluginGDExtensionLibrariesPaths() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the plugin's {@link View} returned in
|
||||
* {@link GodotPlugin#onMainCreate(Activity)} should be placed on top of the main Godot view.
|
||||
* <p>
|
||||
* Returning false causes the plugin's {@link View} to be placed behind, which can be useful
|
||||
* when used with transparency in order to let the Godot view handle inputs.
|
||||
*/
|
||||
public boolean shouldBeOnTop() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the plugin supports the given feature tag.
|
||||
*
|
||||
* @see <a href="https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html">Feature tags</a>
|
||||
*/
|
||||
public boolean supportsFeature(String featureTag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified action on the host thread.
|
||||
*
|
||||
* @param action the action to run on the host thread
|
||||
*
|
||||
* @deprecated Use the {@link GodotPlugin#runOnHostThread} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
protected void runOnUiThread(Runnable action) {
|
||||
runOnHostThread(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified action on the host thread.
|
||||
*
|
||||
* @param action the action to run on the host thread
|
||||
*/
|
||||
protected void runOnHostThread(Runnable action) {
|
||||
godot.runOnHostThread(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the specified action to be run on the render thread.
|
||||
*
|
||||
* @param action the action to run on the render thread
|
||||
*/
|
||||
protected void runOnRenderThread(Runnable action) {
|
||||
godot.runOnRenderThread(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a registered Godot signal.
|
||||
* @param signalName Name of the signal to emit. It will be validated against the set of registered signals.
|
||||
* @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the registered {@link SignalInfo} matching the signalName parameter.
|
||||
*/
|
||||
protected void emitSignal(final String signalName, final Object... signalArgs) {
|
||||
try {
|
||||
// Check that the given signal is among the registered set.
|
||||
SignalInfo signalInfo = registeredSignals.get(signalName);
|
||||
if (signalInfo == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Signal " + signalName + " is not registered for this plugin.");
|
||||
}
|
||||
emitSignal(getGodot(), getPluginName(), signalInfo, signalArgs);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
Log.w(TAG, exception);
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a registered Godot signal.
|
||||
* @param signal Signal to emit. It will be validated against the set of registered signals.
|
||||
* @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the registered {@link SignalInfo} matching the signal parameter.
|
||||
*/
|
||||
protected void emitSignal(SignalInfo signal, final Object... signalArgs) {
|
||||
emitSignal(signal.getName(), signalArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a Godot signal.
|
||||
* @param godot Godot instance
|
||||
* @param pluginName Name of the Godot plugin the signal will be emitted from. The plugin must already be registered with the Godot engine.
|
||||
* @param signalInfo Information about the signal to emit.
|
||||
* @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the given {@link SignalInfo} parameter.
|
||||
*/
|
||||
public static void emitSignal(Godot godot, String pluginName, SignalInfo signalInfo, final Object... signalArgs) {
|
||||
try {
|
||||
if (signalInfo == null) {
|
||||
throw new IllegalArgumentException("Signal must be non null.");
|
||||
}
|
||||
|
||||
// Validate the arguments count.
|
||||
Class<?>[] signalParamTypes = signalInfo.getParamTypes();
|
||||
if (signalArgs.length != signalParamTypes.length) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid arguments count. Should be " + signalParamTypes.length + " but is " + signalArgs.length);
|
||||
}
|
||||
|
||||
// Validate the argument's types.
|
||||
for (int i = 0; i < signalParamTypes.length; i++) {
|
||||
Object signalArg = signalArgs[i];
|
||||
if (signalArg != null && !signalParamTypes[i].isInstance(signalArg)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid type for argument #" + i + ". Should be of type " + signalParamTypes[i].getName());
|
||||
}
|
||||
}
|
||||
|
||||
godot.runOnRenderThread(() -> nativeEmitSignal(pluginName, signalInfo.getName(), signalArgs));
|
||||
|
||||
} catch (IllegalArgumentException exception) {
|
||||
Log.w(TAG, exception);
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to setup a {@link GodotPlugin} instance.
|
||||
* @param p_name Name of the instance.
|
||||
*/
|
||||
private static native boolean nativeRegisterSingleton(String p_name, Object object);
|
||||
|
||||
/**
|
||||
* Used to complete registration of the {@link GodotPlugin} instance's methods.
|
||||
* @param p_sname Name of the instance
|
||||
* @param p_name Name of the method to register
|
||||
* @param p_ret Return type of the registered method
|
||||
* @param p_params Method parameters types
|
||||
*/
|
||||
private static native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params);
|
||||
|
||||
/**
|
||||
* Used to complete registration of the {@link GodotPlugin} instance's signals.
|
||||
* @param pluginName Name of the plugin
|
||||
* @param signalName Name of the signal to register
|
||||
* @param signalParamTypes Signal parameters types
|
||||
*/
|
||||
private static native void nativeRegisterSignal(String pluginName, String signalName, String[] signalParamTypes);
|
||||
|
||||
/**
|
||||
* Used to emit signal by {@link GodotPlugin} instance.
|
||||
* @param pluginName Name of the plugin
|
||||
* @param signalName Name of the signal to emit
|
||||
* @param signalParams Signal parameters
|
||||
*/
|
||||
private static native void nativeEmitSignal(String pluginName, String signalName, Object[] signalParams);
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
/**************************************************************************/
|
||||
/* GodotPluginRegistry.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.plugin;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Registry used to load and access the registered Godot Android plugins.
|
||||
*/
|
||||
public final class GodotPluginRegistry {
|
||||
private static final String TAG = GodotPluginRegistry.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Prefix used for version 1 of the Godot plugin, mostly compatible with Godot 3.x
|
||||
*/
|
||||
private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1.";
|
||||
/**
|
||||
* Prefix used for version 2 of the Godot plugin, compatible with Godot 4.2+
|
||||
*/
|
||||
private static final String GODOT_PLUGIN_V2_NAME_PREFIX = "org.godotengine.plugin.v2.";
|
||||
|
||||
private static GodotPluginRegistry instance;
|
||||
private final ConcurrentHashMap<String, GodotPlugin> registry;
|
||||
|
||||
private GodotPluginRegistry() {
|
||||
registry = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the plugin tied to the given plugin name.
|
||||
* @param pluginName Name of the plugin
|
||||
* @return {@link GodotPlugin} handle if it exists, null otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
public GodotPlugin getPlugin(String pluginName) {
|
||||
return registry.get(pluginName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the full set of loaded plugins.
|
||||
*/
|
||||
public Collection<GodotPlugin> getAllPlugins() {
|
||||
if (registry.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return registry.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the manifest file and load all included Godot Android plugins.
|
||||
* <p>
|
||||
* A plugin manifest entry is a '<meta-data>' tag setup as described in the {@link GodotPlugin}
|
||||
* documentation.
|
||||
*
|
||||
* @param godot Godot instance
|
||||
* @param runtimePlugins Set of plugins provided at runtime for registration
|
||||
* @return A singleton instance of {@link GodotPluginRegistry}. This ensures that only one instance
|
||||
* of each Godot Android plugins is available at runtime.
|
||||
*/
|
||||
public static GodotPluginRegistry initializePluginRegistry(Godot godot, Set<GodotPlugin> runtimePlugins) {
|
||||
if (instance == null) {
|
||||
instance = new GodotPluginRegistry();
|
||||
instance.loadPlugins(godot, runtimePlugins);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin registry if it's initialized.
|
||||
* Throws a {@link IllegalStateException} exception if not.
|
||||
*
|
||||
* @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot, Set)} has not been called prior to calling this method.
|
||||
*/
|
||||
public static GodotPluginRegistry getPluginRegistry() throws IllegalStateException {
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("Plugin registry hasn't been initialized.");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void loadPlugins(Godot godot, Set<GodotPlugin> runtimePlugins) {
|
||||
// Register the runtime plugins
|
||||
if (runtimePlugins != null && !runtimePlugins.isEmpty()) {
|
||||
for (GodotPlugin plugin : runtimePlugins) {
|
||||
Log.i(TAG, "Registering runtime plugin " + plugin.getPluginName());
|
||||
registry.put(plugin.getPluginName(), plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the manifest plugins
|
||||
try {
|
||||
final Context context = godot.getContext();
|
||||
ApplicationInfo appInfo = context
|
||||
.getPackageManager()
|
||||
.getApplicationInfo(context.getPackageName(),
|
||||
PackageManager.GET_META_DATA);
|
||||
Bundle metaData = appInfo.metaData;
|
||||
if (metaData == null || metaData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String metaDataName : metaData.keySet()) {
|
||||
// Parse the meta-data looking for entry with the Godot plugin name prefix.
|
||||
String pluginName = null;
|
||||
if (metaDataName.startsWith(GODOT_PLUGIN_V2_NAME_PREFIX)) {
|
||||
pluginName = metaDataName.substring(GODOT_PLUGIN_V2_NAME_PREFIX.length()).trim();
|
||||
} else if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) {
|
||||
pluginName = metaDataName.substring(GODOT_PLUGIN_V1_NAME_PREFIX.length()).trim();
|
||||
Log.w(TAG, "Godot v1 plugin are deprecated in Godot 4.2 and higher: " + pluginName);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(pluginName)) {
|
||||
Log.i(TAG, "Initializing Godot plugin " + pluginName);
|
||||
|
||||
// Retrieve the plugin class full name.
|
||||
String pluginHandleClassFullName = metaData.getString(metaDataName);
|
||||
if (!TextUtils.isEmpty(pluginHandleClassFullName)) {
|
||||
try {
|
||||
// Attempt to create the plugin init class via reflection.
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<GodotPlugin> pluginClass = (Class<GodotPlugin>)Class
|
||||
.forName(pluginHandleClassFullName);
|
||||
Constructor<GodotPlugin> pluginConstructor = pluginClass
|
||||
.getConstructor(Godot.class);
|
||||
GodotPlugin pluginHandle = pluginConstructor.newInstance(godot);
|
||||
|
||||
// Load the plugin initializer into the registry using the plugin name as key.
|
||||
if (!pluginName.equals(pluginHandle.getPluginName())) {
|
||||
Log.w(TAG,
|
||||
"Meta-data plugin name does not match the value returned by the plugin handle: " + pluginName + " =/= " + pluginHandle.getPluginName());
|
||||
}
|
||||
registry.put(pluginName, pluginHandle);
|
||||
Log.i(TAG, "Completed initialization for Godot plugin " + pluginHandle.getPluginName());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid plugin loader class for " + pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/**************************************************************************/
|
||||
/* SignalInfo.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.plugin;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Store information about a {@link GodotPlugin}'s signal.
|
||||
*/
|
||||
public final class SignalInfo {
|
||||
private final String name;
|
||||
private final Class<?>[] paramTypes;
|
||||
private final String[] paramTypesNames;
|
||||
|
||||
public SignalInfo(@NonNull String signalName, Class<?>... paramTypes) {
|
||||
if (TextUtils.isEmpty(signalName)) {
|
||||
throw new IllegalArgumentException("Invalid signal name: " + signalName);
|
||||
}
|
||||
|
||||
this.name = signalName;
|
||||
this.paramTypes = paramTypes == null ? new Class<?>[0] : paramTypes;
|
||||
this.paramTypesNames = new String[this.paramTypes.length];
|
||||
for (int i = 0; i < this.paramTypes.length; i++) {
|
||||
this.paramTypesNames[i] = this.paramTypes[i].getName();
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
Class<?>[] getParamTypes() {
|
||||
return paramTypes;
|
||||
}
|
||||
|
||||
String[] getParamTypesNames() {
|
||||
return paramTypesNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SignalInfo{"
|
||||
+
|
||||
"name='" + name + '\'' +
|
||||
", paramsTypes=" + Arrays.toString(paramTypes) +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof SignalInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalInfo that = (SignalInfo)o;
|
||||
|
||||
return name.equals(that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**************************************************************************/
|
||||
/* UsedByGodot.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.plugin;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation to indicate a method is being invoked from the Godot game logic.
|
||||
*
|
||||
* At runtime, annotated plugin methods are detected and automatically registered.
|
||||
*/
|
||||
@Target({ ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface UsedByGodot {}
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
/**************************************************************************/
|
||||
/* GodotService.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.os.Process
|
||||
import android.os.RemoteException
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.SurfaceControlViewHost
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.os.bundleOf
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.GodotHost
|
||||
import org.godotengine.godot.R
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Specialized [Service] implementation able to host a Godot engine instance.
|
||||
*
|
||||
* When used remotely (from another process), this component lacks access to an [android.app.Activity]
|
||||
* instance, and as such it does not have full access to the set of Godot UI capabilities.
|
||||
*
|
||||
* Limitations: As of version 4.5, use of vulkan + swappy causes [GodotService] to crash as swappy requires an Activity
|
||||
* context. So [GodotService] should be used with OpenGL or with Vulkan with swappy disabled.
|
||||
*/
|
||||
open class GodotService : Service() {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotService::class.java.simpleName
|
||||
|
||||
const val EXTRA_MSG_PAYLOAD = "extraMsgPayload"
|
||||
|
||||
// Keys to store / retrieve msg payloads
|
||||
const val KEY_COMMAND_LINE_PARAMETERS = "commandLineParameters"
|
||||
const val KEY_HOST_TOKEN = "hostToken"
|
||||
const val KEY_DISPLAY_ID = "displayId"
|
||||
const val KEY_WIDTH = "width"
|
||||
const val KEY_HEIGHT = "height"
|
||||
const val KEY_SURFACE_PACKAGE = "surfacePackage"
|
||||
const val KEY_ENGINE_STATUS = "engineStatus"
|
||||
const val KEY_ENGINE_ERROR = "engineError"
|
||||
|
||||
// Set of commands from the client to the service
|
||||
const val MSG_INIT_ENGINE = 0
|
||||
const val MSG_START_ENGINE = MSG_INIT_ENGINE + 1
|
||||
const val MSG_STOP_ENGINE = MSG_START_ENGINE + 1
|
||||
const val MSG_DESTROY_ENGINE = MSG_STOP_ENGINE + 1
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
const val MSG_WRAP_ENGINE_WITH_SCVH = MSG_DESTROY_ENGINE + 1
|
||||
|
||||
// Set of commands from the service to the client
|
||||
const val MSG_ENGINE_ERROR = 100
|
||||
const val MSG_ENGINE_STATUS_UPDATE = 101
|
||||
const val MSG_ENGINE_RESTART_REQUESTED = 102
|
||||
}
|
||||
|
||||
enum class EngineStatus {
|
||||
INITIALIZED,
|
||||
SCVH_CREATED,
|
||||
STARTED,
|
||||
STOPPED,
|
||||
DESTROYED,
|
||||
}
|
||||
|
||||
enum class EngineError {
|
||||
ALREADY_BOUND,
|
||||
INIT_FAILED,
|
||||
SCVH_CREATION_FAILED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to subscribe to engine's updates.
|
||||
*/
|
||||
private class RemoteListener(val handlerRef: WeakReference<IncomingHandler>, val replyTo: Messenger) {
|
||||
fun onEngineError(error: EngineError, extras: Bundle? = null) {
|
||||
try {
|
||||
replyTo.send(Message.obtain().apply {
|
||||
what = MSG_ENGINE_ERROR
|
||||
data.putString(KEY_ENGINE_ERROR, error.name)
|
||||
if (extras != null && !extras.isEmpty) {
|
||||
data.putAll(extras)
|
||||
}
|
||||
})
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to send engine error", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEngineStatusUpdate(status: EngineStatus, extras: Bundle? = null) {
|
||||
try {
|
||||
replyTo.send(Message.obtain().apply {
|
||||
what = MSG_ENGINE_STATUS_UPDATE
|
||||
data.putString(KEY_ENGINE_STATUS, status.name)
|
||||
if (extras != null && !extras.isEmpty) {
|
||||
data.putAll(extras)
|
||||
}
|
||||
})
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to send engine status update", e)
|
||||
}
|
||||
|
||||
if (status == EngineStatus.DESTROYED) {
|
||||
val handler = handlerRef.get() ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && handler.viewHost != null) {
|
||||
Log.d(TAG, "Releasing SurfaceControlViewHost")
|
||||
handler.viewHost?.release()
|
||||
handler.viewHost = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onEngineRestartRequested() {
|
||||
try {
|
||||
replyTo.send(Message.obtain(null, MSG_ENGINE_RESTART_REQUESTED))
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(TAG, "Unable to send restart request", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler of incoming messages from remote clients.
|
||||
*/
|
||||
private class IncomingHandler(private val serviceRef: WeakReference<GodotService>) : Handler() {
|
||||
|
||||
var viewHost: SurfaceControlViewHost? = null
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
val service = serviceRef.get() ?: return
|
||||
|
||||
Log.d(TAG, "HandleMessage: $msg")
|
||||
|
||||
if (msg.replyTo == null) {
|
||||
// Messages for this handler must have a valid 'replyTo' field
|
||||
super.handleMessage(msg)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val serviceListener = service.listener
|
||||
if (serviceListener == null) {
|
||||
service.listener = RemoteListener(WeakReference(this), msg.replyTo)
|
||||
} else if (serviceListener.replyTo != msg.replyTo) {
|
||||
Log.e(TAG, "Engine is already bound to another client")
|
||||
msg.replyTo.send(Message.obtain().apply {
|
||||
what = MSG_ENGINE_ERROR
|
||||
data.putString(KEY_ENGINE_ERROR, EngineError.ALREADY_BOUND.name)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
when (msg.what) {
|
||||
MSG_INIT_ENGINE -> service.initEngine(msg.data.getStringArray(KEY_COMMAND_LINE_PARAMETERS))
|
||||
|
||||
MSG_START_ENGINE -> service.startEngine()
|
||||
|
||||
MSG_STOP_ENGINE -> service.stopEngine()
|
||||
|
||||
MSG_DESTROY_ENGINE -> service.destroyEngine()
|
||||
|
||||
MSG_WRAP_ENGINE_WITH_SCVH -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
Log.e(TAG, "SDK version is less than the minimum required (${Build.VERSION_CODES.R})")
|
||||
service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
|
||||
return
|
||||
}
|
||||
|
||||
var currentViewHost = viewHost
|
||||
if (currentViewHost != null) {
|
||||
Log.i(TAG, "Attached Godot engine to SurfaceControlViewHost")
|
||||
service.listener?.onEngineStatusUpdate(
|
||||
EngineStatus.SCVH_CREATED,
|
||||
bundleOf(KEY_SURFACE_PACKAGE to currentViewHost.surfacePackage)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val msgData = msg.data
|
||||
if (msgData.isEmpty) {
|
||||
Log.e(TAG, "Invalid message data from binding client.. Aborting")
|
||||
service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
|
||||
return
|
||||
}
|
||||
|
||||
val godotContainerLayout = service.godot.containerLayout
|
||||
if (godotContainerLayout == null) {
|
||||
Log.e(TAG, "Invalid godot layout.. Aborting")
|
||||
service.listener?.onEngineError(EngineError.SCVH_CREATION_FAILED)
|
||||
return
|
||||
}
|
||||
|
||||
val hostToken = msgData.getBinder(KEY_HOST_TOKEN)
|
||||
val width = msgData.getInt(KEY_WIDTH)
|
||||
val height = msgData.getInt(KEY_HEIGHT)
|
||||
val displayId = msgData.getInt(KEY_DISPLAY_ID)
|
||||
val display = service.getSystemService(DisplayManager::class.java)
|
||||
.getDisplay(displayId)
|
||||
|
||||
Log.d(TAG, "Setting up SurfaceControlViewHost")
|
||||
currentViewHost = SurfaceControlViewHost(service, display, hostToken).apply {
|
||||
setView(godotContainerLayout, width, height)
|
||||
|
||||
Log.i(TAG, "Attached Godot engine to SurfaceControlViewHost")
|
||||
service.listener?.onEngineStatusUpdate(
|
||||
EngineStatus.SCVH_CREATED,
|
||||
bundleOf(KEY_SURFACE_PACKAGE to surfacePackage)
|
||||
)
|
||||
}
|
||||
viewHost = currentViewHost
|
||||
}
|
||||
|
||||
else -> super.handleMessage(msg)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to handle message", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GodotServiceHost : GodotHost {
|
||||
override fun getActivity() = null
|
||||
override fun getGodot() = this@GodotService.godot
|
||||
override fun getCommandLine() = commandLineParams
|
||||
|
||||
override fun runOnHostThread(action: Runnable) {
|
||||
if (Thread.currentThread() != handler.looper.thread) {
|
||||
handler.post(action)
|
||||
} else {
|
||||
action.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotForceQuit(instance: Godot) {
|
||||
if (instance === godot) {
|
||||
Log.d(TAG, "Force quitting Godot service")
|
||||
forceQuitService()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotRestartRequested(instance: Godot) {
|
||||
if (instance === godot) {
|
||||
Log.d(TAG, "Restarting Godot service")
|
||||
listener?.onEngineRestartRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val commandLineParams = ArrayList<String>()
|
||||
private val handler = IncomingHandler(WeakReference(this))
|
||||
private val messenger = Messenger(handler)
|
||||
private val godotHost = GodotServiceHost()
|
||||
|
||||
private val godot: Godot by lazy { Godot.getInstance(applicationContext) }
|
||||
private var listener: RemoteListener? = null
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "OnCreate")
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
// Dispatch the start payload to the incoming handler
|
||||
Log.d(TAG, "Processing start command $intent")
|
||||
val msg = intent?.getParcelableExtra<Message>(EXTRA_MSG_PAYLOAD)
|
||||
if (msg != null) {
|
||||
handler.sendMessage(msg)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun updateCommandLineParams(args: List<String>) {
|
||||
// Update the list of command line params with the new args
|
||||
commandLineParams.clear()
|
||||
if (args.isNotEmpty()) {
|
||||
commandLineParams.addAll(args)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performEngineInitialization(): Boolean {
|
||||
Log.d(TAG, "Performing engine initialization")
|
||||
try {
|
||||
// Initialize the Godot instance
|
||||
if (!godot.initEngine(godotHost, godotHost.commandLine, godotHost.getHostPlugins(godot))) {
|
||||
throw IllegalStateException("Unable to initialize Godot engine layer")
|
||||
}
|
||||
|
||||
if (godot.onInitRenderView(godotHost) == null) {
|
||||
throw IllegalStateException("Unable to initialize engine render view")
|
||||
}
|
||||
return true
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Engine initialization failed", e)
|
||||
val errorMessage = if (TextUtils.isEmpty(e.message)
|
||||
) {
|
||||
getString(R.string.error_engine_setup_message)
|
||||
} else {
|
||||
e.message!!
|
||||
}
|
||||
godot.alert(errorMessage, getString(R.string.text_error_title)) { godot.destroyAndKillProcess() }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "OnDestroy")
|
||||
super.onDestroy()
|
||||
destroyEngine()
|
||||
}
|
||||
|
||||
private fun forceQuitService() {
|
||||
Log.d(TAG, "Force quitting service")
|
||||
stopSelf()
|
||||
Process.killProcess(Process.myPid())
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = messenger.binder
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
stopEngine()
|
||||
return false
|
||||
}
|
||||
|
||||
private fun initEngine(args: Array<String>?): FrameLayout? {
|
||||
if (!godot.isInitialized()) {
|
||||
if (!args.isNullOrEmpty()) {
|
||||
updateCommandLineParams(args.asList())
|
||||
}
|
||||
|
||||
if (!performEngineInitialization()) {
|
||||
Log.e(TAG, "Unable to initialize Godot engine")
|
||||
return null
|
||||
} else {
|
||||
Log.i(TAG, "Engine initialization complete!")
|
||||
}
|
||||
}
|
||||
val godotContainerLayout = godot.containerLayout
|
||||
if (godotContainerLayout == null) {
|
||||
listener?.onEngineError(EngineError.INIT_FAILED)
|
||||
} else {
|
||||
Log.i(TAG, "Initialized Godot engine")
|
||||
listener?.onEngineStatusUpdate(EngineStatus.INITIALIZED)
|
||||
}
|
||||
|
||||
return godotContainerLayout
|
||||
}
|
||||
|
||||
private fun startEngine() {
|
||||
if (!godot.isInitialized()) {
|
||||
Log.e(TAG, "Attempting to start uninitialized Godot engine instance")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting Godot engine")
|
||||
godot.onStart(godotHost)
|
||||
godot.onResume(godotHost)
|
||||
|
||||
listener?.onEngineStatusUpdate(EngineStatus.STARTED)
|
||||
}
|
||||
|
||||
private fun stopEngine() {
|
||||
if (!godot.isInitialized()) {
|
||||
Log.e(TAG, "Attempting to stop uninitialized Godot engine instance")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Stopping Godot engine")
|
||||
godot.onPause(godotHost)
|
||||
godot.onStop(godotHost)
|
||||
|
||||
listener?.onEngineStatusUpdate(EngineStatus.STOPPED)
|
||||
}
|
||||
|
||||
private fun destroyEngine() {
|
||||
if (!godot.isInitialized()) {
|
||||
return
|
||||
}
|
||||
|
||||
godot.onDestroy(godotHost)
|
||||
|
||||
listener?.onEngineStatusUpdate(EngineStatus.DESTROYED)
|
||||
listener = null
|
||||
forceQuitService()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
/**************************************************************************/
|
||||
/* RemoteGodotFragment.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.SurfaceControlViewHost
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.godotengine.godot.GodotHost
|
||||
import org.godotengine.godot.R
|
||||
import org.godotengine.godot.service.GodotService.EngineStatus.*
|
||||
import org.godotengine.godot.service.GodotService.EngineError.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Godot [Fragment] component showcasing how to drive rendering from another process using a [GodotService] instance.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class RemoteGodotFragment: Fragment() {
|
||||
|
||||
companion object {
|
||||
internal val TAG = RemoteGodotFragment::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* Target we publish for receiving messages from the service.
|
||||
*/
|
||||
private val messengerForReply = Messenger(IncomingHandler(WeakReference<RemoteGodotFragment>(this)))
|
||||
|
||||
/**
|
||||
* Messenger for sending messages to the [GodotService] implementation.
|
||||
*/
|
||||
private var serviceMessenger: Messenger? = null
|
||||
|
||||
private var remoteSurface : SurfaceView? = null
|
||||
|
||||
private var engineInitialized = false
|
||||
private var fragmentStarted = false
|
||||
private var serviceBound = false
|
||||
private var remoteGameArgs = arrayOf<String>()
|
||||
|
||||
private var godotHost : GodotHost? = null
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
Log.d(TAG, "Connected to service $name")
|
||||
serviceMessenger = Messenger(service)
|
||||
|
||||
// Initialize the Godot engine
|
||||
initGodotEngine()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.d(TAG, "Disconnected from service $name")
|
||||
serviceMessenger = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler of incoming messages from [GodotService] implementations.
|
||||
*/
|
||||
private class IncomingHandler(private val fragmentRef: WeakReference<RemoteGodotFragment>) : Handler() {
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
val fragment = fragmentRef.get() ?: return
|
||||
|
||||
try {
|
||||
Log.d(TAG, "HandleMessage: $msg")
|
||||
|
||||
when (msg.what) {
|
||||
GodotService.MSG_ENGINE_STATUS_UPDATE -> {
|
||||
try {
|
||||
val engineStatus = GodotService.EngineStatus.valueOf(
|
||||
msg.data.getString(GodotService.KEY_ENGINE_STATUS, "")
|
||||
)
|
||||
Log.d(TAG, "Received engine status $engineStatus")
|
||||
|
||||
when (engineStatus) {
|
||||
INITIALIZED -> {
|
||||
Log.d(TAG, "Engine initialized!")
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Creating SurfaceControlViewHost...")
|
||||
fragment.remoteSurface?.let {
|
||||
fragment.serviceMessenger?.send(Message.obtain().apply {
|
||||
what = GodotService.MSG_WRAP_ENGINE_WITH_SCVH
|
||||
data.apply {
|
||||
putBinder(GodotService.KEY_HOST_TOKEN, it.hostToken)
|
||||
putInt(GodotService.KEY_DISPLAY_ID, it.display.displayId)
|
||||
putInt(GodotService.KEY_WIDTH, it.width)
|
||||
putInt(GodotService.KEY_HEIGHT, it.height)
|
||||
}
|
||||
replyTo = fragment.messengerForReply
|
||||
})
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to set up SurfaceControlViewHost", e)
|
||||
}
|
||||
}
|
||||
|
||||
STARTED -> {
|
||||
Log.d(TAG, "Engine started!")
|
||||
}
|
||||
|
||||
STOPPED -> {
|
||||
Log.d(TAG, "Engine stopped!")
|
||||
}
|
||||
|
||||
DESTROYED -> {
|
||||
Log.d(TAG, "Engine destroyed!")
|
||||
fragment.engineInitialized = false
|
||||
}
|
||||
|
||||
SCVH_CREATED -> {
|
||||
Log.d(TAG, "SurfaceControlViewHost created!")
|
||||
|
||||
val surfacePackage = msg.data.getParcelable<SurfaceControlViewHost.SurfacePackage>(
|
||||
GodotService.KEY_SURFACE_PACKAGE)
|
||||
if (surfacePackage == null) {
|
||||
Log.e(TAG, "Unable to retrieve surface package from GodotService")
|
||||
} else {
|
||||
fragment.remoteSurface?.setChildSurfacePackage(surfacePackage)
|
||||
fragment.engineInitialized = true
|
||||
fragment.startGodotEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to retrieve engine status update from $msg")
|
||||
}
|
||||
}
|
||||
|
||||
GodotService.MSG_ENGINE_ERROR -> {
|
||||
try {
|
||||
val engineError = GodotService.EngineError.valueOf(
|
||||
msg.data.getString(GodotService.KEY_ENGINE_ERROR, "")
|
||||
)
|
||||
Log.d(TAG, "Received engine error $engineError")
|
||||
|
||||
when (engineError) {
|
||||
ALREADY_BOUND -> {
|
||||
// Engine is already connected to another client, unbind for now
|
||||
fragment.stopRemoteGame(false)
|
||||
}
|
||||
|
||||
INIT_FAILED -> {
|
||||
Log.e(TAG, "Engine initialization failed")
|
||||
}
|
||||
|
||||
SCVH_CREATION_FAILED -> {
|
||||
Log.e(TAG, "SurfaceControlViewHost creation failed")
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Unable to retrieve engine error from message $msg", e)
|
||||
}
|
||||
}
|
||||
|
||||
GodotService.MSG_ENGINE_RESTART_REQUESTED -> {
|
||||
Log.d(TAG, "Engine restart requested")
|
||||
// Validate the engine is actually running
|
||||
if (!fragment.serviceBound || !fragment.engineInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the current game args since stopping the engine will clear them out
|
||||
val currentArgs = fragment.remoteGameArgs
|
||||
|
||||
// Stop the engine
|
||||
fragment.stopRemoteGame()
|
||||
|
||||
// Restart the engine
|
||||
fragment.startRemoteGame(currentArgs)
|
||||
}
|
||||
|
||||
else -> super.handleMessage(msg)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to handle message $msg", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
val parentActivity = activity
|
||||
if (parentActivity is GodotHost) {
|
||||
godotHost = parentActivity
|
||||
} else {
|
||||
val parentFragment = parentFragment
|
||||
if (parentFragment is GodotHost) {
|
||||
godotHost = parentFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
godotHost = null
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.remote_godot_fragment_layout, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
remoteSurface = view.findViewById(R.id.remote_godot_window_surface)
|
||||
remoteSurface?.setZOrderOnTop(false)
|
||||
|
||||
initGodotEngine()
|
||||
}
|
||||
|
||||
fun startRemoteGame(args: Array<String>) {
|
||||
Log.d(TAG, "Starting remote game with args: ${args.contentToString()}")
|
||||
remoteSurface?.setZOrderOnTop(true)
|
||||
remoteGameArgs = args
|
||||
context?.bindService(
|
||||
Intent(context, GodotService::class.java),
|
||||
serviceConnection,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
serviceBound = true
|
||||
}
|
||||
|
||||
fun stopRemoteGame(destroyEngine: Boolean = true) {
|
||||
Log.d(TAG, "Stopping remote game")
|
||||
remoteSurface?.setZOrderOnTop(false)
|
||||
remoteGameArgs = arrayOf()
|
||||
|
||||
if (serviceBound) {
|
||||
if (destroyEngine) {
|
||||
serviceMessenger?.send(Message.obtain().apply {
|
||||
what = GodotService.MSG_DESTROY_ENGINE
|
||||
replyTo = messengerForReply
|
||||
})
|
||||
}
|
||||
context?.unbindService(serviceConnection)
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun initGodotEngine() {
|
||||
if (!serviceBound) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
serviceMessenger?.send(Message.obtain().apply {
|
||||
what = GodotService.MSG_INIT_ENGINE
|
||||
data.apply {
|
||||
putStringArray(GodotService.KEY_COMMAND_LINE_PARAMETERS, remoteGameArgs)
|
||||
}
|
||||
replyTo = messengerForReply
|
||||
})
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to initialize Godot engine", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startGodotEngine() {
|
||||
if (!serviceBound || !engineInitialized || !fragmentStarted) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
serviceMessenger?.send(Message.obtain().apply {
|
||||
what = GodotService.MSG_START_ENGINE
|
||||
replyTo = messengerForReply
|
||||
})
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to start Godot engine", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopGodotEngine() {
|
||||
if (!serviceBound || !engineInitialized || fragmentStarted) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
serviceMessenger?.send(Message.obtain().apply {
|
||||
what = GodotService.MSG_STOP_ENGINE
|
||||
replyTo = messengerForReply
|
||||
})
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to stop Godot engine", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
fragmentStarted = true
|
||||
startGodotEngine()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
fragmentStarted = false
|
||||
stopGodotEngine()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopRemoteGame()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
/**************************************************************************/
|
||||
/* GodotTTS.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.tts;
|
||||
|
||||
import org.godotengine.godot.GodotLib;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.UtteranceProgressListener;
|
||||
import android.speech.tts.Voice;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Wrapper for Android Text to Speech API and custom utterance query implementation.
|
||||
* <p>
|
||||
* A [GodotTTS] provides the following features:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Access to the Android Text to Speech API.
|
||||
* <li>Utterance pause / resume functions, unsupported by Android TTS API.
|
||||
* </ul>
|
||||
*/
|
||||
@Keep
|
||||
public class GodotTTS extends UtteranceProgressListener implements TextToSpeech.OnInitListener {
|
||||
// Note: These constants must be in sync with DisplayServerEnums::TTSUtteranceEvent enum from "servers/display/display_server.h".
|
||||
final private static int EVENT_START = 0;
|
||||
final private static int EVENT_END = 1;
|
||||
final private static int EVENT_CANCEL = 2;
|
||||
final private static int EVENT_BOUNDARY = 3;
|
||||
|
||||
// Note: These constants must be in sync with TTS_Android constants from "platform/android/tts_android.h".
|
||||
final private static int INIT_STATE_UNKNOWN = 0;
|
||||
final private static int INIT_STATE_SUCCESS = 1;
|
||||
final private static int INIT_STATE_FAIL = -1;
|
||||
|
||||
private final Context context;
|
||||
private TextToSpeech synth;
|
||||
private int state;
|
||||
private LinkedList<GodotUtterance> queue;
|
||||
final private Object lock = new Object();
|
||||
private GodotUtterance lastUtterance;
|
||||
|
||||
private boolean speaking;
|
||||
private boolean paused;
|
||||
|
||||
public GodotTTS(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private void updateTTS() {
|
||||
if (!speaking && queue.size() > 0) {
|
||||
int mode = TextToSpeech.QUEUE_FLUSH;
|
||||
GodotUtterance message = queue.pollFirst();
|
||||
|
||||
Set<Voice> voices = synth.getVoices();
|
||||
if (voices == null) {
|
||||
return;
|
||||
}
|
||||
for (Voice v : voices) {
|
||||
if (v.getName().equals(message.voice)) {
|
||||
synth.setVoice(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
synth.setPitch(message.pitch);
|
||||
synth.setSpeechRate(message.rate);
|
||||
|
||||
Bundle params = new Bundle();
|
||||
params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f);
|
||||
|
||||
lastUtterance = message;
|
||||
lastUtterance.start = 0;
|
||||
lastUtterance.offset = 0;
|
||||
paused = false;
|
||||
|
||||
synth.speak(message.text, mode, params, String.valueOf(message.id));
|
||||
speaking = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when the TTS service is about to speak the specified range.
|
||||
*/
|
||||
@Override
|
||||
public void onRangeStart(String utteranceId, int start, int end, int frame) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
lastUtterance.offset = start;
|
||||
GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when an utterance was canceled in progress.
|
||||
*/
|
||||
@Override
|
||||
public void onStop(String utteranceId, boolean interrupted) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && !paused && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
|
||||
speaking = false;
|
||||
updateTTS();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when an utterance has begun to be spoken..
|
||||
*/
|
||||
@Override
|
||||
public void onStart(String utteranceId) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && lastUtterance.start == 0 && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when an utterance was successfully finished.
|
||||
*/
|
||||
@Override
|
||||
public void onDone(String utteranceId) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && !paused && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0);
|
||||
speaking = false;
|
||||
updateTTS();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when an error has occurred during processing.
|
||||
*/
|
||||
@Override
|
||||
public void onError(String utteranceId, int errorCode) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && !paused && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
|
||||
speaking = false;
|
||||
updateTTS();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when an error has occurred during processing (pre API level 21 version).
|
||||
*/
|
||||
@Override
|
||||
public void onError(String utteranceId) {
|
||||
synchronized (lock) {
|
||||
if (lastUtterance != null && !paused && Long.parseLong(utteranceId) == lastUtterance.id) {
|
||||
GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
|
||||
speaking = false;
|
||||
updateTTS();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize synth and query.
|
||||
*/
|
||||
public void init() {
|
||||
state = INIT_STATE_UNKNOWN;
|
||||
synth = new TextToSpeech(context, this);
|
||||
queue = new LinkedList<GodotUtterance>();
|
||||
|
||||
synth.setOnUtteranceProgressListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by TTS engine when initialization is finished.
|
||||
*/
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
synchronized (lock) {
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
state = INIT_STATE_SUCCESS;
|
||||
} else {
|
||||
state = INIT_STATE_FAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an utterance to the queue.
|
||||
*/
|
||||
public void speak(String text, String voice, int volume, float pitch, float rate, long utterance_id, boolean interrupt) {
|
||||
synchronized (lock) {
|
||||
if (state != INIT_STATE_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id);
|
||||
queue.addLast(message);
|
||||
|
||||
if (isPaused()) {
|
||||
resumeSpeaking();
|
||||
} else {
|
||||
updateTTS();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the synthesizer into a paused state.
|
||||
*/
|
||||
public void pauseSpeaking() {
|
||||
synchronized (lock) {
|
||||
if (state != INIT_STATE_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
if (!paused) {
|
||||
paused = true;
|
||||
synth.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the synthesizer if it was paused.
|
||||
*/
|
||||
public void resumeSpeaking() {
|
||||
synchronized (lock) {
|
||||
if (state != INIT_STATE_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
if (lastUtterance != null && paused) {
|
||||
int mode = TextToSpeech.QUEUE_FLUSH;
|
||||
|
||||
Set<Voice> voices = synth.getVoices();
|
||||
if (voices == null) {
|
||||
return;
|
||||
}
|
||||
for (Voice v : voices) {
|
||||
if (v.getName().equals(lastUtterance.voice)) {
|
||||
synth.setVoice(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
synth.setPitch(lastUtterance.pitch);
|
||||
synth.setSpeechRate(lastUtterance.rate);
|
||||
|
||||
Bundle params = new Bundle();
|
||||
params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f);
|
||||
|
||||
lastUtterance.start = lastUtterance.offset;
|
||||
lastUtterance.offset = 0;
|
||||
paused = false;
|
||||
|
||||
synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id));
|
||||
speaking = true;
|
||||
} else {
|
||||
paused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops synthesis in progress and removes all utterances from the queue.
|
||||
*/
|
||||
public void stopSpeaking() {
|
||||
synchronized (lock) {
|
||||
if (state != INIT_STATE_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
for (GodotUtterance u : queue) {
|
||||
GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0);
|
||||
}
|
||||
queue.clear();
|
||||
|
||||
if (lastUtterance != null) {
|
||||
GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0);
|
||||
}
|
||||
lastUtterance = null;
|
||||
|
||||
paused = false;
|
||||
speaking = false;
|
||||
|
||||
synth.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns voice information.
|
||||
*/
|
||||
public String[] getVoices() {
|
||||
synchronized (lock) {
|
||||
if (state != INIT_STATE_SUCCESS) {
|
||||
return new String[0];
|
||||
}
|
||||
Set<Voice> voices = synth.getVoices();
|
||||
if (voices == null) {
|
||||
return new String[0];
|
||||
}
|
||||
String[] list = new String[voices.size()];
|
||||
int i = 0;
|
||||
for (Voice v : voices) {
|
||||
list[i++] = v.getLocale().toString() + ";" + v.getName();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the synthesizer is generating speech, or have utterance waiting in the queue.
|
||||
*/
|
||||
public boolean isSpeaking() {
|
||||
return speaking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the synthesizer is in a paused state.
|
||||
*/
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns INIT_STATE_SUCCESS if the synthesizer initialization finished successfully, INIT_STATE_FAIL if initialization failed, and INIT_STATE_UNKNOWN otherwise.
|
||||
*/
|
||||
public int getState() {
|
||||
synchronized (lock) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/**************************************************************************/
|
||||
/* GodotUtterance.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.tts;
|
||||
|
||||
/**
|
||||
* A speech request for GodotTTS.
|
||||
*/
|
||||
class GodotUtterance {
|
||||
final String text;
|
||||
final String voice;
|
||||
final int volume;
|
||||
final float pitch;
|
||||
final float rate;
|
||||
final long id;
|
||||
|
||||
int offset = -1;
|
||||
int start = 0;
|
||||
|
||||
GodotUtterance(String text, String voice, int volume, float pitch, float rate, long id) {
|
||||
this.text = text;
|
||||
this.voice = voice;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
this.rate = rate;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/**************************************************************************/
|
||||
/* BenchmarkUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
@file:JvmName("BenchmarkUtils")
|
||||
|
||||
package org.godotengine.godot.utils
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.os.Trace
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.BuildConfig
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.io.file.FileAccessFlags
|
||||
import org.godotengine.godot.io.file.FileAccessHandler
|
||||
import org.json.JSONObject
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* Contains benchmark related utilities methods
|
||||
*/
|
||||
private const val TAG = "GodotBenchmark"
|
||||
|
||||
var useBenchmark = false
|
||||
var benchmarkFile = ""
|
||||
|
||||
private val startBenchmarkFrom = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Long>())
|
||||
private val benchmarkTracker = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Double>())
|
||||
|
||||
/**
|
||||
* Start measuring and tracing the execution of a given section of code using the given label
|
||||
* within the given scope.
|
||||
*
|
||||
* Must be followed by a call to [endBenchmarkMeasure].
|
||||
*
|
||||
* Note: Only enabled on 'editorDev' build variant.
|
||||
*/
|
||||
fun beginBenchmarkMeasure(scope: String, label: String) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
val key = Pair(scope, label)
|
||||
startBenchmarkFrom[key] = SystemClock.elapsedRealtime()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Trace.beginAsyncSection("[$scope] $label", 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End measuring and tracing of the section of code with the given label within the given scope.
|
||||
*
|
||||
* Must be preceded by a call [beginBenchmarkMeasure]
|
||||
*
|
||||
* * Note: Only enabled on 'editorDev' build variant.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
val key = Pair(scope, label)
|
||||
val startTime = startBenchmarkFrom[key] ?: return
|
||||
val total = SystemClock.elapsedRealtime() - startTime
|
||||
benchmarkTracker[key] = total / 1_000.0
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Trace.endAsyncSection("[$scope] $label", 0)
|
||||
}
|
||||
|
||||
if (dumpBenchmark) {
|
||||
dumpBenchmark()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the benchmark measurements.
|
||||
* If [filepath] is valid, the data is also written in json format to the specified file.
|
||||
*
|
||||
* * Note: Only enabled on 'editorDev' build variant.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
if (!useBenchmark || benchmarkTracker.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val results = LinkedHashMap<String, String>()
|
||||
for (entry in benchmarkTracker) {
|
||||
if (!results.containsKey(entry.key.first)) {
|
||||
results[entry.key.first] = ""
|
||||
}
|
||||
results[entry.key.first] += "\t\t- ${entry.key.second}: ${entry.value * 1_000.0} msec.\n"
|
||||
}
|
||||
|
||||
val printOut =
|
||||
results.map { "\t- [${it.key}]\n ${it.value}" }.joinToString("\n")
|
||||
Log.i(TAG, "BENCHMARK:\n$printOut")
|
||||
|
||||
if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
|
||||
val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
|
||||
if (fileError == Error.OK) {
|
||||
val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4)
|
||||
fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray()))
|
||||
fileAccessHandler.fileClose(fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**************************************************************************/
|
||||
/* CommandLineFileParser.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils
|
||||
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
* A class that parses the content of file storing command line params. Usually, this file is saved
|
||||
* in `assets/_cl_` on exporting an apk
|
||||
*
|
||||
* Returns a mutable list of command lines
|
||||
*/
|
||||
internal object CommandLineFileParser {
|
||||
fun parseCommandLine(inputStream: InputStream): MutableList<String> {
|
||||
return try {
|
||||
val headerBytes = ByteArray(4)
|
||||
var argBytes = inputStream.read(headerBytes)
|
||||
if (argBytes < 4) {
|
||||
return mutableListOf()
|
||||
}
|
||||
val argc = decodeHeaderIntValue(headerBytes)
|
||||
|
||||
val cmdline = ArrayList<String>(argc)
|
||||
for (i in 0 until argc) {
|
||||
argBytes = inputStream.read(headerBytes)
|
||||
if (argBytes < 4) {
|
||||
return mutableListOf()
|
||||
}
|
||||
val strlen = decodeHeaderIntValue(headerBytes)
|
||||
|
||||
if (strlen > 65535) {
|
||||
return mutableListOf()
|
||||
}
|
||||
|
||||
val arg = ByteArray(strlen)
|
||||
argBytes = inputStream.read(arg)
|
||||
if (argBytes == strlen) {
|
||||
cmdline.add(String(arg, StandardCharsets.UTF_8))
|
||||
}
|
||||
}
|
||||
cmdline
|
||||
} catch (e: Exception) {
|
||||
// The _cl_ file can be missing with no adverse effect
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeHeaderIntValue(headerBytes: ByteArray): Int =
|
||||
(headerBytes[3].toInt() and 0xFF) shl 24 or
|
||||
((headerBytes[2].toInt() and 0xFF) shl 16) or
|
||||
((headerBytes[1].toInt() and 0xFF) shl 8) or
|
||||
(headerBytes[0].toInt() and 0xFF)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/**************************************************************************/
|
||||
/* Crypt.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Random;
|
||||
|
||||
public class Crypt {
|
||||
public static String md5(String input) {
|
||||
try {
|
||||
// Create MD5 Hash
|
||||
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
|
||||
digest.update(input.getBytes());
|
||||
byte[] messageDigest = digest.digest();
|
||||
|
||||
// Create Hex String
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : messageDigest)
|
||||
hexString.append(Integer.toHexString(0xFF & b));
|
||||
return hexString.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String createRandomHash() {
|
||||
return md5(Long.toString(createRandomLong()));
|
||||
}
|
||||
|
||||
public static long createAbsRandomLong() {
|
||||
return Math.abs(createRandomLong());
|
||||
}
|
||||
|
||||
public static long createRandomLong() {
|
||||
Random r = new Random();
|
||||
return r.nextLong();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
/**************************************************************************/
|
||||
/* DialogUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
|
||||
import org.godotengine.godot.R
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Utility class for managing dialogs.
|
||||
*/
|
||||
class DialogUtils {
|
||||
companion object {
|
||||
private val TAG = DialogUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Invoked on dialog button press.
|
||||
*/
|
||||
@JvmStatic
|
||||
private external fun dialogCallback(buttonIndex: Int)
|
||||
|
||||
/**
|
||||
* Invoked on the input dialog submitted.
|
||||
*/
|
||||
@JvmStatic
|
||||
private external fun inputDialogCallback(text: String)
|
||||
|
||||
/**
|
||||
* Displays a dialog with dynamically arranged buttons based on their text length.
|
||||
*
|
||||
* The buttons are laid out in rows, with a maximum of 2 buttons per row. If a button's text
|
||||
* is too long to fit within half the screen width, it occupies the entire row.
|
||||
*
|
||||
* @param activity The activity where the dialog will be displayed.
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
internal fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
|
||||
var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
|
||||
activity.runOnUiThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setTitle(title)
|
||||
builder.setMessage(message)
|
||||
|
||||
val buttonHeight = activity.resources.getDimensionPixelSize(R.dimen.button_height)
|
||||
val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
|
||||
val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
|
||||
val buttonPadding = activity.resources.getDimensionPixelSize(R.dimen.button_padding)
|
||||
|
||||
// Create a vertical parent layout to hold all rows of buttons.
|
||||
val parentLayout = LinearLayout(activity)
|
||||
parentLayout.orientation = LinearLayout.VERTICAL
|
||||
parentLayout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
|
||||
// Horizontal row layout for arranging buttons.
|
||||
var rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
rowLayout.layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
// Calculate the maximum width for a button to allow two buttons per row.
|
||||
val screenWidth = activity.resources.displayMetrics.widthPixels
|
||||
val availableWidth = screenWidth - (2 * paddingHorizontal)
|
||||
val maxButtonWidth = availableWidth / 2
|
||||
|
||||
buttons.forEachIndexed { index, buttonLabel ->
|
||||
val button = Button(activity)
|
||||
button.text = buttonLabel
|
||||
button.isSingleLine = true
|
||||
button.setPadding(buttonPadding, buttonPadding, buttonPadding, buttonPadding)
|
||||
|
||||
// Measure the button to determine its width.
|
||||
button.measure(0, 0)
|
||||
val buttonWidth = button.measuredWidth
|
||||
|
||||
val params = LinearLayout.LayoutParams(
|
||||
if (buttonWidth > maxButtonWidth) LinearLayout.LayoutParams.MATCH_PARENT else 0,
|
||||
buttonHeight
|
||||
)
|
||||
params.weight = if (buttonWidth > maxButtonWidth) 0f else 1f
|
||||
button.layoutParams = params
|
||||
|
||||
// Handle full-width buttons by finalizing the current row, if needed.
|
||||
if (buttonWidth > maxButtonWidth) {
|
||||
if (rowLayout.childCount > 0) {
|
||||
parentLayout.addView(rowLayout)
|
||||
rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
// Add the full-width button directly to the parent layout.
|
||||
parentLayout.addView(button)
|
||||
} else {
|
||||
rowLayout.addView(button)
|
||||
|
||||
// Finalize the row if it reaches 2 buttons.
|
||||
if (rowLayout.childCount == 2) {
|
||||
parentLayout.addView(rowLayout)
|
||||
rowLayout = LinearLayout(activity)
|
||||
rowLayout.orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
|
||||
// Handle the last button with incomplete row.
|
||||
if (index == buttons.size - 1 && rowLayout.childCount > 0) {
|
||||
parentLayout.addView(rowLayout)
|
||||
}
|
||||
}
|
||||
|
||||
button.setOnClickListener {
|
||||
dialogCallback(index)
|
||||
dismissDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the parent layout to the dialog.
|
||||
builder.setView(parentLayout)
|
||||
val dialog = builder.create()
|
||||
dismissDialog = {dialog.dismiss()}
|
||||
dialog.setCancelable(false)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with a text input field, allowing the user to input text.
|
||||
*
|
||||
* @param activity The activity where the input dialog will be displayed.
|
||||
* @param title The title of the input dialog.
|
||||
* @param message The message displayed in the input dialog.
|
||||
* @param existingText The existing text that will be pre-filled in the input field.
|
||||
*/
|
||||
internal fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
|
||||
val inputField = EditText(activity)
|
||||
val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
|
||||
val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
|
||||
inputField.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
inputField.setText(existingText)
|
||||
activity.runOnUiThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(message).setTitle(title).setView(inputField)
|
||||
builder.setPositiveButton(R.string.dialog_ok) {
|
||||
dialog: DialogInterface, id: Int ->
|
||||
inputDialogCallback(inputField.text.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.setCancelable(false)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a Snackbar with an optional action button.
|
||||
*
|
||||
* @param context The Context in which the Snackbar should be displayed.
|
||||
* @param message The message to display in the Snackbar.
|
||||
* @param duration The duration for which the Snackbar should be visible (in milliseconds).
|
||||
* @param actionText (Optional) The text for the action button. If `null`, the button is hidden.
|
||||
* @param actionCallback (Optional) A callback function to execute when the action button is clicked. If `null`, no action is performed.
|
||||
*/
|
||||
fun showSnackbar(activity: Activity, message: String, duration: Long = 3000, actionText: String? = null, action: (() -> Unit)? = null) {
|
||||
activity.runOnUiThread {
|
||||
val bottomMargin = activity.resources.getDimensionPixelSize(R.dimen.snackbar_bottom_margin)
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
val customView = inflater.inflate(R.layout.snackbar, null)
|
||||
|
||||
val popupWindow = PopupWindow(
|
||||
customView,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
|
||||
val messageView = customView.findViewById<TextView>(R.id.snackbar_text)
|
||||
messageView.text = message
|
||||
|
||||
val actionButton = customView.findViewById<Button>(R.id.snackbar_action)
|
||||
|
||||
if (actionText != null && action != null) {
|
||||
actionButton.text = actionText
|
||||
actionButton.visibility = View.VISIBLE
|
||||
actionButton.setOnClickListener {
|
||||
action.invoke()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
} else {
|
||||
actionButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
addSwipeToDismiss(customView, popupWindow)
|
||||
popupWindow.showAtLocation(customView, Gravity.BOTTOM, 0, bottomMargin)
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({ popupWindow.dismiss() }, duration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSwipeToDismiss(view: View, popupWindow: PopupWindow) {
|
||||
view.setOnTouchListener(object : View.OnTouchListener {
|
||||
private var initialX = 0f
|
||||
private var dX = 0f
|
||||
private val threshold = 300f // Swipe distance threshold.
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
initialX = event.rawX
|
||||
dX = view.translationX
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.rawX - initialX
|
||||
view.translationX = dX + deltaX
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
val finalX = event.rawX - initialX
|
||||
|
||||
if (abs(finalX) > threshold) {
|
||||
// If swipe exceeds threshold, dismiss smoothly.
|
||||
view.animate()
|
||||
.translationX(if (finalX > 0) view.width.toFloat() else -view.width.toFloat())
|
||||
.setDuration(200)
|
||||
.withEndAction { popupWindow.dismiss() }
|
||||
.start()
|
||||
} else {
|
||||
// If swipe is canceled, return smoothly.
|
||||
view.animate()
|
||||
.translationX(0f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/**************************************************************************/
|
||||
/* GLUtils.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
|
||||
/**
|
||||
* Contains GL utilities methods.
|
||||
*/
|
||||
public class GLUtils {
|
||||
private static final String TAG = GLUtils.class.getSimpleName();
|
||||
|
||||
public static final boolean DEBUG = false;
|
||||
|
||||
private static final String[] ATTRIBUTES_NAMES = new String[] {
|
||||
"EGL_BUFFER_SIZE",
|
||||
"EGL_ALPHA_SIZE",
|
||||
"EGL_BLUE_SIZE",
|
||||
"EGL_GREEN_SIZE",
|
||||
"EGL_RED_SIZE",
|
||||
"EGL_DEPTH_SIZE",
|
||||
"EGL_STENCIL_SIZE",
|
||||
"EGL_CONFIG_CAVEAT",
|
||||
"EGL_CONFIG_ID",
|
||||
"EGL_LEVEL",
|
||||
"EGL_MAX_PBUFFER_HEIGHT",
|
||||
"EGL_MAX_PBUFFER_PIXELS",
|
||||
"EGL_MAX_PBUFFER_WIDTH",
|
||||
"EGL_NATIVE_RENDERABLE",
|
||||
"EGL_NATIVE_VISUAL_ID",
|
||||
"EGL_NATIVE_VISUAL_TYPE",
|
||||
"EGL_PRESERVED_RESOURCES",
|
||||
"EGL_SAMPLES",
|
||||
"EGL_SAMPLE_BUFFERS",
|
||||
"EGL_SURFACE_TYPE",
|
||||
"EGL_TRANSPARENT_TYPE",
|
||||
"EGL_TRANSPARENT_RED_VALUE",
|
||||
"EGL_TRANSPARENT_GREEN_VALUE",
|
||||
"EGL_TRANSPARENT_BLUE_VALUE",
|
||||
"EGL_BIND_TO_TEXTURE_RGB",
|
||||
"EGL_BIND_TO_TEXTURE_RGBA",
|
||||
"EGL_MIN_SWAP_INTERVAL",
|
||||
"EGL_MAX_SWAP_INTERVAL",
|
||||
"EGL_LUMINANCE_SIZE",
|
||||
"EGL_ALPHA_MASK_SIZE",
|
||||
"EGL_COLOR_BUFFER_TYPE",
|
||||
"EGL_RENDERABLE_TYPE",
|
||||
"EGL_CONFORMANT"
|
||||
};
|
||||
|
||||
private static final int[] ATTRIBUTES = new int[] {
|
||||
EGL10.EGL_BUFFER_SIZE,
|
||||
EGL10.EGL_ALPHA_SIZE,
|
||||
EGL10.EGL_BLUE_SIZE,
|
||||
EGL10.EGL_GREEN_SIZE,
|
||||
EGL10.EGL_RED_SIZE,
|
||||
EGL10.EGL_DEPTH_SIZE,
|
||||
EGL10.EGL_STENCIL_SIZE,
|
||||
EGL10.EGL_CONFIG_CAVEAT,
|
||||
EGL10.EGL_CONFIG_ID,
|
||||
EGL10.EGL_LEVEL,
|
||||
EGL10.EGL_MAX_PBUFFER_HEIGHT,
|
||||
EGL10.EGL_MAX_PBUFFER_PIXELS,
|
||||
EGL10.EGL_MAX_PBUFFER_WIDTH,
|
||||
EGL10.EGL_NATIVE_RENDERABLE,
|
||||
EGL10.EGL_NATIVE_VISUAL_ID,
|
||||
EGL10.EGL_NATIVE_VISUAL_TYPE,
|
||||
0x3030, // EGL10.EGL_PRESERVED_RESOURCES,
|
||||
EGL10.EGL_SAMPLES,
|
||||
EGL10.EGL_SAMPLE_BUFFERS,
|
||||
EGL10.EGL_SURFACE_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_RED_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_GREEN_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_BLUE_VALUE,
|
||||
0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB,
|
||||
0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA,
|
||||
0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL,
|
||||
0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL,
|
||||
EGL10.EGL_LUMINANCE_SIZE,
|
||||
EGL10.EGL_ALPHA_MASK_SIZE,
|
||||
EGL10.EGL_COLOR_BUFFER_TYPE,
|
||||
EGL10.EGL_RENDERABLE_TYPE,
|
||||
0x3042 // EGL10.EGL_CONFORMANT
|
||||
};
|
||||
|
||||
private GLUtils() {}
|
||||
|
||||
public static void checkEglError(String tag, String prompt, EGL10 egl) {
|
||||
int error;
|
||||
while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) {
|
||||
Log.e(tag, String.format("%s: EGL error: 0x%x", prompt, error));
|
||||
}
|
||||
}
|
||||
|
||||
public static void printConfigs(EGL10 egl, EGLDisplay display,
|
||||
EGLConfig[] configs) {
|
||||
int numConfigs = configs.length;
|
||||
Log.v(TAG, String.format("%d configurations", numConfigs));
|
||||
for (int i = 0; i < numConfigs; i++) {
|
||||
Log.v(TAG, String.format("Configuration %d:\n", i));
|
||||
printConfig(egl, display, configs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printConfig(EGL10 egl, EGLDisplay display,
|
||||
EGLConfig config) {
|
||||
int[] value = new int[1];
|
||||
for (int i = 0; i < ATTRIBUTES.length; i++) {
|
||||
int attribute = ATTRIBUTES[i];
|
||||
String name = ATTRIBUTES_NAMES[i];
|
||||
if (egl.eglGetConfigAttrib(display, config, attribute, value)) {
|
||||
Log.i(TAG, String.format(" %s: %d\n", name, value[0]));
|
||||
} else {
|
||||
// Log.w(TAG, String.format(" %s: failed\n", name));
|
||||
while (egl.eglGetError() != EGL10.EGL_SUCCESS) {
|
||||
// Continue.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/**************************************************************************/
|
||||
/* GodotNetUtils.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* This class handles Android-specific networking functions.
|
||||
* It provides access to the CA certificates KeyStore, and the WifiManager.MulticastLock, which is needed on some devices
|
||||
* to receive broadcast and multicast packets.
|
||||
*/
|
||||
public class GodotNetUtils {
|
||||
/* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */
|
||||
private WifiManager.MulticastLock multicastLock;
|
||||
|
||||
public GodotNetUtils(Context context) {
|
||||
if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
|
||||
WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifi.createMulticastLock("GodotMulticastLock");
|
||||
multicastLock.setReferenceCounted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire the multicast lock. This is required on some devices to receive broadcast/multicast packets.
|
||||
* This is done automatically by Godot when enabling broadcast or joining a multicast group on a socket.
|
||||
*/
|
||||
public void multicastLockAcquire() {
|
||||
if (multicastLock == null)
|
||||
return;
|
||||
try {
|
||||
multicastLock.acquire();
|
||||
} catch (RuntimeException e) {
|
||||
Log.e("Godot", "Exception during multicast lock acquire: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the multicast lock.
|
||||
* This is done automatically by Godot when the lock is no longer needed by a socket.
|
||||
*/
|
||||
public void multicastLockRelease() {
|
||||
if (multicastLock == null)
|
||||
return;
|
||||
try {
|
||||
multicastLock.release();
|
||||
} catch (RuntimeException e) {
|
||||
Log.e("Godot", "Exception during multicast lock release: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of trusted CA certificates from the "AndroidCAStore" and returns them in PRM format.
|
||||
* @see https://developer.android.com/reference/java/security/KeyStore .
|
||||
* @return A string of concatenated X509 certificates in PEM format.
|
||||
*/
|
||||
public static @NonNull String getCACertificates() {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance("AndroidCAStore");
|
||||
StringBuilder writer = new StringBuilder();
|
||||
|
||||
if (ks != null) {
|
||||
ks.load(null, null);
|
||||
Enumeration<String> aliases = ks.aliases();
|
||||
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = (String)aliases.nextElement();
|
||||
|
||||
X509Certificate cert = (X509Certificate)ks.getCertificate(alias);
|
||||
writer.append("-----BEGIN CERTIFICATE-----\n");
|
||||
writer.append(Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT));
|
||||
writer.append("-----END CERTIFICATE-----\n");
|
||||
}
|
||||
}
|
||||
return writer.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e("Godot", "Exception while reading CA certificates: " + e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
/**************************************************************************/
|
||||
/* PermissionsUtil.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PermissionInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class includes utility functions for Android permissions related operations.
|
||||
*/
|
||||
public final class PermissionsUtil {
|
||||
private static final String TAG = PermissionsUtil.class.getSimpleName();
|
||||
|
||||
public static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
|
||||
public static final int REQUEST_CAMERA_PERMISSION = 2;
|
||||
public static final int REQUEST_VIBRATE_PERMISSION = 3;
|
||||
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
|
||||
public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002;
|
||||
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
|
||||
public static final int REQUEST_INSTALL_PACKAGES_REQ_CODE = 3002;
|
||||
|
||||
private PermissionsUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest
|
||||
* @param permissions list of the permissions to request.
|
||||
* @param activity the caller activity for this method.
|
||||
* @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched.
|
||||
*/
|
||||
public static boolean requestPermissions(@NonNull Activity activity, List<String> permissions) {
|
||||
return requestPermissions(activity, permissions, REQUEST_ALL_PERMISSION_REQ_CODE);
|
||||
}
|
||||
|
||||
private static boolean requestPermissions(@NonNull Activity activity, List<String> permissions, int requestCode) {
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean dispatchedPermissionsRequest = false;
|
||||
Set<String> requestedPermissions = new HashSet<>();
|
||||
for (String permission : permissions) {
|
||||
try {
|
||||
if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
|
||||
Log.d(TAG, "Requesting permission " + permission);
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
|
||||
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
|
||||
} catch (Exception ignored) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
|
||||
}
|
||||
dispatchedPermissionsRequest = true;
|
||||
}
|
||||
} else if (permission.equals(Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
|
||||
intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
|
||||
activity.startActivityForResult(intent, REQUEST_INSTALL_PACKAGES_REQ_CODE);
|
||||
dispatchedPermissionsRequest = true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable to request permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PermissionInfo permissionInfo = getPermissionInfo(activity, permission);
|
||||
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
|
||||
if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Requesting permission " + permission);
|
||||
requestedPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// Skip this permission and continue.
|
||||
Log.w(TAG, "Unable to identify permission " + permission, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestedPermissions.isEmpty()) {
|
||||
activity.requestPermissions(requestedPermissions.toArray(new String[0]), requestCode);
|
||||
dispatchedPermissionsRequest = true;
|
||||
}
|
||||
|
||||
return !dispatchedPermissionsRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a dangerous permission. The requested permission must be included in the app's AndroidManifest
|
||||
* @param permissionName the name of the permission to request.
|
||||
* @param activity the caller activity for this method.
|
||||
* @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched.
|
||||
*/
|
||||
public static boolean requestPermission(String permissionName, @NonNull Activity activity) {
|
||||
if (TextUtils.isEmpty(permissionName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final List<String> permissions = new ArrayList<>();
|
||||
|
||||
final int requestCode;
|
||||
switch (permissionName) {
|
||||
case "RECORD_AUDIO":
|
||||
permissions.add(Manifest.permission.RECORD_AUDIO);
|
||||
requestCode = REQUEST_RECORD_AUDIO_PERMISSION;
|
||||
break;
|
||||
|
||||
case "CAMERA":
|
||||
permissions.add(Manifest.permission.CAMERA);
|
||||
if (Godot.getInstance(activity).hasFeature("horizonos")) {
|
||||
// On HorizonOS, these permissions are required to get access to all the device's cameras.
|
||||
permissions.add("horizonos.permission.AVATAR_CAMERA");
|
||||
permissions.add("horizonos.permission.HEADSET_CAMERA");
|
||||
}
|
||||
requestCode = REQUEST_CAMERA_PERMISSION;
|
||||
break;
|
||||
|
||||
case "VIBRATE":
|
||||
permissions.add(Manifest.permission.VIBRATE);
|
||||
requestCode = REQUEST_VIBRATE_PERMISSION;
|
||||
break;
|
||||
|
||||
default:
|
||||
permissions.add(permissionName);
|
||||
requestCode = REQUEST_SINGLE_PERMISSION_REQ_CODE;
|
||||
break;
|
||||
}
|
||||
|
||||
return requestPermissions(activity, permissions, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request dangerous permissions which are defined in the Android manifest file from the user.
|
||||
* @param activity the caller activity for this method.
|
||||
* @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
|
||||
*/
|
||||
public static boolean requestManifestPermissions(Activity activity) {
|
||||
return requestManifestPermissions(activity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request dangerous permissions which are defined in the Android manifest file from the user.
|
||||
* @param activity the caller activity for this method.
|
||||
* @param excludes Set of permissions to exclude from the request
|
||||
* @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
|
||||
*/
|
||||
public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) {
|
||||
if (activity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> manifestPermissions;
|
||||
try {
|
||||
manifestPermissions = getManifestPermissions(activity);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to retrieve manifest permissions", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manifestPermissions.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (excludes != null && !excludes.isEmpty()) {
|
||||
for (String excludedPermission : excludes) {
|
||||
manifestPermissions.remove(excludedPermission);
|
||||
}
|
||||
}
|
||||
|
||||
return requestPermissions(activity, manifestPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* With this function you can get the list of dangerous permissions that have been granted to the Android application.
|
||||
* @param context the caller context for this method.
|
||||
* @return granted permissions list
|
||||
*/
|
||||
public static String[] getGrantedPermissions(Context context) {
|
||||
List<String> manifestPermissions;
|
||||
try {
|
||||
manifestPermissions = getManifestPermissions(context);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to retrieve manifest permissions", e);
|
||||
return new String[0];
|
||||
}
|
||||
if (manifestPermissions.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
List<String> grantedPermissions = new ArrayList<>();
|
||||
for (String manifestPermission : manifestPermissions) {
|
||||
try {
|
||||
if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
|
||||
grantedPermissions.add(manifestPermission);
|
||||
}
|
||||
} else {
|
||||
PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission);
|
||||
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
|
||||
if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
|
||||
grantedPermissions.add(manifestPermission);
|
||||
}
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// Skip this permission and continue.
|
||||
Log.w(TAG, "Unable to identify permission " + manifestPermission, e);
|
||||
}
|
||||
}
|
||||
|
||||
return grantedPermissions.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given permission is in the AndroidManifest.xml file.
|
||||
* @param context the caller context for this method.
|
||||
* @param permission the permission to look for in the manifest file.
|
||||
* @return "true" if the permission is in the manifest file of the activity, "false" otherwise.
|
||||
*/
|
||||
public static boolean hasManifestPermission(Context context, String permission) {
|
||||
try {
|
||||
for (String p : getManifestPermissions(context)) {
|
||||
if (permission.equals(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permissions defined in the AndroidManifest.xml file.
|
||||
* @param context the caller context for this method.
|
||||
* @return mutable copy of manifest permissions list
|
||||
* @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
|
||||
*/
|
||||
public static ArrayList<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
|
||||
if (packageInfo.requestedPermissions == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information of the desired permission.
|
||||
* @param context the caller context for this method.
|
||||
* @param permission the name of the permission.
|
||||
* @return permission info object
|
||||
* @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found.
|
||||
*/
|
||||
private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
return packageManager.getPermissionInfo(permission, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
// clang-format off
|
||||
|
||||
/* Third-party library.
|
||||
* Upstream: https://github.com/JakeWharton/ProcessPhoenix
|
||||
* Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014 Jake Wharton
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.godotengine.godot.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
|
||||
/**
|
||||
* Process Phoenix facilitates restarting your application process. This should only be used for
|
||||
* things like fundamental state changes in your debug builds (e.g., changing from staging to
|
||||
* production).
|
||||
* <p>
|
||||
* Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
|
||||
*/
|
||||
public final class ProcessPhoenix extends Activity {
|
||||
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
|
||||
// -- GODOT start --
|
||||
private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options";
|
||||
// -- GODOT end --
|
||||
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
|
||||
|
||||
/**
|
||||
* Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default}
|
||||
* activity as an intent.
|
||||
* <p>
|
||||
* Behavior of the current process after invoking this method is undefined.
|
||||
*/
|
||||
public static void triggerRebirth(Context context) {
|
||||
triggerRebirth(context, getRestartIntent(context));
|
||||
}
|
||||
|
||||
// -- GODOT start --
|
||||
/**
|
||||
* Call to restart the application process using the specified intents.
|
||||
* <p>
|
||||
* Behavior of the current process after invoking this method is undefined.
|
||||
*/
|
||||
public static void triggerRebirth(Context context, Intent... nextIntents) {
|
||||
triggerRebirth(context, null, nextIntents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to restart the application process using the specified intents launched with the given
|
||||
* {@link ActivityOptions}.
|
||||
* <p>
|
||||
* Behavior of the current process after invoking this method is undefined.
|
||||
*/
|
||||
public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) {
|
||||
if (nextIntents.length < 1) {
|
||||
throw new IllegalArgumentException("intents cannot be empty");
|
||||
}
|
||||
// create a new task for the first activity.
|
||||
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
|
||||
|
||||
Intent intent = new Intent(context, ProcessPhoenix.class);
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
|
||||
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
|
||||
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
|
||||
if (activityOptions != null) {
|
||||
intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the activity and kill its process
|
||||
*/
|
||||
public static void forceQuit(Activity activity) {
|
||||
forceQuit(activity, Process.myPid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the activity and kill its process
|
||||
* @param activity
|
||||
* @param pid
|
||||
*/
|
||||
public static void forceQuit(Activity activity, int pid) {
|
||||
Process.killProcess(pid); // Kill original main process
|
||||
activity.finishAndRemoveTask();
|
||||
Runtime.getRuntime().exit(0); // Kill kill kill!
|
||||
}
|
||||
|
||||
// -- GODOT end --
|
||||
|
||||
private static Intent getRestartIntent(Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
if (defaultIntent != null) {
|
||||
return defaultIntent;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to determine default activity for "
|
||||
+ packageName
|
||||
+ ". Does an activity specify the DEFAULT category in its intent filter?");
|
||||
}
|
||||
|
||||
@Override protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// -- GODOT start --
|
||||
Intent launchIntent = getIntent();
|
||||
ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS);
|
||||
Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS);
|
||||
startActivities(intents.toArray(new Intent[intents.size()]), activityOptions);
|
||||
forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1));
|
||||
// -- GODOT end --
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current process is a temporary Phoenix Process.
|
||||
* This can be used to avoid initialization of unused resources or to prevent running code that
|
||||
* is not multi-process ready.
|
||||
*
|
||||
* @return true if the current process is a temporary Phoenix Process
|
||||
*/
|
||||
public static boolean isPhoenixProcess(Context context) {
|
||||
int currentPid = Process.myPid();
|
||||
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses();
|
||||
if (runningProcesses != null) {
|
||||
for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
|
||||
if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/**************************************************************************/
|
||||
/* Callable.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.variant
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* Android version of a Godot built-in Callable type representing a method or a standalone function.
|
||||
*/
|
||||
@Keep
|
||||
class Callable private constructor(private val nativeCallablePointer: Long) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Invoke method [methodName] on the Godot object specified by [godotObjectId]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun call(godotObjectId: Long, methodName: String, vararg methodParameters: Any): Any? {
|
||||
return nativeCallObject(godotObjectId, methodName, methodParameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke method [methodName] on the Godot object specified by [godotObjectId] during idle time.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun callDeferred(godotObjectId: Long, methodName: String, vararg methodParameters: Any) {
|
||||
nativeCallObjectDeferred(godotObjectId, methodName, methodParameters)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCall(pointer: Long, params: Array<out Any>): Any?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCallObject(godotObjectId: Long, methodName: String, params: Array<out Any>): Any?
|
||||
|
||||
@JvmStatic
|
||||
private external fun nativeCallObjectDeferred(godotObjectId: Long, methodName: String, params: Array<out Any>)
|
||||
|
||||
@JvmStatic
|
||||
private external fun releaseNativePointer(nativePointer: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the method represented by this [Callable]. Arguments can be passed and should match the method's signature.
|
||||
*/
|
||||
fun call(vararg params: Any): Any? {
|
||||
if (nativeCallablePointer == 0L) {
|
||||
return null
|
||||
}
|
||||
|
||||
return nativeCall(nativeCallablePointer, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to provide access to the native callable pointer to the native logic.
|
||||
*/
|
||||
private fun getNativePointer() = nativeCallablePointer
|
||||
|
||||
/** Note that [finalize] is deprecated and shouldn't be used, unfortunately its replacement,
|
||||
* [java.lang.ref.Cleaner], is only available on Android api 33 and higher.
|
||||
* So we resort to using it for the time being until our min api catches up to api 33.
|
||||
**/
|
||||
protected fun finalize() {
|
||||
releaseNativePointer(nativeCallablePointer)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/**************************************************************************/
|
||||
/* VkRenderer.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
@file:JvmName("VkRenderer")
|
||||
package org.godotengine.godot.vulkan
|
||||
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import org.godotengine.godot.GodotLib
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||
|
||||
/**
|
||||
* Responsible to setting up and driving the Vulkan rendering logic.
|
||||
*
|
||||
* <h3>Threading</h3>
|
||||
* The renderer will be called on a separate thread, so that rendering
|
||||
* performance is decoupled from the UI thread. Clients typically need to
|
||||
* communicate with the renderer from the UI thread, because that's where
|
||||
* input events are received. Clients can communicate using any of the
|
||||
* standard Java techniques for cross-thread communication, or they can
|
||||
* use the [VkSurfaceView.queueOnVkThread] convenience method.
|
||||
*
|
||||
* @see [VkSurfaceView.startRenderer]
|
||||
*/
|
||||
internal class VkRenderer {
|
||||
|
||||
companion object {
|
||||
private val TAG = VkRenderer::class.java.simpleName
|
||||
}
|
||||
|
||||
private val pluginRegistry: GodotPluginRegistry = GodotPluginRegistry.getPluginRegistry()
|
||||
|
||||
/**
|
||||
* Called when the surface is created and signals the beginning of rendering.
|
||||
*/
|
||||
fun onVkSurfaceCreated(surface: Surface) {
|
||||
GodotLib.newcontext(surface)
|
||||
|
||||
for (plugin in pluginRegistry.getAllPlugins()) {
|
||||
plugin.onVkSurfaceCreated(surface)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the surface is created and whenever its size changes.
|
||||
*/
|
||||
fun onVkSurfaceChanged(surface: Surface, width: Int, height: Int) {
|
||||
GodotLib.resize(surface, width, height)
|
||||
|
||||
for (plugin in pluginRegistry.getAllPlugins()) {
|
||||
plugin.onVkSurfaceChanged(surface, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to draw the current frame.
|
||||
*/
|
||||
fun onVkDrawFrame() {
|
||||
GodotLib.step()
|
||||
for (plugin in pluginRegistry.getAllPlugins()) {
|
||||
plugin.onVkDrawFrame()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the rendering thread is resumed.
|
||||
*/
|
||||
fun onVkResume() {
|
||||
GodotLib.onRendererResumed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the rendering thread is paused.
|
||||
*/
|
||||
fun onVkPause() {
|
||||
GodotLib.onRendererPaused()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the render thread is in the process of shutting down.
|
||||
*/
|
||||
fun onRenderThreadExiting() {
|
||||
Log.d(TAG, "Destroying Godot Engine")
|
||||
GodotLib.ondestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/**************************************************************************/
|
||||
/* VkSurfaceView.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
@file:JvmName("VkSurfaceView")
|
||||
package org.godotengine.godot.vulkan
|
||||
|
||||
import android.content.Context
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
|
||||
/**
|
||||
* An implementation of SurfaceView that uses the dedicated surface for
|
||||
* displaying Vulkan rendering.
|
||||
* <p>
|
||||
* A [VkSurfaceView] provides the following features:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Manages a surface, which is a special piece of memory that can be
|
||||
* composited into the Android view system.
|
||||
* <li>Accepts a user-provided [VkRenderer] object that does the actual rendering.
|
||||
* <li>Renders on a dedicated [VkThread] thread to decouple rendering performance from the
|
||||
* UI thread.
|
||||
* </ul>
|
||||
*/
|
||||
open internal class VkSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
|
||||
companion object {
|
||||
fun checkState(expression: Boolean, errorMessage: Any) {
|
||||
check(expression) { errorMessage.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread used to drive the vulkan logic.
|
||||
*/
|
||||
private val vkThread: VkThread by lazy {
|
||||
VkThread(this, renderer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual rendering.
|
||||
*/
|
||||
private lateinit var renderer: VkRenderer
|
||||
|
||||
init {
|
||||
isClickable = true
|
||||
holder.addCallback(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the [VkRenderer] associated with the view, and starts the thread that will drive the vulkan
|
||||
* rendering.
|
||||
*
|
||||
* This method should be called once and only once in the life-cycle of [VkSurfaceView].
|
||||
*/
|
||||
fun startRenderer(renderer: VkRenderer) {
|
||||
checkState(!this::renderer.isInitialized, "startRenderer must only be invoked once")
|
||||
this.renderer = renderer
|
||||
vkThread.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a runnable to be run on the Vulkan rendering thread.
|
||||
*
|
||||
* Must not be called before a [VkRenderer] has been set.
|
||||
*/
|
||||
fun queueOnVkThread(runnable: Runnable) {
|
||||
vkThread.queueEvent(runnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the rendering thread.
|
||||
*
|
||||
* Must not be called before a [VkRenderer] has been set.
|
||||
*/
|
||||
protected fun resumeRenderThread() {
|
||||
vkThread.onResume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the rendering thread.
|
||||
*
|
||||
* Must not be called before a [VkRenderer] has been set.
|
||||
*/
|
||||
protected fun pauseRenderThread() {
|
||||
vkThread.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the render thread to exit and block until it does.
|
||||
*/
|
||||
fun requestRenderThreadExitAndWait() {
|
||||
vkThread.requestExitAndWait()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the render thread to exit and block up to the given [timeInMs] until it's done.
|
||||
*
|
||||
* @return true if the thread exited, false otherwise.
|
||||
*/
|
||||
fun requestRenderThreadExitAndWait(timeInMs: Long): Boolean {
|
||||
return vkThread.requestExitAndWait(timeInMs)
|
||||
}
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
vkThread.onSurfaceChanged(width, height)
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
vkThread.onSurfaceDestroyed()
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
vkThread.onSurfaceCreated()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/**************************************************************************/
|
||||
/* VkThread.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
@file:JvmName("VkThread")
|
||||
package org.godotengine.godot.vulkan
|
||||
|
||||
import android.util.Log
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* Thread implementation for the [VkSurfaceView] onto which the vulkan logic is ran.
|
||||
*
|
||||
* The implementation is modeled after [android.opengl.GLSurfaceView]'s GLThread.
|
||||
*/
|
||||
internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vkRenderer: VkRenderer) : Thread(TAG) {
|
||||
companion object {
|
||||
private val TAG = VkThread::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to run events scheduled on the thread.
|
||||
*/
|
||||
private val eventQueue = ArrayList<Runnable>()
|
||||
|
||||
/**
|
||||
* Used to synchronize interaction with other threads (e.g: main thread).
|
||||
*/
|
||||
private val lock = ReentrantLock()
|
||||
private val lockCondition = lock.newCondition()
|
||||
|
||||
private var shouldExit = false
|
||||
private var exited = false
|
||||
private var rendererInitialized = false
|
||||
private var rendererResumed = false
|
||||
private var resumed = false
|
||||
private var surfaceChanged = false
|
||||
private var hasSurface = false
|
||||
private var width = 0
|
||||
private var height = 0
|
||||
|
||||
/**
|
||||
* Determine when drawing can occur on the thread. This usually occurs after the
|
||||
* [android.view.Surface] is available, the app is in a resumed state.
|
||||
*/
|
||||
private val readyToDraw
|
||||
get() = hasSurface && resumed
|
||||
|
||||
private fun threadExiting() {
|
||||
lock.withLock {
|
||||
Log.d(TAG, "Exiting render thread")
|
||||
vkRenderer.onRenderThreadExiting()
|
||||
|
||||
exited = true
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an event on the [VkThread].
|
||||
*/
|
||||
fun queueEvent(event: Runnable) {
|
||||
lock.withLock {
|
||||
eventQueue.add(event)
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the thread to exit and block until it's done.
|
||||
*/
|
||||
fun requestExitAndWait() {
|
||||
lock.withLock {
|
||||
shouldExit = true
|
||||
lockCondition.signalAll()
|
||||
while (!exited) {
|
||||
try {
|
||||
Log.i(TAG, "Waiting on exit for $name")
|
||||
lockCondition.await()
|
||||
} catch (_: InterruptedException) {
|
||||
currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the thread to exit and block up to the given [timeInMs] until it's done.
|
||||
*
|
||||
* @return true if the thread exited, false otherwise.
|
||||
*/
|
||||
fun requestExitAndWait(timeInMs: Long): Boolean {
|
||||
lock.withLock {
|
||||
shouldExit = true
|
||||
lockCondition.signalAll()
|
||||
|
||||
var remainingTimeInNanos = TimeUnit.MILLISECONDS.toNanos(timeInMs)
|
||||
while (!exited && remainingTimeInNanos > 0) {
|
||||
try {
|
||||
Log.i(TAG, "Waiting on exit for $name for $remainingTimeInNanos")
|
||||
remainingTimeInNanos = lockCondition.awaitNanos(remainingTimeInNanos)
|
||||
} catch (_: InterruptedException) {
|
||||
currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
return exited
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the app resumes.
|
||||
*/
|
||||
fun onResume() {
|
||||
lock.withLock {
|
||||
resumed = true
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the app pauses.
|
||||
*/
|
||||
fun onPause() {
|
||||
lock.withLock {
|
||||
resumed = false
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the [android.view.Surface] has been created.
|
||||
*/
|
||||
fun onSurfaceCreated() {
|
||||
// This is a no op because surface creation will always be followed by surfaceChanged()
|
||||
// which provide all the needed information.
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked following structural updates to [android.view.Surface].
|
||||
*/
|
||||
fun onSurfaceChanged(width: Int, height: Int) {
|
||||
lock.withLock {
|
||||
hasSurface = true
|
||||
surfaceChanged = true
|
||||
this.width = width
|
||||
this.height = height
|
||||
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the [android.view.Surface] is no longer available.
|
||||
*/
|
||||
fun onSurfaceDestroyed() {
|
||||
lock.withLock {
|
||||
hasSurface = false
|
||||
lockCondition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread loop modeled after [android.opengl.GLSurfaceView]'s GLThread.
|
||||
*/
|
||||
override fun run() {
|
||||
try {
|
||||
while (true) {
|
||||
var event: Runnable? = null
|
||||
lock.withLock {
|
||||
while (true) {
|
||||
// Code path for exiting the thread loop.
|
||||
if (shouldExit) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for events and execute them outside of the loop if found to avoid
|
||||
// blocking the thread lifecycle by holding onto the lock.
|
||||
if (eventQueue.isNotEmpty()) {
|
||||
event = eventQueue.removeAt(0)
|
||||
break
|
||||
}
|
||||
|
||||
if (readyToDraw) {
|
||||
if (!rendererResumed) {
|
||||
rendererResumed = true
|
||||
vkRenderer.onVkResume()
|
||||
|
||||
if (!rendererInitialized) {
|
||||
rendererInitialized = true
|
||||
vkRenderer.onVkSurfaceCreated(vkSurfaceView.holder.surface)
|
||||
}
|
||||
}
|
||||
|
||||
if (surfaceChanged) {
|
||||
vkRenderer.onVkSurfaceChanged(vkSurfaceView.holder.surface, width, height)
|
||||
surfaceChanged = false
|
||||
}
|
||||
|
||||
// Break out of the loop so drawing can occur without holding onto the lock.
|
||||
break
|
||||
} else if (rendererResumed) {
|
||||
// If we aren't ready to draw but are resumed, that means we either lost a surface
|
||||
// or the app was paused.
|
||||
rendererResumed = false
|
||||
vkRenderer.onVkPause()
|
||||
}
|
||||
// We only reach this state if we are not ready to draw and have no queued events, so
|
||||
// we wait.
|
||||
// On state change, the thread will be awoken using the [lock] and [lockCondition], and
|
||||
// we will resume execution.
|
||||
lockCondition.await()
|
||||
}
|
||||
}
|
||||
|
||||
// Run queued event.
|
||||
if (event != null) {
|
||||
event?.run()
|
||||
continue
|
||||
}
|
||||
|
||||
// Draw only when there no more queued events.
|
||||
vkRenderer.onVkDrawFrame()
|
||||
}
|
||||
} catch (ex: InterruptedException) {
|
||||
Log.i(TAG, "InterruptedException", ex)
|
||||
} catch (ex: IllegalStateException) {
|
||||
Log.i(TAG, "IllegalStateException", ex)
|
||||
} finally {
|
||||
threadExiting()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/**************************************************************************/
|
||||
/* XRMode.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.xr;
|
||||
|
||||
/**
|
||||
* Godot available XR modes.
|
||||
*/
|
||||
public enum XRMode {
|
||||
REGULAR(0, "Regular", "--xr_mode_regular", "Default Android Gamepad"), // Regular/flatscreen
|
||||
OPENXR(1, "OpenXR", "--xr_mode_openxr", "");
|
||||
|
||||
final int index;
|
||||
final String label;
|
||||
public final String cmdLineArg;
|
||||
public final String inputFallbackMapping;
|
||||
|
||||
XRMode(int index, String label, String cmdLineArg, String inputFallbackMapping) {
|
||||
this.index = index;
|
||||
this.label = label;
|
||||
this.cmdLineArg = cmdLineArg;
|
||||
this.inputFallbackMapping = inputFallbackMapping;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue