Merge commit '921d0574a0' as 'engine'

This commit is contained in:
Sara Gerretsen 2026-04-04 14:46:33 +02:00
commit add768b79b
14224 changed files with 7489260 additions and 0 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &lt;application, user&gt; 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 &lt;application, user, device id&gt; 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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
// ===========================================================
}

View file

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

View file

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

View file

@ -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
// ===========================================================
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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