Merged upstream 4.7 into downstream 4.6
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
@@ -27,8 +27,6 @@ allprojects {
|
||||
}
|
||||
|
||||
configurations {
|
||||
// Initializes a placeholder for the devImplementation dependency configuration.
|
||||
devImplementation {}
|
||||
// Initializes a placeholder for the monoImplementation dependency configuration.
|
||||
monoImplementation {}
|
||||
}
|
||||
@@ -43,6 +41,7 @@ dependencies {
|
||||
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.documentfile:documentfile:$versions.documentfileVersion"
|
||||
|
||||
if (rootProject.findProject(":lib")) {
|
||||
implementation project(":lib")
|
||||
@@ -52,15 +51,32 @@ dependencies {
|
||||
// Godot gradle build mode. In this scenario this project is the only one around and the Godot
|
||||
// library is available through the pre-generated godot-lib.*.aar android archive files.
|
||||
debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
|
||||
devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar'])
|
||||
releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// Godot user plugins remote dependencies
|
||||
String[] remoteDeps = getGodotPluginsRemoteBinaries()
|
||||
if (remoteDeps != null && remoteDeps.size() > 0) {
|
||||
def platformPattern = /^\s*(platform|enforcedPlatform)\s*\(\s*['"]*(\S+)['"]*\s*\)$/
|
||||
for (String dep : remoteDeps) {
|
||||
implementation dep
|
||||
def matcher = dep =~ platformPattern
|
||||
if (matcher) {
|
||||
switch (matcher[0][1]) {
|
||||
case "platform":
|
||||
implementation platform(matcher[0][2])
|
||||
break
|
||||
|
||||
case "enforcedPlatform":
|
||||
implementation enforcedPlatform(matcher[0][2])
|
||||
break
|
||||
|
||||
default:
|
||||
throw new GradleException("Invalid remote platform dependency: $dep")
|
||||
break
|
||||
}
|
||||
} else {
|
||||
implementation dep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,18 +221,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
dev {
|
||||
initWith debug
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.debug
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
@@ -250,7 +254,6 @@ android {
|
||||
sourceSets {
|
||||
main.res.srcDirs += ['res']
|
||||
debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers']
|
||||
dev.jniLibs.srcDirs = ['libs/dev']
|
||||
release.jniLibs.srcDirs = ['libs/release']
|
||||
}
|
||||
|
||||
@@ -329,9 +332,6 @@ module, so we're ensuring the ':app:preBuild' task is set to run after those tas
|
||||
if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule"))
|
||||
}
|
||||
if (rootProject.tasks.findByPath("copyDevAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyDevAARToAppModule"))
|
||||
}
|
||||
if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule"))
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
ext.versions = [
|
||||
androidGradlePlugin: '8.6.1',
|
||||
compileSdk : 35,
|
||||
compileSdk : 36,
|
||||
// Also update:
|
||||
// - 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
|
||||
// - 'platform/android/detect.py#get_min_target_api()'
|
||||
minSdk : 24,
|
||||
// Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
|
||||
targetSdk : 35,
|
||||
buildTools : '35.0.1',
|
||||
kotlinVersion : '2.1.20',
|
||||
targetSdk : 36,
|
||||
buildTools : '36.1.0',
|
||||
kotlinVersion : '2.1.21',
|
||||
fragmentVersion : '1.8.6',
|
||||
nexusPublishVersion: '1.3.0',
|
||||
javaVersion : JavaVersion.VERSION_17,
|
||||
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
|
||||
ndkVersion : '28.1.13356709',
|
||||
ndkVersion : '29.0.14206865',
|
||||
splashscreenVersion: '1.0.1',
|
||||
// 'openxrLoaderVersion' should be set to XR_CURRENT_API_VERSION, see 'thirdparty/openxr'
|
||||
openxrLoaderVersion: '1.1.53',
|
||||
openxrVendorsVersion: '4.2.2-stable',
|
||||
openxrVendorsVersion: '4.3.0-stable',
|
||||
junitVersion : '1.3.0',
|
||||
espressoCoreVersion: '3.7.0',
|
||||
kotlinTestVersion : '1.3.11',
|
||||
testRunnerVersion : '1.7.0',
|
||||
testOrchestratorVersion: '1.6.1',
|
||||
documentfileVersion: '1.1.0',
|
||||
]
|
||||
|
||||
ext.getExportPackageName = { ->
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
|
||||
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
|
||||
<item name="android:windowSplashScreenBackground">@mipmap/icon_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/icon_foreground</item>
|
||||
<item name="android:windowSplashScreenBrandingImage">@drawable/splash_branding_image</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<item name="postSplashScreenTheme">@style/GodotAppMainTheme</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
|
||||
+77
@@ -34,8 +34,11 @@ import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.godot.game.test.GodotAppInstrumentedTestPlugin
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.GodotActivity.Companion.EXTRA_COMMAND_LINE_PARAMS
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||
import org.junit.Test
|
||||
@@ -110,6 +113,28 @@ class GodotAppTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs test to validate android plugin signals.
|
||||
*/
|
||||
@Test
|
||||
fun runPluginSignalTests() {
|
||||
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
val testPlugin = getTestPlugin()
|
||||
assertNotNull(testPlugin)
|
||||
|
||||
Log.d(TAG, "Waiting for the Godot main loop to start...")
|
||||
testPlugin.waitForGodotMainLoopStarted()
|
||||
|
||||
Log.d(TAG, "Running Android plugin signal tests...")
|
||||
val result = testPlugin.runPluginSignalTests()
|
||||
assertNotNull(result)
|
||||
result.exceptionOrNull()?.let { throw it }
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test implicit launch of the Godot app, and validates this resolves to the `GodotAppLauncher` activity alias.
|
||||
*/
|
||||
@@ -169,4 +194,56 @@ class GodotAppTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the back press does not quit the game when 'quit_on_go_back' is disabled.
|
||||
*/
|
||||
@Test
|
||||
fun testGameNotQuittingOnBackPress() {
|
||||
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
|
||||
val testPlugin = getTestPlugin()
|
||||
assertNotNull(testPlugin)
|
||||
|
||||
Log.d(TAG, "Waiting for the Godot main loop to start...")
|
||||
testPlugin.waitForGodotMainLoopStarted()
|
||||
|
||||
// Disable 'quit_on_go_back'.
|
||||
testPlugin.updateQuitOnGoBack(false)
|
||||
|
||||
// Trigger the back press event.
|
||||
Espresso.pressBackUnconditionally()
|
||||
|
||||
Log.d(TAG, "Waiting for the engine to terminate...")
|
||||
testPlugin.waitForEngineTermination(5_000L)
|
||||
|
||||
val godot = Godot.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
assertTrue { godot.runStatus != Godot.RunStatus.TERMINATING }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the back press event quits the game when 'quit_on_go_back' is enabled.
|
||||
*/
|
||||
@Test
|
||||
fun testGameQuittingOnBackPress() {
|
||||
ActivityScenario.launch(GodotApp::class.java).use { scenario ->
|
||||
val testPlugin = getTestPlugin()
|
||||
assertNotNull(testPlugin)
|
||||
|
||||
Log.d(TAG, "Waiting for the Godot main loop to start...")
|
||||
testPlugin.waitForGodotMainLoopStarted()
|
||||
|
||||
// Enable 'quit_on_go_back'.
|
||||
testPlugin.updateQuitOnGoBack(true)
|
||||
|
||||
// Trigger the back press event.
|
||||
Espresso.pressBackUnconditionally()
|
||||
|
||||
Log.d(TAG, "Waiting for the engine to terminate...")
|
||||
testPlugin.waitForEngineTermination(5_000L)
|
||||
|
||||
val godot = Godot.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
assertTrue { godot.runStatus == Godot.RunStatus.TERMINATING }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.GodotAppInstrumentedTestPlugin"
|
||||
android:value="com.godot.game.test.GodotAppInstrumentedTestPlugin"/>
|
||||
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.SignalTestPlugin"
|
||||
android:value="com.godot.game.test.SignalTestPlugin"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
list=[{
|
||||
"base": &"BaseTest",
|
||||
"class": &"AndroidPluginSignalTests",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://test/android_plugin/signal_tests.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"BaseTest",
|
||||
"icon": "",
|
||||
|
||||
Binary file not shown.
@@ -3,12 +3,22 @@ extends Node2D
|
||||
var _plugin_name = "GodotAppInstrumentedTestPlugin"
|
||||
var _android_plugin
|
||||
|
||||
func _ready():
|
||||
var _signal_test_plugin_name = "SignalTestPlugin"
|
||||
var _signal_test_plugin
|
||||
|
||||
func _init():
|
||||
# Verify plugin singleton in _init, since plugins should already be registered at this point.
|
||||
if Engine.has_singleton(_plugin_name):
|
||||
_android_plugin = Engine.get_singleton(_plugin_name)
|
||||
_android_plugin.connect("launch_tests", _launch_tests)
|
||||
else:
|
||||
printerr("Couldn't find plugin " + _plugin_name)
|
||||
_android_plugin.connect("update_quit_on_go_back", _update_quit_on_go_back)
|
||||
|
||||
if Engine.has_singleton(_signal_test_plugin_name):
|
||||
_signal_test_plugin = Engine.get_singleton(_signal_test_plugin_name)
|
||||
|
||||
func _ready() -> void:
|
||||
if not _android_plugin:
|
||||
printerr("ERROR: Couldn't find plugin " + _plugin_name)
|
||||
get_tree().quit()
|
||||
|
||||
func _launch_tests(test_label: String) -> void:
|
||||
@@ -18,16 +28,22 @@ func _launch_tests(test_label: String) -> void:
|
||||
test_instance = JavaClassWrapperTests.new()
|
||||
"file_access_tests":
|
||||
test_instance = FileAccessTests.new()
|
||||
"android_plugin_signal_tests":
|
||||
test_instance = AndroidPluginSignalTests.new(_signal_test_plugin)
|
||||
|
||||
if test_instance:
|
||||
test_instance.__reset_tests()
|
||||
test_instance.run_tests()
|
||||
await test_instance.run_tests()
|
||||
var incomplete_tests = test_instance._test_started - test_instance._test_completed
|
||||
_android_plugin.onTestsCompleted(test_label, test_instance._test_completed, test_instance._test_assert_failures + incomplete_tests)
|
||||
else:
|
||||
_android_plugin.onTestsFailed(test_label, "Unable to launch tests")
|
||||
|
||||
|
||||
func _update_quit_on_go_back(quit_on_go_back: bool) -> void:
|
||||
get_tree().quit_on_go_back = quit_on_go_back
|
||||
|
||||
|
||||
func _on_plugin_toast_button_pressed() -> void:
|
||||
if _android_plugin:
|
||||
_android_plugin.helloWorld()
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cg3hylang5fxn"]
|
||||
[gd_scene format=3 uid="uid://cg3hylang5fxn"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bv6y7in6otgcm" path="res://main.gd" id="1_j0gfq"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
[node name="Main" type="Node2D" unique_id=852911723]
|
||||
script = ExtResource("1_j0gfq")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1839352715]
|
||||
offset_left = 68.0
|
||||
offset_top = 102.0
|
||||
offset_right = 506.0
|
||||
offset_bottom = 408.0
|
||||
theme_override_constants/separation = 25
|
||||
|
||||
[node name="PluginToastButton" type="Button" parent="VBoxContainer"]
|
||||
[node name="PluginToastButton" type="Button" parent="VBoxContainer" unique_id=1670434164]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "Plugin Toast
|
||||
"
|
||||
|
||||
[node name="VibrationButton" type="Button" parent="VBoxContainer"]
|
||||
[node name="VibrationButton" type="Button" parent="VBoxContainer" unique_id=648980813]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "Vibration"
|
||||
|
||||
[node name="GDScriptToastButton" type="Button" parent="VBoxContainer"]
|
||||
[node name="GDScriptToastButton" type="Button" parent="VBoxContainer" unique_id=95554078]
|
||||
custom_minimum_size = Vector2(0, 50)
|
||||
layout_mode = 2
|
||||
text = "GDScript Toast
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
|
||||
config_version=5
|
||||
|
||||
[animation]
|
||||
|
||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Godot App Instrumentation Tests"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.5", "GL Compatibility")
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[debug]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
class_name AndroidPluginSignalTests
|
||||
extends BaseTest
|
||||
|
||||
var _plugin: JNISingleton
|
||||
const emission_test_signal = "emission_test_signal"
|
||||
const launch_test_signal = "launch_tests"
|
||||
|
||||
signal emission_test_signal_emitted
|
||||
|
||||
func _init(plugin: JNISingleton) -> void:
|
||||
_plugin = plugin
|
||||
|
||||
func run_tests():
|
||||
print("Android plugin signal tests starting...")
|
||||
|
||||
__exec_test(test_plugin_exists)
|
||||
__exec_test(test_signal_registration)
|
||||
__exec_test(test_signal_connection)
|
||||
await __exec_test(test_signal_emission)
|
||||
|
||||
print("Android plugin signal tests completed.")
|
||||
|
||||
func test_plugin_exists() -> bool:
|
||||
if _plugin == null:
|
||||
printerr("ERROR: Couldn't find SignalTestPlugin plugin; _plugin is null")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func test_signal_registration() -> bool:
|
||||
var signal_registered = _plugin.has_signal(emission_test_signal)
|
||||
assert_true(signal_registered)
|
||||
|
||||
var launch_signal_registered = _plugin.has_signal(launch_test_signal)
|
||||
assert_true(launch_signal_registered)
|
||||
|
||||
return true
|
||||
|
||||
func test_signal_connection() -> bool:
|
||||
_plugin.connect(emission_test_signal, _on_emission_test_signal_emitted)
|
||||
assert_equal(_plugin.has_connections(emission_test_signal), true)
|
||||
|
||||
_plugin.disconnect(emission_test_signal, _on_emission_test_signal_emitted)
|
||||
assert_equal(_plugin.has_connections(emission_test_signal), false)
|
||||
|
||||
_plugin.emission_test_signal.connect(_on_emission_test_signal_emitted)
|
||||
assert_equal(_plugin.has_connections(emission_test_signal), true)
|
||||
|
||||
_plugin.emission_test_signal.disconnect(_on_emission_test_signal_emitted)
|
||||
assert_equal(_plugin.has_connections(emission_test_signal), false)
|
||||
|
||||
return true
|
||||
|
||||
func test_signal_emission() -> bool:
|
||||
var err1 = _plugin.connect(emission_test_signal, _on_emission_test_signal_emitted)
|
||||
assert_equal(err1, OK)
|
||||
_plugin.triggerTestSignal1()
|
||||
await emission_test_signal_emitted
|
||||
|
||||
# Test case: Same signal name, but different type and number of parameters
|
||||
# The "launch_tests" signal is registered by both GodotAppInstrumentedTestPlugin and SignalTestPlugin.
|
||||
# SignalTestPlugin emits it with a boolean and a string arguments, while GodotAppInstrumentedTestPlugin emits it with one string.
|
||||
var err2 = _plugin.connect(launch_test_signal, _on_launch_tests_emitted)
|
||||
assert_equal(err2, OK)
|
||||
_plugin.triggerLaunchTestSignal()
|
||||
await emission_test_signal_emitted
|
||||
|
||||
return true
|
||||
|
||||
func _on_emission_test_signal_emitted() -> void:
|
||||
emission_test_signal_emitted.emit()
|
||||
|
||||
func _on_launch_tests_emitted(param1: bool, param2: String) -> void:
|
||||
assert_true(param1)
|
||||
assert_equal(param2, "second message")
|
||||
emission_test_signal_emitted.emit()
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://cq0mxqlbbug6r
|
||||
@@ -9,8 +9,9 @@ var _test_assert_failures := 0
|
||||
|
||||
func __exec_test(test_func: Callable):
|
||||
_test_started += 1
|
||||
test_func.call()
|
||||
_test_completed += 1
|
||||
var ret = await test_func.call()
|
||||
if ret == true:
|
||||
_test_completed += 1
|
||||
|
||||
func __reset_tests():
|
||||
_test_started = 0
|
||||
|
||||
+20
-9
@@ -9,10 +9,14 @@ func run_tests():
|
||||
__exec_test(test_internal_app_dir_access)
|
||||
__exec_test(test_internal_cache_dir_access)
|
||||
__exec_test(test_external_app_dir_access)
|
||||
__exec_test(test_downloads_dir_access)
|
||||
__exec_test(test_documents_dir_access)
|
||||
|
||||
func _test_dir_access(dir_path: String, data_file_content: String) -> void:
|
||||
# Scoped storage: Testing access to Downloads and Documents directory.
|
||||
var version = JavaClassWrapper.wrap("android.os.Build$VERSION")
|
||||
if version.SDK_INT >= 30:
|
||||
__exec_test(test_downloads_dir_access)
|
||||
__exec_test(test_documents_dir_access)
|
||||
|
||||
func _test_dir_access(dir_path: String, data_file_content: String) -> bool:
|
||||
print("Testing access to " + dir_path)
|
||||
var data_file_path = dir_path.path_join("data.dat")
|
||||
|
||||
@@ -29,45 +33,52 @@ func _test_dir_access(dir_path: String, data_file_content: String) -> void:
|
||||
|
||||
var deletion_result = DirAccess.remove_absolute(data_file_path)
|
||||
assert_equal(deletion_result, OK)
|
||||
return true
|
||||
|
||||
func test_obb_dir_access() -> void:
|
||||
func test_obb_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var obb_dir: String = app_context.getObbDir().getCanonicalPath()
|
||||
_test_dir_access(obb_dir, FILE_CONTENT + "obb dir.")
|
||||
return true
|
||||
|
||||
func test_internal_app_dir_access() -> void:
|
||||
func test_internal_app_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var internal_app_dir: String = app_context.getFilesDir().getCanonicalPath()
|
||||
_test_dir_access(internal_app_dir, FILE_CONTENT + "internal app dir.")
|
||||
return true
|
||||
|
||||
func test_internal_cache_dir_access() -> void:
|
||||
func test_internal_cache_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var internal_cache_dir: String = app_context.getCacheDir().getCanonicalPath()
|
||||
_test_dir_access(internal_cache_dir, FILE_CONTENT + "internal cache dir.")
|
||||
return true
|
||||
|
||||
func test_external_app_dir_access() -> void:
|
||||
func test_external_app_dir_access() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
var app_context = android_runtime.getApplicationContext()
|
||||
var external_app_dir: String = app_context.getExternalFilesDir("").getCanonicalPath()
|
||||
_test_dir_access(external_app_dir, FILE_CONTENT + "external app dir.")
|
||||
return true
|
||||
|
||||
func test_downloads_dir_access() -> void:
|
||||
func test_downloads_dir_access() -> bool:
|
||||
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
|
||||
var downloads_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOWNLOADS).getCanonicalPath()
|
||||
_test_dir_access(downloads_dir, FILE_CONTENT + "downloads dir.")
|
||||
return true
|
||||
|
||||
func test_documents_dir_access() -> void:
|
||||
func test_documents_dir_access() -> bool:
|
||||
var EnvironmentClass = JavaClassWrapper.wrap("android.os.Environment")
|
||||
var documents_dir = EnvironmentClass.getExternalStoragePublicDirectory(EnvironmentClass.DIRECTORY_DOCUMENTS).getCanonicalPath()
|
||||
_test_dir_access(documents_dir, FILE_CONTENT + "documents dir.")
|
||||
return true
|
||||
|
||||
+58
-9
@@ -20,12 +20,16 @@ func run_tests():
|
||||
|
||||
__exec_test(test_callable)
|
||||
|
||||
__exec_test(test_interface_callable_proxy)
|
||||
|
||||
__exec_test(test_interface_object_proxy)
|
||||
|
||||
print("JavaClassWrapper tests finished.")
|
||||
print("Tests started: " + str(_test_started))
|
||||
print("Tests completed: " + str(_test_completed))
|
||||
|
||||
|
||||
func test_exceptions() -> void:
|
||||
func test_exceptions() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
#print(TestClass.get_java_method_list())
|
||||
|
||||
@@ -36,7 +40,9 @@ func test_exceptions() -> void:
|
||||
|
||||
assert_equal(JavaClassWrapper.get_exception(), null)
|
||||
|
||||
func test_multiple_signatures() -> void:
|
||||
return true
|
||||
|
||||
func test_multiple_signatures() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
|
||||
var ai := [1, 2]
|
||||
@@ -55,7 +61,9 @@ func test_multiple_signatures() -> void:
|
||||
]
|
||||
assert_equal(TestClass.testMethod(3, aobjl), "testObjects: 27 135")
|
||||
|
||||
func test_array_arguments() -> void:
|
||||
return true
|
||||
|
||||
func test_array_arguments() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
|
||||
assert_equal(TestClass.testArgBoolArray([true, false, true]), "[true, false, true]")
|
||||
@@ -72,7 +80,9 @@ func test_array_arguments() -> void:
|
||||
assert_equal(TestClass.testArgDoubleArray(PackedFloat64Array([37.1, 38.2, 39.3])), "[37.1, 38.2, 39.3]")
|
||||
assert_equal(TestClass.testArgDoubleArray([37.1, 38.2, 39.3]), "[37.1, 38.2, 39.3]")
|
||||
|
||||
func test_array_return() -> void:
|
||||
return true
|
||||
|
||||
func test_array_return() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
#print(TestClass.get_java_method_list())
|
||||
|
||||
@@ -107,14 +117,17 @@ func test_array_return() -> void:
|
||||
assert_equal(TestClass.testRetStringArray(), PackedStringArray(["I", "am", "String"]))
|
||||
assert_equal(TestClass.testRetCharSequenceArray(), PackedStringArray(["I", "am", "CharSequence"]))
|
||||
|
||||
func test_dictionary():
|
||||
return true
|
||||
|
||||
func test_dictionary() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
assert_equal(TestClass.testDictionary({a = 1, b = 2}), "{a=1, b=2}")
|
||||
assert_equal(TestClass.testRetDictionary(), {a = 1, b = 2})
|
||||
assert_equal(TestClass.testRetDictionaryArray(), [{a = 1, b = 2}])
|
||||
assert_equal(TestClass.testDictionaryNested({a = 1, b = [2, 3], c = 4}), "{a: 1, b: [2, 3], c: 4}")
|
||||
return true
|
||||
|
||||
func test_object_overload():
|
||||
func test_object_overload() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
var TestClass2: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass2')
|
||||
var TestClass3: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass3')
|
||||
@@ -130,22 +143,25 @@ func test_object_overload():
|
||||
|
||||
assert_equal(TestClass.testObjectOverloadArray(arr_of_t2), "TestClass2: [33, 34]")
|
||||
assert_equal(TestClass.testObjectOverloadArray(arr_of_t3), "TestClass3: [thirty three, thirty four]")
|
||||
return true
|
||||
|
||||
func test_variant_conversion_safe_from_stack_overflow():
|
||||
func test_variant_conversion_safe_from_stack_overflow() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
var arr: Array = [42]
|
||||
var dict: Dictionary = {"arr": arr}
|
||||
arr.append(dict)
|
||||
# The following line will crash with stack overflow if not handled property:
|
||||
TestClass.testDictionary(dict)
|
||||
return true
|
||||
|
||||
func test_big_integers():
|
||||
func test_big_integers() -> bool:
|
||||
var TestClass: JavaClass = JavaClassWrapper.wrap('com.godot.game.test.javaclasswrapper.TestClass')
|
||||
assert_equal(TestClass.testArgLong(4242424242), "4242424242")
|
||||
assert_equal(TestClass.testArgLong(-4242424242), "-4242424242")
|
||||
assert_equal(TestClass.testDictionary({a = 4242424242, b = -4242424242}), "{a=4242424242, b=-4242424242}")
|
||||
return true
|
||||
|
||||
func test_callable():
|
||||
func test_callable() -> bool:
|
||||
var android_runtime = Engine.get_singleton("AndroidRuntime")
|
||||
assert_true(android_runtime != null)
|
||||
|
||||
@@ -155,3 +171,36 @@ func test_callable():
|
||||
return null
|
||||
android_runtime.createRunnableFromGodotCallable(cb1).run()
|
||||
assert_equal(cb1_data['called'], true)
|
||||
|
||||
return true
|
||||
|
||||
func test_interface_callable_proxy() -> bool:
|
||||
var cb1_data := {called = false, content = ""}
|
||||
var cb1 = func (content: String) -> void:
|
||||
cb1_data['called'] = true
|
||||
cb1_data['content'] = content
|
||||
|
||||
var printer_proxy = JavaClassWrapper.create_sam_callback("android.util.Printer", cb1)
|
||||
assert_true(printer_proxy != null)
|
||||
|
||||
printer_proxy.println("This is a callback test")
|
||||
assert_equal(cb1_data['called'], true)
|
||||
assert_equal(cb1_data['content'], "This is a callback test")
|
||||
return true
|
||||
|
||||
class PrintProxy:
|
||||
var test_data := {called = false, content = ""}
|
||||
|
||||
func println(content: String) -> void:
|
||||
test_data['called'] = true
|
||||
test_data['content'] = content
|
||||
|
||||
func test_interface_object_proxy() -> bool:
|
||||
var print_object = PrintProxy.new()
|
||||
var proxy = JavaClassWrapper.create_proxy(print_object, ["android.util.Printer"])
|
||||
assert_true(proxy != null)
|
||||
|
||||
proxy.println("This is proxy test")
|
||||
assert_equal(print_object.test_data['called'], true)
|
||||
assert_equal(print_object.test_data['content'], "This is proxy test")
|
||||
return true
|
||||
|
||||
+31
-2
@@ -38,6 +38,7 @@ import org.godotengine.godot.plugin.UsedByGodot
|
||||
import org.godotengine.godot.plugin.SignalInfo
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* [GodotPlugin] used to drive instrumented tests.
|
||||
@@ -47,14 +48,18 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
companion object {
|
||||
private val TAG = GodotAppInstrumentedTestPlugin::class.java.simpleName
|
||||
private const val MAIN_LOOP_STARTED_LATCH_KEY = "main_loop_started_latch"
|
||||
private const val ENGINE_TERMINATING_LATCH_KEY = "engine_terminating_latch"
|
||||
|
||||
private const val JAVACLASSWRAPPER_TESTS = "javaclasswrapper_tests"
|
||||
private const val FILE_ACCESS_TESTS = "file_access_tests"
|
||||
private const val PLUGIN_SIGNAL_TESTS = "android_plugin_signal_tests"
|
||||
|
||||
private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", String::class.java)
|
||||
private val UPDATE_QUIT_ON_GO_BACK_SIGNAL = SignalInfo("update_quit_on_go_back", java.lang.Boolean::class.java)
|
||||
|
||||
private val SIGNALS = setOf(
|
||||
LAUNCH_TESTS_SIGNAL
|
||||
LAUNCH_TESTS_SIGNAL,
|
||||
UPDATE_QUIT_ON_GO_BACK_SIGNAL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,6 +70,8 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
// Add a countdown latch that is triggered when `onGodotMainLoopStarted` is fired.
|
||||
// This will be used by tests to wait until the engine is ready.
|
||||
latches[MAIN_LOOP_STARTED_LATCH_KEY] = CountDownLatch(1)
|
||||
// Add a countdown latch that is triggered when the engine terminates.
|
||||
latches[ENGINE_TERMINATING_LATCH_KEY] = CountDownLatch(1)
|
||||
}
|
||||
|
||||
override fun getPluginName() = "GodotAppInstrumentedTestPlugin"
|
||||
@@ -76,6 +83,11 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
latches.remove(MAIN_LOOP_STARTED_LATCH_KEY)?.countDown()
|
||||
}
|
||||
|
||||
override fun onGodotTerminating() {
|
||||
super.onGodotTerminating()
|
||||
latches.remove(ENGINE_TERMINATING_LATCH_KEY)?.countDown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the instrumented test to wait until the Godot main loop is up and running.
|
||||
*/
|
||||
@@ -88,6 +100,19 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun waitForEngineTermination(timeoutInMs: Long) {
|
||||
// Wait on the CountDownLatch for `onGodotTerminating`.
|
||||
try {
|
||||
latches[ENGINE_TERMINATING_LATCH_KEY]?.await(timeoutInMs, TimeUnit.MILLISECONDS)
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "Unable to wait for engine termination event.", e)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateQuitOnGoBack(quitOnGoBack: Boolean) {
|
||||
emitSignal(UPDATE_QUIT_ON_GO_BACK_SIGNAL, quitOnGoBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* This launches the JavaClassWrapper tests, and wait until the tests are complete before returning.
|
||||
*/
|
||||
@@ -102,9 +127,13 @@ class GodotAppInstrumentedTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
return launchTests(FILE_ACCESS_TESTS)
|
||||
}
|
||||
|
||||
internal fun runPluginSignalTests(): Result<Any>? {
|
||||
return launchTests(PLUGIN_SIGNAL_TESTS)
|
||||
}
|
||||
|
||||
private fun launchTests(testLabel: String): Result<Any>? {
|
||||
val latch = latches.getOrPut(testLabel) { CountDownLatch(1) }
|
||||
emitSignal(LAUNCH_TESTS_SIGNAL.name, testLabel)
|
||||
emitSignal(LAUNCH_TESTS_SIGNAL, testLabel)
|
||||
return try {
|
||||
latch.await()
|
||||
val result = testResults.remove(testLabel)
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.godot.game.test
|
||||
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.Dictionary
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.plugin.GodotPlugin
|
||||
import org.godotengine.godot.plugin.SignalInfo
|
||||
import org.godotengine.godot.plugin.UsedByGodot
|
||||
|
||||
class SignalTestPlugin(godot: Godot) : GodotPlugin(godot) {
|
||||
|
||||
companion object {
|
||||
private val EMISSION_TEST_SIGNAL = SignalInfo("emission_test_signal")
|
||||
private val LAUNCH_TESTS_SIGNAL = SignalInfo("launch_tests", java.lang.Boolean::class.java, String::class.java)
|
||||
}
|
||||
|
||||
override fun getPluginName() = "SignalTestPlugin"
|
||||
|
||||
override fun getPluginSignals(): Set<SignalInfo?> {
|
||||
return setOf(
|
||||
EMISSION_TEST_SIGNAL,
|
||||
LAUNCH_TESTS_SIGNAL
|
||||
)
|
||||
}
|
||||
|
||||
@UsedByGodot
|
||||
fun triggerTestSignal1() {
|
||||
emitSignal(EMISSION_TEST_SIGNAL)
|
||||
}
|
||||
|
||||
@UsedByGodot
|
||||
fun triggerLaunchTestSignal() {
|
||||
emitSignal(LAUNCH_TESTS_SIGNAL, true, "second message")
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:excludeFromRecents="false"
|
||||
android:exported="false"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
|
||||
@@ -67,9 +67,14 @@ public class GodotApp extends GodotActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.installSplashScreen(this);
|
||||
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Godot godot = getGodot();
|
||||
if (godot != null && godot.getDisableGodotSplash()) {
|
||||
splashScreen.setKeepOnScreenCondition(() -> godot.getRunStatus() != Godot.RunStatus.STARTED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,4 +97,9 @@ public class GodotApp extends GodotActivity {
|
||||
super.onGodotForceQuit(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPiPEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ ext {
|
||||
supportedFlavors = ["editor", "template"]
|
||||
supportedAndroidDistributions = ["android", "horizonos", "picoos"]
|
||||
supportedFlavorsBuildTypes = [
|
||||
"editor": ["dev", "debug", "release"],
|
||||
"template": ["dev", "debug", "release"]
|
||||
"editor": ["debug", "release"],
|
||||
"template": ["debug", "release"]
|
||||
]
|
||||
supportedEditions = ["standard", "mono"]
|
||||
|
||||
@@ -50,11 +50,11 @@ def getSconsTaskName(String flavor, String buildType, String abi) {
|
||||
|
||||
/**
|
||||
* Generate Godot gradle build template by zipping the source files from the app directory, as well
|
||||
* as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'.
|
||||
* as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'.
|
||||
* The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
|
||||
*/
|
||||
task zipGradleBuild(type: Zip) {
|
||||
onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed || generateDevTemplate.state.executed }
|
||||
onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed }
|
||||
doFirst {
|
||||
logger.lifecycle("Generating Godot gradle build template")
|
||||
}
|
||||
@@ -124,9 +124,6 @@ def generateBuildTasks(String flavor = "template", String edition = "standard",
|
||||
File targetLibs = new File(libsDir + target)
|
||||
|
||||
String targetSuffix = target
|
||||
if (target == "dev") {
|
||||
targetSuffix = "debug.dev"
|
||||
}
|
||||
|
||||
if (!excludeSconsBuildTasks || (targetLibs != null
|
||||
&& targetLibs.isDirectory()
|
||||
@@ -315,26 +312,20 @@ task cleanGodotTemplates(type: Delete) {
|
||||
|
||||
// Delete the Godot templates in the Godot bin directory
|
||||
delete("$binDir/android_debug.apk")
|
||||
delete("$binDir/android_dev.apk")
|
||||
delete("$binDir/android_release.apk")
|
||||
delete("$binDir/android_monoDebug.apk")
|
||||
delete("$binDir/android_monoDev.apk")
|
||||
delete("$binDir/android_monoRelease.apk")
|
||||
delete("$binDir/android_source.zip")
|
||||
delete("$binDir/godot-lib.template_debug.aar")
|
||||
delete("$binDir/godot-lib.template_debug.dev.aar")
|
||||
delete("$binDir/godot-lib.template_release.aar")
|
||||
|
||||
// Cover deletion for the libs using the previous naming scheme
|
||||
delete("$binDir/godot-lib.debug.aar")
|
||||
delete("$binDir/godot-lib.dev.aar")
|
||||
delete("$binDir/godot-lib.release.aar")
|
||||
|
||||
// Delete the native debug symbols files.
|
||||
delete("$binDir/android-editor-debug-native-symbols.zip")
|
||||
delete("$binDir/android-editor-dev-native-symbols.zip")
|
||||
delete("$binDir/android-editor-release-native-symbols.zip")
|
||||
delete("$binDir/android-template-debug-native-symbols.zip")
|
||||
delete("$binDir/android-template-dev-native-symbols.zip")
|
||||
delete("$binDir/android-template-release-native-symbols.zip")
|
||||
}
|
||||
|
||||
@@ -131,14 +131,7 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
dev {
|
||||
initWith debug
|
||||
applicationIdSuffix ".dev"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (dev)"]
|
||||
}
|
||||
|
||||
debug {
|
||||
initWith release
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (debug)"]
|
||||
signingConfig signingConfigs.debug
|
||||
@@ -147,6 +140,14 @@ android {
|
||||
release {
|
||||
if (hasReleaseSigningConfigs()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
// We default to the debug signingConfigs when the release signing configs are not
|
||||
// available (e.g: development in Android Studio).
|
||||
signingConfig signingConfigs.debug
|
||||
// In addition, we update the application ID to allow installing an Android studio release build
|
||||
// side by side with a production build from the store.
|
||||
applicationIdSuffix ".release"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (release)"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.vulkan.version"
|
||||
android:required="false"
|
||||
android:version="0x401000" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.vulkan.level"
|
||||
android:required="false"
|
||||
android:version="1" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
@@ -52,7 +60,7 @@
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
@@ -85,6 +93,15 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.godot" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
<activity
|
||||
android:name=".GodotGame"
|
||||
@@ -94,6 +111,7 @@
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":GodotGame"
|
||||
android:taskAffinity=":game"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/GodotGameTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
|
||||
+138
-3
@@ -34,9 +34,12 @@ import android.Manifest
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityOptions
|
||||
import android.content.ComponentName
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Debug
|
||||
@@ -71,7 +74,9 @@ import org.godotengine.godot.utils.DialogUtils
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
import org.godotengine.openxr.vendors.utils.*
|
||||
import java.io.File
|
||||
import kotlin.math.min
|
||||
import kotlin.text.indexOf
|
||||
|
||||
/**
|
||||
* Base class for the Godot Android Editor activities.
|
||||
@@ -152,6 +157,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
internal const val GAME_MENU_ACTION_SET_TIME_SCALE = "setTimeScale"
|
||||
|
||||
private const val GAME_WORKSPACE = "Game"
|
||||
private const val SCRIPT_WORKSPACE = "Script"
|
||||
|
||||
internal const val SNACKBAR_SHOW_DURATION_MS = 5000L
|
||||
|
||||
@@ -198,6 +204,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
protected var gameMenuFragment: GameMenuFragment? = null
|
||||
protected val gameMenuState = Bundle()
|
||||
|
||||
private val updatedCommandLineParams = ArrayList<String>()
|
||||
|
||||
private var changingOrientationAllowed = false
|
||||
private var distractionFreeModeEnabled = false
|
||||
private var activeWorkspace: String? = null
|
||||
|
||||
override fun getGodotAppLayout() = R.layout.godot_editor_layout
|
||||
|
||||
internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
|
||||
@@ -254,7 +266,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
|
||||
editorMessageDispatcher.parseStartIntent(packageManager, intent)
|
||||
|
||||
if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
|
||||
if (BuildConfig.BUILD_TYPE == "debug" && WAIT_FOR_DEBUGGER) {
|
||||
Debug.waitForDebugger()
|
||||
}
|
||||
|
||||
@@ -264,6 +276,14 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
setupGameMenuBar()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
// Show EditorTitleBar on small screens only in landscape due to width limitations in portrait.
|
||||
// TODO: Enable for portrait once the title bar width is optimized.
|
||||
EditorUtils.toggleTitleBar(isLargeScreen || newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
gradleBuildProvider.buildEnvDisconnect()
|
||||
super.onDestroy()
|
||||
@@ -319,6 +339,91 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
super.onNewIntent(newIntent)
|
||||
}
|
||||
|
||||
override fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
val rootDir = Environment.getExternalStorageDirectory().canonicalPath
|
||||
|
||||
val dataPath = when (intent.scheme) {
|
||||
ContentResolver.SCHEME_FILE -> {
|
||||
intent.data?.path
|
||||
}
|
||||
|
||||
ContentResolver.SCHEME_CONTENT -> {
|
||||
// This approach is not recommend with 'content' scheme, but we require the filesystem path in
|
||||
// order to open its parent directory and load the project.
|
||||
val uriPath = intent.data?.path
|
||||
if (uriPath != null) {
|
||||
// Try and see if the external storage directory is part of the uri path.
|
||||
val rootDirIndex = uriPath.indexOf(rootDir)
|
||||
if (rootDirIndex != -1) {
|
||||
uriPath.substring(rootDirIndex)
|
||||
} else {
|
||||
// Try and see if we can retrieve an existing relative path.
|
||||
val pathParts = uriPath.split(':', '/')
|
||||
var currentPath = ""
|
||||
for (index in pathParts.size -1 downTo 0) {
|
||||
currentPath = if (currentPath == "") {
|
||||
pathParts[index]
|
||||
} else {
|
||||
"${pathParts[index]}/$currentPath"
|
||||
}
|
||||
val currentFile = File(rootDir, currentPath)
|
||||
if (currentFile.exists()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
currentPath
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (!dataPath.isNullOrBlank()) {
|
||||
var dataFile = File(dataPath)
|
||||
if (!dataFile.isAbsolute) {
|
||||
dataFile = File(rootDir, dataPath)
|
||||
}
|
||||
|
||||
val dataDir = dataFile.parentFile
|
||||
if (dataDir?.isDirectory == true) {
|
||||
val loadProjectArgs = arrayOf(EDITOR_ARG, PATH_ARG, dataDir.absolutePath)
|
||||
if (newLaunch) {
|
||||
// Update the command line parameters to load the specified project.
|
||||
updatedCommandLineParams.addAll(loadProjectArgs)
|
||||
} else {
|
||||
// Check if we are already editing the specified directory.
|
||||
var isEditor = false
|
||||
var nextIsPath = false
|
||||
var currentPath = ""
|
||||
for (arg in commandLine) {
|
||||
if (nextIsPath) {
|
||||
currentPath = arg
|
||||
nextIsPath = false
|
||||
}
|
||||
|
||||
if (arg == EDITOR_ARG || arg == EDITOR_ARG_SHORT) {
|
||||
isEditor = true
|
||||
} else if (arg == PATH_ARG) {
|
||||
nextIsPath = true
|
||||
}
|
||||
}
|
||||
if (!isEditor || currentPath != dataDir.absolutePath) {
|
||||
onNewGodotInstanceRequested(loadProjectArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.handleStartIntent(intent, newLaunch)
|
||||
}
|
||||
|
||||
protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
|
||||
|
||||
private fun setupGameMenuBar() {
|
||||
@@ -345,6 +450,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
val longPressEnabled = enableLongPressGestures()
|
||||
val panScaleEnabled = enablePanAndScaleGestures()
|
||||
val overrideVolumeButtonsEnabled = overrideVolumeButtons()
|
||||
val hapticEnabled = enableHapticOnLongPress()
|
||||
|
||||
runOnUiThread {
|
||||
// Enable long press, panning and scaling gestures
|
||||
@@ -352,6 +458,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
enableLongPress(longPressEnabled)
|
||||
enablePanningAndScalingGestures(panScaleEnabled)
|
||||
setOverrideVolumeButtons(overrideVolumeButtonsEnabled)
|
||||
enableHapticFeedback(hapticEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +511,10 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
|
||||
override fun getCommandLine(): MutableList<String> {
|
||||
val params = super.getCommandLine()
|
||||
if (BuildConfig.BUILD_TYPE == "dev" && !params.contains("--benchmark")) {
|
||||
if (updatedCommandLineParams.isNotEmpty()) {
|
||||
params.addAll(updatedCommandLineParams)
|
||||
}
|
||||
if (BuildConfig.BUILD_TYPE == "debug" && !params.contains("--benchmark")) {
|
||||
params.add("--benchmark")
|
||||
}
|
||||
return params
|
||||
@@ -603,7 +713,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
/**
|
||||
* The Godot Android Editor sets its own orientation via its AndroidManifest
|
||||
*/
|
||||
protected open fun overrideOrientationRequest() = true
|
||||
protected open fun overrideOrientationRequest() = !changingOrientationAllowed
|
||||
|
||||
protected open fun overrideVolumeButtons() = false
|
||||
|
||||
@@ -613,6 +723,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
protected open fun enableLongPressGestures() =
|
||||
java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_long_press_as_right_click"))
|
||||
|
||||
/**
|
||||
* Enable haptic feedback on long-press right-click for the Godot Android editor.
|
||||
*/
|
||||
protected open fun enableHapticOnLongPress() =
|
||||
java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/haptic_on_long_press"))
|
||||
|
||||
/**
|
||||
* Disable scroll deadzone for the Godot Android editor.
|
||||
*/
|
||||
@@ -801,6 +917,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
}
|
||||
|
||||
override fun onEditorWorkspaceSelected(workspace: String) {
|
||||
activeWorkspace = workspace
|
||||
|
||||
if (workspace == GAME_WORKSPACE && shouldShowGameMenuBar()) {
|
||||
if (editorMessageDispatcher.bringEditorWindowToFront(EMBEDDED_RUN_GAME_INFO) || editorMessageDispatcher.bringEditorWindowToFront(RUN_GAME_INFO)) {
|
||||
return
|
||||
@@ -813,6 +931,23 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
|
||||
embeddedGameViewContainerWindow?.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
toggleScriptEditorOrientation()
|
||||
}
|
||||
|
||||
override fun onDistractionFreeModeChanged(enabled: Boolean) {
|
||||
distractionFreeModeEnabled = enabled
|
||||
toggleScriptEditorOrientation()
|
||||
}
|
||||
|
||||
private fun toggleScriptEditorOrientation() {
|
||||
if (activeWorkspace == SCRIPT_WORKSPACE && distractionFreeModeEnabled) {
|
||||
changingOrientationAllowed = true
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
} else if (changingOrientationAllowed) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
changingOrientationAllowed = false
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun bringSelfToFront() {
|
||||
|
||||
@@ -30,12 +30,7 @@
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.view.isVisible
|
||||
@@ -55,7 +50,6 @@ open class GodotGame : BaseGodotGame() {
|
||||
private val TAG = GodotGame::class.java.simpleName
|
||||
}
|
||||
|
||||
private val gameViewSourceRectHint = Rect()
|
||||
private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -75,13 +69,6 @@ open class GodotGame : BaseGodotGame() {
|
||||
gameMenuFragment?.expandGameMenu()
|
||||
}
|
||||
}
|
||||
|
||||
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 getCommandLine(): MutableList<String> {
|
||||
@@ -96,27 +83,7 @@ open class GodotGame : BaseGodotGame() {
|
||||
return updatedArgs
|
||||
}
|
||||
|
||||
override fun enterPiPMode() {
|
||||
if (hasPiPSystemFeature()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setSeamlessResizeEnabled(false)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
|
||||
Log.v(TAG, "Entering PiP mode")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true the if the device supports picture-in-picture (PiP).
|
||||
*/
|
||||
protected fun hasPiPSystemFeature(): Boolean {
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
}
|
||||
override fun isPiPEnabled() = true
|
||||
|
||||
override fun shouldShowGameMenuBar(): Boolean {
|
||||
return intent.getBooleanExtra(
|
||||
@@ -127,21 +94,11 @@ open class GodotGame : BaseGodotGame() {
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
|
||||
// Hide the game menu fragment when in PiP.
|
||||
gameMenuContainer?.isVisible = !isInPictureInPictureMode
|
||||
}
|
||||
|
||||
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 getGodotAppLayout() = R.layout.godot_game_layout
|
||||
|
||||
override fun getEditorWindowInfo() = RUN_GAME_INFO
|
||||
@@ -258,7 +215,7 @@ open class GodotGame : BaseGodotGame() {
|
||||
|
||||
override fun isCloseButtonEnabled() = !isHorizonOSDevice(applicationContext)
|
||||
|
||||
override fun isPiPButtonEnabled() = hasPiPSystemFeature()
|
||||
override fun isPiPButtonEnabled() = isPiPModeSupported()
|
||||
|
||||
override fun isMenuBarCollapsable() = true
|
||||
|
||||
|
||||
+326
-38
@@ -31,13 +31,18 @@
|
||||
package org.godotengine.editor.embed
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Color
|
||||
import android.graphics.Point
|
||||
import android.os.Bundle
|
||||
import android.util.Rational
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
|
||||
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
|
||||
import android.widget.CheckBox
|
||||
import org.godotengine.editor.GodotGame
|
||||
import org.godotengine.editor.R
|
||||
import org.godotengine.godot.editor.utils.GameMenuUtils
|
||||
@@ -50,22 +55,67 @@ class EmbeddedGodotGame : GodotGame() {
|
||||
companion object {
|
||||
private val TAG = EmbeddedGodotGame::class.java.simpleName
|
||||
|
||||
private const val FULL_SCREEN_WIDTH = WindowManager.LayoutParams.MATCH_PARENT
|
||||
private const val FULL_SCREEN_HEIGHT = WindowManager.LayoutParams.MATCH_PARENT
|
||||
private const val PREFS_NAME = "embedded_game_window_prefs"
|
||||
private const val KEY_X = "embedded_window_x"
|
||||
private const val KEY_Y = "embedded_window_y"
|
||||
private const val KEY_WIDTH = "embedded_window_width"
|
||||
private const val KEY_HEIGHT = "embedded_window_height"
|
||||
private const val KEY_FREE_RESIZE = "is_free_resize"
|
||||
|
||||
private const val RESIZE_THRESHOLD = 80f
|
||||
private const val MIN_WINDOW_SIZE = 400
|
||||
private const val MAX_SCREEN_PERCENT = 0.9f
|
||||
|
||||
private const val RESIZE_UI_HIDE_DELAY_MS = 2000L
|
||||
}
|
||||
|
||||
private val defaultWidthInPx : Int by lazy {
|
||||
resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width)
|
||||
}
|
||||
private val defaultHeightInPx : Int by lazy {
|
||||
resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height)
|
||||
}
|
||||
private val defaultWidthInPx: Int by lazy { resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width) }
|
||||
private val defaultHeightInPx: Int by lazy { resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height) }
|
||||
|
||||
private var layoutWidthInPx = 0
|
||||
private var layoutHeightInPx = 0
|
||||
|
||||
private var gameRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var isFullscreen = false
|
||||
private var gameRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
|
||||
private var resizingEnabled = false
|
||||
private var isResizing = false
|
||||
private var activeCorner = 0
|
||||
private var isFreeResize = false
|
||||
|
||||
private var initialWinX = 0
|
||||
private var initialWinY = 0
|
||||
private var initialWidth = 0
|
||||
private var initialHeight = 0
|
||||
private var initialTouchX = 0f
|
||||
private var initialTouchY = 0f
|
||||
|
||||
private val lockAspectRatioCheckBox: CheckBox by lazy { findViewById(R.id.lockAspectRatioCheckBox) }
|
||||
private val cornerHandles = mutableListOf<View>()
|
||||
|
||||
private val screenBounds: android.graphics.Rect by lazy {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
windowManager.currentWindowMetrics.bounds
|
||||
} else {
|
||||
val size = Point()
|
||||
windowManager.defaultDisplay.getRealSize(size)
|
||||
android.graphics.Rect(0, 0, size.x, size.y)
|
||||
}
|
||||
}
|
||||
|
||||
private val maxAllowedWidth: Int get() = (screenBounds.width() * MAX_SCREEN_PERCENT).toInt()
|
||||
private val maxAllowedHeight: Int get() = (screenBounds.height() * MAX_SCREEN_PERCENT).toInt()
|
||||
|
||||
private var lockedAspectRatio: Float = 1.6f
|
||||
|
||||
private val disableResizeHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
|
||||
private val disableResizeRunnable = Runnable {
|
||||
if (isResizing) return@Runnable // Keep it active user is resizing again
|
||||
resizingEnabled = false
|
||||
gameMenuFragment?.toggleDragButton(false)
|
||||
lockAspectRatioCheckBox.visibility = View.GONE
|
||||
cornerHandles.forEach { it.visibility = View.GONE }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -75,14 +125,54 @@ class EmbeddedGodotGame : GodotGame() {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.flags = layoutParams.flags or FLAG_NOT_TOUCH_MODAL or FLAG_WATCH_OUTSIDE_TOUCH
|
||||
layoutParams.flags = layoutParams.flags and FLAG_DIM_BEHIND.inv()
|
||||
layoutParams.gravity = Gravity.END or Gravity.BOTTOM
|
||||
layoutParams.gravity = Gravity.TOP or Gravity.START
|
||||
|
||||
layoutWidthInPx = defaultWidthInPx
|
||||
layoutHeightInPx = defaultHeightInPx
|
||||
|
||||
layoutParams.width = layoutWidthInPx
|
||||
layoutParams.height = layoutHeightInPx
|
||||
loadWindowBounds(layoutParams)
|
||||
window.attributes = layoutParams
|
||||
|
||||
setupOverlayUI()
|
||||
}
|
||||
|
||||
override fun getGodotAppLayout() = R.layout.godot_embedded_game_layout
|
||||
|
||||
private fun setupOverlayUI() {
|
||||
lockAspectRatioCheckBox.isChecked = !isFreeResize
|
||||
|
||||
lockAspectRatioCheckBox.setOnCheckedChangeListener { _, isChecked ->
|
||||
isFreeResize = !isChecked
|
||||
hideResizeUI()
|
||||
if (isChecked) {
|
||||
val lp = window.attributes
|
||||
lockedAspectRatio = lp.width.toFloat() / lp.height.toFloat().coerceAtLeast(1f)
|
||||
}
|
||||
saveWindowBounds(updatePiPParams = false)
|
||||
}
|
||||
|
||||
cornerHandles.apply {
|
||||
clear()
|
||||
add(findViewById(R.id.handleTopLeft))
|
||||
add(findViewById(R.id.handleTopRight))
|
||||
add(findViewById(R.id.handleBottomLeft))
|
||||
add(findViewById(R.id.handleBottomRight))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLabelText(w: Int, h: Int) {
|
||||
lockAspectRatioCheckBox.text = getString(R.string.lock_aspect_ratio_btn_text, w, h)
|
||||
}
|
||||
|
||||
private fun showResizeUI() {
|
||||
disableResizeHandler.removeCallbacks(disableResizeRunnable)
|
||||
if (!resizingEnabled) {
|
||||
resizingEnabled = true
|
||||
lockAspectRatioCheckBox.visibility = View.VISIBLE
|
||||
cornerHandles.forEach { it.visibility = View.VISIBLE }
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideResizeUI() {
|
||||
disableResizeHandler.removeCallbacks(disableResizeRunnable)
|
||||
disableResizeHandler.postDelayed(disableResizeRunnable, RESIZE_UI_HIDE_DELAY_MS)
|
||||
}
|
||||
|
||||
override fun setRequestedOrientation(requestedOrientation: Int) {
|
||||
@@ -97,40 +187,222 @@ class EmbeddedGodotGame : GodotGame() {
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||
if (isFullscreen) return super.dispatchTouchEvent(event)
|
||||
val layoutParams = window.attributes
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_OUTSIDE -> {
|
||||
if (!isFullscreen) {
|
||||
if (gameMenuFragment?.isAlwaysOnTop() == true) {
|
||||
enterPiPMode()
|
||||
} else {
|
||||
minimizeGameWindow()
|
||||
if (gameMenuFragment?.isAlwaysOnTop() == true) {
|
||||
updatePiPParams(aspectRatio = Rational(layoutWidthInPx, layoutHeightInPx))
|
||||
enterPiPMode()
|
||||
} else {
|
||||
minimizeGameWindow()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (resizingEnabled) {
|
||||
// Check if the click is inside the label's bounds
|
||||
val location = IntArray(2)
|
||||
lockAspectRatioCheckBox.getLocationOnScreen(location)
|
||||
val checkBoxRect = android.graphics.Rect(
|
||||
location[0], location[1],
|
||||
location[0] + lockAspectRatioCheckBox.width,
|
||||
location[1] + lockAspectRatioCheckBox.height
|
||||
)
|
||||
|
||||
if (checkBoxRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
|
||||
// Let the CheckBox handle the click itself
|
||||
return super.dispatchTouchEvent(event)
|
||||
}
|
||||
activeCorner = getTouchedCorner(event.x, event.y, layoutParams.width, layoutParams.height)
|
||||
if (activeCorner != 0) {
|
||||
isResizing = true
|
||||
|
||||
initialTouchX = event.rawX
|
||||
initialTouchY = event.rawY
|
||||
initialWinX = layoutParams.x
|
||||
initialWinY = layoutParams.y
|
||||
initialWidth = layoutParams.width
|
||||
initialHeight = layoutParams.height
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// val layoutParams = window.attributes
|
||||
// TODO: Add logic to move the embedded window.
|
||||
// window.attributes = layoutParams
|
||||
if (resizingEnabled && isResizing) {
|
||||
val dx = (event.rawX - initialTouchX).toInt()
|
||||
val dy = (event.rawY - initialTouchY).toInt()
|
||||
applyResizeLogic(layoutParams, dx, dy)
|
||||
updateLabelText(layoutParams.width, layoutParams.height)
|
||||
window.attributes = layoutParams
|
||||
return true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (isResizing) {
|
||||
isResizing = false
|
||||
saveWindowBounds()
|
||||
hideResizeUI()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.dispatchTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun applyResizeLogic(layoutParams: WindowManager.LayoutParams, dx: Int, dy: Int) {
|
||||
var newW = initialWidth
|
||||
var newH = initialHeight
|
||||
|
||||
when (activeCorner) {
|
||||
Gravity.TOP or Gravity.START -> {
|
||||
newW = (initialWidth - dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth)
|
||||
newH = if (isFreeResize) {
|
||||
(initialHeight - dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight)
|
||||
} else {
|
||||
(newW / lockedAspectRatio).toInt()
|
||||
}
|
||||
layoutParams.x = initialWinX + (initialWidth - newW)
|
||||
layoutParams.y = initialWinY + (initialHeight - newH)
|
||||
}
|
||||
|
||||
Gravity.TOP or Gravity.END -> {
|
||||
newW = (initialWidth + dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth)
|
||||
newH = if (isFreeResize) {
|
||||
(initialHeight - dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight)
|
||||
} else {
|
||||
(newW / lockedAspectRatio).toInt()
|
||||
}
|
||||
layoutParams.y = initialWinY + (initialHeight - newH)
|
||||
}
|
||||
|
||||
Gravity.BOTTOM or Gravity.START -> {
|
||||
newW = (initialWidth - dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth)
|
||||
newH = if (isFreeResize) {
|
||||
(initialHeight + dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight)
|
||||
} else {
|
||||
(newW / lockedAspectRatio).toInt()
|
||||
}
|
||||
layoutParams.x = initialWinX + (initialWidth - newW)
|
||||
}
|
||||
|
||||
Gravity.BOTTOM or Gravity.END -> {
|
||||
newW = (initialWidth + dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth)
|
||||
newH = if (isFreeResize) {
|
||||
(initialHeight + dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight)
|
||||
} else {
|
||||
(newW / lockedAspectRatio).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final aspect-lock check
|
||||
if (!isFreeResize) {
|
||||
// Cap height based on both max height and width-constrained limit.
|
||||
// effectiveMaxHeight helps ensure that the width (newW) calculated below is guaranteed to be <= maxAllowedWidth.
|
||||
val maxHeightFromWidth = (maxAllowedWidth / lockedAspectRatio).toInt()
|
||||
val effectiveMaxHeight = minOf(maxAllowedHeight, maxHeightFromWidth)
|
||||
|
||||
val clampedH = newH.coerceIn(MIN_WINDOW_SIZE, effectiveMaxHeight)
|
||||
|
||||
if (clampedH != newH) {
|
||||
newH = clampedH
|
||||
newW = (newH * lockedAspectRatio).toInt()
|
||||
|
||||
// Re-adjust pivots for the new clamped dimensions
|
||||
if (activeCorner == (Gravity.TOP or Gravity.START) || activeCorner == (Gravity.TOP or Gravity.END)) {
|
||||
layoutParams.y = initialWinY + (initialHeight - newH)
|
||||
}
|
||||
if (activeCorner == (Gravity.TOP or Gravity.START) || activeCorner == (Gravity.BOTTOM or Gravity.START)) {
|
||||
layoutParams.x = initialWinX + (initialWidth - newW)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layoutParams.width = newW
|
||||
layoutParams.height = newH
|
||||
|
||||
val hittingLimit = newW >= maxAllowedWidth || newH >= maxAllowedHeight
|
||||
lockAspectRatioCheckBox.setTextColor(if (hittingLimit) Color.RED else Color.WHITE)
|
||||
}
|
||||
|
||||
private fun getTouchedCorner(x: Float, y: Float, w: Int, h: Int): Int {
|
||||
return when {
|
||||
x < RESIZE_THRESHOLD && y < RESIZE_THRESHOLD -> Gravity.TOP or Gravity.START
|
||||
x > w - RESIZE_THRESHOLD && y < RESIZE_THRESHOLD -> Gravity.TOP or Gravity.END
|
||||
x < RESIZE_THRESHOLD && y > h - RESIZE_THRESHOLD -> Gravity.BOTTOM or Gravity.START
|
||||
x > w - RESIZE_THRESHOLD && y > h - RESIZE_THRESHOLD -> Gravity.BOTTOM or Gravity.END
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveWindowBounds(updatePiPParams: Boolean = true) {
|
||||
if (isFullscreen) return
|
||||
|
||||
val layoutParams = window.attributes
|
||||
layoutWidthInPx = layoutParams.width
|
||||
layoutHeightInPx = layoutParams.height
|
||||
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().apply {
|
||||
putInt(KEY_X, layoutParams.x)
|
||||
putInt(KEY_Y, layoutParams.y)
|
||||
putInt(KEY_WIDTH, layoutParams.width)
|
||||
putInt(KEY_HEIGHT, layoutParams.height)
|
||||
putBoolean(KEY_FREE_RESIZE, isFreeResize)
|
||||
apply()
|
||||
}
|
||||
if (updatePiPParams) {
|
||||
updatePiPParams(aspectRatio = Rational(layoutParams.width, layoutParams.height))
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWindowBounds(layoutParams: WindowManager.LayoutParams) {
|
||||
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||
isFreeResize = prefs.getBoolean(KEY_FREE_RESIZE, false)
|
||||
layoutWidthInPx = prefs.getInt(KEY_WIDTH, defaultWidthInPx)
|
||||
layoutHeightInPx = prefs.getInt(KEY_HEIGHT, defaultHeightInPx)
|
||||
layoutParams.x = prefs.getInt(KEY_X, screenBounds.width() - layoutWidthInPx)
|
||||
layoutParams.y = prefs.getInt(KEY_Y, screenBounds.height() - layoutHeightInPx)
|
||||
layoutParams.width = layoutWidthInPx
|
||||
layoutParams.height = layoutHeightInPx
|
||||
lockedAspectRatio = layoutParams.width.toFloat() / layoutParams.height.toFloat().coerceAtLeast(1f)
|
||||
updateLabelText(layoutWidthInPx, layoutHeightInPx)
|
||||
}
|
||||
|
||||
override fun dragGameWindow(view: View, event: MotionEvent): Boolean {
|
||||
if (isFullscreen) return false
|
||||
val lp = window.attributes
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
showResizeUI()
|
||||
gameMenuFragment?.toggleDragButton(true)
|
||||
initialTouchX = event.rawX
|
||||
initialTouchY = event.rawY
|
||||
initialWinX = lp.x
|
||||
initialWinY = lp.y
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
lp.x = (initialWinX + (event.rawX - initialTouchX)).toInt()
|
||||
lp.y = (initialWinY + (event.rawY - initialTouchY)).toInt()
|
||||
window.attributes = lp
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
saveWindowBounds(updatePiPParams = false) // Only window position is changed, no need to update aspect ratio.
|
||||
hideResizeUI()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getEditorWindowInfo() = EMBEDDED_RUN_GAME_INFO
|
||||
|
||||
override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.ENABLED
|
||||
|
||||
override fun isGameEmbedded() = true
|
||||
|
||||
private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.width = widthInPx
|
||||
layoutParams.height = heightInPx
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
|
||||
override fun isMinimizedButtonEnabled() = true
|
||||
override fun isMinimizedButtonEnabled() = isFullscreen
|
||||
|
||||
override fun isCloseButtonEnabled() = true
|
||||
|
||||
@@ -140,24 +412,40 @@ class EmbeddedGodotGame : GodotGame() {
|
||||
|
||||
override fun isMenuBarCollapsable() = false
|
||||
|
||||
override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
|
||||
override fun isDragButtonEnabled() = !isFullscreen
|
||||
|
||||
override fun isAlwaysOnTopSupported() = isPiPModeSupported()
|
||||
|
||||
override fun onFullScreenUpdated(enabled: Boolean) {
|
||||
godot?.enableImmersiveMode(enabled)
|
||||
isFullscreen = enabled
|
||||
|
||||
val layoutParams = window.attributes
|
||||
if (enabled) {
|
||||
layoutWidthInPx = FULL_SCREEN_WIDTH
|
||||
layoutHeightInPx = FULL_SCREEN_HEIGHT
|
||||
layoutWidthInPx = WindowManager.LayoutParams.MATCH_PARENT
|
||||
layoutHeightInPx = WindowManager.LayoutParams.MATCH_PARENT
|
||||
requestedOrientation = gameRequestedOrientation
|
||||
layoutParams.x = 0
|
||||
layoutParams.y = 0
|
||||
if (resizingEnabled) {
|
||||
disableResizeRunnable.run()
|
||||
}
|
||||
} else {
|
||||
layoutWidthInPx = defaultWidthInPx
|
||||
layoutHeightInPx = defaultHeightInPx
|
||||
loadWindowBounds(layoutParams)
|
||||
|
||||
// Cache the last used orientation in fullscreen to reapply when re-entering fullscreen.
|
||||
gameRequestedOrientation = requestedOrientation
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
|
||||
gameMenuFragment?.refreshButtonsVisibility()
|
||||
}
|
||||
|
||||
private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.width = widthInPx
|
||||
layoutParams.height = heightInPx
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
|
||||
+27
-5
@@ -36,6 +36,7 @@ import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
@@ -47,6 +48,7 @@ import androidx.fragment.app.Fragment
|
||||
import org.godotengine.editor.BaseGodotEditor
|
||||
import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
|
||||
import org.godotengine.editor.R
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
/**
|
||||
* Used to be notified of events fired when interacting with the game menu.
|
||||
*/
|
||||
interface GameMenuListener {
|
||||
interface GameMenuListener : PictureInPictureProvider {
|
||||
|
||||
/**
|
||||
* Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
|
||||
@@ -109,16 +111,16 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
fun isGameEmbeddingSupported(): Boolean
|
||||
fun embedGameOnPlay(embedded: Boolean)
|
||||
|
||||
fun enterPiPMode() {}
|
||||
fun minimizeGameWindow() {}
|
||||
fun closeGameWindow() {}
|
||||
fun dragGameWindow(view: View, event: MotionEvent): Boolean { return false}
|
||||
|
||||
fun isMinimizedButtonEnabled() = false
|
||||
fun isFullScreenButtonEnabled() = false
|
||||
fun isCloseButtonEnabled() = false
|
||||
fun isPiPButtonEnabled() = false
|
||||
fun isMenuBarCollapsable() = false
|
||||
|
||||
fun isDragButtonEnabled() = false
|
||||
fun isAlwaysOnTopSupported() = false
|
||||
|
||||
fun onFullScreenUpdated(enabled: Boolean) {}
|
||||
@@ -128,6 +130,9 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
private val collapseMenuButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_collapse_button)
|
||||
}
|
||||
private val dragButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_drag_button)
|
||||
}
|
||||
private val suspendButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_suspend_button)
|
||||
}
|
||||
@@ -181,7 +186,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
PopupMenu(context, optionsButton).apply {
|
||||
setOnMenuItemClickListener(this@GameMenuFragment)
|
||||
inflate(R.menu.options_menu)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
menu.setGroupDividerEnabled(true)
|
||||
}
|
||||
@@ -263,6 +267,7 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
val isCloseButtonEnabled = menuListener?.isCloseButtonEnabled() == true
|
||||
val isPiPButtonEnabled = menuListener?.isPiPButtonEnabled() == true
|
||||
val isMenuBarCollapsable = menuListener?.isMenuBarCollapsable() == true
|
||||
val isDragButtonEnabled = menuListener?.isDragButtonEnabled() == true
|
||||
|
||||
// Show the divider if any of the window controls is visible
|
||||
view.findViewById<View>(R.id.game_menu_window_controls_divider)?.isVisible =
|
||||
@@ -270,7 +275,8 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
isFullScreenButtonEnabled ||
|
||||
isCloseButtonEnabled ||
|
||||
isPiPButtonEnabled ||
|
||||
isMenuBarCollapsable
|
||||
isMenuBarCollapsable ||
|
||||
isDragButtonEnabled
|
||||
|
||||
collapseMenuButton?.apply {
|
||||
isVisible = isMenuBarCollapsable
|
||||
@@ -278,6 +284,13 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
collapseGameMenu()
|
||||
}
|
||||
}
|
||||
dragButton?.apply {
|
||||
isVisible = isDragButtonEnabled
|
||||
setOnTouchListener { v, event ->
|
||||
menuListener?.dragGameWindow(v, event) == true
|
||||
}
|
||||
|
||||
}
|
||||
fullscreenButton?.apply{
|
||||
isVisible = isFullScreenButtonEnabled
|
||||
setOnClickListener {
|
||||
@@ -445,6 +458,10 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
internal fun isAlwaysOnTop() = isGameEmbedded && alwaysOnTopChecked
|
||||
|
||||
internal fun toggleDragButton(pressed: Boolean) {
|
||||
dragButton?.isPressed = pressed
|
||||
}
|
||||
|
||||
private fun collapseGameMenu() {
|
||||
view?.isVisible = false
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
@@ -461,6 +478,11 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
menuListener?.onGameMenuCollapsed(false)
|
||||
}
|
||||
|
||||
internal fun refreshButtonsVisibility() {
|
||||
minimizeButton?.isVisible = menuListener?.isMinimizedButtonEnabled() == true
|
||||
dragButton?.isVisible = menuListener?.isDragButtonEnabled() == true
|
||||
}
|
||||
|
||||
private fun updateAlwaysOnTop(enabled: Boolean) {
|
||||
alwaysOnTopChecked = enabled
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880L310,710L367,653L440,726L440,520L235,520L308,592L250,650L80,480L249,311L306,368L234,440L440,440L440,234L367,307L310,250L480,80L650,250L593,307L520,234L520,440L725,440L652,368L710,310L880,480L710,650L653,593L726,520L520,520L520,725L592,652L650,710L480,880Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:pathData="M20,20 L20,4 M20,20 L4,20"
|
||||
android:strokeColor="#007AFF"
|
||||
android:strokeWidth="4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"/>
|
||||
</vector>
|
||||
@@ -181,6 +181,15 @@
|
||||
android:src="@drawable/baseline_expand_less_24"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_drag_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_selected_button_bg"
|
||||
android:src="@drawable/drag_pan_24px"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_minimize_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/game_menu_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/godot_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/game_menu_fragment_container"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/lockAspectRatioCheckBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="60dp"
|
||||
android:background="#CC000000"
|
||||
android:padding="8dp"
|
||||
android:text="Lock Aspect Ratio"
|
||||
android:textColor="#FFFFFF"
|
||||
android:buttonTint="#FFFFFF"
|
||||
android:checked="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleTopLeft"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="180"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleTopRight"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="270"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleBottomLeft"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="90"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleBottomRight"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -20,4 +20,5 @@
|
||||
<string name="show_game_resume_hint">Tap on \'Game\' to resume</string>
|
||||
<string name="restart_embed_game_hint">Restart game to embed</string>
|
||||
<string name="restart_non_embedded_game_hint">Restart Game to disable embedding</string>
|
||||
<string name="lock_aspect_ratio_btn_text">Lock Aspect ratio (%d x %d)</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,7 +13,7 @@ apply from: "../scripts/publish-module.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||
implementation "androidx.documentfile:documentfile:$versions.documentfileVersion"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
@@ -48,12 +48,6 @@ android {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
dev {
|
||||
initWith debug
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["products"]
|
||||
productFlavors {
|
||||
editor {}
|
||||
@@ -79,13 +73,11 @@ android {
|
||||
|
||||
sourceSets {
|
||||
debug.jniLibs.srcDirs = ['libs/debug']
|
||||
dev.jniLibs.srcDirs = ['libs/dev']
|
||||
release.jniLibs.srcDirs = ['libs/release']
|
||||
|
||||
// Editor jni library
|
||||
editorRelease.jniLibs.srcDirs = ['libs/tools/release']
|
||||
editorDebug.jniLibs.srcDirs = ['libs/tools/debug']
|
||||
editorDev.jniLibs.srcDirs = ['libs/tools/dev']
|
||||
}
|
||||
|
||||
libraryVariants.all { variant ->
|
||||
@@ -99,9 +91,9 @@ android {
|
||||
throw new GradleException("Invalid build type: $buildType")
|
||||
}
|
||||
|
||||
boolean devBuild = buildType == "dev"
|
||||
boolean debugSymbols = devBuild
|
||||
boolean runTests = devBuild
|
||||
boolean debugBuild = buildType == "debug"
|
||||
boolean debugSymbols = debugBuild
|
||||
boolean runTests = debugBuild
|
||||
boolean storeRelease = buildType == "release"
|
||||
boolean productionBuild = storeRelease
|
||||
|
||||
@@ -109,12 +101,13 @@ android {
|
||||
if (sconsTarget == "template") {
|
||||
// Tests are not supported on template builds
|
||||
runTests = false
|
||||
|
||||
//noinspection GroovyFallthrough
|
||||
switch (buildType) {
|
||||
case "release":
|
||||
sconsTarget += "_release"
|
||||
break
|
||||
case "debug":
|
||||
case "dev":
|
||||
default:
|
||||
sconsTarget += "_debug"
|
||||
break
|
||||
@@ -123,9 +116,6 @@ android {
|
||||
|
||||
// Update the name of the generated library
|
||||
def outputSuffix = "${sconsTarget}"
|
||||
if (devBuild) {
|
||||
outputSuffix = "${outputSuffix}.dev"
|
||||
}
|
||||
variant.outputs.all { output ->
|
||||
output.outputFileName = "godot-lib.${outputSuffix}.aar"
|
||||
}
|
||||
@@ -168,7 +158,7 @@ android {
|
||||
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=${devBuild}", "dev_build=${devBuild}", "debug_symbols=${debugSymbols}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
|
||||
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.
|
||||
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
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);
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
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"
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
-297
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
-360
@@ -1,360 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
-229
@@ -1,229 +0,0 @@
|
||||
/*
|
||||
* 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) {
|
||||
}
|
||||
|
||||
}
|
||||
-852
@@ -1,852 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
-1346
File diff suppressed because it is too large
Load Diff
-510
@@ -1,510 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-414
@@ -1,414 +0,0 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.vending.licensing;
|
||||
|
||||
/**
|
||||
* Allows the developer to limit the number of devices using a single license.
|
||||
* <p>
|
||||
* The LICENSED response from the server contains a user identifier unique to
|
||||
* the <application, user> pair. The developer can send this identifier
|
||||
* to their own server along with some device identifier (a random number
|
||||
* generated and stored once per application installation,
|
||||
* {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
|
||||
* {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
|
||||
* The more sources used to identify the device, the harder it will be for an
|
||||
* attacker to spoof.
|
||||
* <p>
|
||||
* The server can look at the <application, user, device id> tuple and
|
||||
* restrict a user's application license to run on at most 10 different devices
|
||||
* in a week (for example). We recommend not being too restrictive because a
|
||||
* user might legitimately have multiple devices or be in the process of
|
||||
* changing phones. This will catch egregious violations of multiple people
|
||||
* sharing one license.
|
||||
*/
|
||||
public interface DeviceLimiter {
|
||||
|
||||
/**
|
||||
* Checks if this device is allowed to use the given user's license.
|
||||
*
|
||||
* @param userId the user whose license the server responded with
|
||||
* @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
|
||||
*/
|
||||
int isDeviceAllowed(String userId);
|
||||
}
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
/*
|
||||
* 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-231
@@ -1,231 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
});
|
||||
}
|
||||
}
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
-578
@@ -1,578 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,16 @@
|
||||
package org.godotengine.godot;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class Dictionary extends HashMap<String, Object> {
|
||||
protected String[] keys_cache;
|
||||
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;
|
||||
@@ -48,6 +53,10 @@ public class Dictionary extends HashMap<String, Object> {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link java.util.HashMap#values()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Object[] get_values() {
|
||||
Object[] ret = new Object[size()];
|
||||
int i = 0;
|
||||
@@ -60,10 +69,18 @@ public class Dictionary extends HashMap<String, Object> {
|
||||
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) {
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
package org.godotengine.godot
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.*
|
||||
@@ -55,44 +54,36 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.vending.expansion.downloader.*
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.input.GodotEditText
|
||||
import org.godotengine.godot.input.GodotInputHandler
|
||||
import org.godotengine.godot.io.FilePicker
|
||||
import org.godotengine.godot.io.StorageScope
|
||||
import org.godotengine.godot.io.directory.DirectoryAccessHandler
|
||||
import org.godotengine.godot.io.file.FileAccessHandler
|
||||
import org.godotengine.godot.nativeapi.GodotNativeBridge
|
||||
import org.godotengine.godot.plugin.AndroidRuntimePlugin
|
||||
import org.godotengine.godot.plugin.GodotPlugin
|
||||
import org.godotengine.godot.plugin.GodotPluginRegistry
|
||||
import org.godotengine.godot.tts.GodotTTS
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
import org.godotengine.godot.utils.GodotNetUtils
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
|
||||
import org.godotengine.godot.utils.beginBenchmarkMeasure
|
||||
import org.godotengine.godot.utils.benchmarkFile
|
||||
import org.godotengine.godot.utils.dumpBenchmark
|
||||
import org.godotengine.godot.utils.endBenchmarkMeasure
|
||||
import org.godotengine.godot.utils.useBenchmark
|
||||
import org.godotengine.godot.variant.Callable as GodotCallable
|
||||
import org.godotengine.godot.xr.XRMode
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.FutureTask
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
/**
|
||||
* Core component used to interface with the native layer of the engine.
|
||||
*
|
||||
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
|
||||
* lifecycle methods are properly invoked.
|
||||
* Can be hosted by [Activity], [androidx.fragment.app.Fragment] or [android.app.Service] android components, so long
|
||||
* as its lifecycle methods are properly invoked.
|
||||
*/
|
||||
class Godot private constructor(val context: Context) {
|
||||
|
||||
@@ -115,15 +106,30 @@ class Godot private constructor(val context: Context) {
|
||||
private const val TEMPLATE_FLAVOR = "template"
|
||||
|
||||
/**
|
||||
* @return true if this is an editor build, false if this is a template build
|
||||
* @return true if this is an editor build, false if this is a template build.
|
||||
*/
|
||||
internal fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
|
||||
|
||||
/**
|
||||
* @return true if this is a template build, false if this is an editor build.
|
||||
*/
|
||||
internal fun isTemplateBuild() = BuildConfig.FLAVOR == TEMPLATE_FLAVOR
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the engine current run status.
|
||||
*/
|
||||
enum class RunStatus {
|
||||
INITIALIZING,
|
||||
STARTED,
|
||||
TERMINATING
|
||||
}
|
||||
|
||||
private val godotNativeBridge = GodotNativeBridge(this)
|
||||
|
||||
private val mSensorManager: SensorManager? by lazy { context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager }
|
||||
private val mClipboard: ClipboardManager? by lazy { context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager }
|
||||
private val vibratorService: Vibrator? by lazy { context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
|
||||
private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() }
|
||||
internal val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() }
|
||||
|
||||
private val accelerometerEnabled = AtomicBoolean(false)
|
||||
private val mAccelerometer: Sensor? by lazy { mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
|
||||
@@ -143,7 +149,7 @@ class Godot private constructor(val context: Context) {
|
||||
val directoryAccessHandler = DirectoryAccessHandler(context)
|
||||
val fileAccessHandler = FileAccessHandler(context)
|
||||
val netUtils = GodotNetUtils(context)
|
||||
private val godotInputHandler = GodotInputHandler(context, this)
|
||||
val godotInputHandler = GodotInputHandler(context, this)
|
||||
|
||||
private val hasClipboardCallable = Callable {
|
||||
mClipboard?.hasPrimaryClip() == true
|
||||
@@ -174,8 +180,7 @@ class Godot private constructor(val context: Context) {
|
||||
* Tracks whether [onInitRenderView] was completed successfully.
|
||||
*/
|
||||
private var renderViewInitialized = false
|
||||
private var primaryHost: GodotHost? = null
|
||||
private var currentConfig = context.resources.configuration
|
||||
internal var primaryHost: GodotHost? = null
|
||||
|
||||
/**
|
||||
* Tracks whether we're in the RESUMED lifecycle state.
|
||||
@@ -184,25 +189,30 @@ class Godot private constructor(val context: Context) {
|
||||
private var resumed = false
|
||||
|
||||
/**
|
||||
* Tracks whether [onGodotSetupCompleted] fired.
|
||||
* Tracks the engine's run status.
|
||||
*/
|
||||
private val godotMainLoopStarted = AtomicBoolean(false)
|
||||
private val _runStatus = AtomicReference<RunStatus>(RunStatus.INITIALIZING)
|
||||
val runStatus: RunStatus
|
||||
get() = _runStatus.get()
|
||||
|
||||
val io = GodotIO(this)
|
||||
|
||||
private var commandLine : MutableList<String> = ArrayList<String>()
|
||||
private var xrMode = XRMode.REGULAR
|
||||
internal var xrMode = XRMode.REGULAR
|
||||
private val useImmersive = AtomicBoolean(false)
|
||||
private val isEdgeToEdge = AtomicBoolean(false)
|
||||
private var useDebugOpengl = false
|
||||
private var darkMode = false
|
||||
internal var darkMode = false
|
||||
private var backgroundColor: Int = Color.BLACK
|
||||
private var orientation = Configuration.ORIENTATION_UNDEFINED
|
||||
var disableGodotSplash = false
|
||||
private set
|
||||
|
||||
internal var containerLayout: FrameLayout? = null
|
||||
var renderView: GodotRenderView? = null
|
||||
|
||||
/**
|
||||
* Returns true if the native engine has been initialized through [onInitNativeLayer], false otherwise.
|
||||
* Returns true if the native engine has been initialized through [initEngine], false otherwise.
|
||||
*/
|
||||
private fun isNativeInitialized() = nativeLayerInitializeCompleted && nativeLayerSetupCompleted
|
||||
|
||||
@@ -212,7 +222,7 @@ class Godot private constructor(val context: Context) {
|
||||
fun isInitialized() = primaryHost != null && isNativeInitialized() && renderViewInitialized
|
||||
|
||||
/**
|
||||
* Provides access to the primary host [Activity]
|
||||
* Provides access to the primary host [Activity].
|
||||
*/
|
||||
fun getActivity() = primaryHost?.activity
|
||||
|
||||
@@ -222,9 +232,6 @@ class Godot private constructor(val context: Context) {
|
||||
* This must be followed by [onInitRenderView] to complete initialization of the engine.
|
||||
*
|
||||
* @return false if initialization of the native layer fails, true otherwise.
|
||||
*
|
||||
* @throws IllegalArgumentException exception if the specified expansion pack (if any)
|
||||
* is invalid.
|
||||
*/
|
||||
fun initEngine(host: GodotHost?, commandLineParams: List<String>, hostPlugins: Set<GodotPlugin> = Collections.emptySet()): Boolean {
|
||||
if (isNativeInitialized()) {
|
||||
@@ -232,25 +239,37 @@ class Godot private constructor(val context: Context) {
|
||||
return true
|
||||
}
|
||||
|
||||
Log.v(TAG, "InitEngine with params: $commandLineParams")
|
||||
|
||||
darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
val config = context.resources.configuration
|
||||
darkMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
orientation = config.orientation
|
||||
|
||||
beginBenchmarkMeasure("Startup", "Godot::initEngine")
|
||||
try {
|
||||
this.primaryHost = host
|
||||
commandLine.addAll(commandLineParams)
|
||||
|
||||
Log.v(TAG, "Initializing Godot plugin registry")
|
||||
val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
|
||||
runtimePlugins.addAll(hostPlugins)
|
||||
GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
|
||||
|
||||
// check for apk expansion API
|
||||
commandLine.addAll(commandLineParams)
|
||||
var mainPackMd5: String? = null
|
||||
var mainPackKey: String? = null
|
||||
var useApkExpansion = false
|
||||
// Let the plugins take a peek at the command line params and provide their own params if desired.
|
||||
val originalCommandLineParams = Collections.unmodifiableList(commandLineParams)
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
try {
|
||||
val pluginCommandLineParams = plugin.getCommandLineParams(originalCommandLineParams)
|
||||
if (pluginCommandLineParams != originalCommandLineParams && pluginCommandLineParams.isNotEmpty()) {
|
||||
Log.d(TAG, "Received command line params from plugin $plugin: $pluginCommandLineParams")
|
||||
commandLine.addAll(pluginCommandLineParams)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get command line params from plugin $plugin", e)
|
||||
}
|
||||
}
|
||||
|
||||
Log.v(TAG, "InitEngine with params: $commandLine")
|
||||
val newArgs: MutableList<String> = ArrayList()
|
||||
var useApkExpansion = false
|
||||
var i = 0
|
||||
while (i < commandLine.size) {
|
||||
val hasExtra: Boolean = i < commandLine.size - 1
|
||||
@@ -265,23 +284,11 @@ class Godot private constructor(val context: Context) {
|
||||
} else if (commandLine[i] == "--fullscreen") {
|
||||
useImmersive.set(true)
|
||||
newArgs.add(commandLine[i])
|
||||
} else if (commandLine[i] == "--background_color") {
|
||||
} else if (hasExtra && commandLine[i] == "--background_color") {
|
||||
setWindowColor(commandLine[i + 1])
|
||||
} else if (commandLine[i] == "--use_apk_expansion") {
|
||||
useApkExpansion = true
|
||||
} else if (hasExtra && commandLine[i] == "--apk_expansion_md5") {
|
||||
mainPackMd5 = commandLine[i + 1]
|
||||
i++
|
||||
} else if (hasExtra && commandLine[i] == "--apk_expansion_key") {
|
||||
mainPackKey = commandLine[i + 1]
|
||||
val prefs = context.getSharedPreferences(
|
||||
"app_data_keys",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val editor = prefs.edit()
|
||||
editor.putString("store_public_key", mainPackKey)
|
||||
editor.apply()
|
||||
i++
|
||||
} else if (commandLine[i] == "--disable_godot_splash") {
|
||||
disableGodotSplash = true
|
||||
} else if (commandLine[i] == "--benchmark") {
|
||||
useBenchmark = true
|
||||
newArgs.add(commandLine[i])
|
||||
@@ -294,49 +301,28 @@ class Godot private constructor(val context: Context) {
|
||||
newArgs.add(commandLine[i + 1])
|
||||
|
||||
i++
|
||||
} else if (commandLine[i].trim().isNotEmpty()) {
|
||||
} else if (hasExtra && commandLine[i] == "--main-pack") {
|
||||
newArgs.add(commandLine[i])
|
||||
|
||||
val mainPackPath = commandLine[i + 1]
|
||||
newArgs.add(commandLine[i + 1])
|
||||
// Check the storage scope of the main pack path. For template builds, `useApkExpansion` is enabled
|
||||
// if the storage scope is APP.
|
||||
val storageScope = fileAccessHandler.storageScopeIdentifier.identifyStorageScope(mainPackPath)
|
||||
if (isTemplateBuild()) {
|
||||
useApkExpansion = storageScope == StorageScope.APP
|
||||
}
|
||||
i++
|
||||
} else if (commandLine[i].trim().isNotEmpty()) { // This block should always be last!
|
||||
newArgs.add(commandLine[i])
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
var expansionPackPath = ""
|
||||
commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs }
|
||||
if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) {
|
||||
// Build the full path to the app's expansion files
|
||||
try {
|
||||
expansionPackPath = Helpers.getSaveFilePath(context)
|
||||
expansionPackPath += "/main." + context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
0
|
||||
).versionCode + "." + context.packageName + ".obb"
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(TAG, "Unable to build full path to the app's expansion files", e)
|
||||
}
|
||||
val f = File(expansionPackPath)
|
||||
var packValid = true
|
||||
if (!f.exists()) {
|
||||
packValid = false
|
||||
} else if (obbIsCorrupted(expansionPackPath, mainPackMd5)) {
|
||||
packValid = false
|
||||
try {
|
||||
f.delete()
|
||||
} catch (_: java.lang.Exception) {
|
||||
}
|
||||
}
|
||||
if (!packValid) {
|
||||
// Aborting engine initialization
|
||||
throw IllegalArgumentException("Invalid expansion pack")
|
||||
}
|
||||
}
|
||||
|
||||
if (expansionPackPath.isNotEmpty()) {
|
||||
commandLine.add("--main-pack")
|
||||
commandLine.add(expansionPackPath)
|
||||
}
|
||||
if (!nativeLayerInitializeCompleted) {
|
||||
nativeLayerInitializeCompleted = GodotLib.initialize(
|
||||
this,
|
||||
godotNativeBridge,
|
||||
context.assets,
|
||||
io,
|
||||
netUtils,
|
||||
@@ -348,6 +334,7 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
|
||||
if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
|
||||
Log.v(TAG, "Setting up native layer with params: $commandLine")
|
||||
nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
|
||||
if (!nativeLayerSetupCompleted) {
|
||||
throw IllegalStateException("Unable to setup the Godot engine! Aborting...")
|
||||
@@ -459,20 +446,8 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked from the render thread to toggle the immersive mode.
|
||||
*/
|
||||
@Keep
|
||||
private fun nativeEnableImmersiveMode(enabled: Boolean) {
|
||||
runOnHostThread {
|
||||
enableImmersiveMode(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun isInImmersiveMode() = useImmersive.get()
|
||||
|
||||
@Keep
|
||||
fun isInEdgeToEdgeMode() = isEdgeToEdge.get()
|
||||
|
||||
fun setSystemBarsAppearance() {
|
||||
@@ -565,19 +540,14 @@ class Godot private constructor(val context: Context) {
|
||||
!isEditorHint() &&
|
||||
java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/per_pixel_transparency/allowed"))
|
||||
Log.d(TAG, "Render view should be transparent: $shouldBeTransparent")
|
||||
renderView = if (usesVulkan()) {
|
||||
if (meetsVulkanRequirements(context.packageManager)) {
|
||||
GodotVulkanRenderView(this, godotInputHandler, shouldBeTransparent)
|
||||
} else if (canFallbackToOpenGL()) {
|
||||
// Fallback to OpenGl.
|
||||
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
||||
} else {
|
||||
throw IllegalStateException(context.getString(R.string.error_missing_vulkan_requirements_message))
|
||||
}
|
||||
|
||||
val nativeRenderer = getNativeRenderer()
|
||||
if (nativeRenderer == "vulkan") {
|
||||
renderView = GodotVulkanRenderView(this, godotInputHandler, shouldBeTransparent)
|
||||
} else if (nativeRenderer == "opengl3") {
|
||||
renderView = GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
||||
} else {
|
||||
// Fallback to OpenGl.
|
||||
GodotGLRenderView(this, godotInputHandler, xrMode, useDebugOpengl, shouldBeTransparent)
|
||||
throw IllegalStateException("No native renderer is available.")
|
||||
}
|
||||
|
||||
renderView?.let {
|
||||
@@ -641,7 +611,7 @@ class Godot private constructor(val context: Context) {
|
||||
// Fixes an issue on Android 10 and older where immersive mode gets auto disabled after the keyboard is hidden on some devices.
|
||||
if (useImmersive.get() && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
runOnHostThread {
|
||||
enableImmersiveMode(true, true)
|
||||
enableImmersiveMode(enabled = true, override = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,6 +619,8 @@ class Godot private constructor(val context: Context) {
|
||||
|
||||
renderView?.queueOnRenderThread {
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
// Plugins should be registered early so they are available as soon as the app starts.
|
||||
// Otherwise, a delay in registration may make them unavailable during _init() of the main script or an autoload.
|
||||
plugin.onRegisterPluginWithGodotNative()
|
||||
}
|
||||
setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")))
|
||||
@@ -684,6 +656,9 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
|
||||
renderView?.onActivityStarted()
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainStart()
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume(host: GodotHost) {
|
||||
@@ -701,7 +676,7 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
|
||||
private fun registerSensorsIfNeeded() {
|
||||
if (!resumed || !godotMainLoopStarted.get()) {
|
||||
if (!resumed || runStatus != RunStatus.STARTED) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -719,6 +694,13 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
runOnRenderThread {
|
||||
GodotLib.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause(host: GodotHost) {
|
||||
Log.v(TAG, "OnPause: $host")
|
||||
resumed = false
|
||||
@@ -726,11 +708,11 @@ class Godot private constructor(val context: Context) {
|
||||
return
|
||||
}
|
||||
|
||||
renderView?.onActivityPaused()
|
||||
mSensorManager?.unregisterListener(godotInputHandler)
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainPause()
|
||||
}
|
||||
renderView?.onActivityPaused()
|
||||
mSensorManager?.unregisterListener(godotInputHandler)
|
||||
}
|
||||
|
||||
fun onStop(host: GodotHost) {
|
||||
@@ -739,6 +721,9 @@ class Godot private constructor(val context: Context) {
|
||||
return
|
||||
}
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainStop()
|
||||
}
|
||||
renderView?.onActivityStopped()
|
||||
}
|
||||
|
||||
@@ -765,7 +750,7 @@ class Godot private constructor(val context: Context) {
|
||||
* Configuration change callback
|
||||
*/
|
||||
fun onConfigurationChanged(newConfig: Configuration) {
|
||||
renderView?.inputHandler?.onConfigurationChanged(newConfig)
|
||||
godotInputHandler.onConfigurationChanged(newConfig)
|
||||
|
||||
val newDarkMode = newConfig.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
if (darkMode != newDarkMode) {
|
||||
@@ -775,12 +760,12 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentConfig.orientation != newConfig.orientation) {
|
||||
if (orientation != newConfig.orientation) {
|
||||
orientation = newConfig.orientation
|
||||
runOnRenderThread {
|
||||
GodotLib.onScreenRotationChange(newConfig.orientation)
|
||||
GodotLib.onOrientationChange(orientation)
|
||||
}
|
||||
}
|
||||
currentConfig = newConfig
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -790,10 +775,8 @@ class Godot private constructor(val context: Context) {
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onMainActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
runOnRenderThread {
|
||||
FilePicker.handleActivityResult(context, requestCode, resultCode, data)
|
||||
}
|
||||
runOnRenderThread {
|
||||
FilePicker.handleActivityResult(context, requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,7 +804,7 @@ class Godot private constructor(val context: Context) {
|
||||
/**
|
||||
* Invoked on the render thread when the Godot setup is complete.
|
||||
*/
|
||||
private fun onGodotSetupCompleted() {
|
||||
internal fun onGodotSetupCompleted() {
|
||||
Log.v(TAG, "OnGodotSetupCompleted")
|
||||
|
||||
// These properties are defined after Godot setup completion, so we retrieve them here.
|
||||
@@ -832,7 +815,7 @@ class Godot private constructor(val context: Context) {
|
||||
val scrollDeadzoneDisabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/disable_scroll_deadzone"))
|
||||
|
||||
runOnHostThread {
|
||||
renderView?.inputHandler?.apply {
|
||||
godotInputHandler.apply {
|
||||
enableLongPress(longPressEnabled)
|
||||
enablePanningAndScalingGestures(panScaleEnabled)
|
||||
setOverrideVolumeButtons(overrideVolumeButtons)
|
||||
@@ -854,9 +837,9 @@ class Godot private constructor(val context: Context) {
|
||||
/**
|
||||
* Invoked on the render thread when the Godot main loop has started.
|
||||
*/
|
||||
private fun onGodotMainLoopStarted() {
|
||||
internal fun onGodotMainLoopStarted() {
|
||||
Log.v(TAG, "OnGodotMainLoopStarted")
|
||||
godotMainLoopStarted.set(true)
|
||||
_runStatus.set(RunStatus.STARTED)
|
||||
|
||||
accelerometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer")))
|
||||
gravityEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity")))
|
||||
@@ -876,15 +859,16 @@ class Godot private constructor(val context: Context) {
|
||||
/**
|
||||
* Invoked on the render thread when the engine is about to terminate.
|
||||
*/
|
||||
@Keep
|
||||
private fun onGodotTerminating() {
|
||||
internal fun onGodotTerminating() {
|
||||
Log.v(TAG, "OnGodotTerminating")
|
||||
_runStatus.set(RunStatus.TERMINATING)
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
plugin.onGodotTerminating()
|
||||
}
|
||||
runOnTerminate.get()?.run()
|
||||
}
|
||||
|
||||
private fun restart() {
|
||||
primaryHost?.onGodotRestartRequested(this)
|
||||
}
|
||||
|
||||
fun alert(
|
||||
@StringRes messageResId: Int,
|
||||
@@ -896,7 +880,6 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@Keep
|
||||
fun alert(message: String, title: String, okCallback: Runnable? = null) {
|
||||
val activity = getActivity() ?: return
|
||||
runOnHostThread {
|
||||
@@ -904,7 +887,7 @@ class Godot private constructor(val context: Context) {
|
||||
builder.setMessage(message).setTitle(title)
|
||||
builder.setPositiveButton(
|
||||
R.string.dialog_ok
|
||||
) { dialog: DialogInterface, id: Int ->
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
okCallback?.run()
|
||||
dialog.cancel()
|
||||
}
|
||||
@@ -929,43 +912,25 @@ class Godot private constructor(val context: Context) {
|
||||
primaryHost?.runOnHostThread(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the call is being made on the Ui thread.
|
||||
/**
|
||||
* Returns the native rendering driver.
|
||||
*/
|
||||
private fun isOnUiThread() = Looper.myLooper() == Looper.getMainLooper()
|
||||
private fun getNativeRenderer(): String {
|
||||
val rendererInfo = GodotLib.getRendererInfo(meetsVulkanRequirements(context.packageManager))
|
||||
val renderingDriverChosen = rendererInfo[0]
|
||||
val renderingDriverOriginal = rendererInfo[1]
|
||||
val renderingMethod = rendererInfo[2]
|
||||
val renderingDriverSource = rendererInfo[3]
|
||||
val renderingMethodSource = rendererInfo[4]
|
||||
Log.d(TAG, """renderingDevice: $renderingDriverChosen (${renderingDriverSource})
|
||||
renderer: $renderingMethod (${renderingMethodSource})""")
|
||||
|
||||
/**
|
||||
* Returns true if `Vulkan` is used for rendering.
|
||||
*/
|
||||
private fun usesVulkan(): Boolean {
|
||||
val rendererInfo = GodotLib.getRendererInfo()
|
||||
var renderingDeviceSource = "ProjectSettings"
|
||||
var renderingDevice = rendererInfo[0]
|
||||
var rendererSource = "ProjectSettings"
|
||||
var renderer = rendererInfo[1]
|
||||
val cmdline = commandLine
|
||||
var index = cmdline.indexOf("--rendering-method")
|
||||
if (index > -1 && cmdline.size > index + 1) {
|
||||
rendererSource = "CommandLine"
|
||||
renderer = cmdline.get(index + 1)
|
||||
if (renderingDriverOriginal == "vulkan" && renderingDriverChosen == "") {
|
||||
// Throw the exception for the case where Vulkan failed to create and no fallback was available.
|
||||
throw IllegalStateException(context.getString(R.string.error_missing_vulkan_requirements_message))
|
||||
}
|
||||
index = cmdline.indexOf("--rendering-driver")
|
||||
if (index > -1 && cmdline.size > index + 1) {
|
||||
renderingDeviceSource = "CommandLine"
|
||||
renderingDevice = cmdline.get(index + 1)
|
||||
}
|
||||
val result = ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice
|
||||
Log.d(TAG, """usesVulkan(): ${result}
|
||||
renderingDevice: ${renderingDevice} (${renderingDeviceSource})
|
||||
renderer: ${renderer} (${rendererSource})""")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if can fallback to OpenGL.
|
||||
*/
|
||||
private fun canFallbackToOpenGL(): Boolean {
|
||||
return java.lang.Boolean.parseBoolean(GodotLib.getGlobal("rendering/rendering_device/fallback_to_opengl3"))
|
||||
return renderingDriverChosen
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -976,15 +941,15 @@ class Godot private constructor(val context: Context) {
|
||||
return false
|
||||
}
|
||||
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) {
|
||||
// Optional requirements.. log as warning if missing
|
||||
// Optional requirements… log as warning if missing
|
||||
Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1")
|
||||
}
|
||||
|
||||
// Check for api version 1.0
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003)
|
||||
// Check for api version 1.1
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x401000)
|
||||
}
|
||||
|
||||
private fun setKeepScreenOn(enabled: Boolean) {
|
||||
internal fun setKeepScreenOn(enabled: Boolean) {
|
||||
runOnHostThread {
|
||||
if (enabled) {
|
||||
getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
@@ -994,22 +959,6 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if dark mode is supported, false otherwise.
|
||||
*/
|
||||
@Keep
|
||||
private fun isDarkModeSupported(): Boolean {
|
||||
return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if dark mode is supported and enabled, false otherwise.
|
||||
*/
|
||||
@Keep
|
||||
private fun isDarkMode(): Boolean {
|
||||
return darkMode
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun hasClipboard(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||
@@ -1039,51 +988,6 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FilePicker.showFilePicker(context, getActivity(), currentDirectory, filename, fileMode, filters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with multiple buttons.
|
||||
*
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
@Keep
|
||||
private fun showDialog(title: String, message: String, buttons: Array<String>) {
|
||||
getActivity()?.let { DialogUtils.showDialog(it, title, message, buttons) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with a text input field, allowing the user to input text.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
@Keep
|
||||
private fun showInputDialog(title: String, message: String, existingText: String) {
|
||||
getActivity()?.let { DialogUtils.showInputDialog(it, title, message, existingText) }
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun getAccentColor(): Int {
|
||||
val value = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.colorAccent, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun getBaseColor(): Int {
|
||||
val value = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.colorBackground, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the Godot Engine and kill the process it's running in.
|
||||
*/
|
||||
@@ -1107,8 +1011,7 @@ class Godot private constructor(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun forceQuit(instanceId: Int): Boolean {
|
||||
internal fun forceQuit(instanceId: Int): Boolean {
|
||||
primaryHost?.let {
|
||||
if (instanceId == 0) {
|
||||
it.onGodotForceQuit(this)
|
||||
@@ -1116,7 +1019,8 @@ class Godot private constructor(val context: Context) {
|
||||
} else {
|
||||
return it.onGodotForceQuit(instanceId)
|
||||
}
|
||||
} ?: return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onBackPressed() {
|
||||
@@ -1126,50 +1030,6 @@ class Godot private constructor(val context: Context) {
|
||||
runOnRenderThread { GodotLib.back() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the native code (java_godot_wrapper.h) to vibrate the device.
|
||||
* @param durationMs
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@Keep
|
||||
private fun vibrate(durationMs: Int, amplitude: Int) {
|
||||
if (durationMs > 0 && requestPermission("VIBRATE")) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (amplitude <= -1) {
|
||||
vibratorService?.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
durationMs.toLong(),
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
} else {
|
||||
vibratorService?.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
durationMs.toLong(),
|
||||
amplitude
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// deprecated in API 26
|
||||
vibratorService?.vibrate(durationMs.toLong())
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "SecurityException: VIBRATE permission not found. Make sure it is declared in the manifest or enabled in the export preset.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
|
||||
* @return The input fallback mapping for the current XR mode.
|
||||
*/
|
||||
@Keep
|
||||
private fun getInputFallbackMapping(): String? {
|
||||
return xrMode.inputFallbackMapping
|
||||
}
|
||||
|
||||
fun requestPermission(name: String?): Boolean {
|
||||
val activity = getActivity() ?: return false
|
||||
return requestPermission(name, activity)
|
||||
@@ -1202,182 +1062,4 @@ class Godot private constructor(val context: Context) {
|
||||
fun hasFeature(feature: String): Boolean {
|
||||
return GodotLib.hasFeature(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used to query whether the host or the registered plugins supports a given feature.
|
||||
*
|
||||
* This is invoked by the native code, and should not be confused with [hasFeature] which is the Android version of
|
||||
* https://docs.godotengine.org/en/stable/classes/class_os.html#class-os-method-has-feature
|
||||
*/
|
||||
@Keep
|
||||
private fun checkInternalFeatureSupport(feature: String): Boolean {
|
||||
if (primaryHost?.supportsFeature(feature) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
if (plugin.supportsFeature(feature)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of gdextension modules to register.
|
||||
*/
|
||||
@Keep
|
||||
private fun getGDExtensionConfigFiles(): Array<String> {
|
||||
val configFiles = mutableSetOf<String>()
|
||||
for (plugin in pluginRegistry.allPlugins) {
|
||||
configFiles.addAll(plugin.pluginGDExtensionLibrariesPaths)
|
||||
}
|
||||
|
||||
return configFiles.toTypedArray()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun getCACertificates(): String {
|
||||
return GodotNetUtils.getCACertificates()
|
||||
}
|
||||
|
||||
private fun obbIsCorrupted(f: String, mainPackMd5: String): Boolean {
|
||||
return try {
|
||||
val fis: InputStream = FileInputStream(f)
|
||||
|
||||
// Create MD5 Hash
|
||||
val buffer = ByteArray(16384)
|
||||
val complete = MessageDigest.getInstance("MD5")
|
||||
var numRead: Int
|
||||
do {
|
||||
numRead = fis.read(buffer)
|
||||
if (numRead > 0) {
|
||||
complete.update(buffer, 0, numRead)
|
||||
}
|
||||
} while (numRead != -1)
|
||||
fis.close()
|
||||
val messageDigest = complete.digest()
|
||||
|
||||
// Create Hex String
|
||||
val hexString = StringBuilder()
|
||||
for (b in messageDigest) {
|
||||
var s = Integer.toHexString(0xFF and b.toInt())
|
||||
if (s.length == 1) {
|
||||
s = "0$s"
|
||||
}
|
||||
hexString.append(s)
|
||||
}
|
||||
val md5str = hexString.toString()
|
||||
md5str != mainPackMd5
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.printStackTrace()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun initInputDevices() {
|
||||
godotInputHandler.initInputDevices()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun createNewGodotInstance(args: Array<String>): Int {
|
||||
return primaryHost?.onNewGodotInstanceRequested(args) ?: -1
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBeginBenchmarkMeasure(scope: String, label: String) {
|
||||
beginBenchmarkMeasure(scope, label)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeEndBenchmarkMeasure(scope: String, label: String) {
|
||||
endBenchmarkMeasure(scope, label)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeDumpBenchmark(benchmarkFile: String) {
|
||||
dumpBenchmark(fileAccessHandler, benchmarkFile)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeSignApk(inputPath: String,
|
||||
outputPath: String,
|
||||
keystorePath: String,
|
||||
keystoreUser: String,
|
||||
keystorePassword: String): Int {
|
||||
val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE
|
||||
return signResult.toNativeValue()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeVerifyApk(apkPath: String): Int {
|
||||
val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
|
||||
return verifyResult.toNativeValue()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
|
||||
primaryHost?.onEditorWorkspaceSelected(workspace)
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBuildEnvConnect(callback: GodotCallable): Boolean {
|
||||
try {
|
||||
val buildProvider = primaryHost?.getBuildProvider()
|
||||
return buildProvider?.buildEnvConnect(callback) ?: false
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to connect to build environment", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBuildEnvDisconnect() {
|
||||
try {
|
||||
val buildProvider = primaryHost?.getBuildProvider()
|
||||
buildProvider?.buildEnvDisconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to disconnect from build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBuildEnvExecute(buildTool: String, arguments: Array<String>, projectPath: String, buildDir: String, outputCallback: GodotCallable, resultCallback: GodotCallable): Int {
|
||||
try {
|
||||
val buildProvider = primaryHost?.getBuildProvider()
|
||||
return buildProvider?.buildEnvExecute(
|
||||
buildTool,
|
||||
arguments,
|
||||
projectPath,
|
||||
buildDir,
|
||||
outputCallback,
|
||||
resultCallback
|
||||
) ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to execute Gradle command in build environment", e);
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBuildEnvCancel(jobId: Int) {
|
||||
try {
|
||||
val buildProvider = primaryHost?.getBuildProvider()
|
||||
buildProvider?.buildEnvCancel(jobId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to cancel command in build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeBuildEnvCleanProject(projectPath: String, buildDir: String, callback: GodotCallable) {
|
||||
try {
|
||||
val buildProvider = primaryHost?.getBuildProvider()
|
||||
buildProvider?.buildEnvCleanProject(projectPath, buildDir, callback)
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to clean project in build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,25 +31,34 @@
|
||||
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.activity.addCallback
|
||||
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
|
||||
* This is also a reference implementation for how to set up and use the [GodotFragment] fragment
|
||||
* within an Android app.
|
||||
*/
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPictureProvider {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotActivity::class.java.simpleName
|
||||
@@ -62,10 +71,22 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
|
||||
// This window must not match those in BaseGodotEditor.RUN_GAME_INFO etc
|
||||
@JvmStatic
|
||||
private final val DEFAULT_WINDOW_ID = 664;
|
||||
private 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>()
|
||||
|
||||
// The bounds of what the aspect ratio can be are between 2.39:1 and 1:2.39 (inclusive).
|
||||
// If aspect ratio does not fall between these values, app will crash.
|
||||
private val minPiPRatio = Rational(100, 239)
|
||||
private val maxPiPRatio = Rational(239, 100)
|
||||
|
||||
/**
|
||||
* Interaction with the [Godot] object is delegated to the [GodotFragment] class.
|
||||
*/
|
||||
@@ -119,6 +140,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
|
||||
setContentView(getGodotAppLayout())
|
||||
|
||||
// Register `OnBackPressedCallback` for the Godot fragment.
|
||||
onBackPressedDispatcher.addCallback { godotFragment?.onBackPressed() }
|
||||
|
||||
handleStartIntent(intent, true)
|
||||
|
||||
val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container)
|
||||
@@ -139,6 +163,13 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
.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 {
|
||||
@@ -149,7 +180,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
|
||||
triggerRebirth(null, intent)
|
||||
// fake 'process' id returned by create_instance() etc
|
||||
return DEFAULT_WINDOW_ID;
|
||||
return DEFAULT_WINDOW_ID
|
||||
}
|
||||
|
||||
protected fun triggerRebirth(bundle: Bundle?, intent: Intent) {
|
||||
@@ -167,6 +198,15 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
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) }
|
||||
}
|
||||
@@ -196,13 +236,31 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
||||
@CallSuper
|
||||
protected open fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
|
||||
if (!newLaunch) {
|
||||
val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
|
||||
if (newLaunchRequested) {
|
||||
@@ -235,10 +293,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
godotFragment?.onBackPressed() ?: super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun getActivity(): Activity? {
|
||||
return this
|
||||
}
|
||||
@@ -256,4 +310,65 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
|
||||
|
||||
@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
|
||||
|
||||
fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) {
|
||||
val fixedAspectRatio = aspectRatio?.let {
|
||||
if (it < minPiPRatio || it > maxPiPRatio) {
|
||||
Log.w(TAG, "The bounds of the aspect ratio must be between 2.39:1 and 1:2.39 (inclusive). Coercing to valid range.")
|
||||
it.coerceIn(minPiPRatio, maxPiPRatio)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
if (isPiPModeSupported()) {
|
||||
autoEnterPiP.set(enableAutoEnter)
|
||||
pipAspectRatio.set(fixedAspectRatio)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setSourceRectHint(gameViewSourceRectHint)
|
||||
.setAspectRatio(fixedAspectRatio)
|
||||
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(fixedAspectRatio)
|
||||
}
|
||||
setPictureInPictureParams(builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enterPiPMode() {
|
||||
if (isPiPModeSupported()) {
|
||||
updatePiPParams()
|
||||
|
||||
Log.v(TAG, "Entering PiP mode")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (autoEnterPiP.get()) {
|
||||
enterPiPMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
/**************************************************************************/
|
||||
/* 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();
|
||||
}
|
||||
}
|
||||
@@ -34,80 +34,38 @@ 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 {
|
||||
public class GodotFragment extends Fragment implements 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;
|
||||
@@ -155,12 +113,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
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");
|
||||
@@ -186,57 +138,17 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
if (godotContainerLayout == null) {
|
||||
throw new IllegalStateException("Unable to initialize engine render view");
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
} catch (Exception 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);
|
||||
@@ -259,53 +171,24 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -313,100 +196,6 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
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() {
|
||||
@@ -496,6 +285,13 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDistractionFreeModeChanged(Boolean enabled) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onDistractionFreeModeChanged(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BuildProvider getBuildProvider() {
|
||||
if (parentHost != null) {
|
||||
|
||||
@@ -153,6 +153,11 @@ public interface GodotHost {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,7 @@ 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.nativeapi.GodotNativeBridge;
|
||||
import org.godotengine.godot.tts.GodotTTS;
|
||||
import org.godotengine.godot.utils.GodotNetUtils;
|
||||
import org.godotengine.godot.variant.Callable;
|
||||
@@ -56,13 +57,13 @@ public class GodotLib {
|
||||
* Invoked on the main thread to initialize Godot native layer.
|
||||
*/
|
||||
public static native boolean initialize(
|
||||
Godot p_instance,
|
||||
AssetManager p_asset_manager,
|
||||
GodotNativeBridge nativeBridge,
|
||||
AssetManager assetManager,
|
||||
GodotIO godotIO,
|
||||
GodotNetUtils netUtils,
|
||||
DirectoryAccessHandler directoryAccessHandler,
|
||||
FileAccessHandler fileAccessHandler,
|
||||
boolean use_apk_expansion);
|
||||
boolean useApkExpansion);
|
||||
|
||||
/**
|
||||
* Invoked on the main thread to clean up Godot native layer.
|
||||
@@ -192,11 +193,14 @@ public class GodotLib {
|
||||
/**
|
||||
* Used to get info about the current rendering system.
|
||||
*
|
||||
* @return A String array with two elements:
|
||||
* [0] Rendering driver name.
|
||||
* [1] Rendering method.
|
||||
* @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();
|
||||
public static native String[] getRendererInfo(boolean p_vulkan_requirements_met);
|
||||
|
||||
/**
|
||||
* Used to access Godot's editor settings.
|
||||
@@ -299,7 +303,7 @@ public class GodotLib {
|
||||
* Invoked when the screen orientation changes.
|
||||
* @param orientation the new screen orientation
|
||||
*/
|
||||
static native void onScreenRotationChange(int orientation);
|
||||
static native void onOrientationChange(int orientation);
|
||||
|
||||
/**
|
||||
* @return true if input must be dispatched from the render thread. If false, input is
|
||||
@@ -317,4 +321,6 @@ public class GodotLib {
|
||||
static native boolean isProjectManagerHint();
|
||||
|
||||
static native boolean hasFeature(String feature);
|
||||
|
||||
static native void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
|
||||
}
|
||||
|
||||
+3
@@ -38,4 +38,7 @@ package org.godotengine.godot.editor.utils
|
||||
object EditorUtils {
|
||||
@JvmStatic
|
||||
external fun runScene(scene: String, sceneArgs: Array<String>)
|
||||
|
||||
@JvmStatic
|
||||
external fun toggleTitleBar(visible: Boolean)
|
||||
}
|
||||
|
||||
+8
-26
@@ -1,5 +1,5 @@
|
||||
/**************************************************************************/
|
||||
/* GodotDownloaderAlarmReceiver.java */
|
||||
/* PictureInPictureProvider.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
@@ -28,32 +28,14 @@
|
||||
/* 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;
|
||||
package org.godotengine.godot.feature
|
||||
|
||||
/**
|
||||
* 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"/>
|
||||
* Provides APIs to enable picture-in-picture.
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
interface PictureInPictureProvider {
|
||||
|
||||
fun enterPiPMode()
|
||||
|
||||
fun isPiPModeSupported(): Boolean
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class GodotEditText extends EditText {
|
||||
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 DisplayServer::VirtualKeyboardType
|
||||
// Enum must be kept up-to-date with DisplayServerEnums::VirtualKeyboardType
|
||||
public enum VirtualKeyboardType {
|
||||
KEYBOARD_TYPE_DEFAULT,
|
||||
KEYBOARD_TYPE_MULTILINE,
|
||||
|
||||
+8
@@ -57,6 +57,11 @@ internal class GodotGestureHandler(private val inputHandler: GodotInputHandler)
|
||||
|
||||
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
|
||||
@@ -80,6 +85,9 @@ internal class GodotGestureHandler(private val inputHandler: GodotInputHandler)
|
||||
override fun onLongPress(event: MotionEvent) {
|
||||
val toolType = GodotInputHandler.getEventToolType(event)
|
||||
if (toolType != MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
if (hapticFeedbackEnabled) {
|
||||
inputHandler.performHapticFeedback()
|
||||
}
|
||||
contextClickRouter(event)
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -47,6 +47,7 @@ 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;
|
||||
@@ -133,6 +134,23 @@ public class GodotInputHandler implements InputManager.InputDeviceListener, Sens
|
||||
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>
|
||||
|
||||
@@ -30,11 +30,10 @@
|
||||
|
||||
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 org.godotengine.godot.Godot
|
||||
import java.io.File
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
@@ -70,6 +69,7 @@ internal enum class StorageScope {
|
||||
class Identifier(context: Context) {
|
||||
|
||||
companion object {
|
||||
internal const val ACCESS_RESOURCES_PREFIX = "res://"
|
||||
internal const val ASSETS_PREFIX = "assets://"
|
||||
internal const val CONTENT_PREFIX = "content://"
|
||||
}
|
||||
@@ -102,6 +102,10 @@ internal enum class StorageScope {
|
||||
return ASSETS
|
||||
}
|
||||
|
||||
if (Godot.isTemplateBuild() && path.startsWith(ACCESS_RESOURCES_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
|
||||
|
||||
@@ -203,8 +203,10 @@ internal abstract class DataAccess {
|
||||
abstract fun write(buffer: ByteBuffer): Boolean
|
||||
|
||||
fun seekFromEnd(positionFromEnd: Long) {
|
||||
val positionFromBeginning = max(0, size() - positionFromEnd)
|
||||
seek(positionFromBeginning)
|
||||
val positionFromBeginning = size() + positionFromEnd
|
||||
if (positionFromBeginning >= 0) {
|
||||
seek(positionFromBeginning)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
|
||||
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
/**************************************************************************/
|
||||
/* GodotNativeBridge.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.nativeapi
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.net.toUri
|
||||
import org.godotengine.godot.Godot
|
||||
import org.godotengine.godot.GodotActivity
|
||||
import org.godotengine.godot.R
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.feature.PictureInPictureProvider
|
||||
import org.godotengine.godot.io.FilePicker
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
import org.godotengine.godot.utils.GodotNetUtils
|
||||
import org.godotengine.godot.utils.beginBenchmarkMeasure
|
||||
import org.godotengine.godot.utils.dumpBenchmark
|
||||
import org.godotengine.godot.utils.endBenchmarkMeasure
|
||||
import org.godotengine.godot.variant.Callable as GodotCallable
|
||||
|
||||
/**
|
||||
* Holds and expose Godot apis to the native layer.
|
||||
*
|
||||
* All the methods in this class are accessed by the native code (java_godot_wrapper.h) and as such are kept private to
|
||||
* not be accessible by the rest of the java/kotlin code.
|
||||
*/
|
||||
@Keep
|
||||
internal class GodotNativeBridge(private val godot: Godot) {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotNativeBridge::class.java.simpleName
|
||||
|
||||
}
|
||||
|
||||
private val vibratorService: Vibrator? by lazy { godot.context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator }
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot setup is complete.
|
||||
*/
|
||||
private fun onGodotSetupCompleted() {
|
||||
godot.onGodotSetupCompleted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot main loop has started.
|
||||
*/
|
||||
private fun onGodotMainLoopStarted() {
|
||||
godot.onGodotMainLoopStarted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the engine is about to terminate.
|
||||
*/
|
||||
private fun onGodotTerminating() = godot.onGodotTerminating()
|
||||
|
||||
/**
|
||||
* Invoked from the render thread to toggle the immersive mode.
|
||||
*/
|
||||
private fun nativeEnableImmersiveMode(enabled: Boolean) {
|
||||
godot.runOnHostThread {
|
||||
godot.enableImmersiveMode(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInImmersiveMode() = godot.isInImmersiveMode()
|
||||
|
||||
private fun isInEdgeToEdgeMode() = godot.isInEdgeToEdgeMode()
|
||||
|
||||
private fun setKeepScreenOn(enabled: Boolean) = godot.setKeepScreenOn(enabled)
|
||||
|
||||
private fun restart() { godot.primaryHost?.onGodotRestartRequested(godot) }
|
||||
|
||||
private fun alert(message: String, title: String) {
|
||||
godot.alert(message, title)
|
||||
}
|
||||
|
||||
private fun forceQuit(instanceId: Int) = godot.forceQuit(instanceId)
|
||||
|
||||
/**
|
||||
* Returns true if dark mode is supported, false otherwise.
|
||||
*/
|
||||
private fun isDarkModeSupported(): Boolean {
|
||||
return godot.context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if dark mode is supported and enabled, false otherwise.
|
||||
*/
|
||||
private fun isDarkMode() = godot.darkMode
|
||||
|
||||
private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
|
||||
FilePicker.showFilePicker(godot.context, godot.getActivity(), currentDirectory, filename, fileMode, filters)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with multiple buttons.
|
||||
*
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
private fun showDialog(title: String, message: String, buttons: Array<String>) {
|
||||
godot.getActivity()?.let { DialogUtils.showDialog(it, title, message, buttons) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This method shows a dialog with a text input field, allowing the user to input text.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
private fun showInputDialog(title: String, message: String, existingText: String) {
|
||||
godot.getActivity()?.let { DialogUtils.showInputDialog(it, title, message, existingText) }
|
||||
}
|
||||
|
||||
private fun getAccentColor(): Int {
|
||||
val value = TypedValue()
|
||||
godot.context.theme.resolveAttribute(android.R.attr.colorAccent, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
private fun getBaseColor(): Int {
|
||||
val value = TypedValue()
|
||||
godot.context.theme.resolveAttribute(android.R.attr.colorBackground, value, true)
|
||||
return value.data
|
||||
}
|
||||
|
||||
private fun requestPermission(name: String?) = godot.requestPermission(name)
|
||||
|
||||
private fun requestPermissions() = godot.requestPermissions()
|
||||
|
||||
private fun getGrantedPermissions() = godot.getGrantedPermissions()
|
||||
|
||||
/**
|
||||
* Used by the native code (java_godot_wrapper.h) to vibrate the device.
|
||||
* @param durationMs
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun vibrate(durationMs: Int, amplitude: Int) {
|
||||
if (durationMs > 0 && godot.requestPermission("VIBRATE")) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (amplitude <= -1) {
|
||||
vibratorService?.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
durationMs.toLong(),
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
} else {
|
||||
vibratorService?.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
durationMs.toLong(),
|
||||
amplitude
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// deprecated in API 26
|
||||
vibratorService?.vibrate(durationMs.toLong())
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"SecurityException: VIBRATE permission not found. Make sure it is declared in the manifest or enabled in the export preset."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used to query whether the host or the registered plugins supports a given feature.
|
||||
*
|
||||
* This is invoked by the native code, and should not be confused with [hasFeature] which is the Android version of
|
||||
* https://docs.godotengine.org/en/stable/classes/class_os.html#class-os-method-has-feature
|
||||
*/
|
||||
private fun checkInternalFeatureSupport(feature: String): Boolean {
|
||||
if (godot.primaryHost?.supportsFeature(feature) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (plugin in godot.pluginRegistry.allPlugins) {
|
||||
if (plugin.supportsFeature(feature)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of gdextension modules to register.
|
||||
*/
|
||||
private fun getGDExtensionConfigFiles(): Array<String> {
|
||||
val configFiles = mutableSetOf<String>()
|
||||
for (plugin in godot.pluginRegistry.allPlugins) {
|
||||
configFiles.addAll(plugin.pluginGDExtensionLibrariesPaths)
|
||||
}
|
||||
|
||||
return configFiles.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getCACertificates(): String {
|
||||
return GodotNetUtils.getCACertificates()
|
||||
}
|
||||
|
||||
private fun getActivity() = godot.getActivity()
|
||||
|
||||
private fun getRenderView() = godot.renderView
|
||||
|
||||
private fun getClipboard() = godot.getClipboard()
|
||||
|
||||
private fun setClipboard(text: String) = godot.setClipboard(text)
|
||||
|
||||
private fun hasClipboard() = godot.hasClipboard()
|
||||
|
||||
private fun setWindowColor(color: String) = godot.setWindowColor(color)
|
||||
|
||||
/**
|
||||
* Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
|
||||
* @return The input fallback mapping for the current XR mode.
|
||||
*/
|
||||
private fun getInputFallbackMapping(): String? {
|
||||
return godot.xrMode.inputFallbackMapping
|
||||
}
|
||||
|
||||
private fun initInputDevices() {
|
||||
godot.godotInputHandler.initInputDevices()
|
||||
}
|
||||
|
||||
private fun createNewGodotInstance(args: Array<String>): Int {
|
||||
return godot.primaryHost?.onNewGodotInstanceRequested(args) ?: -1
|
||||
}
|
||||
|
||||
private fun nativeBeginBenchmarkMeasure(scope: String, label: String) {
|
||||
beginBenchmarkMeasure(scope, label)
|
||||
}
|
||||
|
||||
private fun nativeEndBenchmarkMeasure(scope: String, label: String) {
|
||||
endBenchmarkMeasure(scope, label)
|
||||
}
|
||||
|
||||
private fun nativeDumpBenchmark(benchmarkFile: String) {
|
||||
dumpBenchmark(godot.fileAccessHandler, benchmarkFile)
|
||||
}
|
||||
|
||||
private fun nativeSignApk(
|
||||
inputPath: String,
|
||||
outputPath: String,
|
||||
keystorePath: String,
|
||||
keystoreUser: String,
|
||||
keystorePassword: String
|
||||
): Int {
|
||||
val signResult = godot.primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword)
|
||||
?: org.godotengine.godot.error.Error.ERR_UNAVAILABLE
|
||||
return signResult.toNativeValue()
|
||||
}
|
||||
|
||||
private fun nativeVerifyApk(apkPath: String): Int {
|
||||
val verifyResult = godot.primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
|
||||
return verifyResult.toNativeValue()
|
||||
}
|
||||
|
||||
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
|
||||
godot.primaryHost?.onEditorWorkspaceSelected(workspace)
|
||||
}
|
||||
|
||||
private fun nativeOnDistractionFreeModeChanged(enabled: Boolean) {
|
||||
godot.primaryHost?.onDistractionFreeModeChanged(enabled)
|
||||
}
|
||||
|
||||
private fun nativeBuildEnvConnect(callback: GodotCallable): Boolean {
|
||||
try {
|
||||
val buildProvider = godot.primaryHost?.buildProvider
|
||||
val success = buildProvider?.buildEnvConnect(callback) ?: false
|
||||
if (!success) {
|
||||
val activity = godot.getActivity() ?: return false
|
||||
godot.runOnHostThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setMessage(activity.getString(R.string.gabe_connection_error_message))
|
||||
.setTitle(activity.getString(R.string.gabe_connection_error_title))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(activity.getString(R.string.dialog_download)) { dialog: DialogInterface, _: Int ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, "https://godotengine.org/download/android#gabe".toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
activity.startActivity(intent)
|
||||
dialog.cancel()
|
||||
}
|
||||
.setNegativeButton(activity.getString(R.string.dialog_cancel)) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
return success
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to connect to build environment", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeBuildEnvDisconnect() {
|
||||
try {
|
||||
val buildProvider = godot.primaryHost?.buildProvider
|
||||
buildProvider?.buildEnvDisconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to disconnect from build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeBuildEnvExecute(
|
||||
buildTool: String,
|
||||
arguments: Array<String>,
|
||||
projectPath: String,
|
||||
buildDir: String,
|
||||
outputCallback: GodotCallable,
|
||||
resultCallback: GodotCallable
|
||||
): Int {
|
||||
try {
|
||||
val buildProvider = godot.primaryHost?.buildProvider
|
||||
return buildProvider?.buildEnvExecute(
|
||||
buildTool,
|
||||
arguments,
|
||||
projectPath,
|
||||
buildDir,
|
||||
outputCallback,
|
||||
resultCallback
|
||||
) ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to execute Gradle command in build environment", e);
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeBuildEnvCancel(jobId: Int) {
|
||||
try {
|
||||
val buildProvider = godot.primaryHost?.buildProvider
|
||||
buildProvider?.buildEnvCancel(jobId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to cancel command in build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeBuildEnvCleanProject(projectPath: String, buildDir: String, callback: GodotCallable) {
|
||||
try {
|
||||
val buildProvider = godot.primaryHost?.buildProvider
|
||||
buildProvider?.buildEnvCleanProject(projectPath, buildDir, callback)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clean project in build environment", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeIsPiPModeSupported(): Boolean {
|
||||
val hostActivity = godot.getActivity()
|
||||
if (hostActivity is PictureInPictureProvider) {
|
||||
return hostActivity.isPiPModeSupported()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun nativeIsInPiPMode(): Boolean {
|
||||
val hostActivity = godot.getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
return hostActivity.isInPictureInPictureMode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun nativeEnterPiPMode() {
|
||||
val hostActivity = godot.getActivity()
|
||||
if (hostActivity is PictureInPictureProvider) {
|
||||
godot.runOnHostThread {
|
||||
hostActivity.enterPiPMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeSetPiPModeAspectRatio(numerator: Int, denominator: Int) {
|
||||
val hostActivity = godot.getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
godot.runOnHostThread {
|
||||
hostActivity.updatePiPParams(aspectRatio = Rational(numerator, denominator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeSetAutoEnterPiPModeOnBackground(autoEnterPiPOnBackground: Boolean) {
|
||||
val hostActivity = godot.getActivity()
|
||||
if (hostActivity is GodotActivity) {
|
||||
godot.runOnHostThread {
|
||||
hostActivity.updatePiPParams(enableAutoEnter = autoEnterPiPOnBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
-1
@@ -32,10 +32,13 @@ 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.
|
||||
@@ -43,7 +46,76 @@ import org.godotengine.godot.variant.Callable
|
||||
* @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) {
|
||||
private val TAG = AndroidRuntimePlugin::class.java.simpleName
|
||||
|
||||
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 ?: emptyArray()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.hashCode()
|
||||
|
||||
// Invocation for the remaining interface(s) methods falls here and is dispatched to the
|
||||
// Godot Object.
|
||||
else -> Callable.call(godotObjectID, methodName, *(args ?: emptyArray()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPluginName() = "AndroidRuntime"
|
||||
|
||||
|
||||
+36
-1
@@ -206,6 +206,16 @@ public abstract class GodotPlugin {
|
||||
*/
|
||||
public void onMainPause() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onStop()
|
||||
*/
|
||||
public void onMainStop() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onStart()
|
||||
*/
|
||||
public void onMainStart() {}
|
||||
|
||||
/**
|
||||
* @see Activity#onResume()
|
||||
*/
|
||||
@@ -235,6 +245,11 @@ public abstract class GodotPlugin {
|
||||
*/
|
||||
public void onGodotMainLoopStarted() {}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when the Godot engine is terminating.
|
||||
*/
|
||||
public void onGodotTerminating() {}
|
||||
|
||||
/**
|
||||
* When using the OpenGL renderer, this is invoked once per frame on the GL thread after the
|
||||
* frame is drawn.
|
||||
@@ -271,6 +286,26 @@ public abstract class GodotPlugin {
|
||||
*/
|
||||
public void onVkSurfaceCreated(Surface surface) {}
|
||||
|
||||
/**
|
||||
* Return the list of command line parameters that should be passed to Godot.
|
||||
* <p>
|
||||
* When invoking this method, the engine passes the original set of command line params it was started with,
|
||||
* which can be used by the plugin to enable / disable its own feature(s).
|
||||
* The given set of command line params is unmodifiable and does not include command line parameters passed by other plugins.
|
||||
*
|
||||
* @param unmodifiableOriginalCommandLineParams Original set of command line params the engine was started with.
|
||||
* @return a new set of command line parameters to be passed to the engine.
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getCommandLineParams(List<String> unmodifiableOriginalCommandLineParams) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPluginName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the plugin.
|
||||
* <p>
|
||||
@@ -414,7 +449,7 @@ public abstract class GodotPlugin {
|
||||
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());
|
||||
"Invalid type for argument #" + i + ". Should be of type '" + signalParamTypes[i].getName() + "' but is of type '" + signalArg.getClass().getName() + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ import java.util.Set;
|
||||
*/
|
||||
@Keep
|
||||
public class GodotTTS extends UtteranceProgressListener implements TextToSpeech.OnInitListener {
|
||||
// Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display/display_server.h".
|
||||
// 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;
|
||||
|
||||
+3
-3
@@ -64,7 +64,7 @@ private val benchmarkTracker = Collections.synchronizedMap(LinkedHashMap<Pair<St
|
||||
* Note: Only enabled on 'editorDev' build variant.
|
||||
*/
|
||||
fun beginBenchmarkMeasure(scope: String, label: String) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
val key = Pair(scope, label)
|
||||
@@ -84,7 +84,7 @@ fun beginBenchmarkMeasure(scope: String, label: String) {
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
val key = Pair(scope, label)
|
||||
@@ -109,7 +109,7 @@ fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = f
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
|
||||
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "debug") {
|
||||
return
|
||||
}
|
||||
if (!useBenchmark || benchmarkTracker.isEmpty()) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 102 B |
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -1,165 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/downloaderDashboard"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressAsFraction"
|
||||
style="@android:style/TextAppearance.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="5dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressAsPercentage"
|
||||
style="@android:style/TextAppearance.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignEnd="@+id/progressBar" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/progressAsFraction"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressAverageSpeed"
|
||||
style="@android:style/TextAppearance.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/progressBar"
|
||||
android:layout_marginStart="5dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressTimeRemaining"
|
||||
style="@android:style/TextAppearance.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignEnd="@+id/progressBar"
|
||||
android:layout_below="@+id/progressBar" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/downloadButton"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_weight="0"
|
||||
android:minHeight="40dp"
|
||||
android:minWidth="94dp"
|
||||
android:text="@string/text_button_cancel"
|
||||
android:visibility="gone"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_weight="0"
|
||||
android:minHeight="40dp"
|
||||
android:minWidth="94dp"
|
||||
android:text="@string/text_button_pause"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/approveCellular"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:id="@+id/textPausedParagraph1"
|
||||
android:text="@string/text_paused_cellular" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="10dp"
|
||||
android:id="@+id/textPausedParagraph2"
|
||||
android:text="@string/text_paused_cellular_2" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/buttonRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<Button
|
||||
android:id="@+id/resumeOverCellular"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="10dp"
|
||||
android:text="@string/text_button_resume_cellular"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/wifiSettingsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="10dp"
|
||||
android:text="@string/text_button_wifi_settings"
|
||||
style="?android:attr/buttonBarButtonStyle" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 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.
|
||||
*/
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="fill_parent"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="8dp" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/appIcon"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="25dp"
|
||||
android:scaleType="centerInside"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:src="@android:drawable/stat_sys_download"
|
||||
android:contentDescription="@string/godot_project_name_string" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progress_text"
|
||||
style="@style/NotificationText"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:singleLine="true"
|
||||
android:gravity="center" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.0"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/NotificationTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:singleLine="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_remaining"
|
||||
style="@style/NotificationText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:singleLine="true"
|
||||
tools:ignore="RelativeOverlap" />
|
||||
<!-- Only one of progress_bar and paused_text will be visible. -->
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_bar_frame"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true" >
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="25dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
style="@style/NotificationTextShadow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingEnd="25dp"
|
||||
android:singleLine="true" />
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="text_paused_cellular">آیا می خواهید بر روی اتصال داده همراه دانلود را شروع کنید؟ بر اساس نوع سطح داده شما این ممکن است برای شما هزینه مالی داشته باشد.</string>
|
||||
<string name="text_paused_cellular_2">اگر نمی خواهید بر روی اتصال داده همراه دانلود را شروع کنید ، دانلود به صورت خودکار در زمان دسترسی به وای-فای شروع می شود.</string>
|
||||
<string name="text_button_resume_cellular">ادامه دانلود</string>
|
||||
<string name="text_button_wifi_settings">تنظیمات وای-فای</string>
|
||||
<string name="text_verifying_download">درحال تایید دانلود</string>
|
||||
<string name="text_validation_complete">تایید فایل XAPK تکمیل شد. برای خروج تایید کنید.</string>
|
||||
<string name="text_validation_failed">اعتبارسنجی فایل XAPK ناموق.</string>
|
||||
<string name="text_button_pause">توقف دانلود</string>
|
||||
<string name="text_button_resume">ادامه دانلود</string>
|
||||
<string name="text_button_cancel">انصراف</string>
|
||||
<string name="text_button_cancel_verify">انصراف از تایید شدن</string>
|
||||
</resources>
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="text_paused_cellular">모바일 네트워크를 사용하여 다운로드 하시겠습니까? 남은 데이터 사용량에 따라, 요금이 부과될 수 있습니다.</string>
|
||||
<string name="text_paused_cellular_2">모바일 네트워크를 사용하여 다운로드 하지 않을 경우, 와이파이 연결이 가능할 때 자동적으로 다운로드가 이루어집니다.</string>
|
||||
<string name="text_button_resume_cellular">다운로드 계속하기</string>
|
||||
<string name="text_button_wifi_settings">와이파이 설정</string>
|
||||
<string name="text_verifying_download">다운로드 확인중</string>
|
||||
<string name="text_validation_complete">추가 파일 확인이 완료되었습니다. 확인을 눌러 진행하세요.</string>
|
||||
<string name="text_validation_failed">추가 파일 확인에 실패하였습니다.</string>
|
||||
<string name="text_button_pause">다운로드 일시정지</string>
|
||||
<string name="text_button_resume">다운로드 계속하기</string>
|
||||
<string name="text_button_cancel">취소</string>
|
||||
<string name="text_button_cancel_verify">파일 확인 취소</string>
|
||||
|
||||
<!-- APK Expansion Strings -->
|
||||
|
||||
<!-- When a download completes, a notification is displayed, and this
|
||||
string is used to indicate that the download successfully completed.
|
||||
Note that such a download could have been initiated by a variety of
|
||||
applications, including (but not limited to) the browser, an email
|
||||
application, a content marketplace. -->
|
||||
<string name="notification_download_complete">다운로드 완료</string>
|
||||
|
||||
<!-- When a download completes, a notification is displayed, and this
|
||||
string is used to indicate that the download failed.
|
||||
Note that such a download could have been initiated by a variety of
|
||||
applications, including (but not limited to) the browser, an email
|
||||
application, a content marketplace. -->
|
||||
<string name="notification_download_failed">다운로드 실패</string>
|
||||
|
||||
|
||||
<string name="state_unknown">시작중…</string>
|
||||
<string name="state_idle">다운로드 시작을 기다리는 중</string>
|
||||
<string name="state_fetching_url">다운로드할 항목을 찾는 중</string>
|
||||
<string name="state_connecting">다운로드 서버에 연결 중</string>
|
||||
<string name="state_downloading">다운로드 중</string>
|
||||
<string name="state_completed">다운로드 종료</string>
|
||||
<string name="state_paused_network_unavailable">와이파이를 찾을 수 없어 다운로드가 일시정지 되었습니다.</string>
|
||||
<string name="state_paused_network_setup_failure">다운로드가 일시정지 되었습니다. 네트워크 연결 상태를 확인하세요.</string>
|
||||
<string name="state_paused_by_request">다운로드 일시정지</string>
|
||||
<string name="state_paused_wifi_unavailable">와이파이가 사용하능하지 않아 다운로드가 일시정지 되었습니다.</string>
|
||||
<string name="state_paused_wifi_disabled">와이파이가 비활성화 되어 다운로드가 일시정지 되었습니다.</string>
|
||||
<string name="state_paused_roaming">로밍 상태이어서 다운로드가 일시정지 되었습니다.</string>
|
||||
<string name="state_paused_sdcard_unavailable">외부 저장소를 사용할 수 없어 다운로드가 일시정지 되었습니다.</string>
|
||||
<string name="state_failed_unlicensed">이 앱을 구매하지 않아 다운로드가 정지 되었습니다.</string>
|
||||
<string name="state_failed_fetching_url">다운로드 항목을 찾을 수 없어 다운로드가 정지 되었습니다.</string>
|
||||
<string name="state_failed_sdcard_full">외부 저장소가 가득차서 다운로드가 실패하였습니다.</string>
|
||||
<string name="state_failed_cancelled">다운로드 취소</string>
|
||||
<string name="state_failed">다운로드 실패</string>
|
||||
|
||||
<string name="kilobytes_per_second">%1$s KB/s</string>
|
||||
<string name="time_remaining">남은 시간: %1$s</string>
|
||||
<string name="time_remaining_notification">%1$s 남음</string>
|
||||
</resources>
|
||||
@@ -1,61 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name</string>
|
||||
<string name="text_paused_cellular">Would you like to enable downloading over cellular connections? Depending on your data plan, this may cost you money.</string>
|
||||
<string name="text_paused_cellular_2">If you choose not to enable downloading over cellular connections, the download will automatically resume when wi-fi is available.</string>
|
||||
<string name="text_button_resume_cellular">Resume download</string>
|
||||
<string name="text_button_wifi_settings">Wi-Fi settings</string>
|
||||
<string name="text_verifying_download">Verifying Download</string>
|
||||
<string name="text_validation_complete">XAPK File Validation Complete. Select OK to exit.</string>
|
||||
<string name="text_validation_failed">XAPK File Validation Failed.</string>
|
||||
<string name="text_button_pause">Pause Download</string>
|
||||
<string name="text_button_resume">Resume Download</string>
|
||||
<string name="text_button_cancel">Cancel</string>
|
||||
<string name="text_button_cancel_verify">Cancel Verification</string>
|
||||
<string name="text_error_title">Error!</string>
|
||||
<string name="error_engine_setup_message">Unable to setup the Godot Engine! Aborting…</string>
|
||||
<string name="error_engine_setup_message">Unable to set up the Godot Engine! Aborting…</string>
|
||||
<string name="error_missing_vulkan_requirements_message">Warning - this device does not meet the requirements for Vulkan support</string>
|
||||
|
||||
<!-- APK Expansion Strings -->
|
||||
|
||||
<!-- When a download completes, a notification is displayed, and this
|
||||
string is used to indicate that the download successfully completed.
|
||||
Note that such a download could have been initiated by a variety of
|
||||
applications, including (but not limited to) the browser, an email
|
||||
application, a content marketplace. -->
|
||||
<string name="notification_download_complete">Download complete</string>
|
||||
|
||||
<!-- When a download completes, a notification is displayed, and this
|
||||
string is used to indicate that the download failed.
|
||||
Note that such a download could have been initiated by a variety of
|
||||
applications, including (but not limited to) the browser, an email
|
||||
application, a content marketplace. -->
|
||||
<string name="notification_download_failed">Download unsuccessful</string>
|
||||
|
||||
|
||||
<string name="state_unknown">Starting…</string>
|
||||
<string name="state_idle">Waiting for download to start</string>
|
||||
<string name="state_fetching_url">Looking for resources to download</string>
|
||||
<string name="state_connecting">Connecting to the download server</string>
|
||||
<string name="state_downloading">Downloading resources</string>
|
||||
<string name="state_completed">Download finished</string>
|
||||
<string name="state_paused_network_unavailable">Download paused because no network is available</string>
|
||||
<string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string>
|
||||
<string name="state_paused_by_request">Download paused</string>
|
||||
<string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string>
|
||||
<string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string>
|
||||
<string name="state_paused_roaming">Download paused because you are roaming</string>
|
||||
<string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string>
|
||||
<string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string>
|
||||
<string name="state_failed_fetching_url">Download failed because the resources could not be found</string>
|
||||
<string name="state_failed_sdcard_full">Download failed because the external storage is full</string>
|
||||
<string name="state_failed_cancelled">Download canceled</string>
|
||||
<string name="state_failed">Download failed</string>
|
||||
|
||||
<string name="kilobytes_per_second">%1$s KB/s</string>
|
||||
<string name="time_remaining">Time remaining: %1$s</string>
|
||||
<string name="time_remaining_notification">%1$s left</string>
|
||||
|
||||
<!-- Labels for the dialog action buttons -->
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="dialog_cancel">Cancel</string>
|
||||
<string name="dialog_download">Download</string>
|
||||
<string name="gabe_connection_error_message">Unable to connect to the Godot Android Build Environment (GABE). Please make sure it\'s installed on your device.</string>
|
||||
<string name="gabe_connection_error_title">Connection Failed!</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="NotificationText">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="NotificationTextShadow" parent="NotificationText">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
<item name="android:shadowColor">@android:color/background_dark</item>
|
||||
<item name="android:shadowDx">1.0</item>
|
||||
<item name="android:shadowDy">1.0</item>
|
||||
<item name="android:shadowRadius">1</item>
|
||||
</style>
|
||||
|
||||
<style name="NotificationTitle">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="ButtonBackground">
|
||||
<item name="android:background">@android:color/background_dark</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/**************************************************************************/
|
||||
/* AndroidRuntimePluginTest.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 org.godotengine.godot.variant.Callable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AndroidRuntimePluginTest {
|
||||
|
||||
private val createProxyFromGodotCallable = AndroidRuntimePlugin::class.java
|
||||
.getDeclaredMethod("createProxyFromGodotCallable", String::class.java, Callable::class.java)
|
||||
.apply { isAccessible = true }
|
||||
|
||||
private val createProxyFromGodotObjectID = AndroidRuntimePlugin::class.java
|
||||
.getDeclaredMethod("createProxyFromGodotObjectID", Long::class.javaPrimitiveType, Array<String>::class.java)
|
||||
.apply { isAccessible = true }
|
||||
|
||||
private fun newCallable(): Callable {
|
||||
val ctor = Callable::class.java.getDeclaredConstructor(Long::class.javaPrimitiveType)
|
||||
ctor.isAccessible = true
|
||||
return ctor.newInstance(0L)
|
||||
}
|
||||
|
||||
private fun callableProxy(): Any {
|
||||
val proxy = createProxyFromGodotCallable.invoke(null, Runnable::class.java.name, newCallable())
|
||||
assertNotNull(proxy)
|
||||
return proxy!!
|
||||
}
|
||||
|
||||
private fun objectIDProxy(godotObjectID: Long = 0L): Any {
|
||||
val proxy = createProxyFromGodotObjectID.invoke(null, godotObjectID, arrayOf(Runnable::class.java.name))
|
||||
assertNotNull(proxy)
|
||||
return proxy!!
|
||||
}
|
||||
|
||||
// Regression tests for the infinite recursion in the proxy equals handler.
|
||||
// Kotlin's `proxy == args[0]` compiles to `proxy.equals(args[0])`, which the
|
||||
// JDK proxy dispatches back through the InvocationHandler — the previous
|
||||
// implementation hit the same branch and recursed into StackOverflowError.
|
||||
|
||||
@Test
|
||||
fun `Given Callable proxy, When equals is invoked with itself, Then returns true without recursion`() {
|
||||
val proxy = callableProxy()
|
||||
assertTrue(proxy.equals(proxy))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given Callable proxy, When equals is invoked with another instance, Then returns false without recursion`() {
|
||||
val proxy = callableProxy()
|
||||
val otherProxy = callableProxy()
|
||||
assertFalse(proxy.equals(otherProxy))
|
||||
assertFalse(proxy.equals(Any()))
|
||||
assertFalse(proxy.equals(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given ObjectID proxy, When equals is invoked with itself, Then returns true without recursion`() {
|
||||
val proxy = objectIDProxy()
|
||||
assertTrue(proxy.equals(proxy))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given ObjectID proxy, When equals is invoked with another instance, Then returns false without recursion`() {
|
||||
val proxy = objectIDProxy()
|
||||
val otherProxy = objectIDProxy()
|
||||
assertFalse(proxy.equals(otherProxy))
|
||||
assertFalse(proxy.equals(Any()))
|
||||
assertFalse(proxy.equals(null))
|
||||
}
|
||||
|
||||
// Sanity checks that the proxy's toString / hashCode handlers still operate
|
||||
// as documented and that they do not get caught up in the equals fix.
|
||||
|
||||
@Test
|
||||
fun `Given Callable proxy, When toString is invoked, Then returns the documented label`() {
|
||||
val proxy = callableProxy()
|
||||
assertEquals("Godot Callable Proxy for ${Runnable::class.java.name}", proxy.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given ObjectID proxy, When toString is invoked, Then returns the documented label`() {
|
||||
val proxy = objectIDProxy()
|
||||
assertEquals("Godot Object Proxy for ${Runnable::class.java.name}", proxy.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given Callable proxy, When hashCode is invoked, Then returns the Callable hashCode`() {
|
||||
val callable = newCallable()
|
||||
val proxy = createProxyFromGodotCallable.invoke(null, Runnable::class.java.name, callable)!!
|
||||
assertEquals(callable.hashCode(), proxy.hashCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given ObjectID proxy, When hashCode is invoked, Then returns the ObjectID hashCode`() {
|
||||
val godotObjectID = 0x1_0000_002AL
|
||||
val proxy = objectIDProxy(godotObjectID)
|
||||
assertEquals(godotObjectID.hashCode(), proxy.hashCode())
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(GODOT_ROOT_DIR ../../../..)
|
||||
set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "")
|
||||
set(OPENXR_INCLUDE_DIR "${GODOT_ROOT_DIR}/thirdparty/openxr/include" CACHE STRING "")
|
||||
set(PERFETTO_INCLUDE_DIR "${GODOT_ROOT_DIR}/thirdparty/perfetto" CACHE STRING "")
|
||||
|
||||
# Get sources
|
||||
file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**)
|
||||
@@ -19,7 +20,8 @@ target_include_directories(${PROJECT_NAME}
|
||||
SYSTEM PUBLIC
|
||||
${GODOT_ROOT_DIR}
|
||||
${ANDROID_ROOT_DIR}
|
||||
${OPENXR_INCLUDE_DIR})
|
||||
${OPENXR_INCLUDE_DIR}
|
||||
${PERFETTO_INCLUDE_DIR})
|
||||
|
||||
add_definitions(
|
||||
-DUNIX_ENABLED
|
||||
@@ -29,4 +31,5 @@ add_definitions(
|
||||
-DTOOLS_ENABLED
|
||||
-DDEBUG_ENABLED
|
||||
-DRD_ENABLED
|
||||
-DGODOT_USE_PERFETTO
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user