feat: updated engine

This commit is contained in:
Sara Gerretsen 2026-07-10 17:04:34 +02:00
parent cbe99774ff
commit f4cf6b3999
6607 changed files with 910135 additions and 430025 deletions

View file

@ -82,9 +82,7 @@ elif sys.platform.startswith("win"):
host_subpath = "windows"
if lib_arch_dir != "" and host_subpath != "":
if env.dev_build:
lib_type_dir = "dev"
elif env.debug_features:
if env.debug_features:
if env.editor_build and env["store_release"]:
lib_type_dir = "release"
else:

View file

@ -33,6 +33,8 @@
#include "android_keys_utils.h"
#include "display_server_android.h"
#include "core/input/input.h"
void AndroidInputHandler::process_joy_event(AndroidInputHandler::JoypadEvent p_event) {
switch (p_event.type) {
case JOY_EVENT_BUTTON:
@ -132,7 +134,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod
if (p_physical_keycode == AKEYCODE_BACK && p_pressed) {
if (DisplayServerAndroid *dsa = Object::cast_to<DisplayServerAndroid>(DisplayServer::get_singleton())) {
dsa->send_window_event(DisplayServer::WINDOW_EVENT_GO_BACK_REQUEST, true);
dsa->send_window_event(DisplayServerEnums::WINDOW_EVENT_GO_BACK_REQUEST, true);
}
}
@ -144,7 +146,7 @@ void AndroidInputHandler::_cancel_all_touch() {
touch.clear();
}
void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled, bool p_double_tap) {
void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled) {
if (touch.size()) {
//end all if exist
for (int i = 0; i < touch.size(); i++) {
@ -154,7 +156,7 @@ void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled, bool
ev->set_pressed(p_pressed);
ev->set_canceled(p_canceled);
ev->set_position(touch[i].pos);
ev->set_double_tap(p_double_tap);
ev->set_double_tap(touch[i].double_tap);
Input::get_singleton()->parse_input_event(ev);
}
}
@ -165,7 +167,7 @@ void AndroidInputHandler::_release_all_touch() {
touch.clear();
}
void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points, bool p_double_tap) {
void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points) {
switch (p_event) {
case AMOTION_EVENT_ACTION_DOWN: { //gesture begin
// Release any remaining touches or mouse event
@ -178,10 +180,11 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const
touch.write[i].pos = p_points[i].pos;
touch.write[i].pressure = p_points[i].pressure;
touch.write[i].tilt = p_points[i].tilt;
touch.write[i].double_tap = p_points[i].double_tap;
}
//send touch
_parse_all_touch(true, false, p_double_tap);
_parse_all_touch(true, false);
} break;
case AMOTION_EVENT_ACTION_MOVE: { //motion

View file

@ -30,7 +30,7 @@
#pragma once
#include "core/input/input.h"
#include "core/input/input_event.h"
// This class encapsulates all the handling of input events that come from the Android UI thread.
// Remarks:
@ -43,6 +43,7 @@ public:
Point2 pos;
float pressure = 0;
Vector2 tilt;
bool double_tap = false;
};
struct MouseEventInfo {
@ -90,7 +91,7 @@ private:
void _cancel_mouse_event_info(bool p_source_mouse_relative = false);
void _parse_all_touch(bool p_pressed, bool p_canceled = false, bool p_double_tap = false);
void _parse_all_touch(bool p_pressed, bool p_canceled = false);
void _release_all_touch();
@ -98,7 +99,7 @@ private:
public:
void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative, float p_pressure, Vector2 p_tilt);
void process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points, bool p_double_tap);
void process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points);
void process_magnify(Point2 p_pos, float p_factor);
void process_pan(Point2 p_pos, Vector2 p_delta);
void process_joy_event(JoypadEvent p_event);

View file

@ -34,6 +34,7 @@
#include "jni_singleton.h"
#include "core/config/engine.h"
#include "core/object/class_db.h"
#if !defined(ANDROID_ENABLED)
static JavaClassWrapper *java_class_wrapper = nullptr;
@ -73,6 +74,8 @@ void JavaObject::_bind_methods() {
void JavaClassWrapper::_bind_methods() {
ClassDB::bind_method(D_METHOD("wrap", "name"), &JavaClassWrapper::wrap);
ClassDB::bind_method(D_METHOD("get_exception"), &JavaClassWrapper::get_exception);
ClassDB::bind_method(D_METHOD("create_sam_callback", "sam_interface", "callable"), &JavaClassWrapper::create_sam_callback);
ClassDB::bind_method(D_METHOD("create_proxy", "object", "interfaces"), &JavaClassWrapper::create_proxy);
}
#if !defined(ANDROID_ENABLED)
@ -124,6 +127,14 @@ Ref<JavaClass> JavaClassWrapper::_wrap(const String &, bool) {
return Ref<JavaClass>();
}
Ref<JavaObject> JavaClassWrapper::create_sam_callback(const String &p_interface, const Callable &p_callable) {
return Ref<JavaObject>();
}
Ref<JavaObject> JavaClassWrapper::create_proxy(const Object *p_object, const PackedStringArray &p_interfaces) {
return Ref<JavaObject>();
}
JavaClassWrapper::JavaClassWrapper() {
singleton = this;
}

View file

@ -191,6 +191,7 @@ class JavaClass : public RefCounted {
String java_constructor_name;
HashMap<StringName, List<MethodInfo>> methods;
jclass _class;
bool is_interface;
#endif
protected:
@ -252,8 +253,10 @@ class JavaClassWrapper : public Object {
jmethodID Class_getConstructors;
jmethodID Class_getDeclaredMethods;
jmethodID Class_getFields;
jmethodID Class_getInterfaces;
jmethodID Class_getName;
jmethodID Class_getSuperclass;
jmethodID Class_isInterface;
jmethodID Constructor_getParameterTypes;
jmethodID Constructor_getModifiers;
jmethodID Method_getParameterTypes;
@ -272,7 +275,16 @@ class JavaClassWrapper : public Object {
jmethodID Float_floatValue;
jmethodID Double_doubleValue;
jclass proxy_class;
jmethodID Proxy_isProxyClass;
jclass android_runtime_class;
jmethodID ARP_create_proxy_from_godot_callable;
jmethodID ARP_create_proxy_from_godot_object_id;
bool _is_proxy_class(JNIEnv *env, jclass p_class);
bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig);
bool _wrap_class_components(JNIEnv *p_env, const Ref<JavaClass> &p_java_class, jclass p_class, bool p_allow_non_public_methods_access);
#endif
Ref<JavaObject> exception;
@ -291,6 +303,9 @@ public:
return _wrap(p_class, false);
}
Ref<JavaObject> create_sam_callback(const String &p_sam_interface, const Callable &p_callable);
Ref<JavaObject> create_proxy(const Object *p_object, const PackedStringArray &p_interfaces);
Ref<JavaObject> get_exception() {
return exception;
}

View file

@ -0,0 +1,86 @@
/**************************************************************************/
/* jni_singleton.cpp */
/**************************************************************************/
/* 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. */
/**************************************************************************/
#include "jni_singleton.h"
#include "core/object/class_db.h"
void JNISingleton::_bind_methods() {
ClassDB::bind_method(D_METHOD("has_java_method", "method"), &JNISingleton::has_java_method);
}
bool JNISingleton::_get(const StringName &p_name, Variant &r_property) const {
if (has_signal(p_name)) {
r_property = Signal(this, p_name);
return true;
}
return false;
}
Variant JNISingleton::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
// Godot methods take precedence.
Variant ret = Object::callp(p_method, p_args, p_argcount, r_error);
if (r_error.error == Callable::CallError::CALL_OK) {
return ret;
}
// Check the method we're looking for is in the JNISingleton map.
// This is done because JNISingletons register methods differently than wrapped JavaClass / JavaObject to allow
// for access to private methods annotated with the @UsedByGodot annotation.
// In the future, we should remove access to private methods and require that JNISingletons' methods exposed to
// GDScript be all public, similarly to what we do for wrapped JavaClass / JavaObject methods. Doing so will
// also allow dropping and deprecating the @UsedByGodot annotation.
RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
if (E) {
if (wrapped_object.is_valid()) {
return wrapped_object->callp(p_method, p_args, p_argcount, r_error);
} else {
r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL;
}
}
return Variant();
}
void JNISingleton::add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
MethodData md;
md.argtypes = p_args;
md.ret_type = p_ret_type;
method_map[p_name] = md;
}
void JNISingleton::add_signal(const StringName &p_name, const Vector<Variant::Type> &p_args) {
MethodInfo mi;
mi.name = p_name;
for (int i = 0; i < p_args.size(); i++) {
mi.arguments.push_back(PropertyInfo(p_args[i], "arg" + itos(i + 1)));
}
add_user_signal(mi);
}

View file

@ -32,7 +32,6 @@
#include "java_class_wrapper.h"
#include "core/config/engine.h"
#include "core/templates/rb_map.h"
#include "core/variant/variant.h"
@ -48,34 +47,11 @@ class JNISingleton : public Object {
Ref<JavaObject> wrapped_object;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("has_java_method", "method"), &JNISingleton::has_java_method);
}
static void _bind_methods();
bool _get(const StringName &p_name, Variant &r_property) const;
public:
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
// Godot methods take precedence.
Variant ret = Object::callp(p_method, p_args, p_argcount, r_error);
if (r_error.error == Callable::CallError::CALL_OK) {
return ret;
}
// Check the method we're looking for is in the JNISingleton map.
// This is done because JNISingletons register methods differently than wrapped JavaClass / JavaObject to allow
// for access to private methods annotated with the @UsedByGodot annotation.
// In the future, we should remove access to private methods and require that JNISingletons' methods exposed to
// GDScript be all public, similarly to what we do for wrapped JavaClass / JavaObject methods. Doing so will
// also allow dropping and deprecating the @UsedByGodot annotation.
RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
if (E) {
if (wrapped_object.is_valid()) {
return wrapped_object->callp(p_method, p_args, p_argcount, r_error);
} else {
r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL;
}
}
return Variant();
}
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
Ref<JavaObject> get_wrapped_object() const {
return wrapped_object;
@ -85,21 +61,9 @@ public:
return method_map.has(p_method);
}
void add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
MethodData md;
md.argtypes = p_args;
md.ret_type = p_ret_type;
method_map[p_name] = md;
}
void add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type);
void add_signal(const StringName &p_name, const Vector<Variant::Type> &p_args) {
MethodInfo mi;
mi.name = p_name;
for (int i = 0; i < p_args.size(); i++) {
mi.arguments.push_back(PropertyInfo(p_args[i], "arg" + itos(i + 1)));
}
ADD_SIGNAL(mi);
}
void add_signal(const StringName &p_name, const Vector<Variant::Type> &p_args);
JNISingleton() {}

View file

@ -30,6 +30,8 @@
#include "audio_driver_opensl.h"
#include "core/os/os.h"
#define MAX_NUMBER_INTERFACES 3
#define MAX_NUMBER_OUTPUT_DEVICES 6
@ -132,11 +134,8 @@ void AudioDriverOpenSL::start() {
pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
pcm.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
pcm.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
#ifdef BIG_ENDIAN_ENABLED
pcm.endianness = SL_BYTEORDER_BIGENDIAN;
#else
pcm.endianness = SL_BYTEORDER_LITTLEENDIAN;
#endif
audioSource.pFormat = (void *)&pcm;
audioSource.pLocator = (void *)&loc_bufq;

View file

@ -68,7 +68,7 @@ def get_android_ndk_root(env: "SConsEnvironment"):
# This is kept in sync with the value in 'platform/android/java/app/config.gradle'.
def get_ndk_version():
return "28.1.13356709"
return "29.0.14206865"
# This is kept in sync with the value in 'platform/android/java/app/config.gradle'.
@ -114,6 +114,21 @@ def detect_swappy():
return has_swappy
def should_enable_perfetto(env: "SConsEnvironment"):
from SCons.Script import ARGUMENTS
cmdline_val = ARGUMENTS.get("profiler")
if cmdline_val is not None:
return cmdline_val == "perfetto"
elif env.debug_features and not (env["store_release"]) and not (env["production"]):
return True
return False
def detect_perfetto():
return os.path.exists("thirdparty/perfetto")
def configure(env: "SConsEnvironment"):
# Validate arch.
supported_arches = ["x86_32", "x86_64", "arm32", "arm64"]
@ -188,10 +203,20 @@ def configure(env: "SConsEnvironment"):
CCFLAGS=(["-fpic", "-ffunction-sections", "-funwind-tables", "-fstack-protector-strong", "-fvisibility=hidden"])
)
if should_enable_perfetto(env):
has_perfetto = detect_perfetto()
if not has_perfetto:
print_warning(
f"Perfetto not detected! It is recommended you run `python {os.path.join('misc', 'scripts', 'install_perfetto.py')}` to download and install Perfetto before compiling.\n"
)
else:
env["profiler"] = "perfetto"
env["profiler_path"] = os.path.abspath("thirdparty/perfetto")
has_swappy = detect_swappy()
if not has_swappy:
print_warning(
"Swappy Frame Pacing not detected! It is strongly recommended you run `python misc/scripts/install_swappy_android.py` to download and install Swappy before compiling.\n"
f"Swappy Frame Pacing not detected! It is strongly recommended you run `python {os.path.join('misc', 'scripts', 'install_swappy_android.py')}` to download and install Swappy before compiling.\n"
+ "Without Swappy, Godot apps on Android will inevitably suffer stutter and struggle to keep a consistent framerate. Although Swappy cannot guarantee your app will be stutter-free, not having Swappy will guarantee there will be stutter even on the best phones and the most simple of scenes."
)
if env["swappy"]:

View file

@ -30,12 +30,9 @@
#pragma once
#include "java_godot_lib_jni.h"
#include "core/io/dir_access.h"
#include "drivers/unix/dir_access_unix.h"
#include <cstdio>
#include <jni.h>
/// Android implementation of the DirAccess interface used to provide access to
/// ACCESS_FILESYSTEM and ACCESS_RESOURCES directory resources.

View file

@ -36,6 +36,10 @@
#include "tts_android.h"
#include "core/config/project_settings.h"
#include "core/input/input.h"
#include "core/input/input_event.h"
#include "core/os/os.h"
#include "servers/display/native_menu.h"
#if defined(RD_ENABLED)
#include "servers/rendering/renderer_rd/renderer_compositor_rd.h"
@ -52,37 +56,48 @@
#include <EGL/egl.h>
#endif
#if defined(RD_ENABLED)
static RenderingContextDriver *rendering_context_global = nullptr;
static bool rendering_context_global_checked = false;
#endif
DisplayServerAndroid *DisplayServerAndroid::get_singleton() {
return static_cast<DisplayServerAndroid *>(DisplayServer::get_singleton());
}
bool DisplayServerAndroid::has_feature(Feature p_feature) const {
bool DisplayServerAndroid::has_feature(DisplayServerEnums::Feature p_feature) const {
switch (p_feature) {
#ifndef DISABLE_DEPRECATED
case FEATURE_GLOBAL_MENU: {
case DisplayServerEnums::FEATURE_GLOBAL_MENU: {
return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
} break;
#endif
case FEATURE_CURSOR_SHAPE:
//case FEATURE_CUSTOM_CURSOR_SHAPE:
//case FEATURE_HIDPI:
//case FEATURE_ICON:
//case FEATURE_IME:
case FEATURE_MOUSE:
//case FEATURE_MOUSE_WARP:
case FEATURE_NATIVE_DIALOG:
case FEATURE_NATIVE_DIALOG_INPUT:
case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
case FEATURE_NATIVE_DIALOG_FILE_MIME:
//case FEATURE_NATIVE_ICON:
case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD:
case FEATURE_KEEP_SCREEN_ON:
case FEATURE_ORIENTATION:
case FEATURE_TOUCHSCREEN:
case FEATURE_VIRTUAL_KEYBOARD:
case FEATURE_TEXT_TO_SPEECH:
case DisplayServerEnums::FEATURE_PIP_MODE: {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL_V(godot_java, false);
return godot_java->is_pip_mode_supported();
} break;
case DisplayServerEnums::FEATURE_CURSOR_SHAPE:
//case DisplayServerEnums::FEATURE_CUSTOM_CURSOR_SHAPE:
//case DisplayServerEnums::FEATURE_HIDPI:
//case DisplayServerEnums::FEATURE_ICON:
//case DisplayServerEnums::FEATURE_IME:
case DisplayServerEnums::FEATURE_MOUSE:
//case DisplayServerEnums::FEATURE_MOUSE_WARP:
case DisplayServerEnums::FEATURE_NATIVE_DIALOG:
case DisplayServerEnums::FEATURE_NATIVE_DIALOG_INPUT:
case DisplayServerEnums::FEATURE_NATIVE_DIALOG_FILE:
//case DisplayServerEnums::FEATURE_NATIVE_DIALOG_FILE_EXTRA:
case DisplayServerEnums::FEATURE_NATIVE_DIALOG_FILE_MIME:
//case DisplayServerEnums::FEATURE_NATIVE_ICON:
case DisplayServerEnums::FEATURE_WINDOW_TRANSPARENCY:
case DisplayServerEnums::FEATURE_CLIPBOARD:
case DisplayServerEnums::FEATURE_KEEP_SCREEN_ON:
case DisplayServerEnums::FEATURE_ORIENTATION:
case DisplayServerEnums::FEATURE_TOUCHSCREEN:
case DisplayServerEnums::FEATURE_VIRTUAL_KEYBOARD:
case DisplayServerEnums::FEATURE_TEXT_TO_SPEECH:
return true;
default:
return false;
@ -214,7 +229,7 @@ void DisplayServerAndroid::emit_input_dialog_callback(String p_text) {
}
}
Error DisplayServerAndroid::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback, WindowID p_window_id) {
Error DisplayServerAndroid::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, DisplayServerEnums::FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback, DisplayServerEnums::WindowID p_window_id) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL_V(godot_java, FAILED);
file_picker_callback = p_callback;
@ -239,13 +254,21 @@ Color DisplayServerAndroid::get_base_color() const {
return godot_java->get_base_color();
}
TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const {
TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts(int p_screen) const {
p_screen = _get_screen_index(p_screen);
int screen_count = get_screen_count();
ERR_FAIL_INDEX_V(p_screen, screen_count, TypedArray<Rect2>());
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, Array());
return godot_io_java->get_display_cutouts();
}
Rect2i DisplayServerAndroid::get_display_safe_area() const {
Rect2i DisplayServerAndroid::get_display_safe_area(int p_screen) const {
p_screen = _get_screen_index(p_screen);
int screen_count = get_screen_count();
ERR_FAIL_INDEX_V(p_screen, screen_count, Rect2i());
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, Rect2i());
return godot_io_java->get_display_safe_area();
@ -263,7 +286,7 @@ bool DisplayServerAndroid::screen_is_kept_on() const {
return keep_screen_on;
}
void DisplayServerAndroid::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) {
void DisplayServerAndroid::screen_set_orientation(DisplayServerEnums::ScreenOrientation p_orientation, int p_screen) {
p_screen = _get_screen_index(p_screen);
int screen_count = get_screen_count();
ERR_FAIL_INDEX(p_screen, screen_count);
@ -274,17 +297,17 @@ void DisplayServerAndroid::screen_set_orientation(DisplayServer::ScreenOrientati
godot_io_java->set_screen_orientation(p_orientation);
}
DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(int p_screen) const {
DisplayServerEnums::ScreenOrientation DisplayServerAndroid::screen_get_orientation(int p_screen) const {
p_screen = _get_screen_index(p_screen);
int screen_count = get_screen_count();
ERR_FAIL_INDEX_V(p_screen, screen_count, SCREEN_LANDSCAPE);
ERR_FAIL_INDEX_V(p_screen, screen_count, DisplayServerEnums::SCREEN_LANDSCAPE);
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, SCREEN_LANDSCAPE);
ERR_FAIL_NULL_V(godot_io_java, DisplayServerEnums::SCREEN_LANDSCAPE);
const int orientation = godot_io_java->get_screen_orientation();
ERR_FAIL_INDEX_V_MSG(orientation, 7, SCREEN_LANDSCAPE, "Unrecognized screen orientation");
return (ScreenOrientation)orientation;
ERR_FAIL_INDEX_V_MSG(orientation, 7, DisplayServerEnums::SCREEN_LANDSCAPE, "Unrecognized screen orientation");
return (DisplayServerEnums::ScreenOrientation)orientation;
}
int DisplayServerAndroid::get_display_rotation() const {
@ -377,7 +400,7 @@ bool DisplayServerAndroid::is_touchscreen_available() const {
return true;
}
void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, VirtualKeyboardType p_type, int p_max_length, int p_cursor_start, int p_cursor_end) {
void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, DisplayServerEnums::VirtualKeyboardType p_type, int p_max_length, int p_cursor_start, int p_cursor_end) {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL(godot_io_java);
@ -413,23 +436,23 @@ bool DisplayServerAndroid::has_hardware_keyboard() const {
return godot_io_java->has_hardware_keyboard();
}
void DisplayServerAndroid::window_set_window_event_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_window_event_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window) {
window_event_callback = p_callable;
}
void DisplayServerAndroid::window_set_input_event_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_input_event_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window) {
input_event_callback = p_callable;
}
void DisplayServerAndroid::window_set_input_text_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_input_text_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window) {
input_text_callback = p_callable;
}
void DisplayServerAndroid::window_set_rect_changed_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_rect_changed_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window) {
rect_changed_callback = p_callable;
}
void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_callable, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
@ -444,7 +467,7 @@ void DisplayServerAndroid::_window_callback(const Callable &p_callable, bool p_d
}
}
void DisplayServerAndroid::send_window_event(DisplayServer::WindowEvent p_event, bool p_deferred) const {
void DisplayServerAndroid::send_window_event(DisplayServerEnums::WindowEvent p_event, bool p_deferred) const {
_window_callback(window_event_callback, p_deferred, int(p_event));
}
@ -460,43 +483,43 @@ void DisplayServerAndroid::_dispatch_input_events(const Ref<InputEvent> &p_event
DisplayServerAndroid::get_singleton()->send_input_event(p_event);
}
Vector<DisplayServer::WindowID> DisplayServerAndroid::get_window_list() const {
Vector<WindowID> ret;
ret.push_back(MAIN_WINDOW_ID);
Vector<DisplayServerEnums::WindowID> DisplayServerAndroid::get_window_list() const {
Vector<DisplayServerEnums::WindowID> ret;
ret.push_back(DisplayServerEnums::MAIN_WINDOW_ID);
return ret;
}
DisplayServer::WindowID DisplayServerAndroid::get_window_at_screen_position(const Point2i &p_position) const {
return MAIN_WINDOW_ID;
DisplayServerEnums::WindowID DisplayServerAndroid::get_window_at_screen_position(const Point2i &p_position) const {
return DisplayServerEnums::MAIN_WINDOW_ID;
}
int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type, WindowID p_window) const {
ERR_FAIL_COND_V(p_window != MAIN_WINDOW_ID, 0);
int64_t DisplayServerAndroid::window_get_native_handle(DisplayServerEnums::HandleType p_handle_type, DisplayServerEnums::WindowID p_window) const {
ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, 0);
switch (p_handle_type) {
case WINDOW_HANDLE: {
case DisplayServerEnums::WINDOW_HANDLE: {
return reinterpret_cast<int64_t>(static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity());
}
case WINDOW_VIEW: {
case DisplayServerEnums::WINDOW_VIEW: {
return 0; // Not supported.
}
#ifdef GLES3_ENABLED
case DISPLAY_HANDLE: {
case DisplayServerEnums::DISPLAY_HANDLE: {
if (rendering_driver == "opengl3") {
return reinterpret_cast<int64_t>(eglGetCurrentDisplay());
}
return 0;
}
case OPENGL_CONTEXT: {
case DisplayServerEnums::OPENGL_CONTEXT: {
if (rendering_driver == "opengl3") {
return reinterpret_cast<int64_t>(eglGetCurrentContext());
}
return 0;
}
case EGL_DISPLAY: {
case DisplayServerEnums::EGL_DISPLAY: {
// @todo Find a way to get this from the Java side.
return 0;
}
case EGL_CONFIG: {
case DisplayServerEnums::EGL_CONFIG: {
// @todo Find a way to get this from the Java side.
return 0;
}
@ -507,95 +530,95 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type,
}
}
void DisplayServerAndroid::window_attach_instance_id(ObjectID p_instance, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_attach_instance_id(ObjectID p_instance, DisplayServerEnums::WindowID p_window) {
window_attached_instance_id = p_instance;
}
ObjectID DisplayServerAndroid::window_get_attached_instance_id(DisplayServer::WindowID p_window) const {
ObjectID DisplayServerAndroid::window_get_attached_instance_id(DisplayServerEnums::WindowID p_window) const {
return window_attached_instance_id;
}
void DisplayServerAndroid::window_set_title(const String &p_title, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_title(const String &p_title, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
int DisplayServerAndroid::window_get_current_screen(DisplayServer::WindowID p_window) const {
ERR_FAIL_COND_V(p_window != MAIN_WINDOW_ID, INVALID_SCREEN);
int DisplayServerAndroid::window_get_current_screen(DisplayServerEnums::WindowID p_window) const {
ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, DisplayServerEnums::INVALID_SCREEN);
return 0;
}
void DisplayServerAndroid::window_set_current_screen(int p_screen, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_current_screen(int p_screen, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
Point2i DisplayServerAndroid::window_get_position(DisplayServer::WindowID p_window) const {
Point2i DisplayServerAndroid::window_get_position(DisplayServerEnums::WindowID p_window) const {
return Point2i();
}
Point2i DisplayServerAndroid::window_get_position_with_decorations(DisplayServer::WindowID p_window) const {
Point2i DisplayServerAndroid::window_get_position_with_decorations(DisplayServerEnums::WindowID p_window) const {
return Point2i();
}
void DisplayServerAndroid::window_set_position(const Point2i &p_position, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_position(const Point2i &p_position, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
void DisplayServerAndroid::window_set_transient(DisplayServer::WindowID p_window, DisplayServer::WindowID p_parent) {
void DisplayServerAndroid::window_set_transient(DisplayServerEnums::WindowID p_window, DisplayServerEnums::WindowID p_parent) {
// Not supported on Android.
}
void DisplayServerAndroid::window_set_max_size(const Size2i p_size, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_max_size(const Size2i p_size, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
Size2i DisplayServerAndroid::window_get_max_size(DisplayServer::WindowID p_window) const {
Size2i DisplayServerAndroid::window_get_max_size(DisplayServerEnums::WindowID p_window) const {
return Size2i();
}
void DisplayServerAndroid::window_set_min_size(const Size2i p_size, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_min_size(const Size2i p_size, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
Size2i DisplayServerAndroid::window_get_min_size(DisplayServer::WindowID p_window) const {
Size2i DisplayServerAndroid::window_get_min_size(DisplayServerEnums::WindowID p_window) const {
return Size2i();
}
void DisplayServerAndroid::window_set_size(const Size2i p_size, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_size(const Size2i p_size, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
Size2i DisplayServerAndroid::window_get_size(DisplayServer::WindowID p_window) const {
Size2i DisplayServerAndroid::window_get_size(DisplayServerEnums::WindowID p_window) const {
return OS_Android::get_singleton()->get_display_size();
}
Size2i DisplayServerAndroid::window_get_size_with_decorations(DisplayServer::WindowID p_window) const {
Size2i DisplayServerAndroid::window_get_size_with_decorations(DisplayServerEnums::WindowID p_window) const {
return OS_Android::get_singleton()->get_display_size();
}
void DisplayServerAndroid::window_set_mode(DisplayServer::WindowMode p_mode, DisplayServer::WindowID p_window) {
OS_Android::get_singleton()->get_godot_java()->enable_immersive_mode(p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN);
void DisplayServerAndroid::window_set_mode(DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::WindowID p_window) {
OS_Android::get_singleton()->get_godot_java()->enable_immersive_mode(p_mode == DisplayServerEnums::WINDOW_MODE_FULLSCREEN || p_mode == DisplayServerEnums::WINDOW_MODE_EXCLUSIVE_FULLSCREEN);
}
DisplayServer::WindowMode DisplayServerAndroid::window_get_mode(DisplayServer::WindowID p_window) const {
DisplayServerEnums::WindowMode DisplayServerAndroid::window_get_mode(DisplayServerEnums::WindowID p_window) const {
if (OS_Android::get_singleton()->get_godot_java()->is_in_immersive_mode()) {
return WINDOW_MODE_FULLSCREEN;
return DisplayServerEnums::WINDOW_MODE_FULLSCREEN;
} else {
return WINDOW_MODE_MAXIMIZED;
return DisplayServerEnums::WINDOW_MODE_MAXIMIZED;
}
}
bool DisplayServerAndroid::window_is_maximize_allowed(DisplayServer::WindowID p_window) const {
bool DisplayServerAndroid::window_is_maximize_allowed(DisplayServerEnums::WindowID p_window) const {
return false;
}
void DisplayServerAndroid::window_set_flag(DisplayServer::WindowFlags p_flag, bool p_enabled, DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_set_flag(DisplayServerEnums::WindowFlags p_flag, bool p_enabled, DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
bool DisplayServerAndroid::window_get_flag(DisplayServer::WindowFlags p_flag, DisplayServer::WindowID p_window) const {
ERR_FAIL_COND_V(p_window != MAIN_WINDOW_ID, false);
bool DisplayServerAndroid::window_get_flag(DisplayServerEnums::WindowFlags p_flag, DisplayServerEnums::WindowID p_window) const {
ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, false);
switch (p_flag) {
case WindowFlags::WINDOW_FLAG_TRANSPARENT:
case DisplayServerEnums::WindowFlags::WINDOW_FLAG_TRANSPARENT:
return is_window_transparency_available();
default:
@ -603,19 +626,19 @@ bool DisplayServerAndroid::window_get_flag(DisplayServer::WindowFlags p_flag, Di
}
}
void DisplayServerAndroid::window_request_attention(DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_request_attention(DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
void DisplayServerAndroid::window_move_to_foreground(DisplayServer::WindowID p_window) {
void DisplayServerAndroid::window_move_to_foreground(DisplayServerEnums::WindowID p_window) {
// Not supported on Android.
}
bool DisplayServerAndroid::window_is_focused(WindowID p_window) const {
bool DisplayServerAndroid::window_is_focused(DisplayServerEnums::WindowID p_window) const {
return true;
}
bool DisplayServerAndroid::window_can_draw(DisplayServer::WindowID p_window) const {
bool DisplayServerAndroid::window_can_draw(DisplayServerEnums::WindowID p_window) const {
return true;
}
@ -646,7 +669,7 @@ Vector<String> DisplayServerAndroid::get_rendering_drivers_func() {
return drivers;
}
DisplayServer *DisplayServerAndroid::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
DisplayServer *DisplayServerAndroid::create_func(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error) {
DisplayServer *ds = memnew(DisplayServerAndroid(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, p_parent_window, r_error));
if (r_error != OK) {
if (p_rendering_driver == "vulkan") {
@ -667,15 +690,58 @@ void DisplayServerAndroid::register_android_driver() {
register_create_function("android", create_func, get_rendering_drivers_func);
}
void DisplayServerAndroid::reset_window() {
#if defined(RD_ENABLED)
if (rendering_context) {
if (rendering_device) {
rendering_device->screen_free(MAIN_WINDOW_ID);
#ifdef VULKAN_ENABLED
bool DisplayServerAndroid::check_vulkan_global_context(bool p_vulkan_requirements_met) {
if (!rendering_context_global_checked) {
bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
Error err = ERR_CANT_CREATE;
if (p_vulkan_requirements_met) {
rendering_context_global = memnew(RenderingContextDriverVulkanAndroid);
err = rendering_context_global->initialize();
}
VSyncMode last_vsync_mode = rendering_context->window_get_vsync_mode(MAIN_WINDOW_ID);
rendering_context->window_destroy(MAIN_WINDOW_ID);
if (err != OK) {
if (rendering_context_global != nullptr) {
memdelete(rendering_context_global);
rendering_context_global = nullptr;
}
#if defined(GLES3_ENABLED)
if (fallback_to_opengl3) {
WARN_PRINT("Your device does not seem to support Vulkan, switching to OpenGL 3.");
OS::get_singleton()->set_current_rendering_driver_name("opengl3", OS::RENDERING_SOURCE_FALLBACK);
OS::get_singleton()->set_current_rendering_method("gl_compatibility", OS::RENDERING_SOURCE_FALLBACK);
} else
#endif
{
ERR_PRINT("Failed to initialize Vulkan context.");
}
}
rendering_context_global_checked = true;
}
return rendering_context_global != nullptr;
}
void DisplayServerAndroid::free_vulkan_global_context() {
if (rendering_context_global != nullptr) {
memdelete(rendering_context_global);
rendering_context_global = nullptr;
rendering_context_global_checked = false;
}
}
#endif
void DisplayServerAndroid::reset_window() {
#if defined(RD_ENABLED)
if (rendering_context_global) {
if (rendering_device) {
rendering_device->screen_free(DisplayServerEnums::MAIN_WINDOW_ID);
}
DisplayServerEnums::VSyncMode last_vsync_mode = rendering_context_global->window_get_vsync_mode(DisplayServerEnums::MAIN_WINDOW_ID);
rendering_context_global->window_destroy(DisplayServerEnums::MAIN_WINDOW_ID);
union {
#ifdef VULKAN_ENABLED
@ -690,19 +756,17 @@ void DisplayServerAndroid::reset_window() {
}
#endif
if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) {
if (rendering_context_global->window_create(DisplayServerEnums::MAIN_WINDOW_ID, &wpd) != OK) {
ERR_PRINT(vformat("Failed to reset %s window.", rendering_driver));
memdelete(rendering_context);
rendering_context = nullptr;
return;
}
Size2i display_size = OS_Android::get_singleton()->get_display_size();
rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height);
rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, last_vsync_mode);
rendering_context_global->window_set_size(DisplayServerEnums::MAIN_WINDOW_ID, display_size.width, display_size.height);
rendering_context_global->window_set_vsync_mode(DisplayServerEnums::MAIN_WINDOW_ID, last_vsync_mode);
if (rendering_device) {
rendering_device->screen_create(MAIN_WINDOW_ID);
rendering_device->screen_create(DisplayServerEnums::MAIN_WINDOW_ID);
}
}
#endif
@ -722,79 +786,46 @@ void DisplayServerAndroid::notify_application_paused() {
#endif // defined(RD_ENABLED)
}
DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error) {
rendering_driver = p_rendering_driver;
keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on");
native_menu = memnew(NativeMenu);
#if defined(RD_ENABLED)
rendering_context = nullptr;
rendering_device = nullptr;
#if defined(VULKAN_ENABLED)
#ifdef VULKAN_ENABLED
if (rendering_driver == "vulkan") {
rendering_context = memnew(RenderingContextDriverVulkanAndroid);
}
#endif
if (rendering_context) {
if (rendering_context->initialize() != OK) {
memdelete(rendering_context);
rendering_context = nullptr;
#if defined(GLES3_ENABLED)
bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
if (fallback_to_opengl3 && rendering_driver != "opengl3") {
WARN_PRINT("Your device does not seem to support Vulkan, switching to OpenGL 3.");
rendering_driver = "opengl3";
OS::get_singleton()->set_current_rendering_method("gl_compatibility");
OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
} else
#endif
{
ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
r_error = ERR_UNAVAILABLE;
return;
}
check_vulkan_global_context(true);
if (rendering_context_global == nullptr) {
ERR_PRINT("Can't initialize display server with Vulkan driver because no Vulkan context is available.");
r_error = ERR_UNAVAILABLE;
return;
}
}
if (rendering_context) {
union {
#ifdef VULKAN_ENABLED
RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan;
#endif
} wpd;
#ifdef VULKAN_ENABLED
if (rendering_driver == "vulkan") {
ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
ERR_FAIL_NULL(native_window);
wpd.vulkan.window = native_window;
}
#endif
ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
ERR_FAIL_NULL(native_window);
if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) {
RenderingContextDriverVulkanAndroid::WindowPlatformData wpd;
wpd.window = native_window;
if (rendering_context_global->window_create(DisplayServerEnums::MAIN_WINDOW_ID, &wpd) != OK) {
ERR_PRINT(vformat("Failed to create %s window.", rendering_driver));
memdelete(rendering_context);
rendering_context = nullptr;
r_error = ERR_UNAVAILABLE;
return;
}
Size2i display_size = OS_Android::get_singleton()->get_display_size();
rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height);
rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, p_vsync_mode);
rendering_context_global->window_set_size(DisplayServerEnums::MAIN_WINDOW_ID, display_size.width, display_size.height);
rendering_context_global->window_set_vsync_mode(DisplayServerEnums::MAIN_WINDOW_ID, p_vsync_mode);
rendering_device = memnew(RenderingDevice);
if (rendering_device->initialize(rendering_context, MAIN_WINDOW_ID) != OK) {
if (rendering_device->initialize(rendering_context_global, DisplayServerEnums::MAIN_WINDOW_ID) != OK) {
rendering_device = nullptr;
memdelete(rendering_context);
rendering_context = nullptr;
r_error = ERR_UNAVAILABLE;
return;
}
rendering_device->screen_create(MAIN_WINDOW_ID);
rendering_device->screen_create(DisplayServerEnums::MAIN_WINDOW_ID);
RendererCompositorRD::make_current();
}
@ -821,9 +852,8 @@ DisplayServerAndroid::~DisplayServerAndroid() {
if (rendering_device) {
memdelete(rendering_device);
}
if (rendering_context) {
memdelete(rendering_context);
}
free_vulkan_global_context();
#endif
}
@ -844,7 +874,7 @@ void DisplayServerAndroid::process_gyroscope(const Vector3 &p_gyroscope) {
}
void DisplayServerAndroid::_mouse_update_mode() {
MouseMode wanted_mouse_mode = mouse_mode_override_enabled
DisplayServerEnums::MouseMode wanted_mouse_mode = mouse_mode_override_enabled
? mouse_mode_override
: mouse_mode_base;
@ -855,13 +885,13 @@ void DisplayServerAndroid::_mouse_update_mode() {
return;
}
if (wanted_mouse_mode == MouseMode::MOUSE_MODE_HIDDEN) {
if (wanted_mouse_mode == DisplayServerEnums::MouseMode::MOUSE_MODE_HIDDEN) {
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->set_pointer_icon(CURSOR_TYPE_NULL);
} else {
cursor_set_shape(cursor_shape);
}
if (wanted_mouse_mode == MouseMode::MOUSE_MODE_CAPTURED) {
if (wanted_mouse_mode == DisplayServerEnums::MouseMode::MOUSE_MODE_CAPTURED) {
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->request_pointer_capture();
} else {
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->release_pointer_capture();
@ -870,8 +900,8 @@ void DisplayServerAndroid::_mouse_update_mode() {
mouse_mode = wanted_mouse_mode;
}
void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
void DisplayServerAndroid::mouse_set_mode(DisplayServerEnums::MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, DisplayServerEnums::MouseMode::MOUSE_MODE_MAX);
if (p_mode == mouse_mode_base) {
return;
}
@ -879,12 +909,12 @@ void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) {
_mouse_update_mode();
}
DisplayServer::MouseMode DisplayServerAndroid::mouse_get_mode() const {
DisplayServerEnums::MouseMode DisplayServerAndroid::mouse_get_mode() const {
return mouse_mode;
}
void DisplayServerAndroid::mouse_set_mode_override(MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
void DisplayServerAndroid::mouse_set_mode_override(DisplayServerEnums::MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, DisplayServerEnums::MouseMode::MOUSE_MODE_MAX);
if (p_mode == mouse_mode_override) {
return;
}
@ -892,7 +922,7 @@ void DisplayServerAndroid::mouse_set_mode_override(MouseMode p_mode) {
_mouse_update_mode();
}
DisplayServer::MouseMode DisplayServerAndroid::mouse_get_mode_override() const {
DisplayServerEnums::MouseMode DisplayServerAndroid::mouse_get_mode_override() const {
return mouse_mode_override;
}
@ -913,7 +943,7 @@ BitField<MouseButtonMask> DisplayServerAndroid::mouse_get_button_state() const {
return Input::get_singleton()->get_mouse_button_mask();
}
void DisplayServerAndroid::_cursor_set_shape_helper(CursorShape p_shape, bool force) {
void DisplayServerAndroid::_cursor_set_shape_helper(DisplayServerEnums::CursorShape p_shape, bool force) {
if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon()) {
return;
}
@ -923,22 +953,22 @@ void DisplayServerAndroid::_cursor_set_shape_helper(CursorShape p_shape, bool fo
cursor_shape = p_shape;
if (mouse_mode == MouseMode::MOUSE_MODE_VISIBLE || mouse_mode == MouseMode::MOUSE_MODE_CONFINED) {
if (mouse_mode == DisplayServerEnums::MouseMode::MOUSE_MODE_VISIBLE || mouse_mode == DisplayServerEnums::MouseMode::MOUSE_MODE_CONFINED) {
OS_Android::get_singleton()->get_godot_java()->get_godot_view()->set_pointer_icon(android_cursors[cursor_shape]);
}
}
void DisplayServerAndroid::cursor_set_shape(DisplayServer::CursorShape p_shape) {
ERR_FAIL_INDEX(p_shape, CURSOR_MAX);
void DisplayServerAndroid::cursor_set_shape(DisplayServerEnums::CursorShape p_shape) {
ERR_FAIL_INDEX(p_shape, DisplayServerEnums::CURSOR_MAX);
_cursor_set_shape_helper(p_shape);
}
DisplayServer::CursorShape DisplayServerAndroid::cursor_get_shape() const {
DisplayServerEnums::CursorShape DisplayServerAndroid::cursor_get_shape() const {
return cursor_shape;
}
void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {
ERR_FAIL_INDEX(p_shape, CURSOR_MAX);
void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor, DisplayServerEnums::CursorShape p_shape, const Vector2 &p_hotspot) {
ERR_FAIL_INDEX(p_shape, DisplayServerEnums::CURSOR_MAX);
String cursor_path = p_cursor.is_valid() ? p_cursor->get_path() : "";
if (!cursor_path.is_empty()) {
cursor_path = ProjectSettings::get_singleton()->globalize_path(cursor_path);
@ -947,21 +977,21 @@ void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor
_cursor_set_shape_helper(p_shape, true);
}
void DisplayServerAndroid::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) {
void DisplayServerAndroid::window_set_vsync_mode(DisplayServerEnums::VSyncMode p_vsync_mode, DisplayServerEnums::WindowID p_window) {
#if defined(RD_ENABLED)
if (rendering_context) {
rendering_context->window_set_vsync_mode(p_window, p_vsync_mode);
if (rendering_context_global) {
rendering_context_global->window_set_vsync_mode(p_window, p_vsync_mode);
}
#endif
}
DisplayServer::VSyncMode DisplayServerAndroid::window_get_vsync_mode(WindowID p_window) const {
DisplayServerEnums::VSyncMode DisplayServerAndroid::window_get_vsync_mode(DisplayServerEnums::WindowID p_window) const {
#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_vsync_mode(p_window);
if (rendering_context_global) {
return rendering_context_global->window_get_vsync_mode(p_window);
}
#endif
return DisplayServer::VSYNC_ENABLED;
return DisplayServerEnums::VSYNC_ENABLED;
}
void DisplayServerAndroid::reset_swap_buffers_flag() {
@ -987,3 +1017,31 @@ void DisplayServerAndroid::set_icon(const Ref<Image> &p_icon) {
bool DisplayServerAndroid::is_window_transparency_available() const {
return GLOBAL_GET_CACHED(bool, "display/window/per_pixel_transparency/allowed");
}
bool DisplayServerAndroid::is_in_pip_mode(DisplayServerEnums::WindowID p_window) {
ERR_FAIL_COND_V(p_window != DisplayServerEnums::MAIN_WINDOW_ID, false);
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL_V(godot_java, false);
return godot_java->is_in_pip_mode();
}
void DisplayServerAndroid::pip_mode_enter(DisplayServerEnums::WindowID p_window) {
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL(godot_java);
godot_java->enter_pip_mode();
}
void DisplayServerAndroid::pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window) {
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL(godot_java);
godot_java->set_pip_mode_aspect_ratio(p_numerator, p_denominator);
}
void DisplayServerAndroid::pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window) {
ERR_FAIL_COND(p_window != DisplayServerEnums::MAIN_WINDOW_ID);
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL(godot_java);
godot_java->set_auto_enter_pip_mode_on_background(p_auto_enter_on_background);
}

View file

@ -32,6 +32,9 @@
#include "servers/display/display_server.h"
class InputEvent;
class NativeMenu;
#if defined(RD_ENABLED)
class RenderingContextDriver;
class RenderingDevice;
@ -44,39 +47,38 @@ class DisplayServerAndroid : public DisplayServer {
// https://developer.android.com/reference/android/view/PointerIcon
// mapping between Godot's cursor shape to Android's'
int android_cursors[CURSOR_MAX] = {
1000, //CURSOR_ARROW
1008, //CURSOR_IBEAM
1002, //CURSOR_POINTIN
1007, //CURSOR_CROSS
1004, //CURSOR_WAIT
1004, //CURSOR_BUSY
1021, //CURSOR_DRAG
1021, //CURSOR_CAN_DRO
1000, //CURSOR_FORBIDD (no corresponding icon in Android's icon so fallback to default)
1015, //CURSOR_VSIZE
1014, //CURSOR_HSIZE
1017, //CURSOR_BDIAGSI
1016, //CURSOR_FDIAGSI
1020, //CURSOR_MOVE
1015, //CURSOR_VSPLIT
1014, //CURSOR_HSPLIT
1003, //CURSOR_HELP
int android_cursors[DisplayServerEnums::CURSOR_MAX] = {
1000, //DisplayServerEnums::CURSOR_ARROW
1008, //DisplayServerEnums::CURSOR_IBEAM
1002, //DisplayServerEnums::CURSOR_POINTIN
1007, //DisplayServerEnums::CURSOR_CROSS
1004, //DisplayServerEnums::CURSOR_WAIT
1004, //DisplayServerEnums::CURSOR_BUSY
1021, //DisplayServerEnums::CURSOR_DRAG
1021, //DisplayServerEnums::CURSOR_CAN_DRO
1000, //DisplayServerEnums::CURSOR_FORBIDD (no corresponding icon in Android's icon so fallback to default)
1015, //DisplayServerEnums::CURSOR_VSIZE
1014, //DisplayServerEnums::CURSOR_HSIZE
1017, //DisplayServerEnums::CURSOR_BDIAGSI
1016, //DisplayServerEnums::CURSOR_FDIAGSI
1020, //DisplayServerEnums::CURSOR_MOVE
1015, //DisplayServerEnums::CURSOR_VSPLIT
1014, //DisplayServerEnums::CURSOR_HSPLIT
1003, //DisplayServerEnums::CURSOR_HELP
};
const int CURSOR_TYPE_NULL = 0;
MouseMode mouse_mode = MouseMode::MOUSE_MODE_VISIBLE;
MouseMode mouse_mode_base = MouseMode::MOUSE_MODE_VISIBLE;
MouseMode mouse_mode_override = MouseMode::MOUSE_MODE_VISIBLE;
DisplayServerEnums::MouseMode mouse_mode = DisplayServerEnums::MouseMode::MOUSE_MODE_VISIBLE;
DisplayServerEnums::MouseMode mouse_mode_base = DisplayServerEnums::MouseMode::MOUSE_MODE_VISIBLE;
DisplayServerEnums::MouseMode mouse_mode_override = DisplayServerEnums::MouseMode::MOUSE_MODE_VISIBLE;
bool mouse_mode_override_enabled = false;
void _mouse_update_mode();
bool keep_screen_on;
bool swap_buffers_flag;
CursorShape cursor_shape = CursorShape::CURSOR_ARROW;
DisplayServerEnums::CursorShape cursor_shape = DisplayServerEnums::CursorShape::CURSOR_ARROW;
#if defined(RD_ENABLED)
RenderingContextDriver *rendering_context = nullptr;
RenderingDevice *rendering_device = nullptr;
#endif
NativeMenu *native_menu = nullptr;
@ -104,7 +106,7 @@ class DisplayServerAndroid : public DisplayServer {
public:
static DisplayServerAndroid *get_singleton();
virtual bool has_feature(Feature p_feature) const override;
virtual bool has_feature(DisplayServerEnums::Feature p_feature) const override;
virtual String get_name() const override;
virtual bool tts_is_speaking() const override;
@ -131,95 +133,95 @@ public:
virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
void emit_input_dialog_callback(String p_text);
virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback, WindowID p_window_id) override;
virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const DisplayServerEnums::FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback, DisplayServerEnums::WindowID p_window_id) override;
void emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths);
virtual Color get_accent_color() const override;
virtual Color get_base_color() const override;
virtual TypedArray<Rect2> get_display_cutouts() const override;
virtual Rect2i get_display_safe_area() const override;
virtual TypedArray<Rect2> get_display_cutouts(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual Rect2i get_display_safe_area(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual void screen_set_keep_on(bool p_enable) override;
virtual bool screen_is_kept_on() const override;
virtual void screen_set_orientation(ScreenOrientation p_orientation, int p_screen = SCREEN_OF_MAIN_WINDOW) override;
virtual ScreenOrientation screen_get_orientation(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual void screen_set_orientation(DisplayServerEnums::ScreenOrientation p_orientation, int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) override;
virtual DisplayServerEnums::ScreenOrientation screen_get_orientation(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
int get_display_rotation() const;
virtual int get_screen_count() const override;
virtual int get_primary_screen() const override;
virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual Point2i screen_get_position(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual Size2i screen_get_size(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual Rect2i screen_get_usable_rect(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual int screen_get_dpi(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_scale(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_refresh_rate(int p_screen = DisplayServerEnums::SCREEN_OF_MAIN_WINDOW) const override;
virtual bool is_touchscreen_available() const override;
virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), DisplayServerEnums::VirtualKeyboardType p_type = DisplayServerEnums::KEYBOARD_TYPE_DEFAULT, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
virtual void virtual_keyboard_hide() override;
virtual int virtual_keyboard_get_height() const override;
virtual bool has_hardware_keyboard() const override;
virtual void set_hardware_keyboard_connection_change_callback(const Callable &p_callable) override;
void emit_hardware_keyboard_connection_changed(bool p_connected);
virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_set_window_event_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_set_input_event_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_set_input_text_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_set_rect_changed_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_set_drop_files_callback(const Callable &p_callable, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
void send_window_event(WindowEvent p_event, bool p_deferred = false) const;
void send_window_event(DisplayServerEnums::WindowEvent p_event, bool p_deferred = false) const;
void send_input_event(const Ref<InputEvent> &p_event) const;
void send_input_text(const String &p_text) const;
virtual Vector<WindowID> get_window_list() const override;
virtual WindowID get_window_at_screen_position(const Point2i &p_position) const override;
virtual Vector<DisplayServerEnums::WindowID> get_window_list() const override;
virtual DisplayServerEnums::WindowID get_window_at_screen_position(const Point2i &p_position) const override;
virtual int64_t window_get_native_handle(HandleType p_handle_type, WindowID p_window = MAIN_WINDOW_ID) const override;
virtual int64_t window_get_native_handle(DisplayServerEnums::HandleType p_handle_type, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override;
virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_attach_instance_id(ObjectID p_instance, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual ObjectID window_get_attached_instance_id(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_title(const String &p_title, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override;
virtual int window_get_current_screen(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_current_screen(int p_screen, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual Point2i window_get_position_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override;
virtual Point2i window_get_position(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual Point2i window_get_position_with_decorations(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_position(const Point2i &p_position, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_set_transient(WindowID p_window, WindowID p_parent) override;
virtual void window_set_transient(DisplayServerEnums::WindowID p_window, DisplayServerEnums::WindowID p_parent) override;
virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_max_size(const Size2i p_size, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual Size2i window_get_max_size(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_min_size(const Size2i p_size, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual Size2i window_get_min_size(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual Size2i window_get_size_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_size(const Size2i p_size, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual Size2i window_get_size(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual Size2i window_get_size_with_decorations(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override;
virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_mode(DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual DisplayServerEnums::WindowMode window_get_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual bool window_is_maximize_allowed(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override;
virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_set_flag(DisplayServerEnums::WindowFlags p_flag, bool p_enabled, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual bool window_get_flag(DisplayServerEnums::WindowFlags p_flag, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override;
virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override;
virtual bool window_is_focused(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual void window_request_attention(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void window_move_to_foreground(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual bool window_is_focused(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override;
virtual bool window_can_draw(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) const override;
virtual bool can_any_window_draw() const override;
virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override;
virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override;
virtual void window_set_vsync_mode(DisplayServerEnums::VSyncMode p_vsync_mode, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual DisplayServerEnums::VSyncMode window_get_vsync_mode(DisplayServerEnums::WindowID p_vsync_mode) const override;
virtual void window_set_color(const Color &p_color) override;
@ -230,22 +232,27 @@ public:
void process_magnetometer(const Vector3 &p_magnetometer);
void process_gyroscope(const Vector3 &p_gyroscope);
void _cursor_set_shape_helper(CursorShape p_shape, bool force = false);
virtual void cursor_set_shape(CursorShape p_shape) override;
virtual CursorShape cursor_get_shape() const override;
virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
void _cursor_set_shape_helper(DisplayServerEnums::CursorShape p_shape, bool force = false);
virtual void cursor_set_shape(DisplayServerEnums::CursorShape p_shape) override;
virtual DisplayServerEnums::CursorShape cursor_get_shape() const override;
virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, DisplayServerEnums::CursorShape p_shape = DisplayServerEnums::CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;
virtual void mouse_set_mode_override(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode_override() const override;
virtual void mouse_set_mode(DisplayServerEnums::MouseMode p_mode) override;
virtual DisplayServerEnums::MouseMode mouse_get_mode() const override;
virtual void mouse_set_mode_override(DisplayServerEnums::MouseMode p_mode) override;
virtual DisplayServerEnums::MouseMode mouse_get_mode_override() const override;
virtual void mouse_set_mode_override_enabled(bool p_override_enabled) override;
virtual bool mouse_is_mode_override_enabled() const override;
static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
static DisplayServer *create_func(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error);
static Vector<String> get_rendering_drivers_func();
static void register_android_driver();
#ifdef VULKAN_ENABLED
static bool check_vulkan_global_context(bool p_vulkan_requirements_met);
static void free_vulkan_global_context();
#endif
void reset_window();
void notify_surface_changed(int p_width, int p_height);
void notify_application_paused();
@ -262,6 +269,11 @@ public:
virtual bool is_window_transparency_available() const override;
DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
virtual bool is_in_pip_mode(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void pip_mode_enter(DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void pip_mode_set_aspect_ratio(int p_numerator, int p_denominator, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
virtual void pip_mode_set_auto_enter_on_background(bool p_auto_enter_on_background, DisplayServerEnums::WindowID p_window = DisplayServerEnums::MAIN_WINDOW_ID) override;
DisplayServerAndroid(const String &p_rendering_driver, DisplayServerEnums::WindowMode p_mode, DisplayServerEnums::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, DisplayServerEnums::Context p_context, int64_t p_parent_window, Error &r_error);
~DisplayServerAndroid();
};

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" api_type="editor" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Exporter for Android.
</brief_description>
@ -11,16 +11,6 @@
<link title="Android plugins documentation index">$DOCS_URL/tutorials/platform/index.html</link>
</tutorials>
<members>
<member name="apk_expansion/SALT" type="String" setter="" getter="">
Array of random bytes that the licensing policy uses to create an [url=https://developer.android.com/google/play/licensing/adding-licensing#impl-Obfuscator]Obfuscator[/url].
</member>
<member name="apk_expansion/enable" type="bool" setter="" getter="">
If [code]true[/code], project resources are stored in the separate APK expansion file, instead of the APK.
[b]Note:[/b] APK expansion should be enabled to use PCK encryption. See [url=https://developer.android.com/google/play/expansion-files]APK Expansion Files[/url]
</member>
<member name="apk_expansion/public_key" type="String" setter="" getter="">
Base64 encoded RSA public key for your publisher account, available from the profile page on the "Google Play Console".
</member>
<member name="architectures/arm64-v8a" type="bool" setter="" getter="">
If [code]true[/code], [code]arm64[/code] binaries are included into exported project.
</member>
@ -632,6 +622,22 @@
If [code]true[/code], shaders will be compiled and embedded in the application. This option is only supported when using the Forward+ or Mobile renderers.
[b]Note:[/b] When exporting as a dedicated server, the shader baker is always disabled since no rendering is performed.
</member>
<member name="splash_screen/background_color" type="Color" setter="" getter="">
The background color used for the system splash screen window.
If not set, it will fallback to [member EditorExportPlatformAndroid.launcher_icons/adaptive_background_432x432].
[b]Note:[/b] This is only applied if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is enabled.
</member>
<member name="splash_screen/branding_image" type="String" setter="" getter="">
System splash screen branding image file. If left empty, no branding image will be used. See [url=https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions]splash-screen dimensions[/url].
[b]Note:[/b] Can be used to set an image to be shown at the bottom of the splash screen.
</member>
<member name="splash_screen/disable_godot_boot_splash" type="bool" setter="" getter="">
If [code]true[/code], Godot's boot splash will not be shown, and the system boot splash will remain visible for a longer time, until the mainloop starts.
</member>
<member name="splash_screen/icon" type="String" setter="" getter="">
System splash screen icon file. If left empty, it will fall back to [member EditorExportPlatformAndroid.launcher_icons/adaptive_foreground_432x432]. See [url=https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions]splash-screen dimensions[/url].
[b]Note:[/b] You can provide an [url=https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable]AnimatedVectorDrawable (AVD)[/url] XML. However, the XML file will only be used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is enabled. If not, it will fall back to [member EditorExportPlatformAndroid.launcher_icons/adaptive_background_432x432].
</member>
<member name="user_data_backup/allow" type="bool" setter="" getter="">
If [code]true[/code], allows the application to participate in the backup and restore infrastructure.
</member>

View file

@ -30,11 +30,14 @@
#include "editor_utils_jni.h"
#ifdef TOOLS_ENABLED
#include "jni_utils.h"
#ifdef TOOLS_ENABLED
#include "core/os/os.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/editor_node.h"
#include "editor/gui/editor_title_bar.h"
#include "editor/run/editor_run_bar.h"
#include "main/main.h"
#endif
@ -92,4 +95,15 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runSc
}
#endif
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_toggleTitleBar(JNIEnv *p_env, jclass, jboolean p_visible) {
#ifdef TOOLS_ENABLED
if (EditorNode::get_singleton() != nullptr) {
EditorTitleBar *title_bar = EditorNode::get_singleton()->get_title_bar();
if (title_bar != nullptr) {
title_bar->call_deferred("set_visible", p_visible);
}
}
#endif
}
}

View file

@ -34,4 +34,5 @@
extern "C" {
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runScene(JNIEnv *p_env, jclass, jstring p_scene, jobjectArray p_scene_args);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_toggleTitleBar(JNIEnv *p_env, jclass, jboolean p_enable);
}

View file

@ -90,6 +90,24 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_set
#endif
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionAvoidLocked(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
game_view_plugin->get_debugger()->set_selection_avoid_locked(enabled);
}
#endif
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionPreferGroup(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin();
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
game_view_plugin->get_debugger()->set_selection_prefer_group(enabled);
}
#endif
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
#ifdef TOOLS_ENABLED
GameViewPlugin *game_view_plugin = _get_game_view_plugin();

View file

@ -38,6 +38,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nex
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionAvoidLocked(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionPreferGroup(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);

View file

@ -31,14 +31,15 @@
#ifdef ANDROID_ENABLED
#include "android_editor_gradle_runner.h"
#include "../java_godot_wrapper.h"
#include "../os_android.h"
#include "core/object/callable_mp.h"
#include "editor/editor_interface.h"
#include "editor/settings/editor_settings.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/rich_text_label.h"
#include "../java_godot_wrapper.h"
#include "../os_android.h"
void AndroidEditorGradleRunner::run_gradle(const String &p_project_path, const String &p_build_path, const String &p_output_path, const String &p_export_format, const List<String> &p_gradle_build_args, const List<String> &p_gradle_copy_args) {
project_path = p_project_path;
build_path = p_build_path;

View file

@ -32,6 +32,7 @@
#include "export_plugin.h"
#include "core/object/class_db.h"
#include "core/os/os.h"
#include "editor/export/editor_export.h"
#include "editor/file_system/editor_paths.h"

View file

@ -36,23 +36,24 @@
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/io/image_loader.h"
#include "core/io/json.h"
#include "core/io/marshalls.h"
#include "core/math/random_pcg.h"
#include "core/os/os.h"
#include "core/os/shared_object.h"
#include "core/string/translation_server.h"
#include "core/version.h"
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/export/editor_export.h"
#include "editor/export/editor_export_plugin.h"
#include "editor/export/export_template_manager.h"
#include "editor/file_system/editor_paths.h"
#include "editor/import/resource_importer_texture_settings.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "main/splash.gen.h"
#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For mono.
#include "modules/modules_enabled.gen.h" // IWYU pragma: keep. For mono.
#include "modules/svg/image_loader_svg.h"
#ifdef MODULE_MONO_ENABLED
@ -60,11 +61,16 @@
#endif
#ifdef ANDROID_ENABLED
#include "../java_godot_wrapper.h"
#include "../os_android.h"
#include "android_editor_gradle_runner.h"
#endif
#ifndef ANDROID_ENABLED
#include "core/object/callable_mp.h"
#include "editor/editor_log.h"
#include "editor/editor_string_names.h"
#endif
static const char *ANDROID_PERMS[] = {
"ACCESS_CHECKIN_PROPERTIES",
"ACCESS_COARSE_LOCATION",
@ -238,6 +244,14 @@ static const String ICON_XML_TEMPLATE =
static const String ICON_XML_PATH = "res/mipmap-anydpi-v26/icon.xml";
static const String THEMED_ICON_XML_PATH = "res/mipmap-anydpi-v26/themed_icon.xml";
static const String ANDROID_SPLASH_ICON_PATH = "res/drawable/splash_icon.webp";
static const String ANDROID_SPLASH_BRANDING_IMAGE_PATH = "res/drawable/splash_branding_image.webp";
static const char *DISABLE_GODOT_SPLASH_OPTION = PNAME("splash_screen/disable_godot_boot_splash");
static const char *ANDROID_SPLASH_ICON_OPTION = PNAME("splash_screen/icon");
static const char *ANDROID_SPLASH_BACKGROUND_COLOR_OPTION = PNAME("splash_screen/background_color");
static const char *ANDROID_SPLASH_BRANDING_IMAGE_OPTION = PNAME("splash_screen/branding_image");
static const int ICON_DENSITIES_COUNT = 6;
static const char *LAUNCHER_ICON_OPTION = PNAME("launcher_icons/main_192x192");
static const char *LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION = PNAME("launcher_icons/adaptive_foreground_432x432");
@ -287,7 +301,8 @@ static const char *APK_ASSETS_DIRECTORY = "src/main/assets";
static const char *AAB_ASSETS_DIRECTORY = "assetPackInstallTime/src/main/assets";
static const int DEFAULT_MIN_SDK_VERSION = 24; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
static const int DEFAULT_TARGET_SDK_VERSION = 35; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
static const int VULKAN_MIN_SDK_VERSION = 29; // Minimum recommended sdk version for Vulkan 1.1 support. See https://developer.android.com/games/develop/vulkan/native-engine-support#recommendations
static const int DEFAULT_TARGET_SDK_VERSION = 36; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
#ifndef ANDROID_ENABLED
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
@ -465,24 +480,30 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
void EditorExportPlatformAndroid::_update_preset_status() {
const int preset_count = EditorExport::get_singleton()->get_export_preset_count();
bool has_runnable = false;
for (int i = 0; i < preset_count; i++) {
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
if (preset->get_platform() == this && preset->is_runnable()) {
has_runnable = true;
break;
}
}
bool has_runnable = EditorExport::get_singleton()->get_runnable_preset_for_platform(this).is_valid();
if (has_runnable) {
has_runnable_preset.set();
_start_check_for_changes_poll_thread();
} else {
has_runnable_preset.clear();
_stop_check_for_changes_poll_thread();
}
devices_changed.set();
}
void EditorExportPlatformAndroid::_start_check_for_changes_poll_thread() {
quit_request.clear();
if (!check_for_changes_thread.is_started()) {
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
}
}
void EditorExportPlatformAndroid::_stop_check_for_changes_poll_thread() {
quit_request.set();
if (check_for_changes_thread.is_started()) {
check_for_changes_thread.wait_to_finish();
}
}
#endif
String EditorExportPlatformAndroid::get_project_name(const Ref<EditorExportPreset> &p_preset, const String &p_name) const {
@ -821,7 +842,12 @@ Error EditorExportPlatformAndroid::save_apk_file(const Ref<EditorExportPreset> &
return err;
}
const String dst_path = String("assets/") + simplified_path.trim_prefix("res://");
String dst_path;
if (ed->pd.salt.length() == 32) {
dst_path = String("assets/") + (simplified_path + ed->pd.salt).sha256_text();
} else {
dst_path = String("assets/") + simplified_path.trim_prefix("res://");
}
print_verbose("Saving project files from " + simplified_path + " into " + dst_path);
store_in_apk(ed, dst_path, enc_data, _should_compress_asset(simplified_path, enc_data) ? Z_DEFLATED : 0);
@ -981,7 +1007,7 @@ void EditorExportPlatformAndroid::_get_manifest_info(const Ref<EditorExportPrese
}
if (_uses_vulkan(p_preset)) {
// Require vulkan hardware level 1 support
// Optionally request vulkan hardware level 1 support.
FeatureInfo vulkan_level = {
"android.hardware.vulkan.level", // name
false, // required
@ -989,11 +1015,12 @@ void EditorExportPlatformAndroid::_get_manifest_info(const Ref<EditorExportPrese
};
r_features.append(vulkan_level);
// Require vulkan version 1.0
// Require vulkan version 1.1 if fallback_to_opengl3 is disabled.
bool vulkan_1_1_required = !GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
FeatureInfo vulkan_version = {
"android.hardware.vulkan.version", // name
true, // required
"0x400003" // version - Encoded value for api version 1.0
vulkan_1_1_required, // required
"0x401000" // version - Encoded value for api version 1.1
};
r_features.append(vulkan_version);
}
@ -1065,7 +1092,8 @@ bool EditorExportPlatformAndroid::_is_transparency_allowed(const Ref<EditorExpor
}
void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset> &p_preset) {
const String themes_xml_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join("res/values/themes.xml");
String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
const String themes_xml_path = gradle_build_dir.path_join("res/values/themes.xml");
if (!FileAccess::exists(themes_xml_path)) {
print_error("res/values/themes.xml does not exist.");
@ -1085,8 +1113,24 @@ void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset>
}
Dictionary splash_theme_attributes;
splash_theme_attributes["android:windowSplashScreenBackground"] = "@mipmap/icon_background";
splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@mipmap/icon_foreground";
Color color = p_preset->get(ANDROID_SPLASH_BACKGROUND_COLOR_OPTION);
if (color == Color()) {
splash_theme_attributes["android:windowSplashScreenBackground"] = "@mipmap/icon_background";
} else {
splash_theme_attributes["android:windowSplashScreenBackground"] = "#" + color.to_html(false);
}
splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@drawable/splash_icon";
String splash_icon_path = p_preset->get(ANDROID_SPLASH_ICON_OPTION);
if (!splash_icon_path.is_empty() && splash_icon_path.get_extension() == "xml") {
Vector<uint8_t> data = FileAccess::get_file_as_bytes(splash_icon_path);
store_file_at_path(gradle_build_dir.path_join("res/drawable/splash_icon_vector.xml"), data);
splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@drawable/splash_icon_vector";
}
String splash_branding_image_path = p_preset->get(ANDROID_SPLASH_BRANDING_IMAGE_OPTION);
if (!splash_branding_image_path.is_empty()) {
splash_theme_attributes["android:windowSplashScreenBrandingImage"] = "@drawable/splash_branding_image";
}
splash_theme_attributes["postSplashScreenTheme"] = "@style/GodotAppMainTheme";
splash_theme_attributes["android:windowIsTranslucent"] = bool_to_string(transparency_allowed);
@ -1204,7 +1248,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
String package_name = p_preset->get("package/unique_name");
const int screen_orientation =
_get_android_orientation_value(DisplayServer::ScreenOrientation(int(get_project_setting(p_preset, "display/window/handheld/orientation"))));
_get_android_orientation_value(DisplayServerEnums::ScreenOrientation(int(get_project_setting(p_preset, "display/window/handheld/orientation"))));
bool screen_support_small = p_preset->get("screen/support_small");
bool screen_support_normal = p_preset->get("screen/support_normal");
@ -1878,7 +1922,7 @@ void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_n
memcpy(p_data.ptrw(), buffer.ptr(), p_data.size());
}
void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome) {
void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome, Ref<Image> &splash_icon, Ref<Image> &splash_branding_image) {
String project_icon_path = get_project_setting(p_preset, "application/config/icon");
Error err = OK;
@ -1922,15 +1966,56 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &
print_verbose("Loading adaptive monochrome icon from " + path);
monochrome = _load_icon_or_splash_image(path, &err);
}
// Splash icon: user selection -> adaptive foreground icon (user selection -> regular icon).
path = static_cast<String>(p_preset->get(ANDROID_SPLASH_ICON_OPTION)).strip_edges();
if (path.get_extension() != "xml") {
// XML file is handled in _fix_themes_xml().
print_verbose("Loading splash screen icon from " + path);
bool loaded_ok = false;
if (!path.is_empty()) {
splash_icon = _load_icon_or_splash_image(path, &err);
loaded_ok = (err == OK && splash_icon.is_valid() && !splash_icon->is_empty());
}
if (!loaded_ok) {
print_verbose("- falling back to using the adaptive foreground icon");
splash_icon = foreground;
}
}
// Splash branding image: user selection -> No image.
// - Gradle build: If the path is empty, the splash theme attribute is not added in _fix_themes_xml().
// - Legacy build: If the path is empty, a transparent image is used.
path = static_cast<String>(p_preset->get(ANDROID_SPLASH_BRANDING_IMAGE_OPTION)).strip_edges();
if (!path.is_empty()) {
print_verbose("Loading splash screen branding image from " + path);
splash_branding_image = _load_icon_or_splash_image(path, &err);
}
}
void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
const Ref<Image> &p_main_image,
const Ref<Image> &p_foreground,
const Ref<Image> &p_background,
const Ref<Image> &p_monochrome) {
const Ref<Image> &p_monochrome,
const Ref<Image> &p_splash_icon,
const Ref<Image> &p_splash_branding_image) {
String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
// Copy splash screen icon to the drawable directory.
// This is only for png/webp/svg file; XML file is handled in _fix_themes_xml().
if (p_splash_icon.is_valid() && !p_splash_icon->is_empty()) {
print_verbose("Copying splash screen icon into " + ANDROID_SPLASH_ICON_PATH);
Vector<uint8_t> buffer = p_splash_icon->save_webp_to_buffer();
store_file_at_path(gradle_build_dir.path_join(ANDROID_SPLASH_ICON_PATH), buffer);
}
if (p_splash_branding_image.is_valid() && !p_splash_branding_image->is_empty()) {
print_verbose("Copying splash screen branding image into " + ANDROID_SPLASH_BRANDING_IMAGE_PATH);
Vector<uint8_t> buffer = p_splash_branding_image->save_webp_to_buffer();
store_file_at_path(gradle_build_dir.path_join(ANDROID_SPLASH_BRANDING_IMAGE_PATH), buffer);
}
String monochrome_tag = "";
// Prepare images to be resized for the icons. If some image ends up being uninitialized,
@ -2003,13 +2088,7 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres
String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {
if (p_preset) {
if (p_name == ("apk_expansion/public_key")) {
bool apk_expansion = p_preset->get("apk_expansion/enable");
String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
if (apk_expansion && apk_expansion_pkey.is_empty()) {
return TTR("Invalid public key for APK expansion.");
}
} else if (p_name == "package/unique_name") {
if (p_name == "package/unique_name") {
String pn = p_preset->get("package/unique_name");
String pn_err;
@ -2027,11 +2106,6 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
if (!enabled_deprecated_plugins_names.is_empty() && !gradle_build_enabled) {
return TTR("\"Use Gradle Build\" must be enabled to use the plugins.");
}
#ifdef ANDROID_ENABLED
if (gradle_build_enabled) {
return TTR("Support for \"Use Gradle Build\" on Android is currently experimental.");
}
#endif // ANDROID_ENABLED
} else if (p_name == "gradle_build/compress_native_libraries") {
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) {
@ -2063,7 +2137,7 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
String min_sdk_str = p_preset->get("gradle_build/min_sdk");
int min_sdk_int = DEFAULT_MIN_SDK_VERSION;
int min_sdk_int = _uses_vulkan(Ref<EditorExportPreset>(p_preset)) ? VULKAN_MIN_SDK_VERSION : DEFAULT_MIN_SDK_VERSION;
if (min_sdk_str.is_valid_int()) {
min_sdk_int = min_sdk_str.to_int();
}
@ -2124,7 +2198,8 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));
// Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).
// This implies doing validation that the string is a proper int.
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), "", false, true));
const String min_sdk_placeholder = _uses_vulkan(nullptr) ? vformat("%d (Vulkan default)", VULKAN_MIN_SDK_VERSION) : vformat("%d (default)", DEFAULT_MIN_SDK_VERSION);
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, min_sdk_placeholder), "", false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "gradle_build/custom_theme_attributes", PROPERTY_HINT_DICTIONARY_TYPE, "String;String"), Dictionary()));
@ -2192,14 +2267,15 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "screen/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, DISABLE_GODOT_SPLASH_OPTION), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, ANDROID_SPLASH_ICON_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.xml"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, ANDROID_SPLASH_BRANDING_IMAGE_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, ANDROID_SPLASH_BACKGROUND_COLOR_OPTION, PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args", PROPERTY_HINT_NONE, "monospace"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT, "monospace,no_wrap"), "", false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray()));
const char **perms = ANDROID_PERMS;
@ -2227,9 +2303,10 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor
p_option == "package/show_in_app_library" ||
p_option == "package/show_as_launcher_app" ||
p_option == "gesture/swipe_to_dismiss" ||
p_option == "apk_expansion/enable" ||
p_option == "apk_expansion/SALT" ||
p_option == "apk_expansion/public_key") {
p_option == DISABLE_GODOT_SPLASH_OPTION ||
p_option == ANDROID_SPLASH_ICON_OPTION ||
p_option == ANDROID_SPLASH_BACKGROUND_COLOR_OPTION ||
p_option == ANDROID_SPLASH_BRANDING_IMAGE_OPTION) {
return advanced_options_enabled;
}
if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {
@ -2373,14 +2450,14 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset,
String tmp_export_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
#define CLEANUP_AND_RETURN(m_err) \
{ \
DirAccess::remove_file_or_error(tmp_export_path); \
if (FileAccess::exists(tmp_export_path + ".idsig")) { \
#define CLEANUP_AND_RETURN(m_err) \
{ \
DirAccess::remove_file_or_error(tmp_export_path); \
if (FileAccess::exists(tmp_export_path + ".idsig")) { \
DirAccess::remove_file_or_error(tmp_export_path + ".idsig"); \
} \
return m_err; \
} \
} \
return m_err; \
} \
((void)0)
// Export to temporary APK before sending to device.
@ -3070,6 +3147,9 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit
if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
valid = false;
if (EditorNode::is_cmdline_mode()) {
err += TTR("ETC2/ASTC texture compression is required for Android export. In the Project Settings, search for 'ETC2' in the search field, or enable 'Advanced Settings', and go to Rendering > Textures > VRAM Compression to enable 'Import ETC2 ASTC'.") + "\n";
}
}
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
@ -3112,6 +3192,22 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit
err += "\n";
}
if (_uses_vulkan(p_preset)) {
String min_sdk_str = p_preset->get("gradle_build/min_sdk");
int min_sdk_int = VULKAN_MIN_SDK_VERSION;
if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
if (min_sdk_str.is_valid_int()) {
min_sdk_int = min_sdk_str.to_int();
}
}
bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
if (min_sdk_int < VULKAN_MIN_SDK_VERSION && !fallback_to_opengl3) {
// Warning only, so don't override `valid`.
err += vformat(TTR("\"Min SDK\" should be greater or equal to %d for the \"%s\" renderer."), VULKAN_MIN_SDK_VERSION, current_renderer);
err += "\n";
}
}
String package_name = p_preset->get("package/unique_name");
if (package_name.contains("$genname") && !is_project_name_valid(p_preset)) {
// Warning only, so don't override `valid`.
@ -3134,20 +3230,6 @@ List<String> EditorExportPlatformAndroid::get_binary_extensions(const Ref<Editor
return list;
}
String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
int version_code = p_preset->get("version/code");
String package_name = p_preset->get("package/unique_name");
String apk_file_name = "main." + itos(version_code) + "." + get_package_name(p_preset, package_name) + ".obb";
String fullpath = p_path.get_base_dir().path_join(apk_file_name);
return fullpath;
}
Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
Error err = save_pack(p_preset, p_debug, fullpath);
return err;
}
void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags) {
String cmdline = p_preset->get("command_line/extra_args");
Vector<String> command_line_strings = cmdline.strip_edges().split(" ");
@ -3160,18 +3242,6 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
command_line_strings.append_array(gen_export_flags(p_flags));
bool apk_expansion = p_preset->get("apk_expansion/enable");
if (apk_expansion) {
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
String apk_expansion_public_key = p_preset->get("apk_expansion/public_key");
command_line_strings.push_back("--use_apk_expansion");
command_line_strings.push_back("--apk_expansion_md5");
command_line_strings.push_back(FileAccess::get_md5(fullpath));
command_line_strings.push_back("--apk_expansion_key");
command_line_strings.push_back(apk_expansion_public_key.strip_edges());
}
#ifndef XR_DISABLED
int xr_mode_index = p_preset->get("xr_features/xr_mode");
if (xr_mode_index == XR_MODE_OPENXR) {
@ -3208,6 +3278,11 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
command_line_strings.push_back("--background_color");
command_line_strings.push_back(background_color);
bool disable_godot_splash = p_preset->get(DISABLE_GODOT_SPLASH_OPTION);
if (disable_godot_splash) {
command_line_strings.push_back("--disable_godot_splash");
}
bool debug_opengl = p_preset->get("graphics/opengl_debug");
if (debug_opengl) {
command_line_strings.push_back("--debug_opengl");
@ -3528,7 +3603,7 @@ bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExpor
return have_plugins_changed || has_build_dir_changed || first_build;
}
Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, bool p_notify) {
int export_format = int(p_preset->get("gradle_build/export_format"));
bool should_sign = p_preset->get("package/signed");
return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);
@ -3544,7 +3619,7 @@ Error EditorExportPlatformAndroid::_generate_sparse_pck_metadata(const Ref<Edito
int64_t pck_start_pos = ftmp->get_position();
uint64_t file_base_ofs = 0;
uint64_t dir_base_ofs = 0;
EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs);
EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs, p_pack_data.salt);
// Write directory.
uint64_t dir_offset = ftmp->get_position();
@ -3637,7 +3712,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";
bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG);
bool apk_expansion = p_preset->get("apk_expansion/enable");
Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
print_verbose("Exporting for Android...");
@ -3646,7 +3720,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("- export format: " + itos(export_format));
print_verbose("- sign build: " + bool_to_string(should_sign));
print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build));
print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion));
print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false));
print_verbose("- export filter: " + itos(p_preset->get_export_filter()));
print_verbose("- include filter: " + p_preset->get_include_filter());
@ -3656,8 +3729,10 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
Ref<Image> foreground;
Ref<Image> background;
Ref<Image> monochrome;
Ref<Image> splash_icon;
Ref<Image> splash_branding_image;
load_icon_refs(p_preset, main_image, foreground, background, monochrome);
load_icon_refs(p_preset, main_image, foreground, background, monochrome, splash_icon, splash_branding_image);
Vector<uint8_t> command_line_flags;
// Write command line flags into the command_line_flags variable.
@ -3668,10 +3743,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
return ERR_UNCONFIGURED;
}
if (apk_expansion) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));
return ERR_UNCONFIGURED;
}
}
if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));
@ -3730,7 +3801,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));
}
// Copies the project icon files into the appropriate Gradle project directory.
_copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome);
_copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome, splash_icon, splash_branding_image);
// Write an AndroidManifest.xml file into the Gradle project directory.
_write_tmp_manifest(p_preset, p_give_internet, p_debug);
// Modify res/values/themes.xml file.
@ -3740,47 +3811,44 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
_clear_assets_directory(p_preset);
String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);
_remove_copied_libs(gdextension_libs_path);
if (!apk_expansion) {
print_verbose("Exporting project files...");
CustomExportData user_data;
user_data.assets_directory = assets_directory;
user_data.libs_directory = gradle_build_directory.path_join("libs");
user_data.debug = p_debug;
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);
} else {
user_data.pd.path = "assets.sparsepck";
user_data.pd.use_sparse_pck = true;
err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);
Vector<uint8_t> enc_data;
err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
return err;
}
err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!"));
return err;
}
}
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
return err;
}
if (user_data.libs.size() > 0) {
Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
fa->store_string(JSON::stringify(user_data.libs, "\t"));
}
print_verbose("Exporting project files...");
CustomExportData user_data;
user_data.assets_directory = assets_directory;
user_data.libs_directory = gradle_build_directory.path_join("libs");
user_data.debug = p_debug;
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);
} else {
print_verbose("Saving apk expansion file...");
err = save_apk_expansion_file(p_preset, p_debug, p_path);
user_data.pd.path = "assets.sparsepck";
user_data.pd.use_sparse_pck = true;
if (p_preset->get_enc_directory()) {
RandomPCG rng = RandomPCG(p_preset->get_seed());
for (int i = 0; i < 32; i++) {
user_data.pd.salt += String::chr(1 + rng.rand() % 254);
}
}
err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);
Vector<uint8_t> enc_data;
err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
return err;
}
err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!"));
return err;
}
}
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
return err;
}
if (user_data.libs.size() > 0) {
Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
fa->store_string(JSON::stringify(user_data.libs, "\t"));
}
print_verbose("Storing command line flags...");
@ -3809,7 +3877,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String version_name = p_preset->get_version("version/name");
String min_sdk_version = p_preset->get("gradle_build/min_sdk");
if (!min_sdk_version.is_valid_int()) {
min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION);
min_sdk_version = _uses_vulkan(p_preset) ? itos(VULKAN_MIN_SDK_VERSION) : itos(DEFAULT_MIN_SDK_VERSION);
}
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
if (!target_sdk_version.is_valid_int()) {
@ -4063,11 +4131,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String tmp_unaligned_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
#define CLEANUP_AND_RETURN(m_err) \
{ \
#define CLEANUP_AND_RETURN(m_err) \
{ \
DirAccess::remove_file_or_error(tmp_unaligned_path); \
return m_err; \
} \
return m_err; \
} \
((void)0)
zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
@ -4077,8 +4145,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String version_name = p_preset->get_version("version/name");
String package_name = p_preset->get("package/unique_name");
String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
Vector<ABI> invalid_abis(enabled_abis);
//To temporarily store icon xml data.
@ -4125,6 +4191,22 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
_fix_resources(p_preset, data);
}
if (file == ANDROID_SPLASH_ICON_PATH) {
if (splash_icon.is_valid() && !splash_icon->is_empty()) {
Vector<uint8_t> buffer = splash_icon->save_webp_to_buffer();
data.resize(buffer.size());
memcpy(data.ptrw(), buffer.ptr(), data.size());
}
}
if (file == ANDROID_SPLASH_BRANDING_IMAGE_PATH) {
if (splash_branding_image.is_valid() && !splash_branding_image->is_empty()) {
Vector<uint8_t> buffer = splash_branding_image->save_webp_to_buffer();
data.resize(buffer.size());
memcpy(data.ptrw(), buffer.ptr(), data.size());
}
}
if (file == THEMED_ICON_XML_PATH) {
// Store themed_icon.xml data.
themed_icon_xml_data = data;
@ -4246,29 +4328,27 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
ed.apk = unaligned_apk;
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &ed, save_apk_so);
} else {
if (apk_expansion) {
err = save_apk_expansion_file(p_preset, p_debug, p_path);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
return err;
APKExportData ed;
ed.ep = &ep;
ed.apk = unaligned_apk;
ed.pd.path = "assets.sparsepck";
ed.pd.use_sparse_pck = true;
if (p_preset->get_enc_directory()) {
RandomPCG rng = RandomPCG(p_preset->get_seed());
for (int i = 0; i < 32; i++) {
ed.pd.salt += String::chr(1 + rng.rand() % 254);
}
} else {
APKExportData ed;
ed.ep = &ep;
ed.apk = unaligned_apk;
ed.pd.path = "assets.sparsepck";
ed.pd.use_sparse_pck = true;
err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);
Vector<uint8_t> enc_data;
err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
return err;
}
store_in_apk(&ed, "assets/assets.sparsepck", enc_data, 0);
}
err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);
Vector<uint8_t> enc_data;
err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
return err;
}
store_in_apk(&ed, "assets/assets.sparsepck", enc_data, 0);
}
if (err != OK) {
@ -4419,7 +4499,6 @@ void EditorExportPlatformAndroid::initialize() {
devices_changed.set();
_create_editor_debug_keystore_if_needed();
_update_preset_status();
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
use_scrcpy = EditorSettings::get_singleton()->get_project_metadata("android", "use_scrcpy", false);
#else // ANDROID_ENABLED
android_editor_gradle_runner = memnew(AndroidEditorGradleRunner);
@ -4429,10 +4508,7 @@ void EditorExportPlatformAndroid::initialize() {
EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
#ifndef ANDROID_ENABLED
quit_request.set();
if (check_for_changes_thread.is_started()) {
check_for_changes_thread.wait_to_finish();
}
_stop_check_for_changes_poll_thread();
#else
if (android_editor_gradle_runner) {
memdelete(android_editor_gradle_runner);

View file

@ -38,7 +38,6 @@
#include "core/io/image.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
#include "editor/export/editor_export_platform.h"
class ImageTexture;
@ -108,6 +107,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
SafeFlag has_runnable_preset;
static void _check_for_changes_poll_thread(void *ud);
void _start_check_for_changes_poll_thread();
void _stop_check_for_changes_poll_thread();
void _update_preset_status();
#else // ANDROID_ENABLED
AndroidEditorGradleRunner *android_editor_gradle_runner = nullptr;
@ -186,13 +187,15 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
void _process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data);
void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome);
void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome, Ref<Image> &splash_icon, Ref<Image> &splash_branding_image);
void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
const Ref<Image> &p_main_image,
const Ref<Image> &p_foreground,
const Ref<Image> &p_background,
const Ref<Image> &p_monochrome);
const Ref<Image> &p_monochrome,
const Ref<Image> &p_splash_icon,
const Ref<Image> &p_splash_branding_image);
static void _create_editor_debug_keystore_if_needed();
@ -268,10 +271,6 @@ public:
bool _is_clean_build_required(const Ref<EditorExportPreset> &p_preset);
String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path);
Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path);
void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags);
Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep);
@ -283,7 +282,7 @@ public:
static String join_list(const List<String> &p_parts, const String &p_separator);
static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch);
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override;
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0, bool p_notify = true) override;
Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags);

View file

@ -32,6 +32,8 @@
#ifndef DISABLE_DEPRECATED
#include "core/config/project_settings.h"
/*
* Set of prebuilt plugins.
* Currently unused, this is just for future reference:

View file

@ -32,7 +32,6 @@
#ifndef DISABLE_DEPRECATED
#include "core/config/project_settings.h"
#include "core/io/config_file.h"
#include "core/string/ustring.h"

View file

@ -30,44 +30,50 @@
#include "gradle_export_util.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/os/os.h"
#include "core/string/translation_server.h"
#include "editor/export/editor_export.h"
#include "editor/export/editor_export_plugin.h"
#include "modules/regex/regex.h"
int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {
int _get_android_orientation_value(DisplayServerEnums::ScreenOrientation screen_orientation) {
switch (screen_orientation) {
case DisplayServer::SCREEN_PORTRAIT:
case DisplayServerEnums::SCREEN_PORTRAIT:
return 1;
case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
case DisplayServerEnums::SCREEN_REVERSE_LANDSCAPE:
return 8;
case DisplayServer::SCREEN_REVERSE_PORTRAIT:
case DisplayServerEnums::SCREEN_REVERSE_PORTRAIT:
return 9;
case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
case DisplayServerEnums::SCREEN_SENSOR_LANDSCAPE:
return 11;
case DisplayServer::SCREEN_SENSOR_PORTRAIT:
case DisplayServerEnums::SCREEN_SENSOR_PORTRAIT:
return 12;
case DisplayServer::SCREEN_SENSOR:
case DisplayServerEnums::SCREEN_SENSOR:
return 13;
case DisplayServer::SCREEN_LANDSCAPE:
case DisplayServerEnums::SCREEN_LANDSCAPE:
default:
return 0;
}
}
String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation) {
String _get_android_orientation_label(DisplayServerEnums::ScreenOrientation screen_orientation) {
switch (screen_orientation) {
case DisplayServer::SCREEN_PORTRAIT:
case DisplayServerEnums::SCREEN_PORTRAIT:
return "portrait";
case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
case DisplayServerEnums::SCREEN_REVERSE_LANDSCAPE:
return "reverseLandscape";
case DisplayServer::SCREEN_REVERSE_PORTRAIT:
case DisplayServerEnums::SCREEN_REVERSE_PORTRAIT:
return "reversePortrait";
case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
case DisplayServerEnums::SCREEN_SENSOR_LANDSCAPE:
return "userLandscape";
case DisplayServer::SCREEN_SENSOR_PORTRAIT:
case DisplayServerEnums::SCREEN_SENSOR_PORTRAIT:
return "userPortrait";
case DisplayServer::SCREEN_SENSOR:
case DisplayServerEnums::SCREEN_SENSOR:
return "fullUser";
case DisplayServer::SCREEN_LANDSCAPE:
case DisplayServerEnums::SCREEN_LANDSCAPE:
default:
return "landscape";
}
@ -182,7 +188,12 @@ Error rename_and_store_file_in_gradle_project(const Ref<EditorExportPreset> &p_p
return err;
}
const String dst_path = export_data->assets_directory + String("/") + simplified_path.trim_prefix("res://");
String dst_path;
if (export_data->pd.salt.length() == 32) {
dst_path = export_data->assets_directory + String("/") + (simplified_path + export_data->pd.salt).sha256_text();
} else {
dst_path = export_data->assets_directory + String("/") + simplified_path.trim_prefix("res://");
}
print_verbose("Saving project files from " + simplified_path + " into " + dst_path);
err = store_file_at_path(dst_path, enc_data);
@ -301,7 +312,7 @@ String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, con
}
// Update the GodotApp activity tag.
String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(p_export_platform->get_project_setting(p_preset, "display/window/handheld/orientation"))));
String orientation = _get_android_orientation_label(DisplayServerEnums::ScreenOrientation(int(p_export_platform->get_project_setting(p_preset, "display/window/handheld/orientation"))));
String manifest_activity_text = vformat(
" <activity android:name=\".GodotApp\" "
"tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" "

View file

@ -31,13 +31,8 @@
#pragma once
#include "core/crypto/crypto_core.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
#include "editor/export/editor_export.h"
#include "editor/export/editor_export_platform.h"
#include "servers/display/display_server.h"
#include "servers/display/display_server_enums.h"
const String GODOT_PROJECT_NAME_XML_STRING = R"(<?xml version="1.0" encoding="utf-8"?>
<!--WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
@ -77,9 +72,9 @@ struct MetadataInfo {
String value;
};
int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation);
int _get_android_orientation_value(DisplayServerEnums::ScreenOrientation screen_orientation);
String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation);
String _get_android_orientation_label(DisplayServerEnums::ScreenOrientation screen_orientation);
int _get_app_category_value(int category_index);

View file

@ -30,9 +30,10 @@
#include "file_access_android.h"
#include "core/string/print_string.h"
#include "thread_jandroid.h"
#include "core/string/print_string.h"
#include <android/asset_manager_jni.h>
AAssetManager *FileAccessAndroid::asset_manager = nullptr;

View file

@ -35,7 +35,6 @@
#include <android/asset_manager.h>
#include <android/log.h>
#include <jni.h>
#include <cstdio>
class FileAccessAndroid : public FileAccess {
GDSOFTCLASS(FileAccessAndroid, FileAccess);

View file

@ -32,7 +32,6 @@
#include "thread_jandroid.h"
#include "core/os/os.h"
#include "core/templates/local_vector.h"
#include <unistd.h>

View file

@ -30,10 +30,10 @@
#pragma once
#include "java_godot_lib_jni.h"
#include "core/io/file_access.h"
#include <jni.h>
class FileAccessFilesystemJAndroid : public FileAccess {
GDSOFTCLASS(FileAccessFilesystemJAndroid, FileAccess);
static jobject file_access_handler;

View file

@ -27,8 +27,6 @@ allprojects {
}
configurations {
// Initializes a placeholder for the devImplementation dependency configuration.
devImplementation {}
// Initializes a placeholder for the monoImplementation dependency configuration.
monoImplementation {}
}
@ -53,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
}
}
}
@ -206,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.
@ -251,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']
}
@ -330,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"))
}

View file

@ -1,23 +1,23 @@
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',

View file

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

View file

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

View file

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

View file

@ -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": "",

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ var _test_assert_failures := 0
func __exec_test(test_func: Callable):
_test_started += 1
var ret = test_func.call()
var ret = await test_func.call()
if ret == true:
_test_completed += 1

View file

@ -12,7 +12,7 @@ func run_tests():
# Scoped storage: Testing access to Downloads and Documents directory.
var version = JavaClassWrapper.wrap("android.os.Build$VERSION")
if version.SDK_INT >= 29:
if version.SDK_INT >= 30:
__exec_test(test_downloads_dir_access)
__exec_test(test_documents_dir_access)

View file

@ -20,6 +20,10 @@ 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))
@ -169,3 +173,34 @@ func test_callable() -> bool:
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
@ -142,6 +147,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
internal const val GAME_MENU_ACTION_SET_NODE_TYPE = "setNodeType"
internal const val GAME_MENU_ACTION_SET_SELECT_MODE = "setSelectMode"
internal const val GAME_MENU_ACTION_SET_SELECTION_VISIBLE = "setSelectionVisible"
internal const val GAME_MENU_ACTION_SET_SELECTION_AVOID_LOCKED = "setSelectionAvoidLocked"
internal const val GAME_MENU_ACTION_SET_SELECTION_PREFER_GROUP = "setSelectionPreferGroup"
internal const val GAME_MENU_ACTION_SET_CAMERA_OVERRIDE = "setCameraOverride"
internal const val GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE = "setCameraManipulateMode"
internal const val GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION = "resetCamera2DPosition"
@ -152,6 +159,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 +206,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 +268,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 +278,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 +341,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 +452,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 +460,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
setOverrideVolumeButtons(overrideVolumeButtonsEnabled)
enableHapticFeedback(hapticEnabled)
}
}
}
@ -404,7 +513,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 +715,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 +725,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 +919,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 +933,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() {
@ -845,6 +982,14 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
toggleSelectionVisibility(enabled)
}
GAME_MENU_ACTION_SET_SELECTION_AVOID_LOCKED -> {
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
toggleSelectionAvoidLocked(enabled)
}
GAME_MENU_ACTION_SET_SELECTION_PREFER_GROUP -> {
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
toggleSelectionPreferGroup(enabled)
}
GAME_MENU_ACTION_SET_CAMERA_OVERRIDE -> {
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
overrideCamera(enabled)
@ -905,6 +1050,20 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
}
}
override fun toggleSelectionAvoidLocked(enabled: Boolean) {
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SELECTION_AVOID_LOCKED, enabled)
godot?.runOnRenderThread {
GameMenuUtils.setSelectionAvoidLocked(enabled)
}
}
override fun toggleSelectionPreferGroup(enabled: Boolean) {
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SELECTION_PREFER_GROUP, enabled)
godot?.runOnRenderThread {
GameMenuUtils.setSelectionPreferGroup(enabled)
}
}
override fun overrideCamera(enabled: Boolean) {
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_CAMERA_OVERRIDE, enabled)
godot?.runOnRenderThread {

View file

@ -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
@ -173,6 +130,22 @@ open class GodotGame : BaseGodotGame() {
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun toggleSelectionAvoidLocked(enabled: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_AVOID_LOCKED)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun toggleSelectionPreferGroup(enabled: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_PREFER_GROUP)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun overrideCamera(enabled: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_OVERRIDE)
@ -258,7 +231,7 @@ open class GodotGame : BaseGodotGame() {
override fun isCloseButtonEnabled() = !isHorizonOSDevice(applicationContext)
override fun isPiPButtonEnabled() = hasPiPSystemFeature()
override fun isPiPButtonEnabled() = isPiPModeSupported()
override fun isMenuBarCollapsable() = true

View file

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

View file

@ -36,9 +36,11 @@ 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
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.RadioButton
import androidx.core.content.edit
@ -47,6 +49,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 +68,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'.
@ -96,6 +99,8 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
fun suspendGame(suspended: Boolean)
fun dispatchNextFrame()
fun toggleSelectionVisibility(enabled: Boolean)
fun toggleSelectionAvoidLocked(enabled: Boolean)
fun toggleSelectionPreferGroup(enabled: Boolean)
fun overrideCamera(enabled: Boolean)
fun selectRuntimeNode(nodeType: NodeType)
fun selectRuntimeNodeSelectMode(selectMode: SelectMode)
@ -109,16 +114,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 +133,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)
}
@ -137,9 +145,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
private val setTimeScaleButton: Button? by lazy {
view?.findViewById<Button>(R.id.game_menu_set_time_scale_button)
}
private val resetTimeScaleButton: View? by lazy {
view?.findViewById(R.id.game_menu_reset_time_scale_button)
}
private val unselectNodesButton: RadioButton? by lazy {
view?.findViewById(R.id.game_menu_unselect_nodes_button)
}
@ -149,15 +154,15 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
private val select3DNodesButton: RadioButton? by lazy {
view?.findViewById(R.id.game_menu_select_3d_nodes_button)
}
private val guiVisibilityButton: View? by lazy {
view?.findViewById(R.id.game_menu_gui_visibility_button)
}
private val toolSelectButton: RadioButton? by lazy {
view?.findViewById(R.id.game_menu_tool_select_button)
}
private val listSelectButton: RadioButton? by lazy {
view?.findViewById(R.id.game_menu_list_select_button)
}
private val selectDropdownButton: ImageButton? by lazy {
view?.findViewById(R.id.game_menu_select_dropdown_button)
}
private val audioMuteButton: View? by lazy {
view?.findViewById(R.id.game_menu_audio_mute_button)
}
@ -181,13 +186,35 @@ 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)
}
}
}
private val selectDropdownMenu: PopupMenu by lazy {
PopupMenu(context, selectDropdownButton).apply {
inflate(R.menu.select_dropdown_menu)
setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) {
R.id.menu_show_selection_visibility -> {
item.isChecked = !item.isChecked
menuListener?.toggleSelectionVisibility(item.isChecked)
}
R.id.menu_dont_select_locked_nodes -> {
item.isChecked = !item.isChecked
menuListener?.toggleSelectionAvoidLocked(item.isChecked)
}
R.id.menu_select_group_over_children -> {
item.isChecked = !item.isChecked
menuListener?.toggleSelectionPreferGroup(item.isChecked)
}
}
true
}
}
}
private val timeScaleMenu: PopupMenu by lazy {
PopupMenu(context, setTimeScaleButton).apply {
inflate(R.menu.time_scale_options)
@ -263,6 +290,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 +298,8 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
isFullScreenButtonEnabled ||
isCloseButtonEnabled ||
isPiPButtonEnabled ||
isMenuBarCollapsable
isMenuBarCollapsable ||
isDragButtonEnabled
collapseMenuButton?.apply {
isVisible = isMenuBarCollapsable
@ -278,6 +307,13 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
collapseGameMenu()
}
}
dragButton?.apply {
isVisible = isDragButtonEnabled
setOnTouchListener { v, event ->
menuListener?.dragGameWindow(v, event) == true
}
}
fullscreenButton?.apply{
isVisible = isFullScreenButtonEnabled
setOnClickListener {
@ -322,13 +358,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
}
}
resetTimeScaleButton?.apply {
setOnClickListener {
menuListener?.resetTimeScale()
setTimeScaleButton?.text = "1.0x"
}
}
unselectNodesButton?.apply{
setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
@ -350,13 +379,6 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
}
}
}
guiVisibilityButton?.apply{
setOnClickListener {
val isActivated = !it.isActivated
menuListener?.toggleSelectionVisibility(!isActivated)
it.isActivated = isActivated
}
}
toolSelectButton?.apply{
setOnCheckedChangeListener { buttonView, isChecked ->
@ -372,6 +394,9 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
}
}
}
selectDropdownButton?.setOnClickListener {
selectDropdownMenu.show()
}
audioMuteButton?.apply{
setOnClickListener {
val isActivated = !it.isActivated
@ -405,12 +430,16 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
select2DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_2D
select3DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_3D
guiVisibilityButton?.isActivated = !gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_VISIBLE, true)
val selectMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECT_MODE) as GameMenuListener.SelectMode? ?: GameMenuListener.SelectMode.SINGLE
toolSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.SINGLE
listSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.LIST
selectDropdownMenu.menu.apply {
findItem(R.id.menu_show_selection_visibility)?.isChecked = gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_VISIBLE, true)
findItem(R.id.menu_dont_select_locked_nodes)?.isChecked = gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_AVOID_LOCKED, false)
findItem(R.id.menu_select_group_over_children)?.isChecked = gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_PREFER_GROUP, false)
}
audioMuteButton?.isActivated = gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_DEBUG_MUTE_AUDIO, false)
popupMenu.menu.apply {
@ -445,6 +474,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 +494,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 {

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="#FFFFFF" />
<item android:color="#808080" />
</selector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:tint="@color/game_menu_icons_color_state"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
</vector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1C1C1C" />
<corners android:radius="8dp" />
</shape>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<shape>
<solid android:color="#333333" />
<corners android:radius="6dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View file

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

View file

@ -2,9 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#3333b5e5" />
<corners android:radius="5dp" />
<stroke
android:width="1dp"
android:color="@android:color/holo_blue_dark" />
<solid android:color="#333333" />
<corners android:radius="6dp" />
</shape>

View file

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

View file

@ -3,6 +3,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="4dp"
android:background="@android:color/black">
<HorizontalScrollView
@ -14,142 +16,159 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="48dp"
android:paddingVertical="4dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/game_menu_suspend_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/suspend" />
android:src="@drawable/pause" />
<ImageButton
android:id="@+id/game_menu_next_frame_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/next_frame" />
<View
android:layout_width="1dp"
android:layout_height="24dp"
android:layout_marginHorizontal="4dp"
android:background="#262626" />
<Button
android:id="@+id/game_menu_set_time_scale_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:background="@drawable/game_menu_button_bg"
android:text="1.0x"/>
<ImageButton
android:id="@+id/game_menu_reset_time_scale_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/reset" />
android:text="1.0x"
android:textColor="#CCCCCC"
android:textSize="12sp"
android:paddingHorizontal="4dp"
android:minWidth="0dp"
android:minHeight="0dp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
android:background="@color/game_menu_divider_color" />
android:layout_height="24dp"
android:layout_marginHorizontal="4dp"
android:background="#262626" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_group_background"
android:padding="2dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/game_menu_unselect_nodes_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:layout_height="32dp"
android:background="@drawable/button_group_item_bg"
android:button="@null"
android:checked="true"
android:textColor="@color/button_group_item_text"
android:drawableStart="@drawable/input_event_joypad_motion"
android:padding="5dp"
android:drawablePadding="4dp"
android:paddingHorizontal="8dp"
android:textSize="12sp"
android:text="@string/game_menu_input_event_joypad_motion_label" />
<RadioButton
android:id="@+id/game_menu_select_2d_nodes_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:layout_width="54dp"
android:layout_height="32dp"
android:background="@drawable/button_group_item_bg"
android:button="@null"
android:drawableStart="@drawable/nodes_2d"
android:padding="5dp"
android:text="@string/game_menu_nodes_2d_button_label" />
android:drawablePadding="4dp"
android:paddingHorizontal="8dp"
android:text="@string/game_menu_nodes_2d_button_label"
android:textColor="@color/button_group_item_text"
android:textSize="12sp" />
<RadioButton
android:id="@+id/game_menu_select_3d_nodes_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:layout_width="54dp"
android:layout_height="32dp"
android:background="@drawable/button_group_item_bg"
android:button="@null"
android:textColor="@color/button_group_item_text"
android:drawableStart="@drawable/node_3d"
android:padding="5dp"
android:drawablePadding="4dp"
android:paddingHorizontal="8dp"
android:textSize="12sp"
android:text="@string/game_menu_node_3d_button_label" />
</RadioGroup>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
android:background="@color/game_menu_divider_color" />
<ImageButton
android:id="@+id/game_menu_gui_visibility_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/gui_visibility_selector" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
android:background="@color/game_menu_divider_color" />
android:layout_height="24dp"
android:layout_marginHorizontal="4dp"
android:background="#262626" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_group_background"
android:padding="2dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/game_menu_tool_select_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/button_group_item_bg"
android:button="@null"
android:checked="true"
android:drawableStart="@drawable/tool_select"
android:padding="15dp" />
android:paddingHorizontal="8dp" />
<RadioButton
android:id="@+id/game_menu_list_select_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/game_menu_button_bg"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/button_group_item_bg"
android:button="@null"
android:drawableStart="@drawable/list_select"
android:padding="15dp" />
android:paddingHorizontal="8dp" />
<ImageButton
android:id="@+id/game_menu_select_dropdown_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="24dp"
android:layout_height="32dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/baseline_expand_more_16"
android:padding="6dp" />
</RadioGroup>
<View
android:layout_width="1dp"
android:layout_height="24dp"
android:layout_marginHorizontal="4dp"
android:background="#262626" />
<ImageButton
android:id="@+id/game_menu_audio_mute_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/audio_player_icon_selector" />
@ -159,8 +178,8 @@
<ImageButton
android:id="@+id/game_menu_options_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/gui_tab_menu" />
@ -175,41 +194,48 @@
<ImageButton
android:id="@+id/game_menu_collapse_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_selected_button_bg"
android:src="@drawable/baseline_expand_less_24"
/>
android:src="@drawable/baseline_expand_less_24"/>
<ImageButton
android:id="@+id/game_menu_drag_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="36dp"
android:layout_height="36dp"
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"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/baseline_minimize_24"/>
<ImageButton
android:id="@+id/game_menu_pip_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_selected_button_bg"
android:src="@drawable/baseline_picture_in_picture_alt_24"/>
<ImageButton
android:id="@+id/game_menu_fullscreen_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_selected_button_bg"
android:src="@drawable/baseline_fullscreen_selector"/>
<ImageButton
android:id="@+id/game_menu_close_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/game_menu_button_bg"
android:src="@drawable/baseline_close_24"/>
</LinearLayout>

View file

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

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/group_menu_selection_options"
android:checkableBehavior="all">
<item
android:id="@+id/menu_show_selection_visibility"
android:checkable="true"
android:checked="true"
android:title="@string/show_selection_visibility"
tools:ignore="HardcodedText" />
<item
android:id="@+id/menu_dont_select_locked_nodes"
android:checkable="true"
android:checked="false"
android:title="@string/don_t_select_locked_nodes" />
<item
android:id="@+id/menu_select_group_over_children"
android:checkable="true"
android:checked="false"
android:title="@string/select_group_over_children" />
</group>
</menu>

View file

@ -20,4 +20,8 @@
<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 (%1$d x %2$d)</string>
<string name="don_t_select_locked_nodes">Don\'t Select Locked Nodes</string>
<string name="select_group_over_children">Select Group Over Children</string>
<string name="show_selection_visibility">Show Selection Visibility</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &lt;application, user&gt; pair. The developer can send this identifier
* to their own server along with some device identifier (a random number
* generated and stored once per application installation,
* {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
* {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
* The more sources used to identify the device, the harder it will be for an
* attacker to spoof.
* <p>
* The server can look at the &lt;application, user, device id&gt; tuple and
* restrict a user's application license to run on at most 10 different devices
* in a week (for example). We recommend not being too restrictive because a
* user might legitimately have multiple devices or be in the process of
* changing phones. This will catch egregious violations of multiple people
* sharing one license.
*/
public interface DeviceLimiter {
/**
* Checks if this device is allowed to use the given user's license.
*
* @param userId the user whose license the server responded with
* @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
*/
int isDeviceAllowed(String userId);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more