diff options
214 files changed, 7960 insertions, 7558 deletions
diff --git a/core/compressed_translation.cpp b/core/compressed_translation.cpp index 7dd5308fab..73affd8fe9 100644 --- a/core/compressed_translation.cpp +++ b/core/compressed_translation.cpp @@ -83,7 +83,7 @@ void PHashTranslation::generate(const Ref<Translation> &p_from) { if (ps.orig_len != 0) { CharString dst_s; dst_s.resize(src_s.size()); - int ret = smaz_compress(src_s.get_data(), src_s.size(), &dst_s[0], src_s.size()); + int ret = smaz_compress(src_s.get_data(), src_s.size(), dst_s.ptrw(), src_s.size()); if (ret >= src_s.size()) { //if compressed is larger than original, just use original ps.orig_len = src_s.size(); diff --git a/core/io/image_loader.h b/core/io/image_loader.h index 7a58d46f93..15ee5e9ae1 100644 --- a/core/io/image_loader.h +++ b/core/io/image_loader.h @@ -88,6 +88,7 @@ public: }; class ResourceFormatLoaderImage : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderImage, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/core/io/resource_format_binary.h b/core/io/resource_format_binary.h index 513252055f..e449ff216b 100644 --- a/core/io/resource_format_binary.h +++ b/core/io/resource_format_binary.h @@ -100,6 +100,7 @@ public: }; class ResourceFormatLoaderBinary : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderBinary, ResourceFormatLoader) public: virtual Ref<ResourceInteractiveLoader> load_interactive(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const; @@ -152,7 +153,7 @@ public: }; class ResourceFormatSaverBinary : public ResourceFormatSaver { - + GDCLASS(ResourceFormatSaverBinary, ResourceFormatSaver) public: static ResourceFormatSaverBinary *singleton; virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); diff --git a/core/io/resource_import.h b/core/io/resource_import.h index 53718bd789..ec66a422a3 100644 --- a/core/io/resource_import.h +++ b/core/io/resource_import.h @@ -37,6 +37,8 @@ class ResourceImporter; class ResourceFormatImporter : public ResourceFormatLoader { + GDCLASS(ResourceFormatImporter, ResourceFormatLoader) + struct PathAndType { String path; String type; diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index 71b01aa94a..41f3b2935c 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -39,7 +39,7 @@ #include "core/translation.h" #include "core/variant_parser.h" -ResourceFormatLoader *ResourceLoader::loader[MAX_LOADERS]; +Ref<ResourceFormatLoader> ResourceLoader::loader[ResourceLoader::MAX_LOADERS]; int ResourceLoader::loader_count = 0; @@ -73,6 +73,25 @@ bool ResourceFormatLoader::recognize_path(const String &p_path, const String &p_ return false; } +bool ResourceFormatLoader::handles_type(const String &p_type) const { + + if (get_script_instance() && get_script_instance()->has_method("handles_type")) { + // I guess custom loaders for custom resources should use "Resource" + return get_script_instance()->call("handles_type", p_type); + } + + return false; +} + +String ResourceFormatLoader::get_resource_type(const String &p_path) const { + + if (get_script_instance() && get_script_instance()->has_method("get_resource_type")) { + return get_script_instance()->call("get_resource_type", p_path); + } + + return ""; +} + void ResourceFormatLoader::get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const { if (p_type == "" || handles_type(p_type)) @@ -129,9 +148,37 @@ bool ResourceFormatLoader::exists(const String &p_path) const { return FileAccess::exists(p_path); //by default just check file } +void ResourceFormatLoader::get_recognized_extensions(List<String> *p_extensions) const { + + if (get_script_instance() && get_script_instance()->has_method("get_recognized_extensions")) { + PoolStringArray exts = get_script_instance()->call("get_recognized_extensions"); + + { + PoolStringArray::Read r = exts.read(); + for (int i = 0; i < exts.size(); ++i) { + p_extensions->push_back(r[i]); + } + } + } +} + RES ResourceFormatLoader::load(const String &p_path, const String &p_original_path, Error *r_error) { - String path = p_path; + if (get_script_instance() && get_script_instance()->has_method("load")) { + Variant res = get_script_instance()->call("load", p_path, p_original_path); + + if (res.get_type() == Variant::INT) { + + if (r_error) + *r_error = (Error)res.operator int64_t(); + + } else { + + if (r_error) + *r_error = OK; + return res; + } + } //or this must be implemented Ref<ResourceInteractiveLoader> ril = load_interactive(p_path, p_original_path, r_error); @@ -160,7 +207,47 @@ RES ResourceFormatLoader::load(const String &p_path, const String &p_original_pa void ResourceFormatLoader::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) { - //do nothing by default + if (get_script_instance() && get_script_instance()->has_method("get_dependencies")) { + PoolStringArray deps = get_script_instance()->call("get_dependencies", p_path, p_add_types); + + { + PoolStringArray::Read r = deps.read(); + for (int i = 0; i < deps.size(); ++i) { + p_dependencies->push_back(r[i]); + } + } + } +} + +Error ResourceFormatLoader::rename_dependencies(const String &p_path, const Map<String, String> &p_map) { + + if (get_script_instance() && get_script_instance()->has_method("rename_dependencies")) { + + Dictionary deps_dict; + for (Map<String, String>::Element *E = p_map.front(); E; E = E->next()) { + deps_dict[E->key()] = E->value(); + } + + int64_t res = get_script_instance()->call("rename_dependencies", deps_dict); + return (Error)res; + } + + return OK; +} + +void ResourceFormatLoader::_bind_methods() { + + { + MethodInfo info = MethodInfo(Variant::NIL, "load", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::STRING, "original_path")); + info.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; + ClassDB::add_virtual_method(get_class_static(), info); + } + + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::POOL_STRING_ARRAY, "get_recognized_extensions")); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "handles_type", PropertyInfo(Variant::STRING, "typename"))); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::STRING, "get_resource_type", PropertyInfo(Variant::STRING, "path"))); + ClassDB::add_virtual_method(get_class_static(), MethodInfo("get_dependencies", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::STRING, "add_types"))); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::INT, "rename_dependencies", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::STRING, "renames"))); } /////////////////////////////////// @@ -348,9 +435,11 @@ Ref<ResourceInteractiveLoader> ResourceLoader::load_interactive(const String &p_ return Ref<ResourceInteractiveLoader>(); } -void ResourceLoader::add_resource_format_loader(ResourceFormatLoader *p_format_loader, bool p_at_front) { +void ResourceLoader::add_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader, bool p_at_front) { + ERR_FAIL_COND(p_format_loader.is_null()); ERR_FAIL_COND(loader_count >= MAX_LOADERS); + if (p_at_front) { for (int i = loader_count; i > 0; i--) { loader[i] = loader[i - 1]; @@ -362,6 +451,27 @@ void ResourceLoader::add_resource_format_loader(ResourceFormatLoader *p_format_l } } +void ResourceLoader::remove_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader) { + + ERR_FAIL_COND(p_format_loader.is_null()); + + // Find loader + int i = 0; + for (; i < loader_count; ++i) { + if (loader[i] == p_format_loader) + break; + } + + ERR_FAIL_COND(i >= loader_count); // Not found + + // Shift next loaders up + for (; i < loader_count - 1; ++i) { + loader[i] = loader[i + 1]; + } + loader[loader_count - 1].unref(); + --loader_count; +} + int ResourceLoader::get_import_order(const String &p_path) { String path = _path_remap(p_path); @@ -645,6 +755,84 @@ void ResourceLoader::set_load_callback(ResourceLoadedCallback p_callback) { ResourceLoadedCallback ResourceLoader::_loaded_callback = NULL; +Ref<ResourceFormatLoader> ResourceLoader::_find_custom_resource_format_loader(String path) { + for (int i = 0; i < loader_count; ++i) { + if (loader[i]->get_script_instance() && loader[i]->get_script_instance()->get_script()->get_path() == path) { + return loader[i]; + } + } + return Ref<ResourceFormatLoader>(); +} + +bool ResourceLoader::add_custom_resource_format_loader(String script_path) { + + if (_find_custom_resource_format_loader(script_path).is_valid()) + return false; + + Ref<Resource> res = ResourceLoader::load(script_path); + ERR_FAIL_COND_V(res.is_null(), false); + ERR_FAIL_COND_V(!res->is_class("Script"), false); + + Ref<Script> s = res; + StringName ibt = s->get_instance_base_type(); + bool valid_type = ClassDB::is_parent_class(ibt, "ResourceFormatLoader"); + ERR_EXPLAIN("Script does not inherit a CustomResourceLoader: " + script_path); + ERR_FAIL_COND_V(!valid_type, false); + + Object *obj = ClassDB::instance(ibt); + + ERR_EXPLAIN("Cannot instance script as custom resource loader, expected 'ResourceFormatLoader' inheritance, got: " + String(ibt)); + ERR_FAIL_COND_V(obj == NULL, false); + + ResourceFormatLoader *crl = NULL; + crl = Object::cast_to<ResourceFormatLoader>(obj); + crl->set_script(s.get_ref_ptr()); + ResourceLoader::add_resource_format_loader(crl); + + return true; +} + +void ResourceLoader::remove_custom_resource_format_loader(String script_path) { + + Ref<ResourceFormatLoader> loader = _find_custom_resource_format_loader(script_path); + if (loader.is_valid()) + remove_resource_format_loader(loader); +} + +void ResourceLoader::add_custom_loaders() { + // Custom loaders registration exploits global class names + + String custom_loader_base_class = ResourceFormatLoader::get_class_static(); + + List<StringName> global_classes; + ScriptServer::get_global_class_list(&global_classes); + + for (List<StringName>::Element *E = global_classes.front(); E; E = E->next()) { + + StringName class_name = E->get(); + StringName base_class = ScriptServer::get_global_class_base(class_name); + + if (base_class == custom_loader_base_class) { + String path = ScriptServer::get_global_class_path(class_name); + add_custom_resource_format_loader(path); + } + } +} + +void ResourceLoader::remove_custom_loaders() { + + Vector<Ref<ResourceFormatLoader> > custom_loaders; + for (int i = 0; i < loader_count; ++i) { + if (loader[i]->get_script_instance()) { + custom_loaders.push_back(loader[i]); + } + } + + for (int i = 0; i < custom_loaders.size(); ++i) { + remove_resource_format_loader(custom_loaders[i]); + } +} + ResourceLoadErrorNotify ResourceLoader::err_notify = NULL; void *ResourceLoader::err_notify_ud = NULL; diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index a46a00203f..c411b3c398 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -56,18 +56,24 @@ public: ResourceInteractiveLoader() {} }; -class ResourceFormatLoader { +class ResourceFormatLoader : public Reference { + + GDCLASS(ResourceFormatLoader, Reference) + +protected: + static void _bind_methods(); + public: virtual Ref<ResourceInteractiveLoader> load_interactive(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual bool exists(const String &p_path) const; - virtual void get_recognized_extensions(List<String> *p_extensions) const = 0; + virtual void get_recognized_extensions(List<String> *p_extensions) const; virtual void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const; virtual bool recognize_path(const String &p_path, const String &p_for_type = String()) const; - virtual bool handles_type(const String &p_type) const = 0; - virtual String get_resource_type(const String &p_path) const = 0; + virtual bool handles_type(const String &p_type) const; + virtual String get_resource_type(const String &p_path) const; virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false); - virtual Error rename_dependencies(const String &p_path, const Map<String, String> &p_map) { return OK; } + virtual Error rename_dependencies(const String &p_path, const Map<String, String> &p_map); virtual bool is_import_valid(const String &p_path) const { return true; } virtual int get_import_order(const String &p_path) const { return 0; } @@ -86,7 +92,7 @@ class ResourceLoader { MAX_LOADERS = 64 }; - static ResourceFormatLoader *loader[MAX_LOADERS]; + static Ref<ResourceFormatLoader> loader[MAX_LOADERS]; static int loader_count; static bool timestamp_on_load; @@ -109,13 +115,16 @@ class ResourceLoader { static ResourceLoadedCallback _loaded_callback; + static Ref<ResourceFormatLoader> _find_custom_resource_format_loader(String path); + public: static Ref<ResourceInteractiveLoader> load_interactive(const String &p_path, const String &p_type_hint = "", bool p_no_cache = false, Error *r_error = NULL); static RES load(const String &p_path, const String &p_type_hint = "", bool p_no_cache = false, Error *r_error = NULL); static bool exists(const String &p_path, const String &p_type_hint = ""); static void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions); - static void add_resource_format_loader(ResourceFormatLoader *p_format_loader, bool p_at_front = false); + static void add_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader, bool p_at_front = false); + static void remove_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader); static String get_resource_type(const String &p_path); static void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false); static Error rename_dependencies(const String &p_path, const Map<String, String> &p_map); @@ -156,6 +165,11 @@ public: static void set_load_callback(ResourceLoadedCallback p_callback); static ResourceLoaderImport import; + + static bool add_custom_resource_format_loader(String script_path); + static void remove_custom_resource_format_loader(String script_path); + static void add_custom_loaders(); + static void remove_custom_loaders(); }; #endif diff --git a/core/io/resource_saver.cpp b/core/io/resource_saver.cpp index 097e81e308..af726d9e2d 100644 --- a/core/io/resource_saver.cpp +++ b/core/io/resource_saver.cpp @@ -29,18 +29,62 @@ /*************************************************************************/ #include "resource_saver.h" - #include "core/io/resource_loader.h" #include "core/os/file_access.h" #include "core/project_settings.h" #include "core/script_language.h" -ResourceFormatSaver *ResourceSaver::saver[MAX_SAVERS]; +Ref<ResourceFormatSaver> ResourceSaver::saver[MAX_SAVERS]; int ResourceSaver::saver_count = 0; bool ResourceSaver::timestamp_on_save = false; ResourceSavedCallback ResourceSaver::save_callback = 0; +Error ResourceFormatSaver::save(const String &p_path, const RES &p_resource, uint32_t p_flags) { + + if (get_script_instance() && get_script_instance()->has_method("save")) { + return (Error)get_script_instance()->call("save", p_path, p_resource, p_flags).operator int64_t(); + } + + return ERR_METHOD_NOT_FOUND; +} + +bool ResourceFormatSaver::recognize(const RES &p_resource) const { + + if (get_script_instance() && get_script_instance()->has_method("recognize")) { + return get_script_instance()->call("recognize", p_resource); + } + + return false; +} + +void ResourceFormatSaver::get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const { + + if (get_script_instance() && get_script_instance()->has_method("get_recognized_extensions")) { + PoolStringArray exts = get_script_instance()->call("get_recognized_extensions", p_resource); + + { + PoolStringArray::Read r = exts.read(); + for (int i = 0; i < exts.size(); ++i) { + p_extensions->push_back(r[i]); + } + } + } +} + +void ResourceFormatSaver::_bind_methods() { + + { + PropertyInfo arg0 = PropertyInfo(Variant::STRING, "path"); + PropertyInfo arg1 = PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "Resource"); + PropertyInfo arg2 = PropertyInfo(Variant::INT, "flags"); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::INT, "save", arg0, arg1, arg2)); + } + + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::POOL_STRING_ARRAY, "get_recognized_extensions", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "Resource"))); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "recognize", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "Resource"))); +} + Error ResourceSaver::save(const String &p_path, const RES &p_resource, uint32_t p_flags) { String extension = p_path.get_extension(); @@ -113,8 +157,9 @@ void ResourceSaver::get_recognized_extensions(const RES &p_resource, List<String } } -void ResourceSaver::add_resource_format_saver(ResourceFormatSaver *p_format_saver, bool p_at_front) { +void ResourceSaver::add_resource_format_saver(Ref<ResourceFormatSaver> p_format_saver, bool p_at_front) { + ERR_FAIL_COND(p_format_saver.is_null()); ERR_FAIL_COND(saver_count >= MAX_SAVERS); if (p_at_front) { @@ -127,3 +172,102 @@ void ResourceSaver::add_resource_format_saver(ResourceFormatSaver *p_format_save saver[saver_count++] = p_format_saver; } } + +void ResourceSaver::remove_resource_format_saver(Ref<ResourceFormatSaver> p_format_saver) { + + ERR_FAIL_COND(p_format_saver.is_null()); + + // Find saver + int i = 0; + for (; i < saver_count; ++i) { + if (saver[i] == p_format_saver) + break; + } + + ERR_FAIL_COND(i >= saver_count); // Not found + + // Shift next savers up + for (; i < saver_count - 1; ++i) { + saver[i] = saver[i + 1]; + } + saver[saver_count - 1].unref(); + --saver_count; +} + +Ref<ResourceFormatSaver> ResourceSaver::_find_custom_resource_format_saver(String path) { + for (int i = 0; i < saver_count; ++i) { + if (saver[i]->get_script_instance() && saver[i]->get_script_instance()->get_script()->get_path() == path) { + return saver[i]; + } + } + return Ref<ResourceFormatSaver>(); +} + +bool ResourceSaver::add_custom_resource_format_saver(String script_path) { + + if (_find_custom_resource_format_saver(script_path).is_valid()) + return false; + + Ref<Resource> res = ResourceLoader::load(script_path); + ERR_FAIL_COND_V(res.is_null(), false); + ERR_FAIL_COND_V(!res->is_class("Script"), false); + + Ref<Script> s = res; + StringName ibt = s->get_instance_base_type(); + bool valid_type = ClassDB::is_parent_class(ibt, "ResourceFormatSaver"); + ERR_EXPLAIN("Script does not inherit a CustomResourceSaver: " + script_path); + ERR_FAIL_COND_V(!valid_type, false); + + Object *obj = ClassDB::instance(ibt); + + ERR_EXPLAIN("Cannot instance script as custom resource saver, expected 'ResourceFormatSaver' inheritance, got: " + String(ibt)); + ERR_FAIL_COND_V(obj == NULL, false); + + ResourceFormatSaver *crl = NULL; + crl = Object::cast_to<ResourceFormatSaver>(obj); + crl->set_script(s.get_ref_ptr()); + ResourceSaver::add_resource_format_saver(crl); + + return true; +} + +void ResourceSaver::remove_custom_resource_format_saver(String script_path) { + + Ref<ResourceFormatSaver> saver = _find_custom_resource_format_saver(script_path); + if (saver.is_valid()) + remove_resource_format_saver(saver); +} + +void ResourceSaver::add_custom_savers() { + // Custom resource savers exploits global class names + + String custom_saver_base_class = ResourceFormatSaver::get_class_static(); + + List<StringName> global_classes; + ScriptServer::get_global_class_list(&global_classes); + + for (List<StringName>::Element *E = global_classes.front(); E; E = E->next()) { + + StringName class_name = E->get(); + StringName base_class = ScriptServer::get_global_class_base(class_name); + + if (base_class == custom_saver_base_class) { + String path = ScriptServer::get_global_class_path(class_name); + add_custom_resource_format_saver(path); + } + } +} + +void ResourceSaver::remove_custom_savers() { + + Vector<Ref<ResourceFormatSaver> > custom_savers; + for (int i = 0; i < saver_count; ++i) { + if (saver[i]->get_script_instance()) { + custom_savers.push_back(saver[i]); + } + } + + for (int i = 0; i < custom_savers.size(); ++i) { + remove_resource_format_saver(custom_savers[i]); + } +} diff --git a/core/io/resource_saver.h b/core/io/resource_saver.h index cdd43292a2..1a250e84ea 100644 --- a/core/io/resource_saver.h +++ b/core/io/resource_saver.h @@ -37,11 +37,16 @@ @author Juan Linietsky <reduzio@gmail.com> */ -class ResourceFormatSaver { +class ResourceFormatSaver : public Reference { + GDCLASS(ResourceFormatSaver, Reference) + +protected: + static void _bind_methods(); + public: - virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0) = 0; - virtual bool recognize(const RES &p_resource) const = 0; - virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const = 0; + virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); + virtual bool recognize(const RES &p_resource) const; + virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const; virtual ~ResourceFormatSaver() {} }; @@ -54,11 +59,13 @@ class ResourceSaver { MAX_SAVERS = 64 }; - static ResourceFormatSaver *saver[MAX_SAVERS]; + static Ref<ResourceFormatSaver> saver[MAX_SAVERS]; static int saver_count; static bool timestamp_on_save; static ResourceSavedCallback save_callback; + static Ref<ResourceFormatSaver> _find_custom_resource_format_saver(String path); + public: enum SaverFlags { @@ -73,12 +80,18 @@ public: static Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); static void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions); - static void add_resource_format_saver(ResourceFormatSaver *p_format_saver, bool p_at_front = false); + static void add_resource_format_saver(Ref<ResourceFormatSaver> p_format_saver, bool p_at_front = false); + static void remove_resource_format_saver(Ref<ResourceFormatSaver> p_format_saver); static void set_timestamp_on_save(bool p_timestamp) { timestamp_on_save = p_timestamp; } static bool get_timestamp_on_save() { return timestamp_on_save; } static void set_save_callback(ResourceSavedCallback p_callback); + + static bool add_custom_resource_format_saver(String script_path); + static void remove_custom_resource_format_saver(String script_path); + static void add_custom_savers(); + static void remove_custom_savers(); }; #endif diff --git a/core/io/translation_loader_po.h b/core/io/translation_loader_po.h index 670a9fdd7e..9543307cb5 100644 --- a/core/io/translation_loader_po.h +++ b/core/io/translation_loader_po.h @@ -36,6 +36,7 @@ #include "core/translation.h" class TranslationLoaderPO : public ResourceFormatLoader { + GDCLASS(TranslationLoaderPO, ResourceFormatLoader) public: static RES load_translation(FileAccess *f, Error *r_error, const String &p_path = String()); virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); diff --git a/core/os/file_access.cpp b/core/os/file_access.cpp index 679b1c9054..e7ad544a93 100644 --- a/core/os/file_access.cpp +++ b/core/os/file_access.cpp @@ -353,7 +353,8 @@ Vector<String> FileAccess::get_csv_line(const String &p_delim) const { String l; int qc = 0; do { - ERR_FAIL_COND_V(eof_reached(), Vector<String>()); + if (eof_reached()) + break; l += get_line() + "\n"; qc = 0; diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 6b776cb0b1..61fd3043a5 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -65,11 +65,11 @@ #include "core/translation.h" #include "core/undo_redo.h" -static ResourceFormatSaverBinary *resource_saver_binary = NULL; -static ResourceFormatLoaderBinary *resource_loader_binary = NULL; -static ResourceFormatImporter *resource_format_importer = NULL; +static Ref<ResourceFormatSaverBinary> resource_saver_binary; +static Ref<ResourceFormatLoaderBinary> resource_loader_binary; +static Ref<ResourceFormatImporter> resource_format_importer; -static ResourceFormatLoaderImage *resource_format_image = NULL; +static Ref<ResourceFormatLoaderImage> resource_format_image; static _ResourceLoader *_resource_loader = NULL; static _ResourceSaver *_resource_saver = NULL; @@ -77,7 +77,7 @@ static _OS *_os = NULL; static _Engine *_engine = NULL; static _ClassDB *_classdb = NULL; static _Marshalls *_marshalls = NULL; -static TranslationLoaderPO *resource_format_po = NULL; +static Ref<TranslationLoaderPO> resource_format_po; static _JSON *_json = NULL; static IP *ip = NULL; @@ -106,18 +106,18 @@ void register_core_types() { CoreStringNames::create(); - resource_format_po = memnew(TranslationLoaderPO); + resource_format_po.instance(); ResourceLoader::add_resource_format_loader(resource_format_po); - resource_saver_binary = memnew(ResourceFormatSaverBinary); + resource_saver_binary.instance(); ResourceSaver::add_resource_format_saver(resource_saver_binary); - resource_loader_binary = memnew(ResourceFormatLoaderBinary); + resource_loader_binary.instance(); ResourceLoader::add_resource_format_loader(resource_loader_binary); - resource_format_importer = memnew(ResourceFormatImporter); + resource_format_importer.instance(); ResourceLoader::add_resource_format_loader(resource_format_importer); - resource_format_image = memnew(ResourceFormatLoaderImage); + resource_format_image.instance(); ResourceLoader::add_resource_format_loader(resource_format_image); ClassDB::register_class<Object>(); @@ -165,6 +165,9 @@ void register_core_types() { ClassDB::register_virtual_class<ResourceInteractiveLoader>(); + ClassDB::register_class<ResourceFormatLoader>(); + ClassDB::register_class<ResourceFormatSaver>(); + ClassDB::register_class<_File>(); ClassDB::register_class<_Directory>(); ClassDB::register_class<_Thread>(); @@ -248,16 +251,28 @@ void unregister_core_types() { memdelete(_geometry); - if (resource_format_image) - memdelete(resource_format_image); - if (resource_saver_binary) - memdelete(resource_saver_binary); - if (resource_loader_binary) - memdelete(resource_loader_binary); - if (resource_format_importer) - memdelete(resource_format_importer); + if (resource_format_image.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_format_image); + resource_format_image.unref(); + } + + if (resource_saver_binary.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_binary); + resource_saver_binary.unref(); + } + + if (resource_loader_binary.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_binary); + resource_loader_binary.unref(); + } + + if (resource_format_importer.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_format_importer); + resource_format_importer.unref(); + } - memdelete(resource_format_po); + ResourceLoader::remove_resource_format_loader(resource_format_po); + resource_format_po.unref(); if (ip) memdelete(ip); diff --git a/core/ustring.cpp b/core/ustring.cpp index 3f017fa985..083a1eaed6 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -179,7 +179,7 @@ void String::copy_from_unchecked(const CharType *p_char, const int p_length) { resize(p_length + 1); set(p_length, 0); - CharType *dst = &operator[](0); + CharType *dst = ptrw(); for (int i = 0; i < p_length; i++) { dst[i] = p_char[i]; @@ -250,7 +250,7 @@ String &String::operator+=(const String &p_str) { resize(length() + p_str.size()); const CharType *src = p_str.c_str(); - CharType *dst = &operator[](0); + CharType *dst = ptrw(); set(length(), 0); @@ -289,7 +289,7 @@ String &String::operator+=(const char *p_str) { resize(from + src_len + 1); - CharType *dst = &operator[](0); + CharType *dst = ptrw(); set(length(), 0); @@ -1431,7 +1431,7 @@ bool String::parse_utf8(const char *p_utf8, int p_len) { } resize(str_size + 1); - CharType *dst = &operator[](0); + CharType *dst = ptrw(); dst[str_size] = 0; while (cstr_size) { @@ -3476,7 +3476,7 @@ String String::xml_unescape() const { if (len == 0) return String(); str.resize(len + 1); - _xml_unescape(c_str(), l, &str[0]); + _xml_unescape(c_str(), l, str.ptrw()); str[len] = 0; return str; } diff --git a/core/ustring.h b/core/ustring.h index 8e4dbd8031..e7b13027ea 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -40,6 +40,36 @@ @author Juan Linietsky <reduzio@gmail.com> */ +template <class T> +class CharProxy { + friend class CharString; + friend class String; + + const int _index; + CowData<T> &_cowdata; + + _FORCE_INLINE_ CharProxy(const int &p_index, CowData<T> &cowdata) : + _index(p_index), + _cowdata(cowdata) {} + +public: + _FORCE_INLINE_ operator T() const { + return _cowdata.get(_index); + } + + _FORCE_INLINE_ const T *operator&() const { + return _cowdata.ptr() + _index; + } + + _FORCE_INLINE_ void operator=(const T &other) const { + _cowdata.set(_index, other); + } + + _FORCE_INLINE_ void operator=(const CharProxy<T> &other) const { + _cowdata.set(_index, other.operator T()); + } +}; + class CharString { CowData<char> _cowdata; @@ -53,8 +83,8 @@ public: _FORCE_INLINE_ char get(int p_index) { return _cowdata.get(p_index); } _FORCE_INLINE_ const char get(int p_index) const { return _cowdata.get(p_index); } _FORCE_INLINE_ void set(int p_index, const char &p_elem) { _cowdata.set(p_index, p_elem); } - _FORCE_INLINE_ char &operator[](int p_index) { return _cowdata.get_m(p_index); } _FORCE_INLINE_ const char &operator[](int p_index) const { return _cowdata.get(p_index); } + _FORCE_INLINE_ CharProxy<char> operator[](int p_index) { return CharProxy<char>(p_index, _cowdata); } _FORCE_INLINE_ CharString() {} _FORCE_INLINE_ CharString(const CharString &p_str) { _cowdata._ref(p_str._cowdata); } @@ -107,8 +137,9 @@ public: _FORCE_INLINE_ void set(int p_index, const CharType &p_elem) { _cowdata.set(p_index, p_elem); } _FORCE_INLINE_ int size() const { return _cowdata.size(); } Error resize(int p_size) { return _cowdata.resize(p_size); } - _FORCE_INLINE_ CharType &operator[](int p_index) { return _cowdata.get_m(p_index); } + _FORCE_INLINE_ const CharType &operator[](int p_index) const { return _cowdata.get(p_index); } + _FORCE_INLINE_ CharProxy<CharType> operator[](int p_index) { return CharProxy<CharType>(p_index, _cowdata); } bool operator==(const String &p_str) const; bool operator!=(const String &p_str) const; diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml index 80281a603a..ced9ab7269 100644 --- a/doc/classes/Environment.xml +++ b/doc/classes/Environment.xml @@ -77,6 +77,15 @@ <member name="background_sky_custom_fov" type="float" setter="set_sky_custom_fov" getter="get_sky_custom_fov"> [Sky] resource's custom field of view. </member> + <member name="background_sky_orientation" type="Basis" setter="set_sky_orientation" getter="get_sky_orientation"> + [Sky] resource's rotation expressed as a [Basis] + </member> + <member name="background_sky_rotation" type="Vector3" setter="set_sky_rotation" getter="get_sky_rotation"> + [Sky] resource's rotation expressed as euler angles in radians + </member> + <member name="background_sky_rotation_degrees" type="Vector3" setter="set_sky_rotation_degrees" getter="get_sky_rotation_degrees"> + [Sky] resource's rotation expressed as euler angles in degrees + </member> <member name="dof_blur_far_amount" type="float" setter="set_dof_blur_far_amount" getter="get_dof_blur_far_amount"> Amount of far blur. </member> diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml index ce0b619d67..5ac3db1e8e 100644 --- a/doc/classes/GraphEdit.xml +++ b/doc/classes/GraphEdit.xml @@ -82,7 +82,7 @@ <return type="Array"> </return> <description> - Return an Array containing the list of connections. A connection consists in a structure of the form {from_slot: 0, from: "GraphNode name 0", to_slot: 1, to: "GraphNode name 1" } + Return an Array containing the list of connections. A connection consists in a structure of the form {from_port: 0, from: "GraphNode name 0", to_port: 1, to: "GraphNode name 1" } </description> </method> <method name="get_zoom_hbox"> diff --git a/doc/classes/GraphNode.xml b/doc/classes/GraphNode.xml index 4c39720d69..6e09926c11 100644 --- a/doc/classes/GraphNode.xml +++ b/doc/classes/GraphNode.xml @@ -236,6 +236,8 @@ </theme_item> <theme_item name="close" type="Texture"> </theme_item> + <theme_item name="close_color" type="Color"> + </theme_item> <theme_item name="close_offset" type="int"> </theme_item> <theme_item name="comment" type="StyleBox"> diff --git a/doc/classes/ResourceFormatDDS.xml b/doc/classes/ResourceFormatDDS.xml new file mode 100644 index 0000000000..e237dfc6a5 --- /dev/null +++ b/doc/classes/ResourceFormatDDS.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatDDS" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatImporter.xml b/doc/classes/ResourceFormatImporter.xml new file mode 100644 index 0000000000..fa5b44fa18 --- /dev/null +++ b/doc/classes/ResourceFormatImporter.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatImporter" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoader.xml b/doc/classes/ResourceFormatLoader.xml new file mode 100644 index 0000000000..f03f0bbf54 --- /dev/null +++ b/doc/classes/ResourceFormatLoader.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoader" inherits="Reference" category="Core" version="3.1"> + <brief_description> + Loads a specific resource type from a file. + </brief_description> + <description> + Godot loads resources in the editor or in exported games using ResourceFormatLoaders. They get queried when you call `load`, or when a resource with internal dependencies is loaded. Each file type may load as a different resource type, so multiple ResourceFormatLoader are registered in the engine. + Extending this class allows you to define your own. You should give it a global class name with `class_name` for it to be registered. You may as well implement a [ResourceFormatSaver]. + Note: you can also extend [EditorImportPlugin] if the resource type you need exists but Godot is unable to load its format. Choosing one way over another depends if the format is suitable or not for the final exported game. Example: it's better to import .PNG textures as .STEX first, so they can be loaded with better efficiency on the graphics card. + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + <method name="get_dependencies" qualifiers="virtual"> + <return type="void"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <argument index="1" name="add_types" type="String"> + </argument> + <description> + If implemented, gets the dependencies of a given resource. If add_types is true, paths should be appended "::TypeName", where `TypeName` is the class name of the dependency. Note that custom resource types defined by scripts aren't known by the [ClassDB], so you might just return "Resource" for them. + </description> + </method> + <method name="get_recognized_extensions" qualifiers="virtual"> + <return type="PoolStringArray"> + </return> + <description> + Gets the list of extensions for files this loader is able to read. + </description> + </method> + <method name="get_resource_type" qualifiers="virtual"> + <return type="String"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <description> + Gets the class name of the resource associated with the given path. If the loader cannot handle it, it should return "". Note that custom resource types defined by scripts aren't known by the [ClassDB], so you might just return "Resource" for them. + </description> + </method> + <method name="handles_type" qualifiers="virtual"> + <return type="bool"> + </return> + <argument index="0" name="typename" type="String"> + </argument> + <description> + Tells which resource class this loader can load. Note that custom resource types defined by scripts aren't known by the [ClassDB], so you might just return "Resource" for them. + </description> + </method> + <method name="load" qualifiers="virtual"> + <return type="Variant"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <argument index="1" name="original_path" type="String"> + </argument> + <description> + Loads a resource when the engine finds this loader to be compatible. original_path: If the loaded resource is the result of an import, original_path will target the source file. Returns a resource object if succeeded, or an ERR_* constant listed in [@GlobalScope] if it failed. + </description> + </method> + <method name="rename_dependencies" qualifiers="virtual"> + <return type="int"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <argument index="1" name="renames" type="String"> + </argument> + <description> + If implemented, renames dependencies within the given resource and saves it. renames is a dictionary { String => String } mapping old dependency paths to new paths. Returns OK on success, or an ERR_* constant listed in [@GlobalScope] in case of failure. + </description> + </method> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderBMFont.xml b/doc/classes/ResourceFormatLoaderBMFont.xml new file mode 100644 index 0000000000..1e999353f8 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderBMFont.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderBMFont" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderBinary.xml b/doc/classes/ResourceFormatLoaderBinary.xml new file mode 100644 index 0000000000..3c768851e2 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderBinary.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderBinary" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderDynamicFont.xml b/doc/classes/ResourceFormatLoaderDynamicFont.xml new file mode 100644 index 0000000000..4cd5c9b2a6 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderDynamicFont.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderDynamicFont" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderGDScript.xml b/doc/classes/ResourceFormatLoaderGDScript.xml new file mode 100644 index 0000000000..b661e4746a --- /dev/null +++ b/doc/classes/ResourceFormatLoaderGDScript.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderGDScript" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderImage.xml b/doc/classes/ResourceFormatLoaderImage.xml new file mode 100644 index 0000000000..5f7e55af28 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderImage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderImage" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderNativeScript.xml b/doc/classes/ResourceFormatLoaderNativeScript.xml new file mode 100644 index 0000000000..496602e7dd --- /dev/null +++ b/doc/classes/ResourceFormatLoaderNativeScript.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderNativeScript" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderShader.xml b/doc/classes/ResourceFormatLoaderShader.xml new file mode 100644 index 0000000000..0948810532 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderShader.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderShader" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderStreamTexture.xml b/doc/classes/ResourceFormatLoaderStreamTexture.xml new file mode 100644 index 0000000000..34843f9e1a --- /dev/null +++ b/doc/classes/ResourceFormatLoaderStreamTexture.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderStreamTexture" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderText.xml b/doc/classes/ResourceFormatLoaderText.xml new file mode 100644 index 0000000000..db3b28f233 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderText.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderText" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderTextureLayered.xml b/doc/classes/ResourceFormatLoaderTextureLayered.xml new file mode 100644 index 0000000000..cd31f35e49 --- /dev/null +++ b/doc/classes/ResourceFormatLoaderTextureLayered.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderTextureLayered" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderTheora.xml b/doc/classes/ResourceFormatLoaderTheora.xml new file mode 100644 index 0000000000..dff7e2f3dc --- /dev/null +++ b/doc/classes/ResourceFormatLoaderTheora.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderTheora" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatLoaderWebm.xml b/doc/classes/ResourceFormatLoaderWebm.xml new file mode 100644 index 0000000000..f2b403161e --- /dev/null +++ b/doc/classes/ResourceFormatLoaderWebm.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatLoaderWebm" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatPKM.xml b/doc/classes/ResourceFormatPKM.xml new file mode 100644 index 0000000000..aa511823d3 --- /dev/null +++ b/doc/classes/ResourceFormatPKM.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatPKM" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatPVR.xml b/doc/classes/ResourceFormatPVR.xml new file mode 100644 index 0000000000..4f3b4f670f --- /dev/null +++ b/doc/classes/ResourceFormatPVR.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatPVR" inherits="ResourceFormatLoader" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaver.xml b/doc/classes/ResourceFormatSaver.xml new file mode 100644 index 0000000000..d15d97fc84 --- /dev/null +++ b/doc/classes/ResourceFormatSaver.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaver" inherits="Reference" category="Core" version="3.1"> + <brief_description> + Saves a specific resource type to a file. + </brief_description> + <description> + The engine can save resources when you do it from the editor, or when you call `ResourceSaver.save(resource)`. This is accomplished with multiple `ResourceFormatSavers`, each handling its own format. + By default, Godot saves resources as `.tres`, `.res` or another built-in format, but you can choose to create your own format by extending this class. You should give it a global class name with `class_name` for it to be registered. You may as well implement a [ResourceFormatLoader]. + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + <method name="get_recognized_extensions" qualifiers="virtual"> + <return type="PoolStringArray"> + </return> + <argument index="0" name="resource" type="Resource"> + </argument> + <description> + Gets the list of extensions for files this saver is able to write. + </description> + </method> + <method name="recognize" qualifiers="virtual"> + <return type="bool"> + </return> + <argument index="0" name="resource" type="Resource"> + </argument> + <description> + Returns true if the given resource object can be saved by this saver. + </description> + </method> + <method name="save" qualifiers="virtual"> + <return type="int"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <argument index="1" name="resource" type="Resource"> + </argument> + <argument index="2" name="flags" type="int"> + </argument> + <description> + Saves the given resource object to a file. flags is a bitmask composed with FLAG_* constants defined in [ResourceSaver]. Returns OK on success, or an ERR_* constant listed in [@GlobalScope] if it failed. + </description> + </method> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaverBinary.xml b/doc/classes/ResourceFormatSaverBinary.xml new file mode 100644 index 0000000000..7afec9ba23 --- /dev/null +++ b/doc/classes/ResourceFormatSaverBinary.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaverBinary" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaverGDScript.xml b/doc/classes/ResourceFormatSaverGDScript.xml new file mode 100644 index 0000000000..27964c7c9b --- /dev/null +++ b/doc/classes/ResourceFormatSaverGDScript.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaverGDScript" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaverNativeScript.xml b/doc/classes/ResourceFormatSaverNativeScript.xml new file mode 100644 index 0000000000..9f8c0ccc78 --- /dev/null +++ b/doc/classes/ResourceFormatSaverNativeScript.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaverNativeScript" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaverShader.xml b/doc/classes/ResourceFormatSaverShader.xml new file mode 100644 index 0000000000..cdc512dfea --- /dev/null +++ b/doc/classes/ResourceFormatSaverShader.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaverShader" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceFormatSaverText.xml b/doc/classes/ResourceFormatSaverText.xml new file mode 100644 index 0000000000..6ae4ff4a2c --- /dev/null +++ b/doc/classes/ResourceFormatSaverText.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceFormatSaverText" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/ResourceSaverPNG.xml b/doc/classes/ResourceSaverPNG.xml new file mode 100644 index 0000000000..a161a4de5f --- /dev/null +++ b/doc/classes/ResourceSaverPNG.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ResourceSaverPNG" inherits="ResourceFormatSaver" category="Core" version="3.1"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <demos> + </demos> + <methods> + </methods> + <constants> + </constants> +</class> diff --git a/drivers/dummy/rasterizer_dummy.h b/drivers/dummy/rasterizer_dummy.h index dfdb3a6bba..5f46c1d053 100644 --- a/drivers/dummy/rasterizer_dummy.h +++ b/drivers/dummy/rasterizer_dummy.h @@ -56,6 +56,7 @@ public: void environment_set_background(RID p_env, VS::EnvironmentBG p_bg) {} void environment_set_sky(RID p_env, RID p_sky) {} void environment_set_sky_custom_fov(RID p_env, float p_scale) {} + void environment_set_sky_orientation(RID p_env, const Basis &p_orientation) {} void environment_set_bg_color(RID p_env, const Color &p_color) {} void environment_set_bg_energy(RID p_env, float p_energy) {} void environment_set_canvas_max_layer(RID p_env, int p_max_layer) {} diff --git a/drivers/dummy/texture_loader_dummy.h b/drivers/dummy/texture_loader_dummy.h index f0a355ec12..6809e76331 100644 --- a/drivers/dummy/texture_loader_dummy.h +++ b/drivers/dummy/texture_loader_dummy.h @@ -35,6 +35,7 @@ #include "scene/resources/texture.h" class ResourceFormatDummyTexture : public ResourceFormatLoader { + GDCLASS(ResourceFormatDummyTexture, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/drivers/gles2/rasterizer_scene_gles2.cpp b/drivers/gles2/rasterizer_scene_gles2.cpp index 25d7df8ebc..47108e1fa5 100644 --- a/drivers/gles2/rasterizer_scene_gles2.cpp +++ b/drivers/gles2/rasterizer_scene_gles2.cpp @@ -668,6 +668,13 @@ void RasterizerSceneGLES2::environment_set_sky_custom_fov(RID p_env, float p_sca env->sky_custom_fov = p_scale; } +void RasterizerSceneGLES2::environment_set_sky_orientation(RID p_env, const Basis &p_orientation) { + Environment *env = environment_owner.getornull(p_env); + ERR_FAIL_COND(!env); + + env->sky_orientation = p_orientation; +} + void RasterizerSceneGLES2::environment_set_bg_color(RID p_env, const Color &p_color) { Environment *env = environment_owner.getornull(p_env); ERR_FAIL_COND(!env); @@ -2283,7 +2290,13 @@ void RasterizerSceneGLES2::_render_render_list(RenderList::Element **p_elements, } } else { if (use_radiance_map) { - state.scene_shader.set_uniform(SceneShaderGLES2::RADIANCE_INVERSE_XFORM, p_view_transform); + if (p_env) { + Transform sky_orientation(p_env->sky_orientation, Vector3(0.0, 0.0, 0.0)); + state.scene_shader.set_uniform(SceneShaderGLES2::RADIANCE_INVERSE_XFORM, sky_orientation.affine_inverse() * p_view_transform); + } else { + // would be a bit weird if we dont have this... + state.scene_shader.set_uniform(SceneShaderGLES2::RADIANCE_INVERSE_XFORM, p_view_transform); + } } if (p_env) { @@ -2393,7 +2406,7 @@ void RasterizerSceneGLES2::_render_render_list(RenderList::Element **p_elements, state.scene_shader.set_conditional(SceneShaderGLES2::FOG_HEIGHT_ENABLED, false); } -void RasterizerSceneGLES2::_draw_sky(RasterizerStorageGLES2::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy) { +void RasterizerSceneGLES2::_draw_sky(RasterizerStorageGLES2::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy, const Basis &p_sky_orientation) { ERR_FAIL_COND(!p_sky); RasterizerStorageGLES2::Texture *tex = storage->texture_owner.getornull(p_sky->panorama); @@ -2473,6 +2486,10 @@ void RasterizerSceneGLES2::_draw_sky(RasterizerStorageGLES2::Sky *p_sky, const C storage->shaders.copy.set_conditional(CopyShaderGLES2::USE_CUSTOM_ALPHA, false); storage->shaders.copy.bind(); storage->shaders.copy.set_uniform(CopyShaderGLES2::MULTIPLIER, p_energy); + + // don't know why but I always have problems setting a uniform mat3, so we're using a transform + storage->shaders.copy.set_uniform(CopyShaderGLES2::SKY_TRANSFORM, Transform(p_sky_orientation, Vector3(0.0, 0.0, 0.0)).affine_inverse()); + if (asymmetrical) { // pack the bits we need from our projection matrix storage->shaders.copy.set_uniform(CopyShaderGLES2::ASYM_PROJ, camera.matrix[2][0], camera.matrix[0][0], camera.matrix[2][1], camera.matrix[1][1]); @@ -2648,7 +2665,7 @@ void RasterizerSceneGLES2::render_scene(const Transform &p_cam_transform, const if (env && env->bg_mode == VS::ENV_BG_SKY && (!storage->frame.current_rt || !storage->frame.current_rt->flags[RasterizerStorage::RENDER_TARGET_TRANSPARENT])) { if (sky && sky->panorama.is_valid()) { - _draw_sky(sky, p_cam_projection, p_cam_transform, false, env->sky_custom_fov, env->bg_energy); + _draw_sky(sky, p_cam_projection, p_cam_transform, false, env->sky_custom_fov, env->bg_energy, env->sky_orientation); } } diff --git a/drivers/gles2/rasterizer_scene_gles2.h b/drivers/gles2/rasterizer_scene_gles2.h index 4236554d9d..9cb87bb019 100644 --- a/drivers/gles2/rasterizer_scene_gles2.h +++ b/drivers/gles2/rasterizer_scene_gles2.h @@ -344,6 +344,7 @@ public: RID sky; float sky_custom_fov; + Basis sky_orientation; Color bg_color; float bg_energy; @@ -403,6 +404,7 @@ public: virtual void environment_set_background(RID p_env, VS::EnvironmentBG p_bg); virtual void environment_set_sky(RID p_env, RID p_sky); virtual void environment_set_sky_custom_fov(RID p_env, float p_scale); + virtual void environment_set_sky_orientation(RID p_env, const Basis &p_orientation); virtual void environment_set_bg_color(RID p_env, const Color &p_color); virtual void environment_set_bg_energy(RID p_env, float p_energy); virtual void environment_set_canvas_max_layer(RID p_env, int p_max_layer); @@ -654,7 +656,7 @@ public: bool p_alpha_pass, bool p_shadow); - void _draw_sky(RasterizerStorageGLES2::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy); + void _draw_sky(RasterizerStorageGLES2::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy, const Basis &p_sky_orientation); _FORCE_INLINE_ bool _setup_material(RasterizerStorageGLES2::Material *p_material, bool p_reverse_cull, bool p_alpha_pass, Size2i p_skeleton_tex_size = Size2i(0, 0)); _FORCE_INLINE_ void _setup_geometry(RenderList::Element *p_element, RasterizerStorageGLES2::Skeleton *p_skeleton); diff --git a/drivers/gles2/shaders/copy.glsl b/drivers/gles2/shaders/copy.glsl index 0b8da4f875..97a24212a5 100644 --- a/drivers/gles2/shaders/copy.glsl +++ b/drivers/gles2/shaders/copy.glsl @@ -92,6 +92,7 @@ uniform float custom_alpha; #endif #if defined(USE_PANORAMA) || defined(USE_ASYM_PANO) +uniform highp mat4 sky_transform; vec4 texturePanorama(sampler2D pano, vec3 normal) { @@ -113,7 +114,12 @@ void main() { #ifdef USE_PANORAMA - vec4 color = texturePanorama(source, normalize(cube_interp)); + vec3 cube_normal = normalize(cube_interp); + cube_normal.z = -cube_normal.z; + cube_normal = mat3(sky_transform) * cube_normal; + cube_normal.z = -cube_normal.z; + + vec4 color = texturePanorama(source, cube_normal); #elif defined(USE_ASYM_PANO) @@ -125,7 +131,7 @@ void main() { cube_normal.z = -1000000.0; cube_normal.x = (cube_normal.z * (-uv_interp.x - asym_proj.x)) / asym_proj.y; cube_normal.y = (cube_normal.z * (-uv_interp.y - asym_proj.z)) / asym_proj.a; - cube_normal = mat3(pano_transform) * cube_normal; + cube_normal = mat3(sky_transform) * mat3(pano_transform) * cube_normal; cube_normal.z = -cube_normal.z; vec4 color = texturePanorama(source, normalize(cube_normal.xyz)); diff --git a/drivers/gles2/shaders/lens_distorted.glsl b/drivers/gles2/shaders/lens_distorted.glsl index d541db9bf9..efa35adf7d 100644 --- a/drivers/gles2/shaders/lens_distorted.glsl +++ b/drivers/gles2/shaders/lens_distorted.glsl @@ -1,6 +1,14 @@ /* clang-format off */ [vertex] +#ifdef USE_GLES_OVER_GL +#define mediump +#define highp +#else +precision highp float; +precision highp int; +#endif + attribute highp vec2 vertex; // attrib:0 /* clang-format on */ @@ -20,6 +28,14 @@ void main() { /* clang-format off */ [fragment] +#ifdef USE_GLES_OVER_GL +#define mediump +#define highp +#else +precision mediump float; +precision highp int; +#endif + uniform sampler2D source; //texunit:0 /* clang-format on */ diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index ffe9e1c831..183fe8c5f8 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -791,6 +791,14 @@ void RasterizerSceneGLES3::environment_set_sky_custom_fov(RID p_env, float p_sca env->sky_custom_fov = p_scale; } +void RasterizerSceneGLES3::environment_set_sky_orientation(RID p_env, const Basis &p_orientation) { + + Environment *env = environment_owner.getornull(p_env); + ERR_FAIL_COND(!env); + + env->sky_orientation = p_orientation; +} + void RasterizerSceneGLES3::environment_set_bg_color(RID p_env, const Color &p_color) { Environment *env = environment_owner.getornull(p_env); @@ -2427,7 +2435,7 @@ void RasterizerSceneGLES3::_add_geometry_with_material(RasterizerStorageGLES3::G } } -void RasterizerSceneGLES3::_draw_sky(RasterizerStorageGLES3::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy) { +void RasterizerSceneGLES3::_draw_sky(RasterizerStorageGLES3::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy, const Basis &p_sky_orientation) { ERR_FAIL_COND(!p_sky); @@ -2519,7 +2527,12 @@ void RasterizerSceneGLES3::_draw_sky(RasterizerStorageGLES3::Sky *p_sky, const C storage->shaders.copy.set_conditional(CopyShaderGLES3::USE_PANORAMA, !asymmetrical); storage->shaders.copy.set_conditional(CopyShaderGLES3::USE_MULTIPLIER, true); storage->shaders.copy.bind(); + storage->shaders.copy.set_uniform(CopyShaderGLES3::MULTIPLIER, p_energy); + + // don't know why but I always have problems setting a uniform mat3, so we're using a transform + storage->shaders.copy.set_uniform(CopyShaderGLES3::SKY_TRANSFORM, Transform(p_sky_orientation, Vector3(0.0, 0.0, 0.0)).affine_inverse()); + if (asymmetrical) { // pack the bits we need from our projection matrix storage->shaders.copy.set_uniform(CopyShaderGLES3::ASYM_PROJ, camera.matrix[2][0], camera.matrix[0][0], camera.matrix[2][1], camera.matrix[1][1]); @@ -2538,6 +2551,7 @@ void RasterizerSceneGLES3::_draw_sky(RasterizerStorageGLES3::Sky *p_sky, const C } void RasterizerSceneGLES3::_setup_environment(Environment *env, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, bool p_no_fog) { + Transform sky_orientation; //store camera into ubo store_camera(p_cam_projection, state.ubo_data.projection_matrix); @@ -2578,6 +2592,9 @@ void RasterizerSceneGLES3::_setup_environment(Environment *env, const CameraMatr state.ubo_data.bg_color[2] = bg_color.b; state.ubo_data.bg_color[3] = bg_color.a; + //use the inverse of our sky_orientation, we may need to skip this if we're using a reflection probe? + sky_orientation = Transform(env->sky_orientation, Vector3(0.0, 0.0, 0.0)).affine_inverse(); + state.env_radiance_data.ambient_contribution = env->ambient_sky_contribution; state.ubo_data.ambient_occlusion_affect_light = env->ssao_light_affect; state.ubo_data.ambient_occlusion_affect_ssao = env->ssao_ao_channel_affect; @@ -2646,7 +2663,7 @@ void RasterizerSceneGLES3::_setup_environment(Environment *env, const CameraMatr //fill up environment - store_transform(p_cam_transform, state.env_radiance_data.transform); + store_transform(sky_orientation * p_cam_transform, state.env_radiance_data.transform); glBindBuffer(GL_UNIFORM_BUFFER, state.env_radiance_ubo); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(State::EnvironmentRadianceUBO), &state.env_radiance_data); @@ -4389,7 +4406,7 @@ void RasterizerSceneGLES3::render_scene(const Transform &p_cam_transform, const */ if (sky && sky->panorama.is_valid()) - _draw_sky(sky, p_cam_projection, p_cam_transform, false, env->sky_custom_fov, env->bg_energy); + _draw_sky(sky, p_cam_projection, p_cam_transform, false, env->sky_custom_fov, env->bg_energy, env->sky_orientation); } //_render_list_forward(&alpha_render_list,camera_transform,camera_transform_inverse,camera_projection,false,fragment_lighting,true); diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h index 1d84fb487b..a198168da8 100644 --- a/drivers/gles3/rasterizer_scene_gles3.h +++ b/drivers/gles3/rasterizer_scene_gles3.h @@ -114,7 +114,7 @@ public: TonemapShaderGLES3 tonemap_shader; struct SceneDataUBO { - //this is a std140 compatible struct. Please read the OpenGL 3.3 Specificaiton spec before doing any changes + //this is a std140 compatible struct. Please read the OpenGL 3.3 Specification spec before doing any changes float projection_matrix[16]; float inv_projection_matrix[16]; float camera_inverse_matrix[16]; @@ -365,6 +365,7 @@ public: RID sky; float sky_custom_fov; + Basis sky_orientation; Color bg_color; float bg_energy; @@ -531,6 +532,7 @@ public: virtual void environment_set_background(RID p_env, VS::EnvironmentBG p_bg); virtual void environment_set_sky(RID p_env, RID p_sky); virtual void environment_set_sky_custom_fov(RID p_env, float p_scale); + virtual void environment_set_sky_orientation(RID p_env, const Basis &p_orientation); virtual void environment_set_bg_color(RID p_env, const Color &p_color); virtual void environment_set_bg_energy(RID p_env, float p_energy); virtual void environment_set_canvas_max_layer(RID p_env, int p_max_layer); @@ -823,7 +825,7 @@ public: _FORCE_INLINE_ void _add_geometry_with_material(RasterizerStorageGLES3::Geometry *p_geometry, InstanceBase *p_instance, RasterizerStorageGLES3::GeometryOwner *p_owner, RasterizerStorageGLES3::Material *p_material, bool p_depth_pass, bool p_shadow_pass); - void _draw_sky(RasterizerStorageGLES3::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy); + void _draw_sky(RasterizerStorageGLES3::Sky *p_sky, const CameraMatrix &p_projection, const Transform &p_transform, bool p_vflip, float p_custom_fov, float p_energy, const Basis &p_sky_orientation); void _setup_environment(Environment *env, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, bool p_no_fog = false); void _setup_directional_light(int p_index, const Transform &p_camera_inverse_transform, bool p_use_shadows); diff --git a/drivers/gles3/shaders/copy.glsl b/drivers/gles3/shaders/copy.glsl index a5637537d2..3b36bc7cc1 100644 --- a/drivers/gles3/shaders/copy.glsl +++ b/drivers/gles3/shaders/copy.glsl @@ -79,6 +79,7 @@ uniform float multiplier; #endif #if defined(USE_PANORAMA) || defined(USE_ASYM_PANO) +uniform highp mat4 sky_transform; vec4 texturePanorama(vec3 normal, sampler2D pano) { @@ -121,7 +122,12 @@ void main() { #ifdef USE_PANORAMA - vec4 color = texturePanorama(normalize(cube_interp), source); + vec3 cube_normal = normalize(cube_interp); + cube_normal.z = -cube_normal.z; + cube_normal = mat3(sky_transform) * cube_normal; + cube_normal.z = -cube_normal.z; + + vec4 color = texturePanorama(cube_normal, source); #elif defined(USE_ASYM_PANO) @@ -133,7 +139,7 @@ void main() { cube_normal.z = -1000000.0; cube_normal.x = (cube_normal.z * (-uv_interp.x - asym_proj.x)) / asym_proj.y; cube_normal.y = (cube_normal.z * (-uv_interp.y - asym_proj.z)) / asym_proj.a; - cube_normal = mat3(pano_transform) * cube_normal; + cube_normal = mat3(sky_transform) * mat3(pano_transform) * cube_normal; cube_normal.z = -cube_normal.z; vec4 color = texturePanorama(normalize(cube_normal.xyz), source); diff --git a/drivers/png/resource_saver_png.h b/drivers/png/resource_saver_png.h index 34950f6723..2d5fd0882c 100644 --- a/drivers/png/resource_saver_png.h +++ b/drivers/png/resource_saver_png.h @@ -35,6 +35,7 @@ #include "core/io/resource_saver.h" class ResourceSaverPNG : public ResourceFormatSaver { + GDCLASS(ResourceSaverPNG, ResourceFormatSaver) public: static Error save_image(const String &p_path, const Ref<Image> &p_img); diff --git a/drivers/register_driver_types.cpp b/drivers/register_driver_types.cpp index 9f5d9c1abf..9a6e0f45fc 100644 --- a/drivers/register_driver_types.cpp +++ b/drivers/register_driver_types.cpp @@ -42,15 +42,15 @@ #include "platform/windows/export/export.h" #endif -static ImageLoaderPNG *image_loader_png = NULL; -static ResourceSaverPNG *resource_saver_png = NULL; +static ImageLoaderPNG *image_loader_png; +static Ref<ResourceSaverPNG> resource_saver_png; void register_core_driver_types() { image_loader_png = memnew(ImageLoaderPNG); ImageLoader::add_image_format_loader(image_loader_png); - resource_saver_png = memnew(ResourceSaverPNG); + resource_saver_png.instance(); ResourceSaver::add_resource_format_saver(resource_saver_png); } @@ -58,8 +58,11 @@ void unregister_core_driver_types() { if (image_loader_png) memdelete(image_loader_png); - if (resource_saver_png) - memdelete(resource_saver_png); + + if (resource_saver_png.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_png); + resource_saver_png.unref(); + } } void register_driver_types() { diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index 94eb1a3399..37e2079b64 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -1401,6 +1401,14 @@ void EditorFileSystem::update_script_classes() { ScriptServer::save_global_classes(); EditorNode::get_editor_data().script_class_save_icon_paths(); + + // Rescan custom loaders and savers. + // Doing the following here because the `filesystem_changed` signal fires multiple times and isn't always followed by script classes update. + // So I thought it's better to do this when script classes really get updated + ResourceLoader::remove_custom_loaders(); + ResourceLoader::add_custom_loaders(); + ResourceSaver::remove_custom_savers(); + ResourceSaver::add_custom_savers(); } void EditorFileSystem::_queue_update_script_classes() { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index d100d7f618..3bd7678746 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -2471,7 +2471,7 @@ void EditorNode::_editor_select(int p_which) { } } -void EditorNode::add_editor_plugin(EditorPlugin *p_editor) { +void EditorNode::add_editor_plugin(EditorPlugin *p_editor, bool p_config_changed) { if (p_editor->has_main_screen()) { @@ -2496,9 +2496,11 @@ void EditorNode::add_editor_plugin(EditorPlugin *p_editor) { } singleton->editor_data.add_editor_plugin(p_editor); singleton->add_child(p_editor); + if (p_config_changed) + p_editor->enable_plugin(); } -void EditorNode::remove_editor_plugin(EditorPlugin *p_editor) { +void EditorNode::remove_editor_plugin(EditorPlugin *p_editor, bool p_config_changed) { if (p_editor->has_main_screen()) { @@ -2521,6 +2523,8 @@ void EditorNode::remove_editor_plugin(EditorPlugin *p_editor) { } p_editor->make_visible(false); p_editor->clear(); + if (p_config_changed) + p_editor->disable_plugin(); singleton->editor_plugins_over->get_plugins_list().erase(p_editor); singleton->remove_child(p_editor); singleton->editor_data.remove_editor_plugin(p_editor); @@ -2546,7 +2550,7 @@ void EditorNode::_update_addon_config() { project_settings->queue_save(); } -void EditorNode::set_addon_plugin_enabled(const String &p_addon, bool p_enabled) { +void EditorNode::set_addon_plugin_enabled(const String &p_addon, bool p_enabled, bool p_config_changed) { ERR_FAIL_COND(p_enabled && plugin_addons.has(p_addon)); ERR_FAIL_COND(!p_enabled && !plugin_addons.has(p_addon)); @@ -2554,7 +2558,7 @@ void EditorNode::set_addon_plugin_enabled(const String &p_addon, bool p_enabled) if (!p_enabled) { EditorPlugin *addon = plugin_addons[p_addon]; - remove_editor_plugin(addon); + remove_editor_plugin(addon, p_config_changed); memdelete(addon); //bye plugin_addons.erase(p_addon); _update_addon_config(); @@ -2606,7 +2610,7 @@ void EditorNode::set_addon_plugin_enabled(const String &p_addon, bool p_enabled) ep->set_script(script.get_ref_ptr()); ep->set_dir_cache(p_addon); plugin_addons[p_addon] = ep; - add_editor_plugin(ep); + add_editor_plugin(ep, p_config_changed); _update_addon_config(); } @@ -3131,6 +3135,9 @@ bool EditorNode::is_scene_in_use(const String &p_path) { void EditorNode::register_editor_types() { + ResourceLoader::set_timestamp_on_load(true); + ResourceSaver::set_timestamp_on_save(true); + ClassDB::register_class<EditorPlugin>(); ClassDB::register_class<EditorImportPlugin>(); ClassDB::register_class<EditorScript>(); @@ -4822,9 +4829,6 @@ EditorNode::EditorNode() { ResourceLoader::set_error_notify_func(this, _load_error_notify); ResourceLoader::set_dependency_error_notify_func(this, _dependency_error_report); - ResourceLoader::set_timestamp_on_load(true); - ResourceSaver::set_timestamp_on_save(true); - { //register importers at the beginning, so dialogs are created with the right extensions Ref<ResourceImporterTexture> import_texture; import_texture.instance(); diff --git a/editor/editor_node.h b/editor/editor_node.h index e5670e5e7c..f4c0375c2e 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -637,8 +637,8 @@ public: ProjectSettingsEditor *get_project_settings() { return project_settings; } - static void add_editor_plugin(EditorPlugin *p_editor); - static void remove_editor_plugin(EditorPlugin *p_editor); + static void add_editor_plugin(EditorPlugin *p_editor, bool p_config_changed = false); + static void remove_editor_plugin(EditorPlugin *p_editor, bool p_config_changed = false); void new_inherited_scene() { _menu_option_confirm(FILE_NEW_INHERITED_SCENE, false); } @@ -651,7 +651,7 @@ public: void add_control_to_dock(DockSlot p_slot, Control *p_control); void remove_control_from_dock(Control *p_control); - void set_addon_plugin_enabled(const String &p_addon, bool p_enabled); + void set_addon_plugin_enabled(const String &p_addon, bool p_enabled, bool p_config_changed = false); bool is_addon_plugin_enabled(const String &p_addon) const; void edit_node(Node *p_node); diff --git a/editor/editor_plugin.cpp b/editor/editor_plugin.cpp index cd024ff870..403db4b8ba 100644 --- a/editor/editor_plugin.cpp +++ b/editor/editor_plugin.cpp @@ -698,6 +698,34 @@ void EditorPlugin::remove_scene_import_plugin(const Ref<EditorSceneImporter> &p_ ResourceImporterScene::get_singleton()->remove_importer(p_importer); } +int find(const PoolStringArray &a, const String &v) { + PoolStringArray::Read r = a.read(); + for (int j = 0; j < a.size(); ++j) { + if (r[j] == v) { + return j; + } + } + return -1; +} + +void EditorPlugin::enable_plugin() { + // Called when the plugin gets enabled in project settings, after it's added to the tree. + // You can implement it to register autoloads. + + if (get_script_instance() && get_script_instance()->has_method("enable_plugin")) { + get_script_instance()->call("enable_plugin"); + } +} + +void EditorPlugin::disable_plugin() { + // Last function called when the plugin gets disabled in project settings. + // Implement it to cleanup things from the project, such as unregister autoloads. + + if (get_script_instance() && get_script_instance()->has_method("disable_plugin")) { + get_script_instance()->call("disable_plugin"); + } +} + void EditorPlugin::set_window_layout(Ref<ConfigFile> p_layout) { if (get_script_instance() && get_script_instance()->has_method("set_window_layout")) { @@ -801,6 +829,8 @@ void EditorPlugin::_bind_methods() { ClassDB::add_virtual_method(get_class_static(), MethodInfo("set_window_layout", PropertyInfo(Variant::OBJECT, "layout", PROPERTY_HINT_RESOURCE_TYPE, "ConfigFile"))); ClassDB::add_virtual_method(get_class_static(), MethodInfo("get_window_layout", PropertyInfo(Variant::OBJECT, "layout", PROPERTY_HINT_RESOURCE_TYPE, "ConfigFile"))); ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "build")); + ClassDB::add_virtual_method(get_class_static(), MethodInfo("enable_plugin")); + ClassDB::add_virtual_method(get_class_static(), MethodInfo("disable_plugin")); ADD_SIGNAL(MethodInfo("scene_changed", PropertyInfo(Variant::OBJECT, "scene_root", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); ADD_SIGNAL(MethodInfo("scene_closed", PropertyInfo(Variant::STRING, "filepath"))); diff --git a/editor/editor_plugin.h b/editor/editor_plugin.h index e03aeb5d30..5f060a4e96 100644 --- a/editor/editor_plugin.h +++ b/editor/editor_plugin.h @@ -232,6 +232,9 @@ public: String get_dir_cache() { return _dir_cache; } Ref<ConfigFile> get_config(); + void enable_plugin(); + void disable_plugin(); + EditorPlugin(); virtual ~EditorPlugin(); }; diff --git a/editor/editor_plugin_settings.cpp b/editor/editor_plugin_settings.cpp index 30027c0c34..a4d543c567 100644 --- a/editor/editor_plugin_settings.cpp +++ b/editor/editor_plugin_settings.cpp @@ -152,7 +152,7 @@ void EditorPluginSettings::_plugin_activity_changed() { bool active = ti->get_range(3); String name = ti->get_metadata(0); - EditorNode::get_singleton()->set_addon_plugin_enabled(name, active); + EditorNode::get_singleton()->set_addon_plugin_enabled(name, active, true); bool is_active = EditorNode::get_singleton()->is_addon_plugin_enabled(name); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 039ba22a77..fbcab3cbb6 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -581,6 +581,9 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { /* Extra config */ + _initial_set("project_manager/sorting_order", 0); + hints["project_manager/sorting_order"] = PropertyInfo(Variant::INT, "project_manager/sorting_order", PROPERTY_HINT_ENUM, "Name,Last Modified"); + if (p_extra_config.is_valid()) { if (p_extra_config->has_section("init_projects") && p_extra_config->has_section_key("init_projects", "list")) { diff --git a/editor/editor_spin_slider.cpp b/editor/editor_spin_slider.cpp index 1b7322fd13..3d4cf41b80 100644 --- a/editor/editor_spin_slider.cpp +++ b/editor/editor_spin_slider.cpp @@ -155,7 +155,9 @@ void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) { void EditorSpinSlider::_notification(int p_what) { - if (p_what == MainLoop::NOTIFICATION_WM_FOCUS_OUT || p_what == MainLoop::NOTIFICATION_WM_FOCUS_IN) { + if (p_what == MainLoop::NOTIFICATION_WM_FOCUS_OUT || + p_what == MainLoop::NOTIFICATION_WM_FOCUS_IN || + p_what == NOTIFICATION_EXIT_TREE) { if (grabbing_spinner) { Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); grabbing_spinner = false; diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index 36053d7534..eacf2ee1b4 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -351,18 +351,18 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_color("mono_color", "Editor", mono_color); - Color success_color = accent_color.linear_interpolate(Color(0.2, 1, 0.2), 0.6) * 1.2; - Color warning_color = accent_color.linear_interpolate(Color(1, 1, 0), 0.7) * 1.0; - Color error_color = accent_color.linear_interpolate(Color(1, 0, 0), 0.8) * 1.7; + Color success_color = Color(0.45, 0.95, 0.5); + Color warning_color = Color(1, 0.87, 0.4); + Color error_color = Color(1, 0.47, 0.42); Color property_color = font_color.linear_interpolate(Color(0.5, 0.5, 0.5), 0.5); if (!dark_theme) { - // yellow on white themes is a P.I.T.A. - warning_color = accent_color.linear_interpolate(Color(0.9, 0.7, 0), 0.9); - warning_color = warning_color.linear_interpolate(mono_color, 0.2); - success_color = success_color.linear_interpolate(mono_color, 0.2); - error_color = error_color.linear_interpolate(mono_color, 0.2); + // Darken some colors to be readable on a light background + success_color = success_color.linear_interpolate(mono_color, 0.35); + warning_color = warning_color.linear_interpolate(mono_color, 0.35); + error_color = error_color.linear_interpolate(mono_color, 0.25); } + theme->set_color("success_color", "Editor", success_color); theme->set_color("warning_color", "Editor", warning_color); theme->set_color("error_color", "Editor", error_color); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index b2368fff6b..4cc0b67b8d 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -1180,7 +1180,7 @@ void FileSystemDock::_make_dir_confirm() { String dir_name = make_dir_dialog_text->get_text().strip_edges(); if (dir_name.length() == 0) { - EditorNode::get_singleton()->show_warning(TTR("No name provided")); + EditorNode::get_singleton()->show_warning(TTR("No name provided.")); return; } else if (dir_name.find("/") != -1 || dir_name.find("\\") != -1 || dir_name.find(":") != -1 || dir_name.find("*") != -1 || dir_name.find("|") != -1 || dir_name.find(">") != -1 || dir_name.ends_with(".") || dir_name.ends_with(" ")) { diff --git a/editor/plugins/animation_blend_space_1d_editor.cpp b/editor/plugins/animation_blend_space_1d_editor.cpp index 4b1e710705..cb18832641 100644 --- a/editor/plugins/animation_blend_space_1d_editor.cpp +++ b/editor/plugins/animation_blend_space_1d_editor.cpp @@ -72,7 +72,7 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_gui_input(const Ref<InputEven ap->get_animation_list(&names); for (List<StringName>::Element *E = names.front(); E; E = E->next()) { - animations_menu->add_icon_item(get_icon("Animation", "Editoricons"), E->get()); + animations_menu->add_icon_item(get_icon("Animation", "EditorIcons"), E->get()); animations_to_add.push_back(E->get()); } } @@ -650,6 +650,7 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() { snap->set_toggle_mode(true); top_hb->add_child(snap); snap->set_pressed(true); + snap->set_tooltip(TTR("Enable snap and show grid.")); snap->connect("pressed", this, "_snap_toggled"); snap_value = memnew(SpinBox); diff --git a/editor/plugins/animation_blend_space_2d_editor.cpp b/editor/plugins/animation_blend_space_2d_editor.cpp index e2fe9a91d8..0b7b4910da 100644 --- a/editor/plugins/animation_blend_space_2d_editor.cpp +++ b/editor/plugins/animation_blend_space_2d_editor.cpp @@ -904,8 +904,8 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() { snap = memnew(ToolButton); snap->set_toggle_mode(true); top_hb->add_child(snap); - //snap->set_text(TTR("Snap")); snap->set_pressed(true); + snap->set_tooltip(TTR("Enable snap and show grid.")); snap->connect("pressed", this, "_snap_toggled"); snap_x = memnew(SpinBox); diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp index eb3c432ee7..06c2db74af 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.cpp +++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp @@ -237,23 +237,17 @@ void AnimationNodeBlendTreeEditor::_update_graph() { mb->get_popup()->connect("index_pressed", this, "_anim_selected", varray(options, E->get()), CONNECT_DEFERRED); } - /* should be no longer necessary, as the boolean works - Ref<AnimationNodeOneShot> oneshot = agnode; - if (oneshot.is_valid()) { - - HBoxContainer *play_stop = memnew(HBoxContainer); - play_stop->add_spacer(); - Button *play = memnew(Button); - play->set_icon(get_icon("Play", "EditorIcons")); - play->connect("pressed", this, "_oneshot_start", varray(E->get()), CONNECT_DEFERRED); - play_stop->add_child(play); - Button *stop = memnew(Button); - stop->set_icon(get_icon("Stop", "EditorIcons")); - stop->connect("pressed", this, "_oneshot_stop", varray(E->get()), CONNECT_DEFERRED); - play_stop->add_child(stop); - play_stop->add_spacer(); - node->add_child(play_stop); - } */ + if (EditorSettings::get_singleton()->get("interface/theme/use_graph_node_headers")) { + Ref<StyleBoxFlat> sb = node->get_stylebox("frame", "GraphNode"); + Color c = sb->get_border_color(MARGIN_TOP); + Color mono_color = ((c.r + c.g + c.b) / 3) < 0.7 ? Color(1.0, 1.0, 1.0) : Color(0.0, 0.0, 0.0); + mono_color.a = 0.85; + c = mono_color; + + node->add_color_override("title_color", c); + c.a = 0.7; + node->add_color_override("close_color", c); + } } List<AnimationNodeBlendTree::NodeConnection> connections; @@ -401,7 +395,7 @@ void AnimationNodeBlendTreeEditor::_delete_request(const String &p_which) { undo_redo->create_action("Delete Node"); undo_redo->add_do_method(blend_tree.ptr(), "remove_node", p_which); - undo_redo->add_undo_method(blend_tree.ptr(), "add_node", p_which, blend_tree->get_node(p_which)); + undo_redo->add_undo_method(blend_tree.ptr(), "add_node", p_which, blend_tree->get_node(p_which), blend_tree.ptr()->get_node_position(p_which)); List<AnimationNodeBlendTree::NodeConnection> conns; blend_tree->get_node_connections(&conns); @@ -646,6 +640,9 @@ void AnimationNodeBlendTreeEditor::_notification(int p_what) { error_panel->add_style_override("panel", get_stylebox("bg", "Tree")); error_label->add_color_override("font_color", get_color("error_color", "Editor")); + + if (p_what == NOTIFICATION_THEME_CHANGED && is_visible_in_tree()) + _update_graph(); } if (p_what == NOTIFICATION_PROCESS) { @@ -816,6 +813,8 @@ void AnimationNodeBlendTreeEditor::_node_renamed(const String &p_text, Ref<Anima break; } } + + _update_graph(); // Needed to update the signal connections with the new name. } void AnimationNodeBlendTreeEditor::_node_renamed_focus_out(Node *le, Ref<AnimationNode> p_node) { diff --git a/editor/plugins/animation_state_machine_editor.cpp b/editor/plugins/animation_state_machine_editor.cpp index 990c77430f..8efd12ecf9 100644 --- a/editor/plugins/animation_state_machine_editor.cpp +++ b/editor/plugins/animation_state_machine_editor.cpp @@ -118,7 +118,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv menu->add_item(TTR("Paste"), MENU_PASTE); } menu->add_separator(); - menu->add_item(TTR("Load.."), MENU_LOAD_FILE); + menu->add_item(TTR("Load..."), MENU_LOAD_FILE); menu->set_global_position(state_machine_draw->get_global_transform().xform(mb->get_position())); menu->popup(); @@ -1073,7 +1073,9 @@ void AnimationNodeStateMachineEditor::_name_edited(const String &p_text) { ERR_FAIL_COND(new_name == "" || new_name.find(".") != -1 || new_name.find("/") != -1) - ERR_FAIL_COND(new_name == prev_name); + if (new_name == prev_name) { + return; // Nothing to do. + } String base_name = new_name; int base = 1; @@ -1111,7 +1113,7 @@ void AnimationNodeStateMachineEditor::_erase_selected() { updating = true; undo_redo->create_action("Node Removed"); undo_redo->add_do_method(state_machine.ptr(), "remove_node", selected_node); - undo_redo->add_undo_method(state_machine.ptr(), "add_node", selected_node, state_machine->get_node(selected_node)); + undo_redo->add_undo_method(state_machine.ptr(), "add_node", selected_node, state_machine->get_node(selected_node), state_machine->get_node_position(selected_node)); for (int i = 0; i < state_machine->get_transition_count(); i++) { String from = state_machine->get_transition_from(i); String to = state_machine->get_transition_to(i); @@ -1267,7 +1269,7 @@ AnimationNodeStateMachineEditor::AnimationNodeStateMachineEditor() { top_hb->add_child(tool_erase_hb); tool_erase_hb->add_child(memnew(VSeparator)); tool_erase = memnew(ToolButton); - tool_erase->set_tooltip(TTR("Remove selected node or transition")); + tool_erase->set_tooltip(TTR("Remove selected node or transition.")); tool_erase_hb->add_child(tool_erase); tool_erase->connect("pressed", this, "_erase_selected"); tool_erase->set_disabled(true); diff --git a/editor/plugins/animation_tree_editor_plugin.cpp b/editor/plugins/animation_tree_editor_plugin.cpp index 24787a78e9..387a254c4f 100644 --- a/editor/plugins/animation_tree_editor_plugin.cpp +++ b/editor/plugins/animation_tree_editor_plugin.cpp @@ -64,34 +64,15 @@ void AnimationTreeEditor::edit(AnimationTree *p_tree) { void AnimationTreeEditor::_path_button_pressed(int p_path) { - Ref<AnimationNode> node = tree->get_tree_root(); - if (node.is_null()) - return; - edited_path.clear(); - if (p_path >= 0) { - for (int i = 0; i <= p_path; i++) { - Ref<AnimationNode> child = node->get_child_by_name(button_path[i]); - ERR_BREAK(child.is_null()); - node = child; - edited_path.push_back(button_path[i]); - } - } - - for (int i = 0; i < editors.size(); i++) { - if (editors[i]->can_edit(node)) { - editors[i]->edit(node); - editors[i]->show(); - } else { - editors[i]->edit(Ref<AnimationNode>()); - editors[i]->hide(); - } + for (int i = 0; i <= p_path; i++) { + edited_path.push_back(button_path[i]); } } void AnimationTreeEditor::_update_path() { - while (path_hb->get_child_count()) { - memdelete(path_hb->get_child(0)); + while (path_hb->get_child_count() > 1) { + memdelete(path_hb->get_child(1)); } Ref<ButtonGroup> group; @@ -176,6 +157,10 @@ void AnimationTreeEditor::_notification(int p_what) { if (root != current_root) { edit_path(Vector<String>()); } + + if (button_path.size() != edited_path.size()) { + edit_path(edited_path); + } } } @@ -251,6 +236,9 @@ AnimationTreeEditor::AnimationTreeEditor() { path_edit->set_enable_v_scroll(false); path_hb = memnew(HBoxContainer); path_edit->add_child(path_hb); + path_hb->add_child(memnew(Label(TTR("Path:")))); + + add_child(memnew(HSeparator)); current_root = 0; singleton = this; diff --git a/editor/plugins/polygon_2d_editor_plugin.cpp b/editor/plugins/polygon_2d_editor_plugin.cpp index 97448f3f88..91639a40f9 100644 --- a/editor/plugins/polygon_2d_editor_plugin.cpp +++ b/editor/plugins/polygon_2d_editor_plugin.cpp @@ -91,7 +91,7 @@ void Polygon2DEditor::_notification(int p_what) { void Polygon2DEditor::_sync_bones() { - Skeleton2D *skeleton; + Skeleton2D *skeleton = NULL; if (!node->has_node(node->get_skeleton())) { error->set_text(TTR("The skeleton property of the Polygon2D does not point to a Skeleton2D node")); error->popup_centered_minsize(); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index d3295c0f51..aff465728b 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -341,6 +341,18 @@ void VisualShaderEditor::_update_graph() { node->add_child(hb); node->set_slot(i + port_offset, valid_left, port_left, type_color[port_left], valid_right, port_right, type_color[port_right]); + + if (EditorSettings::get_singleton()->get("interface/theme/use_graph_node_headers")) { + Ref<StyleBoxFlat> sb = node->get_stylebox("frame", "GraphNode"); + Color c = sb->get_border_color(MARGIN_TOP); + Color mono_color = ((c.r + c.g + c.b) / 3) < 0.7 ? Color(1.0, 1.0, 1.0) : Color(0.0, 0.0, 0.0); + mono_color.a = 0.85; + c = mono_color; + + node->add_color_override("title_color", c); + c.a = 0.7; + node->add_color_override("close_color", c); + } } if (vsnode->get_output_port_for_preview() >= 0) { @@ -605,6 +617,9 @@ void VisualShaderEditor::_notification(int p_what) { error_panel->add_style_override("panel", get_stylebox("bg", "Tree")); error_label->add_color_override("font_color", get_color("error_color", "Editor")); + + if (p_what == NOTIFICATION_THEME_CHANGED && is_visible_in_tree()) + _update_graph(); } if (p_what == NOTIFICATION_PROCESS) { diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 0e2e957333..ac728e8d7c 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -1165,10 +1165,12 @@ void ProjectManager::_load_recent_projects() { bool set_ordered_latest_modification; ProjectListFilter::FilterOption filter_order_option = project_order_filter->get_filter_option(); - if (filter_order_option == ProjectListFilter::FILTER_NAME) + if (filter_order_option == ProjectListFilter::FILTER_NAME) { set_ordered_latest_modification = false; - else + } else { set_ordered_latest_modification = true; + } + EditorSettings::get_singleton()->set("project_manager/sorting_order", (int)filter_order_option); List<ProjectItem> projects; List<ProjectItem> favorite_projects; @@ -1838,16 +1840,19 @@ ProjectManager::ProjectManager() { Label *sort_label = memnew(Label); sort_label->set_text(TTR("Sort:")); sort_filters->add_child(sort_label); - Vector<String> vec1; - vec1.push_back("Name"); - vec1.push_back("Last Modified"); + Vector<String> sort_filter_titles; + sort_filter_titles.push_back("Name"); + sort_filter_titles.push_back("Last Modified"); project_order_filter = memnew(ProjectListFilter); - project_order_filter->_setup_filters(vec1); + project_order_filter->_setup_filters(sort_filter_titles); project_order_filter->set_filter_size(150); sort_filters->add_child(project_order_filter); project_order_filter->connect("filter_changed", this, "_load_recent_projects"); project_order_filter->set_custom_minimum_size(Size2(180, 10) * EDSCALE); + int projects_sorting_order = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); + project_order_filter->set_filter_option((ProjectListFilter::FilterOption)projects_sorting_order); + sort_filters->add_spacer(true); Label *search_label = memnew(Label); search_label->set_text(TTR("Search:")); @@ -2074,6 +2079,11 @@ ProjectListFilter::FilterOption ProjectListFilter::get_filter_option() { return _current_filter; } +void ProjectListFilter::set_filter_option(FilterOption option) { + filter_option->select((int)option); + _filter_option_selected(0); +} + void ProjectListFilter::_filter_option_selected(int p_idx) { FilterOption selected = (FilterOption)(filter_option->get_selected()); if (_current_filter != selected) { diff --git a/editor/project_manager.h b/editor/project_manager.h index 88fc081272..e0a0932cf8 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -152,6 +152,7 @@ public: void set_filter_size(int h_size); String get_search_term(); FilterOption get_filter_option(); + void set_filter_option(FilterOption); ProjectListFilter(); }; diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index 7205f34343..657293ea7f 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -668,7 +668,7 @@ void SceneTreeEditor::_renamed() { // Empty node names are not allowed, so resets it to previous text and show warning if (which->get_text(0).strip_edges().empty()) { which->set_text(0, n->get_name()); - EditorNode::get_singleton()->show_warning(TTR("No name provided")); + EditorNode::get_singleton()->show_warning(TTR("No name provided.")); return; } diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp index df704706af..e0c95e3bbc 100644 --- a/editor/script_create_dialog.cpp +++ b/editor/script_create_dialog.cpp @@ -308,8 +308,10 @@ void ScriptCreateDialog::_built_in_pressed() { if (internal->is_pressed()) { is_built_in = true; + is_new_script_created = true; } else { is_built_in = false; + _path_changed(file_path->get_text()); } _update_dialog(); } diff --git a/main/main.cpp b/main/main.cpp index 7383294167..f6902ffe23 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1230,6 +1230,7 @@ Error Main::setup2(Thread::ID p_main_tid_override) { register_driver_types(); + // This loads global classes, so it must happen before custom loaders and savers are registered ScriptServer::init_languages(); MAIN_PRINT("Main: Load Translations"); @@ -1489,6 +1490,9 @@ bool Main::start() { } #endif + ResourceLoader::add_custom_loaders(); + ResourceSaver::add_custom_savers(); + if (!project_manager && !editor) { // game if (game_path != "" || script != "") { //autoload @@ -1961,11 +1965,13 @@ void Main::force_redraw() { * so that the engine closes cleanly without leaking memory or crashing. * The order matters as some of those steps are linked with each other. */ - void Main::cleanup() { ERR_FAIL_COND(!_start_success); + ResourceLoader::remove_custom_loaders(); + ResourceSaver::remove_custom_savers(); + message_queue->flush(); memdelete(message_queue); diff --git a/main/tests/test_oa_hash_map.cpp b/main/tests/test_oa_hash_map.cpp index deaba285cf..984a661aaa 100644 --- a/main/tests/test_oa_hash_map.cpp +++ b/main/tests/test_oa_hash_map.cpp @@ -95,7 +95,7 @@ MainLoop *test() { // stress test / test for issue #22928 { OAHashMap<int, int> map; - int dummy; + int dummy = 0; const int N = 1000; uint32_t *keys = new uint32_t[N]; diff --git a/modules/dds/register_types.cpp b/modules/dds/register_types.cpp index d6351fb6fe..94f0ebfd5f 100644 --- a/modules/dds/register_types.cpp +++ b/modules/dds/register_types.cpp @@ -32,15 +32,16 @@ #include "texture_loader_dds.h" -static ResourceFormatDDS *resource_loader_dds = NULL; +static Ref<ResourceFormatDDS> resource_loader_dds; void register_dds_types() { - resource_loader_dds = memnew(ResourceFormatDDS); + resource_loader_dds.instance(); ResourceLoader::add_resource_format_loader(resource_loader_dds); } void unregister_dds_types() { - memdelete(resource_loader_dds); + ResourceLoader::remove_resource_format_loader(resource_loader_dds); + resource_loader_dds.unref(); } diff --git a/modules/dds/texture_loader_dds.h b/modules/dds/texture_loader_dds.h index 4e2593c744..abd747f63e 100644 --- a/modules/dds/texture_loader_dds.h +++ b/modules/dds/texture_loader_dds.h @@ -35,6 +35,7 @@ #include "scene/resources/texture.h" class ResourceFormatDDS : public ResourceFormatLoader { + GDCLASS(ResourceFormatDDS, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/etc/register_types.cpp b/modules/etc/register_types.cpp index 1d1f0e1b77..301193874d 100644 --- a/modules/etc/register_types.cpp +++ b/modules/etc/register_types.cpp @@ -33,11 +33,11 @@ #include "image_etc.h" #include "texture_loader_pkm.h" -static ResourceFormatPKM *resource_loader_pkm = NULL; +static Ref<ResourceFormatPKM> resource_loader_pkm; void register_etc_types() { - resource_loader_pkm = memnew(ResourceFormatPKM); + resource_loader_pkm.instance(); ResourceLoader::add_resource_format_loader(resource_loader_pkm); _register_etc_compress_func(); @@ -45,5 +45,6 @@ void register_etc_types() { void unregister_etc_types() { - memdelete(resource_loader_pkm); + ResourceLoader::remove_resource_format_loader(resource_loader_pkm); + resource_loader_pkm.unref(); } diff --git a/modules/etc/texture_loader_pkm.h b/modules/etc/texture_loader_pkm.h index b5a95767c7..8308b84109 100644 --- a/modules/etc/texture_loader_pkm.h +++ b/modules/etc/texture_loader_pkm.h @@ -35,6 +35,7 @@ #include "scene/resources/texture.h" class ResourceFormatPKM : public ResourceFormatLoader { + GDCLASS(ResourceFormatPKM, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/gdnative/doc_classes/PluginScript.xml b/modules/gdnative/doc_classes/PluginScript.xml index 27c6adae3f..1876d06c20 100644 --- a/modules/gdnative/doc_classes/PluginScript.xml +++ b/modules/gdnative/doc_classes/PluginScript.xml @@ -9,6 +9,13 @@ <demos> </demos> <methods> + <method name="new" qualifiers="vararg"> + <return type="Object"> + </return> + <description> + Returns a new instance of the script. + </description> + </method> </methods> <constants> </constants> diff --git a/modules/gdnative/gdnative.h b/modules/gdnative/gdnative.h index c5364a72ac..528344a814 100644 --- a/modules/gdnative/gdnative.h +++ b/modules/gdnative/gdnative.h @@ -161,6 +161,7 @@ public: }; class GDNativeLibraryResourceLoader : public ResourceFormatLoader { + GDCLASS(GDNativeLibraryResourceLoader, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path, Error *r_error); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -169,6 +170,7 @@ public: }; class GDNativeLibraryResourceSaver : public ResourceFormatSaver { + GDCLASS(GDNativeLibraryResourceSaver, ResourceFormatSaver) public: virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags); virtual bool recognize(const RES &p_resource) const; diff --git a/modules/gdnative/gdnative/string.cpp b/modules/gdnative/gdnative/string.cpp index 0996633b70..c0c7ab5420 100644 --- a/modules/gdnative/gdnative/string.cpp +++ b/modules/gdnative/gdnative/string.cpp @@ -74,7 +74,7 @@ void GDAPI godot_string_new_with_wide_string(godot_string *r_dest, const wchar_t memnew_placement(dest, String(p_contents, p_size)); } -wchar_t GDAPI *godot_string_operator_index(godot_string *p_self, const godot_int p_idx) { +const wchar_t GDAPI *godot_string_operator_index(godot_string *p_self, const godot_int p_idx) { String *self = (String *)p_self; return &(self->operator[](p_idx)); } diff --git a/modules/gdnative/gdnative_api.json b/modules/gdnative/gdnative_api.json index c5a1fa139e..df7110f075 100644 --- a/modules/gdnative/gdnative_api.json +++ b/modules/gdnative/gdnative_api.json @@ -4755,7 +4755,7 @@ }, { "name": "godot_string_operator_index", - "return_type": "wchar_t *", + "return_type": "const wchar_t *", "arguments": [ ["godot_string *", "p_self"], ["const godot_int", "p_idx"] diff --git a/modules/gdnative/include/gdnative/string.h b/modules/gdnative/include/gdnative/string.h index 95ae42a9ec..2ca9fd9e15 100644 --- a/modules/gdnative/include/gdnative/string.h +++ b/modules/gdnative/include/gdnative/string.h @@ -79,7 +79,7 @@ void GDAPI godot_string_new(godot_string *r_dest); void GDAPI godot_string_new_copy(godot_string *r_dest, const godot_string *p_src); void GDAPI godot_string_new_with_wide_string(godot_string *r_dest, const wchar_t *p_contents, const int p_size); -wchar_t GDAPI *godot_string_operator_index(godot_string *p_self, const godot_int p_idx); +const wchar_t GDAPI *godot_string_operator_index(godot_string *p_self, const godot_int p_idx); wchar_t GDAPI godot_string_operator_index_const(const godot_string *p_self, const godot_int p_idx); const wchar_t GDAPI *godot_string_wide_str(const godot_string *p_self); diff --git a/modules/gdnative/nativescript/nativescript.h b/modules/gdnative/nativescript/nativescript.h index e6f3c06ee5..67fb54387d 100644 --- a/modules/gdnative/nativescript/nativescript.h +++ b/modules/gdnative/nativescript/nativescript.h @@ -380,6 +380,7 @@ public: }; class ResourceFormatLoaderNativeScript : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderNativeScript, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -388,6 +389,7 @@ public: }; class ResourceFormatSaverNativeScript : public ResourceFormatSaver { + GDCLASS(ResourceFormatSaverNativeScript, ResourceFormatSaver) virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); virtual bool recognize(const RES &p_resource) const; virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const; diff --git a/modules/gdnative/nativescript/register_types.cpp b/modules/gdnative/nativescript/register_types.cpp index 4433c0a638..1e0c3d9fd8 100644 --- a/modules/gdnative/nativescript/register_types.cpp +++ b/modules/gdnative/nativescript/register_types.cpp @@ -39,8 +39,8 @@ NativeScriptLanguage *native_script_language; -ResourceFormatLoaderNativeScript *resource_loader_gdns = NULL; -ResourceFormatSaverNativeScript *resource_saver_gdns = NULL; +Ref<ResourceFormatLoaderNativeScript> resource_loader_gdns; +Ref<ResourceFormatSaverNativeScript> resource_saver_gdns; void register_nativescript_types() { native_script_language = memnew(NativeScriptLanguage); @@ -50,18 +50,20 @@ void register_nativescript_types() { native_script_language->set_language_index(ScriptServer::get_language_count()); ScriptServer::register_language(native_script_language); - resource_saver_gdns = memnew(ResourceFormatSaverNativeScript); + resource_saver_gdns.instance(); ResourceSaver::add_resource_format_saver(resource_saver_gdns); - resource_loader_gdns = memnew(ResourceFormatLoaderNativeScript); + resource_loader_gdns.instance(); ResourceLoader::add_resource_format_loader(resource_loader_gdns); } void unregister_nativescript_types() { - memdelete(resource_loader_gdns); + ResourceLoader::remove_resource_format_loader(resource_loader_gdns); + resource_loader_gdns.unref(); - memdelete(resource_saver_gdns); + ResourceSaver::remove_resource_format_saver(resource_saver_gdns); + resource_saver_gdns.unref(); if (native_script_language) { ScriptServer::unregister_language(native_script_language); diff --git a/modules/gdnative/pluginscript/pluginscript_language.cpp b/modules/gdnative/pluginscript/pluginscript_language.cpp index 2b538c4a36..0cda8859dd 100644 --- a/modules/gdnative/pluginscript/pluginscript_language.cpp +++ b/modules/gdnative/pluginscript/pluginscript_language.cpp @@ -417,8 +417,8 @@ void PluginScriptLanguage::unlock() { PluginScriptLanguage::PluginScriptLanguage(const godot_pluginscript_language_desc *desc) : _desc(*desc) { - _resource_loader = memnew(ResourceFormatLoaderPluginScript(this)); - _resource_saver = memnew(ResourceFormatSaverPluginScript(this)); + _resource_loader = Ref<ResourceFormatLoaderPluginScript>(memnew(ResourceFormatLoaderPluginScript(this))); + _resource_saver = Ref<ResourceFormatSaverPluginScript>(memnew(ResourceFormatSaverPluginScript(this))); // TODO: totally remove _lock attribute if NO_THREADS is set #ifdef NO_THREADS @@ -429,8 +429,6 @@ PluginScriptLanguage::PluginScriptLanguage(const godot_pluginscript_language_des } PluginScriptLanguage::~PluginScriptLanguage() { - memdelete(_resource_loader); - memdelete(_resource_saver); #ifndef NO_THREADS if (_lock) { memdelete(_lock); diff --git a/modules/gdnative/pluginscript/pluginscript_language.h b/modules/gdnative/pluginscript/pluginscript_language.h index c4df6f3a33..f749e900f5 100644 --- a/modules/gdnative/pluginscript/pluginscript_language.h +++ b/modules/gdnative/pluginscript/pluginscript_language.h @@ -48,8 +48,8 @@ class PluginScriptLanguage : public ScriptLanguage { friend class PluginScript; friend class PluginScriptInstance; - ResourceFormatLoaderPluginScript *_resource_loader; - ResourceFormatSaverPluginScript *_resource_saver; + Ref<ResourceFormatLoaderPluginScript> _resource_loader; + Ref<ResourceFormatSaverPluginScript> _resource_saver; const godot_pluginscript_language_desc _desc; godot_pluginscript_language_data *_data; @@ -59,8 +59,8 @@ class PluginScriptLanguage : public ScriptLanguage { public: virtual String get_name() const; - _FORCE_INLINE_ ResourceFormatLoaderPluginScript *get_resource_loader() { return _resource_loader; }; - _FORCE_INLINE_ ResourceFormatSaverPluginScript *get_resource_saver() { return _resource_saver; }; + _FORCE_INLINE_ Ref<ResourceFormatLoaderPluginScript> get_resource_loader() { return _resource_loader; } + _FORCE_INLINE_ Ref<ResourceFormatSaverPluginScript> get_resource_saver() { return _resource_saver; } /* LANGUAGE FUNCTIONS */ virtual void init(); diff --git a/modules/gdnative/pluginscript/pluginscript_loader.h b/modules/gdnative/pluginscript/pluginscript_loader.h index 5c17bb932e..ee9869be84 100644 --- a/modules/gdnative/pluginscript/pluginscript_loader.h +++ b/modules/gdnative/pluginscript/pluginscript_loader.h @@ -39,6 +39,9 @@ class PluginScriptLanguage; class ResourceFormatLoaderPluginScript : public ResourceFormatLoader { + + GDCLASS(ResourceFormatLoaderPluginScript, ResourceFormatLoader) + PluginScriptLanguage *_language; public: @@ -50,6 +53,9 @@ public: }; class ResourceFormatSaverPluginScript : public ResourceFormatSaver { + + GDCLASS(ResourceFormatSaverPluginScript, ResourceFormatSaver) + PluginScriptLanguage *_language; public: diff --git a/modules/gdnative/pluginscript/pluginscript_script.cpp b/modules/gdnative/pluginscript/pluginscript_script.cpp index c3a623e9a1..9821cf39e4 100644 --- a/modules/gdnative/pluginscript/pluginscript_script.cpp +++ b/modules/gdnative/pluginscript/pluginscript_script.cpp @@ -52,6 +52,79 @@ #endif void PluginScript::_bind_methods() { + ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "new", &PluginScript::_new, MethodInfo(Variant::OBJECT, "new")); +} + +PluginScriptInstance *PluginScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, Variant::CallError &r_error) { + + r_error.error = Variant::CallError::CALL_OK; + + // Create instance + PluginScriptInstance *instance = memnew(PluginScriptInstance()); + + if (instance->init(this, p_owner)) { + _language->lock(); + _instances.insert(instance->get_owner()); + _language->unlock(); + } else { + r_error.error = Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL; + memdelete(instance); + ERR_FAIL_V(NULL); + } + + // Construct + // TODO: Support arguments in the constructor? + // There is currently no way to get the constructor function name of the script. + // instance->call("__init__", p_args, p_argcount, r_error); + if (p_argcount > 0) { + WARN_PRINT("PluginScript doesn't support arguments in the constructor") + } + + return instance; +} + +Variant PluginScript::_new(const Variant **p_args, int p_argcount, Variant::CallError &r_error) { + + r_error.error = Variant::CallError::CALL_OK; + + if (!_valid) { + r_error.error = Variant::CallError::CALL_ERROR_INVALID_METHOD; + return Variant(); + } + + REF ref; + Object *owner = NULL; + + if (get_instance_base_type() == "") { + owner = memnew(Reference); + } else { + owner = ClassDB::instance(get_instance_base_type()); + } + + if (!owner) { + r_error.error = Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL; + return Variant(); + } + + Reference *r = Object::cast_to<Reference>(owner); + if (r) { + ref = REF(r); + } + + PluginScriptInstance *instance = _create_instance(p_args, p_argcount, owner, r_error); + + if (!instance) { + if (ref.is_null()) { + memdelete(owner); //no owner, sorry + } + return Variant(); + } + + if (ref.is_valid()) { + return ref; + } else { + return owner; + } } #ifdef TOOLS_ENABLED @@ -129,17 +202,8 @@ ScriptInstance *PluginScript::instance_create(Object *p_this) { } } - PluginScriptInstance *instance = memnew(PluginScriptInstance()); - const bool success = instance->init(this, p_this); - if (success) { - _language->lock(); - _instances.insert(instance->get_owner()); - _language->unlock(); - return instance; - } else { - memdelete(instance); - ERR_FAIL_V(NULL); - } + Variant::CallError unchecked_error; + return _create_instance(NULL, 0, p_this, unchecked_error); } bool PluginScript::instance_has(const Object *p_this) const { diff --git a/modules/gdnative/pluginscript/pluginscript_script.h b/modules/gdnative/pluginscript/pluginscript_script.h index 3ade8ac004..68f26b514c 100644 --- a/modules/gdnative/pluginscript/pluginscript_script.h +++ b/modules/gdnative/pluginscript/pluginscript_script.h @@ -37,8 +37,6 @@ #include "pluginscript_language.h" #include <pluginscript/godot_pluginscript.h> -class PyInstance; - class PluginScript : public Script { GDCLASS(PluginScript, Script); @@ -74,6 +72,9 @@ private: protected: static void _bind_methods(); + PluginScriptInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, Variant::CallError &r_error); + Variant _new(const Variant **p_args, int p_argcount, Variant::CallError &r_error); + #ifdef TOOLS_ENABLED Set<PlaceHolderScriptInstance *> placeholders; //void _update_placeholder(PlaceHolderScriptInstance *p_placeholder); diff --git a/modules/gdnative/register_types.cpp b/modules/gdnative/register_types.cpp index 62e87c3651..51f0b9afcc 100644 --- a/modules/gdnative/register_types.cpp +++ b/modules/gdnative/register_types.cpp @@ -299,8 +299,8 @@ GDNativeCallRegistry *GDNativeCallRegistry::singleton; Vector<Ref<GDNative> > singleton_gdnatives; -GDNativeLibraryResourceLoader *resource_loader_gdnlib = NULL; -GDNativeLibraryResourceSaver *resource_saver_gdnlib = NULL; +Ref<GDNativeLibraryResourceLoader> resource_loader_gdnlib; +Ref<GDNativeLibraryResourceSaver> resource_saver_gdnlib; void register_gdnative_types() { @@ -312,8 +312,8 @@ void register_gdnative_types() { ClassDB::register_class<GDNativeLibrary>(); ClassDB::register_class<GDNative>(); - resource_loader_gdnlib = memnew(GDNativeLibraryResourceLoader); - resource_saver_gdnlib = memnew(GDNativeLibraryResourceSaver); + resource_loader_gdnlib.instance(); + resource_saver_gdnlib.instance(); ResourceLoader::add_resource_format_loader(resource_loader_gdnlib); ResourceSaver::add_resource_format_saver(resource_saver_gdnlib); @@ -391,8 +391,11 @@ void unregister_gdnative_types() { } #endif - memdelete(resource_loader_gdnlib); - memdelete(resource_saver_gdnlib); + ResourceLoader::remove_resource_format_loader(resource_loader_gdnlib); + ResourceSaver::remove_resource_format_saver(resource_saver_gdnlib); + + resource_loader_gdnlib.unref(); + resource_saver_gdnlib.unref(); // This is for printing out the sizes of the core types diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 752d660ffb..3e7cb076aa 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -500,6 +500,7 @@ public: }; class ResourceFormatLoaderGDScript : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderGDScript, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -508,6 +509,7 @@ public: }; class ResourceFormatSaverGDScript : public ResourceFormatSaver { + GDCLASS(ResourceFormatSaverGDScript, ResourceFormatSaver) public: virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const; diff --git a/modules/gdscript/register_types.cpp b/modules/gdscript/register_types.cpp index 26dcbdcf89..9c4dc0c926 100644 --- a/modules/gdscript/register_types.cpp +++ b/modules/gdscript/register_types.cpp @@ -38,8 +38,8 @@ #include "gdscript_tokenizer.h" GDScriptLanguage *script_language_gd = NULL; -ResourceFormatLoaderGDScript *resource_loader_gd = NULL; -ResourceFormatSaverGDScript *resource_saver_gd = NULL; +Ref<ResourceFormatLoaderGDScript> resource_loader_gd; +Ref<ResourceFormatSaverGDScript> resource_saver_gd; #ifdef TOOLS_ENABLED @@ -87,9 +87,11 @@ void register_gdscript_types() { script_language_gd = memnew(GDScriptLanguage); ScriptServer::register_language(script_language_gd); - resource_loader_gd = memnew(ResourceFormatLoaderGDScript); + + resource_loader_gd.instance(); ResourceLoader::add_resource_format_loader(resource_loader_gd); - resource_saver_gd = memnew(ResourceFormatSaverGDScript); + + resource_saver_gd.instance(); ResourceSaver::add_resource_format_saver(resource_saver_gd); #ifdef TOOLS_ENABLED @@ -104,8 +106,14 @@ void unregister_gdscript_types() { if (script_language_gd) memdelete(script_language_gd); - if (resource_loader_gd) - memdelete(resource_loader_gd); - if (resource_saver_gd) - memdelete(resource_saver_gd); + + if (resource_loader_gd.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_gd); + resource_loader_gd.unref(); + } + + if (resource_saver_gd.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_gd); + resource_saver_gd.unref(); + } } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 08466bae58..695889b2a8 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -398,6 +398,7 @@ public: }; class ResourceFormatLoaderCSharpScript : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderCSharpScript, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -406,6 +407,7 @@ public: }; class ResourceFormatSaverCSharpScript : public ResourceFormatSaver { + GDCLASS(ResourceFormatSaverCSharpScript, ResourceFormatSaver) public: virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const; diff --git a/modules/mono/mono_gd/gd_mono_marshal.cpp b/modules/mono/mono_gd/gd_mono_marshal.cpp index 3f0a5d6e50..b0b2aec667 100644 --- a/modules/mono/mono_gd/gd_mono_marshal.cpp +++ b/modules/mono/mono_gd/gd_mono_marshal.cpp @@ -216,7 +216,7 @@ String mono_to_utf16_string(MonoString *p_mono_string) { ret.set(len, 0); CharType *src = (CharType *)mono_string_chars(p_mono_string); - CharType *dst = &(ret.operator[](0)); + CharType *dst = ret.ptrw(); for (int i = 0; i < len; i++) { dst[i] = src[i]; diff --git a/modules/mono/register_types.cpp b/modules/mono/register_types.cpp index f6cb143e8e..9ee7a0b5c4 100644 --- a/modules/mono/register_types.cpp +++ b/modules/mono/register_types.cpp @@ -35,8 +35,8 @@ #include "csharp_script.h" CSharpLanguage *script_language_cs = NULL; -ResourceFormatLoaderCSharpScript *resource_loader_cs = NULL; -ResourceFormatSaverCSharpScript *resource_saver_cs = NULL; +Ref<ResourceFormatLoaderCSharpScript> resource_loader_cs; +Ref<ResourceFormatSaverCSharpScript> resource_saver_cs; _GodotSharp *_godotsharp = NULL; @@ -52,9 +52,10 @@ void register_mono_types() { script_language_cs->set_language_index(ScriptServer::get_language_count()); ScriptServer::register_language(script_language_cs); - resource_loader_cs = memnew(ResourceFormatLoaderCSharpScript); + resource_loader_cs.instance(); ResourceLoader::add_resource_format_loader(resource_loader_cs); - resource_saver_cs = memnew(ResourceFormatSaverCSharpScript); + + resource_saver_cs.instance(); ResourceSaver::add_resource_format_saver(resource_saver_cs); } @@ -63,10 +64,16 @@ void unregister_mono_types() { if (script_language_cs) memdelete(script_language_cs); - if (resource_loader_cs) - memdelete(resource_loader_cs); - if (resource_saver_cs) - memdelete(resource_saver_cs); + + if (resource_loader_cs.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_cs); + resource_loader_cs.unref(); + } + + if (resource_saver_cs.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_cs); + resource_saver_cs.unref(); + } if (_godotsharp) memdelete(_godotsharp); diff --git a/modules/opus/audio_stream_opus.h b/modules/opus/audio_stream_opus.h index c004adeb77..c962936238 100644 --- a/modules/opus/audio_stream_opus.h +++ b/modules/opus/audio_stream_opus.h @@ -132,6 +132,7 @@ public: }; class ResourceFormatLoaderAudioStreamOpus : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderAudioStreamOpus, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/opus/register_types.cpp b/modules/opus/register_types.cpp index f34555841e..3367fc1090 100644 --- a/modules/opus/register_types.cpp +++ b/modules/opus/register_types.cpp @@ -32,7 +32,7 @@ #include "audio_stream_opus.h" -static ResourceFormatLoaderAudioStreamOpus *opus_stream_loader = NULL; +static Ref<ResourceFormatLoaderAudioStreamOpus> opus_stream_loader; void register_opus_types() { // Sorry guys, do not enable this unless you can figure out a way diff --git a/modules/pvr/register_types.cpp b/modules/pvr/register_types.cpp index 0991828ef2..cf5bdabf3f 100644 --- a/modules/pvr/register_types.cpp +++ b/modules/pvr/register_types.cpp @@ -32,15 +32,16 @@ #include "texture_loader_pvr.h" -static ResourceFormatPVR *resource_loader_pvr = NULL; +static Ref<ResourceFormatPVR> resource_loader_pvr; void register_pvr_types() { - resource_loader_pvr = memnew(ResourceFormatPVR); + resource_loader_pvr.instance(); ResourceLoader::add_resource_format_loader(resource_loader_pvr); } void unregister_pvr_types() { - memdelete(resource_loader_pvr); + ResourceLoader::remove_resource_format_loader(resource_loader_pvr); + resource_loader_pvr.unref(); } diff --git a/modules/pvr/texture_loader_pvr.h b/modules/pvr/texture_loader_pvr.h index c859a4cdda..530b6948f0 100644 --- a/modules/pvr/texture_loader_pvr.h +++ b/modules/pvr/texture_loader_pvr.h @@ -35,6 +35,7 @@ #include "scene/resources/texture.h" class ResourceFormatPVR : public ResourceFormatLoader { + GDCLASS(ResourceFormatPVR, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path, Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/theora/register_types.cpp b/modules/theora/register_types.cpp index 971fe39c44..3da721c6a0 100644 --- a/modules/theora/register_types.cpp +++ b/modules/theora/register_types.cpp @@ -32,11 +32,11 @@ #include "video_stream_theora.h" -static ResourceFormatLoaderTheora *resource_loader_theora = NULL; +static Ref<ResourceFormatLoaderTheora> resource_loader_theora; void register_theora_types() { - resource_loader_theora = memnew(ResourceFormatLoaderTheora); + resource_loader_theora.instance(); ResourceLoader::add_resource_format_loader(resource_loader_theora, true); ClassDB::register_class<VideoStreamTheora>(); @@ -44,7 +44,9 @@ void register_theora_types() { void unregister_theora_types() { - if (resource_loader_theora) { - memdelete(resource_loader_theora); + ResourceLoader::remove_resource_format_loader(resource_loader_theora); + + if (resource_loader_theora.is_valid()) { + resource_loader_theora.unref(); } } diff --git a/modules/theora/video_stream_theora.h b/modules/theora/video_stream_theora.h index 4be723f85b..c9629fbf9f 100644 --- a/modules/theora/video_stream_theora.h +++ b/modules/theora/video_stream_theora.h @@ -186,6 +186,7 @@ public: }; class ResourceFormatLoaderTheora : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderTheora, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/vorbis/audio_stream_ogg_vorbis.h b/modules/vorbis/audio_stream_ogg_vorbis.h index 73c4b5f3f4..7683f38991 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.h +++ b/modules/vorbis/audio_stream_ogg_vorbis.h @@ -127,6 +127,7 @@ public: }; class ResourceFormatLoaderAudioStreamOGGVorbis : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderAudioStreamOGGVorbis, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/modules/vorbis/register_types.cpp b/modules/vorbis/register_types.cpp index 0ea1fbe8b2..8219e9e7ba 100644 --- a/modules/vorbis/register_types.cpp +++ b/modules/vorbis/register_types.cpp @@ -32,16 +32,17 @@ #include "audio_stream_ogg_vorbis.h" -static ResourceFormatLoaderAudioStreamOGGVorbis *vorbis_stream_loader = NULL; +static Ref<ResourceFormatLoaderAudioStreamOGGVorbis> vorbis_stream_loader; void register_vorbis_types() { - vorbis_stream_loader = memnew(ResourceFormatLoaderAudioStreamOGGVorbis); + vorbis_stream_loader.instance(); ResourceLoader::add_resource_format_loader(vorbis_stream_loader); ClassDB::register_class<AudioStreamOGGVorbis>(); } void unregister_vorbis_types() { - memdelete(vorbis_stream_loader); + ResourceLoader::remove_resource_format_loader(vorbis_stream_loader); + vorbis_stream_loader.unref(); } diff --git a/modules/webm/register_types.cpp b/modules/webm/register_types.cpp index 121b528d5b..dfa2dcf21c 100644 --- a/modules/webm/register_types.cpp +++ b/modules/webm/register_types.cpp @@ -32,11 +32,11 @@ #include "video_stream_webm.h" -static ResourceFormatLoaderWebm *resource_loader_webm = NULL; +static Ref<ResourceFormatLoaderWebm> resource_loader_webm; void register_webm_types() { - resource_loader_webm = memnew(ResourceFormatLoaderWebm); + resource_loader_webm.instance(); ResourceLoader::add_resource_format_loader(resource_loader_webm, true); ClassDB::register_class<VideoStreamWebm>(); @@ -44,7 +44,9 @@ void register_webm_types() { void unregister_webm_types() { - if (resource_loader_webm) { - memdelete(resource_loader_webm); + ResourceLoader::remove_resource_format_loader(resource_loader_webm); + + if (resource_loader_webm.is_valid()) { + resource_loader_webm.unref(); } } diff --git a/modules/webm/video_stream_webm.h b/modules/webm/video_stream_webm.h index 3739a73114..9501f513a0 100644 --- a/modules/webm/video_stream_webm.h +++ b/modules/webm/video_stream_webm.h @@ -127,6 +127,7 @@ public: }; class ResourceFormatLoaderWebm : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderWebm, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/platform/android/AndroidManifest.xml.template b/platform/android/AndroidManifest.xml.template index 81f4c15849..d8fb9326ea 100644 --- a/platform/android/AndroidManifest.xml.template +++ b/platform/android/AndroidManifest.xml.template @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.godot.game" + xmlns:tools="http://schemas.android.com/tools" + package="com.godot.game" android:versionCode="1" android:versionName="1.0" android:installLocation="auto" @@ -10,32 +11,29 @@ android:largeScreens="true" android:xlargeScreens="true"/> - <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" $$ADD_APPATTRIBUTE_CHUNKS$$ > + <uses-feature android:glEsVersion="0x00020000" android:required="true" /> + +$$ADD_PERMISSION_CHUNKS$$ + + <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" $$ADD_APPATTRIBUTE_CHUNKS$$ > <activity android:name="org.godotengine.godot.Godot" android:label="@string/godot_project_name_string" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:launchMode="singleTask" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize" - android:resizeableActivity="false"> + android:resizeableActivity="false" + tools:ignore="UnusedAttribute"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name="org.godotengine.godot.GodotDownloaderService" /> - - - + <service android:name="org.godotengine.godot.GodotDownloaderService" /> $$ADD_APPLICATION_CHUNKS$$ </application> - <uses-feature android:glEsVersion="0x00020000" android:required="true" /> - -$$ADD_PERMISSION_CHUNKS$$ - -<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="27"/> </manifest> diff --git a/platform/android/SCsub b/platform/android/SCsub index 6d5af99bc5..fb0ec75b16 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -108,7 +108,7 @@ for x in env.android_asset_dirs: gradle_default_config_text = "" minSdk = 18 -targetSdk = 27 +targetSdk = 28 for x in env.android_default_config: if x.startswith("minSdkVersion") and int(x.split(" ")[-1]) < minSdk: diff --git a/platform/android/build.gradle.template b/platform/android/build.gradle.template index 18ffc74fc3..2fea250061 100644 --- a/platform/android/build.gradle.template +++ b/platform/android/build.gradle.template @@ -5,7 +5,7 @@ buildscript { $$GRADLE_REPOSITORY_URLS$$ } dependencies { - classpath 'com.android.tools.build:gradle:3.2.0' + classpath 'com.android.tools.build:gradle:3.2.1' $$GRADLE_CLASSPATH$$ } } @@ -22,6 +22,7 @@ allprojects { } dependencies { + implementation "com.android.support:support-core-utils:28.0.0" $$GRADLE_DEPENDENCIES$$ } @@ -29,10 +30,10 @@ android { lintOptions { abortOnError false - disable 'MissingTranslation' + disable 'MissingTranslation','UnusedResources' } - compileSdkVersion 27 + compileSdkVersion 28 buildToolsVersion "28.0.3" useLibrary 'org.apache.http.legacy' diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 7eda32b754..7f8d50b8ab 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -1564,7 +1564,7 @@ public: _fix_resources(p_preset, data); } - if (file == "res/drawable/icon.png") { + if (file == "res/drawable-nodpi-v4/icon.png") { bool found = false; for (unsigned int i = 0; i < sizeof(launcher_icons) / sizeof(launcher_icons[0]); ++i) { String icon_path = String(p_preset->get(launcher_icons[i].option_id)).strip_edges(); diff --git a/platform/android/java/README.md b/platform/android/java/README.md new file mode 100644 index 0000000000..58d2b10706 --- /dev/null +++ b/platform/android/java/README.md @@ -0,0 +1,47 @@ +# Third party libraries + + +## Google's vending library + +- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/src/main/java/com/google/android/vending +- Version: git (eb57657, 2018) with modifications +- License: Apache 2.0 + +Overwrite all files under `com/google/android/vending` + +### Modify some files to avoid compile error and lint warning + +#### com/google/android/vending/licensing/util/Base64.java +``` +@@ -338,7 +338,8 @@ public class Base64 { + e += 4; + } + +- assert (e == outBuff.length); ++ if (BuildConfig.DEBUG && e != outBuff.length) ++ throw new RuntimeException(); + return outBuff; + } +``` + +#### com/google/android/vending/licensing/LicenseChecker.java +``` +@@ -29,8 +29,8 @@ 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.ILicenseResultListener; ++import com.google.android.vending.licensing.ILicensingService; + import com.google.android.vending.licensing.util.Base64; + import com.google.android.vending.licensing.util.Base64DecoderException; +``` +``` +@@ -287,13 +287,15 @@ public class LicenseChecker implements ServiceConnection { + if (logResponse) { +- String android_id = Secure.getString(mContext.getContentResolver(), +- Secure.ANDROID_ID); ++ String android_id = Secure.ANDROID_ID; + Date date = new Date(); +``` diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.jar b/platform/android/java/gradle/wrapper/gradle-wrapper.jar Binary files differindex 13372aef5e..f6b961fd5a 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.jar +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.jar diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index 6fb3a79546..bf3de21830 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sat Jul 29 16:10:03 ICT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/platform/android/java/gradlew b/platform/android/java/gradlew index 9d82f78915..cccdd3d517 100755 --- a/platform/android/java/gradlew +++ b/platform/android/java/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/platform/android/java/gradlew.bat b/platform/android/java/gradlew.bat index 8a0b282aa6..e95643d6a2 100644 --- a/platform/android/java/gradlew.bat +++ b/platform/android/java/gradlew.bat @@ -1,90 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differindex 372b763ec5..2c246b04a4 100644 --- a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png +++ b/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differindex c61c440636..8bcd464bed 100644 --- a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png +++ b/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable/icon.png b/platform/android/java/res/drawable-nodpi/icon.png Binary files differindex 6ad9b43117..6ad9b43117 100644 --- a/platform/android/java/res/drawable/icon.png +++ b/platform/android/java/res/drawable-nodpi/icon.png diff --git a/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..372b763ec5 --- /dev/null +++ b/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..b458ff3057 --- /dev/null +++ b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/layout/downloading_expansion.xml b/platform/android/java/res/layout/downloading_expansion.xml index d678d94eac..4a9700965f 100644 --- a/platform/android/java/res/layout/downloading_expansion.xml +++ b/platform/android/java/res/layout/downloading_expansion.xml @@ -15,7 +15,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" - android:layout_marginLeft="5dp" + android:layout_marginStart="5dp" android:layout_marginTop="10dp" android:textStyle="bold" /> @@ -23,12 +23,11 @@ android:id="@+id/downloaderDashboard" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_below="@id/statusText" android:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_weight="1" > <TextView @@ -36,18 +35,15 @@ style="@android:style/TextAppearance.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_marginLeft="5dp" - android:text="0MB / 0MB" > - </TextView> + android:layout_alignParentStart="true" + android:layout_marginStart="5dp" /> <TextView android:id="@+id/progressAsPercentage" style="@android:style/TextAppearance.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignRight="@+id/progressBar" - android:text="0%" /> + android:layout_alignEnd="@+id/progressBar" /> <ProgressBar android:id="@+id/progressBar" @@ -58,24 +54,23 @@ android:layout_marginBottom="10dp" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" - android:layout_marginTop="10dp" - android:layout_weight="1" /> + android:layout_marginTop="10dp" /> <TextView android:id="@+id/progressAverageSpeed" style="@android:style/TextAppearance.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_below="@+id/progressBar" - android:layout_marginLeft="5dp" /> + android:layout_marginStart="5dp" /> <TextView android:id="@+id/progressTimeRemaining" style="@android:style/TextAppearance.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignRight="@+id/progressBar" + android:layout_alignEnd="@+id/progressBar" android:layout_below="@+id/progressBar" /> </RelativeLayout> @@ -86,33 +81,35 @@ android:orientation="horizontal" > <Button - android:id="@+id/pauseButton" + android:id="@+id/cancelButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginBottom="10dp" - android:layout_marginLeft="10dp" + android:layout_marginLeft="5dp" android:layout_marginRight="5dp" android:layout_marginTop="10dp" android:layout_weight="0" android:minHeight="40dp" android:minWidth="94dp" - android:text="@string/text_button_pause" /> + android:text="@string/text_button_cancel" + android:visibility="gone" + style="?android:attr/buttonBarButtonStyle" /> <Button - android:id="@+id/cancelButton" + android:id="@+id/pauseButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginBottom="10dp" - android:layout_marginLeft="5dp" - android:layout_marginRight="5dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="5dp" android:layout_marginTop="10dp" android:layout_weight="0" android:minHeight="40dp" android:minWidth="94dp" - android:text="@string/text_button_cancel" - android:visibility="gone" /> + android:text="@string/text_button_pause" + style="?android:attr/buttonBarButtonStyle" /> </LinearLayout> </LinearLayout> </LinearLayout> @@ -151,7 +148,8 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="10dp" - android:text="@string/text_button_resume_cellular" /> + android:text="@string/text_button_resume_cellular" + style="?android:attr/buttonBarButtonStyle" /> <Button android:id="@+id/wifiSettingsButton" @@ -159,7 +157,8 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="10dp" - android:text="@string/text_button_wifi_settings" /> + android:text="@string/text_button_wifi_settings" + style="?android:attr/buttonBarButtonStyle" /> </LinearLayout> </LinearLayout> diff --git a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml index 104993da7e..fae1faeb60 100644 --- a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml +++ b/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml @@ -17,7 +17,8 @@ */ --> -<LinearLayout android:layout_width="match_parent" +<LinearLayout xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent" android:baselineAligned="false" android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android"> @@ -33,16 +34,17 @@ android:layout_width="fill_parent" android:layout_height="25dp" android:scaleType="centerInside" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:src="@android:drawable/stat_sys_download" /> + android:src="@android:drawable/stat_sys_download" + android:contentDescription="@string/godot_project_name_string" /> <TextView android:id="@+id/progress_text" style="@style/NotificationText" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:layout_gravity="center_horizontal" android:singleLine="true" @@ -56,15 +58,16 @@ android:clickable="true" android:focusable="true" android:paddingTop="10dp" - android:paddingRight="8dp" - android:paddingBottom="8dp" > + android:paddingEnd="8dp" + android:paddingBottom="8dp" + tools:ignore="RtlSymmetry"> <TextView android:id="@+id/title" style="@style/NotificationTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:singleLine="true"/> <TextView @@ -72,8 +75,9 @@ style="@style/NotificationText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:singleLine="true"/> + android:layout_alignParentEnd="true" + android:singleLine="true" + tools:ignore="RelativeOverlap" /> <!-- Only one of progress_bar and paused_text will be visible. --> <FrameLayout @@ -87,7 +91,7 @@ style="?android:attr/progressBarStyleHorizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:paddingRight="25dp" /> + android:paddingEnd="25dp" /> <TextView android:id="@+id/description" @@ -95,7 +99,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:paddingRight="25dp" + android:paddingEnd="25dp" android:singleLine="true" /> </FrameLayout> diff --git a/platform/android/java/res/values-ko/strings.xml b/platform/android/java/res/values-ko/strings.xml index b997b934b2..fab0bdd753 100644 --- a/platform/android/java/res/values-ko/strings.xml +++ b/platform/android/java/res/values-ko/strings.xml @@ -30,7 +30,7 @@ <string name="notification_download_failed">다운로드 실패</string> - <string name="state_unknown">시작중...</string> + <string name="state_unknown">시작중…</string> <string name="state_idle">다운로드 시작을 기다리는 중</string> <string name="state_fetching_url">다운로드할 항목을 찾는 중</string> <string name="state_connecting">다운로드 서버에 연결 중</string> diff --git a/platform/android/java/res/values-v11/styles.xml b/platform/android/java/res/values-v11/styles.xml deleted file mode 100644 index f2013bc0bf..0000000000 --- a/platform/android/java/res/values-v11/styles.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <style name="NotificationTextSecondary" parent="NotificationText"> - <item name="android:textSize">12sp</item> - </style> -</resources>
\ No newline at end of file diff --git a/platform/android/java/res/values-v9/styles.xml b/platform/android/java/res/values-v9/styles.xml deleted file mode 100644 index 736e77a5d6..0000000000 --- a/platform/android/java/res/values-v9/styles.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" /> - <style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" /> -</resources>
\ No newline at end of file diff --git a/platform/android/java/res/values/strings.xml b/platform/android/java/res/values/strings.xml index f0ea56148f..a1b81a6186 100644 --- a/platform/android/java/res/values/strings.xml +++ b/platform/android/java/res/values/strings.xml @@ -30,7 +30,7 @@ <string name="notification_download_failed">Download unsuccessful</string> - <string name="state_unknown">Starting...</string> + <string name="state_unknown">Starting…</string> <string name="state_idle">Waiting for download to start</string> <string name="state_fetching_url">Looking for resources to download</string> <string name="state_connecting">Connecting to the download server</string> diff --git a/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java b/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java deleted file mode 100644 index ee12c68deb..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java +++ /dev/null @@ -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.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); - } - } -} diff --git a/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java deleted file mode 100644 index 17cc7a7cfd..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java +++ /dev/null @@ -1,397 +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 org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -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, and - * error retry count. - * <p> - * These values will vary based on the the way the application is configured in - * the Android Market 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.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 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 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)); - } - - /** - * 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 - * </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); - } - - if (response == Policy.LICENSED) { - // Update server policy data - Map<String, String> extras = decodeExtras(rawData.extra); - mLastResponse = response; - 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 policy data - setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); - setRetryUntil(DEFAULT_RETRY_UNTIL); - setMaxRetries(DEFAULT_MAX_RETRIES); - } - - 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; - } - - /** - * 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 - * @param URL the URL to set - */ - 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(String extras) { - Map<String, String> results = new HashMap<String, String>(); - try { - URI rawExtras = new URI("?" + extras); - List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); - for (NameValuePair item : extraList) { - String name = item.getName(); - int i = 0; - while (results.containsKey(name)) { - name = item.getName() + ++i; - } - results.put(name, item.getValue()); - } - } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); - } - return results; - } - -} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java b/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java deleted file mode 100644 index 63720999a7..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java +++ /dev/null @@ -1,115 +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. -*/ - -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicenseResultListener.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicenseResultListener extends android.os.IInterface -{ -/** Local-side IPC implementation stub class. */ -public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener -{ -private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; -/** Construct the stub at attach it to the interface. */ -public Stub() -{ -this.attachInterface(this, DESCRIPTOR); -} -/** - * Cast an IBinder object into an ILicenseResultListener interface, - * generating a proxy if needed. - */ -public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) -{ -if ((obj==null)) { -return null; -} -android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { -return ((com.google.android.vending.licensing.ILicenseResultListener)iin); -} -return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); -} -public android.os.IBinder asBinder() -{ -return this; -} -public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException -{ -switch (code) -{ -case INTERFACE_TRANSACTION: -{ -reply.writeString(DESCRIPTOR); -return true; -} -case TRANSACTION_verifyLicense: -{ -data.enforceInterface(DESCRIPTOR); -int _arg0; -_arg0 = data.readInt(); -java.lang.String _arg1; -_arg1 = data.readString(); -java.lang.String _arg2; -_arg2 = data.readString(); -this.verifyLicense(_arg0, _arg1, _arg2); -return true; -} -} -return super.onTransact(code, data, reply, flags); -} -private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener -{ -private android.os.IBinder mRemote; -Proxy(android.os.IBinder remote) -{ -mRemote = remote; -} -public android.os.IBinder asBinder() -{ -return mRemote; -} -public java.lang.String getInterfaceDescriptor() -{ -return DESCRIPTOR; -} -public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException -{ -android.os.Parcel _data = android.os.Parcel.obtain(); -try { -_data.writeInterfaceToken(DESCRIPTOR); -_data.writeInt(responseCode); -_data.writeString(signedData); -_data.writeString(signature); -mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); -} -finally { -_data.recycle(); -} -} -} -static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); -} -public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; -} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicensingService.java b/platform/android/java/src/com/android/vending/licensing/ILicensingService.java deleted file mode 100644 index 36afc0537d..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ILicensingService.java +++ /dev/null @@ -1,115 +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. -*/ - -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicensingService.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicensingService extends android.os.IInterface -{ -/** Local-side IPC implementation stub class. */ -public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService -{ -private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; -/** Construct the stub at attach it to the interface. */ -public Stub() -{ -this.attachInterface(this, DESCRIPTOR); -} -/** - * Cast an IBinder object into an ILicensingService interface, - * generating a proxy if needed. - */ -public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) -{ -if ((obj==null)) { -return null; -} -android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) { -return ((com.google.android.vending.licensing.ILicensingService)iin); -} -return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); -} -public android.os.IBinder asBinder() -{ -return this; -} -public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException -{ -switch (code) -{ -case INTERFACE_TRANSACTION: -{ -reply.writeString(DESCRIPTOR); -return true; -} -case TRANSACTION_checkLicense: -{ -data.enforceInterface(DESCRIPTOR); -long _arg0; -_arg0 = data.readLong(); -java.lang.String _arg1; -_arg1 = data.readString(); -com.google.android.vending.licensing.ILicenseResultListener _arg2; -_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); -this.checkLicense(_arg0, _arg1, _arg2); -return true; -} -} -return super.onTransact(code, data, reply, flags); -} -private static class Proxy implements com.google.android.vending.licensing.ILicensingService -{ -private android.os.IBinder mRemote; -Proxy(android.os.IBinder remote) -{ -mRemote = remote; -} -public android.os.IBinder asBinder() -{ -return mRemote; -} -public java.lang.String getInterfaceDescriptor() -{ -return DESCRIPTOR; -} -public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException -{ -android.os.Parcel _data = android.os.Parcel.obtain(); -try { -_data.writeInterfaceToken(DESCRIPTOR); -_data.writeLong(nonce); -_data.writeString(packageName); -_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null))); -mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); -} -finally { -_data.recycle(); -} -} -} -static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); -} -public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; -} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java b/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java deleted file mode 100644 index 531cb22f8c..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java +++ /dev/null @@ -1,351 +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.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager.NameNotFoundException; -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 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 Android Market 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 { - Intent serviceIntent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))); - serviceIntent.setPackage("com.android.vending"); - boolean bindResult = mContext - .bindService( - serviceIntent, - 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(); - } - } - } - - 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 ""; - } - } -} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java b/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java deleted file mode 100644 index 61d3c7e79e..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java +++ /dev/null @@ -1,224 +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 { - 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); - } -} diff --git a/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java deleted file mode 100644 index 7c42bfc28a..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java +++ /dev/null @@ -1,77 +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(); - } - 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; - } - } -} diff --git a/platform/android/java/src/com/android/vending/licensing/ResponseData.java b/platform/android/java/src/com/android/vending/licensing/ResponseData.java deleted file mode 100644 index 2adef3709e..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ResponseData.java +++ /dev/null @@ -1,79 +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.util.regex.Pattern; - -import android.text.TextUtils; - -/** - * 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 }); - } -} diff --git a/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java deleted file mode 100644 index fbf8cf6d00..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java +++ /dev/null @@ -1,276 +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 org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -/** - * 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, and - * error retry count. - * <p> - * These values will vary based on the the way the application is configured in - * the Android Market 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.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 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 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)); - } - - /** - * 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 - * </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); - } - - if (response == Policy.LICENSED) { - // Update server policy data - Map<String, String> extras = decodeExtras(rawData.extra); - mLastResponse = response; - setValidityTimestamp(extras.get("VT")); - setRetryUntil(extras.get("GT")); - setMaxRetries(extras.get("GR")); - } else if (response == Policy.NOT_LICENSED) { - // Clear out stale policy data - setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); - setRetryUntil(DEFAULT_RETRY_UNTIL); - setMaxRetries(DEFAULT_MAX_RETRIES); - } - - 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; - } - - /** - * {@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(String extras) { - Map<String, String> results = new HashMap<String, String>(); - try { - URI rawExtras = new URI("?" + extras); - List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); - for (NameValuePair item : extraList) { - results.put(item.getName(), item.getValue()); - } - } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); - } - return results; - } - -} diff --git a/platform/android/java/src/com/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/android/vending/licensing/util/Base64.java deleted file mode 100644 index a0d2779af2..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/util/Base64.java +++ /dev/null @@ -1,570 +0,0 @@ -// Portions copyright 2002, Google, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.android.vending.licensing.util; - -// This code was converted from code at http://iharder.sourceforge.net/base64/ -// Lots of extraneous features were removed. -/* The original code said: - * <p> - * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit - * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> - * periodically to check for updates or to contribute improvements. - * </p> - * - * @author Robert Harder - * @author rharder@usa.net - * @version 1.3 - */ - -/** - * Base64 converter class. This code is not a full-blown MIME encoder; - * it simply converts binary data to base64 data and back. - * - * <p>Note {@link CharBase64} is a GWT-compatible implementation of this - * class. - */ -public class Base64 { - /** Specify encoding (value is {@code true}). */ - public final static boolean ENCODE = true; - - /** Specify decoding (value is {@code false}). */ - public final static boolean DECODE = false; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - /** - * The 64 valid Base64 values. - */ - private final static byte[] ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '+', (byte) '/'}; - - /** - * The 64 valid web safe Base64 values. - */ - private final static byte[] WEBSAFE_ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '-', (byte) '_'}; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - /** The web safe decodabet */ - private final static byte[] WEBSAFE_DECODABET = - {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 - 62, // Dash '-' sign at decimal 45 - -9, -9, // Decimal 46-47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91-94 - 63, // Underscore '_' at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - // Indicates white space in encoding - private final static byte WHITE_SPACE_ENC = -5; - // Indicates equals sign in encoding - private final static byte EQUALS_SIGN_ENC = -1; - - /** Defeats instantiation. */ - private Base64() { - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Encodes up to three bytes of the array <var>source</var> - * and writes the resulting four Base64 bytes to <var>destination</var>. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * <var>srcOffset</var> and <var>destOffset</var>. - * This method does not check to make sure your arrays - * are large enough to accommodate <var>srcOffset</var> + 3 for - * the <var>source</var> array or <var>destOffset</var> + 4 for - * the <var>destination</var> array. - * The actual number of significant bytes in your array is - * given by <var>numSigBytes</var>. - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param alphabet is the encoding alphabet - * @return the <var>destination</var> array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index alphabet - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = - (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; - return destination; - case 2: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - case 1: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Encodes a byte array into Base64 notation. - * Equivalent to calling - * {@code encodeBytes(source, 0, source.length)} - * - * @param source The data to convert - * @since 1.4 - */ - public static String encode(byte[] source) { - return encode(source, 0, source.length, ALPHABET, true); - } - - /** - * Encodes a byte array into web safe Base64 notation. - * - * @param source The data to convert - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - */ - public static String encodeWebSafe(byte[] source, boolean doPadding) { - return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - * @since 1.4 - */ - public static String encode(byte[] source, int off, int len, byte[] alphabet, - boolean doPadding) { - byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); - int outLen = outBuff.length; - - // If doPadding is false, set length to truncate '=' - // padding characters - while (doPadding == false && outLen > 0) { - if (outBuff[outLen - 1] != '=') { - break; - } - outLen -= 1; - } - - return new String(outBuff, 0, outLen); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param maxLineLength maximum length of one line. - * @return the BASE64-encoded byte array - */ - public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, - int maxLineLength) { - int lenDiv3 = (len + 2) / 3; // ceil(len / 3) - int len43 = lenDiv3 * 4; - byte[] outBuff = new byte[len43 // Main 4:3 - + (len43 / maxLineLength)]; // New lines - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - - // The following block of code is the same as - // encode3to4( source, d + off, 3, outBuff, e, alphabet ); - // but inlined for faster encoding (~20% improvement) - int inBuff = - ((source[d + off] << 24) >>> 8) - | ((source[d + 1 + off] << 24) >>> 16) - | ((source[d + 2 + off] << 24) >>> 24); - outBuff[e] = alphabet[(inBuff >>> 18)]; - outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; - - lineLength += 4; - if (lineLength == maxLineLength) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // end for: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, alphabet); - - lineLength += 4; - if (lineLength == maxLineLength) { - // Add a last newline - outBuff[e + 4] = NEW_LINE; - e++; - } - e += 4; - } - - assert (e == outBuff.length); - return outBuff; - } - - - /* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array <var>source</var> - * and writes the resulting bytes (up to three of them) - * to <var>destination</var>. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * <var>srcOffset</var> and <var>destOffset</var>. - * This method does not check to make sure your arrays - * are large enough to accommodate <var>srcOffset</var> + 4 for - * the <var>source</var> array or <var>destOffset</var> + 3 for - * the <var>destination</var> array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param decodabet the decodabet for decoding Base64 content - * @return the number of decoded bytes converted - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, byte[] decodabet) { - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Example: DkL= - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } else { - // Example: DkLE - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) - | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - return 3; - } - } // end decodeToBytes - - - /** - * Decodes data from Base64 notation. - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decode(bytes, 0, bytes.length); - } - - /** - * Decodes data from web safe Base64 notation. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decodeWebSafe(bytes, 0, bytes.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source) throws Base64DecoderException { - return decode(source, 0, source.length); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded data. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(byte[] source) - throws Base64DecoderException { - return decodeWebSafe(source, 0, source.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, DECODABET); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded byte array. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - */ - public static byte[] decodeWebSafe(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, WEBSAFE_DECODABET); - } - - /** - * Decodes Base64 content using the supplied decodabet and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param decodabet the decodabet for decoding Base64 content - * @return decoded data - */ - public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) - throws Base64DecoderException { - int len34 = len * 3 / 4; - byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for (i = 0; i < len; i++) { - sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits - sbiDecode = decodabet[sbiCrop]; - - if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better - if (sbiDecode >= EQUALS_SIGN_ENC) { - // An equals sign (for padding) must not occur at position 0 or 1 - // and must be the last byte[s] in the encoded value - if (sbiCrop == EQUALS_SIGN) { - int bytesLeft = len - i; - byte lastByte = (byte) (source[len - 1 + off] & 0x7f); - if (b4Posn == 0 || b4Posn == 1) { - throw new Base64DecoderException( - "invalid padding byte '=' at byte offset " + i); - } else if ((b4Posn == 3 && bytesLeft > 2) - || (b4Posn == 4 && bytesLeft > 1)) { - throw new Base64DecoderException( - "padding byte '=' falsely signals end of encoded value " - + "at offset " + i); - } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { - throw new Base64DecoderException( - "encoded value has invalid trailing byte"); - } - break; - } - - b4[b4Posn++] = sbiCrop; - if (b4Posn == 4) { - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - b4Posn = 0; - } - } - } else { - throw new Base64DecoderException("Bad Base64 input character at " + i - + ": " + source[i + off] + "(decimal)"); - } - } - - // Because web safe encoding allows non padding base64 encodes, we - // need to pad the rest of the b4 buffer with equal signs when - // b4Posn != 0. There can be at most 2 equal signs at the end of - // four characters, so the b4 buffer must have two or three - // characters. This also catches the case where the input is - // padded with EQUALS_SIGN - if (b4Posn != 0) { - if (b4Posn == 1) { - throw new Base64DecoderException("single trailing character at offset " - + (len - 1)); - } - b4[b4Posn++] = EQUALS_SIGN; - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - } - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java index 2af33b96b9..ff1eee528f 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java @@ -18,119 +18,113 @@ 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"; +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; - - // save to private app's data on Android 6.0 to skip requesting permission. - public static final String EXP_PATH_API23 = File.separator + "Android" - + File.separator + "data" + 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"; + 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 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 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"; + /** 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 = "-"; + public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; - /** The default user agent used for downloads */ - public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; + /** 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 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 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 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 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 = 10; + 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 + 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 + 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. + 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; + public static final int RETRY_FIRST_DELAY = 30; + + /** Enable separate connectivity logging */ + public static final boolean LOGX = true; - /** Enable separate connectivity logging */ - public static final boolean LOGX = true; + /** Enable verbose logging */ + public static final boolean LOGV = false; - /** 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; - - /** + /** 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; + 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; + 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; + 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 @@ -139,102 +133,101 @@ public class Constants { * accurately (therefore making it impossible to know when a download * completes). */ - public static final int STATUS_LENGTH_REQUIRED = 411; + 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; + 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; + 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; + 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; + public static final int STATUS_CANNOT_RESUME = 489; - /** + /** * This download was canceled */ - public static final int STATUS_CANCELED = 490; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; + 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; - + public static final long ACTIVE_THREAD_WATCHDOG = 5 * 1000; }
\ No newline at end of file diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java index 9cb294d721..9a78a6d3df 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java @@ -19,7 +19,6 @@ 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). * @@ -31,50 +30,49 @@ import android.os.Parcelable; * 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; - } + public long mOverallTotal; + public long mOverallProgress; + public long mTimeRemaining; // time remaining + public float mCurrentSpeed; // speed in KB/S - @Override - public void writeToParcel(Parcel p, int i) { - p.writeLong(mOverallTotal); - p.writeLong(mOverallProgress); - p.writeLong(mTimeRemaining); - p.writeFloat(mCurrentSpeed); - } + @Override + public int describeContents() { + return 0; + } - public DownloadProgressInfo(Parcel p) { - mOverallTotal = p.readLong(); - mOverallProgress = p.readLong(); - mTimeRemaining = p.readLong(); - mCurrentSpeed = p.readFloat(); - } + @Override + public void writeToParcel(Parcel p, int i) { + p.writeLong(mOverallTotal); + p.writeLong(mOverallProgress); + p.writeLong(mTimeRemaining); + p.writeFloat(mCurrentSpeed); + } - public DownloadProgressInfo(long overallTotal, long overallProgress, - long timeRemaining, - float currentSpeed) { - this.mOverallTotal = overallTotal; - this.mOverallProgress = overallProgress; - this.mTimeRemaining = timeRemaining; - this.mCurrentSpeed = currentSpeed; - } + public DownloadProgressInfo(Parcel p) { + mOverallTotal = p.readLong(); + mOverallProgress = p.readLong(); + mTimeRemaining = p.readLong(); + mCurrentSpeed = p.readFloat(); + } - public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { - @Override - public DownloadProgressInfo createFromParcel(Parcel parcel) { - return new DownloadProgressInfo(parcel); - } + public DownloadProgressInfo(long overallTotal, long overallProgress, + long timeRemaining, + float currentSpeed) { + this.mOverallTotal = overallTotal; + this.mOverallProgress = overallProgress; + this.mTimeRemaining = timeRemaining; + this.mCurrentSpeed = currentSpeed; + } - @Override - public DownloadProgressInfo[] newArray(int i) { - return new DownloadProgressInfo[i]; - } - }; + 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]; + } + }; } 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 2201751254..146426ef83 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,13 +32,13 @@ import android.os.Messenger; import android.os.RemoteException; import android.util.Log; - +import java.lang.ref.WeakReference; /** * 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 @@ -58,162 +58,176 @@ import android.util.Log; * 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 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 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; - 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; - 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 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); + } - @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(); + } + } - 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; - } + public Proxy(Messenger msg) { + mServiceMessenger = msg; + } - @Override - public void onServiceConnected(Messenger m) { - /** + @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; - /** + 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. */ - final Messenger mMessenger = new Messenger(new Handler() { - @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; - } - } - }); + 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); + } + } + } - public Stub(IDownloaderClient itf, Class<?> downloaderService) { - mItf = itf; - mDownloaderServiceClass = downloaderService; - } + 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; + } + } - /** + 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); - } + 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; - } - }; + 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 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 void disconnect(Context c) { + if (mBound) { + c.unbindService(mConnection); + mBound = false; + } + mContext = null; + } - @Override - public Messenger getMessenger() { - return mMessenger; - } - } + @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); - } + 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 @@ -221,11 +235,11 @@ public class DownloaderClientMarshaller { * @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); - } - - /** + 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, @@ -237,7 +251,7 @@ public class DownloaderClientMarshaller { * 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 @@ -248,30 +262,29 @@ public class DownloaderClientMarshaller { * #DOWNLOAD_REQUIRED}. * @throws NameNotFoundException */ - public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, - Class<?> serviceClass) - throws NameNotFoundException { - return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, - serviceClass); - } - - /** + 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 + * <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); - } - + public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, + Class<?> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } } 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 054eaa9895..f75debe32d 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,7 +25,7 @@ import android.os.Message; import android.os.Messenger; import android.os.RemoteException; - +import java.lang.ref.WeakReference; /** * This class is used by the client activity to proxy requests to the Downloader @@ -38,144 +38,156 @@ import android.os.RemoteException; */ 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; - final Messenger mMessenger = new Messenger(new Handler() { - @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; - } - } - }); - - public Stub(IDownloaderService itf) { - mItf = itf; - } - - @Override - public Messenger getMessenger() { - return mMessenger; - } - - @Override - public void connect(Context c) { - - } - - @Override - public void disconnect(Context c) { - - } - } - - /** + 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; + 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; + } + } + + 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); - } + 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); - } - + public static IStub CreateStub(IDownloaderService itf) { + return new Stub(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 fb56f917be..cd8726533f 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 @@ -16,8 +16,7 @@ package com.google.android.vending.expansion.downloader; -import com.godot.game.R; - +import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Environment; @@ -25,6 +24,8 @@ import android.os.StatFs; import android.os.SystemClock; import android.util.Log; +import com.godot.game.R; + import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; @@ -39,273 +40,316 @@ import java.util.regex.Pattern; */ public class Helpers { - public static Random sRandom = new Random(SystemClock.uptimeMillis()); + 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*\"([^\"]*)\""); + /** Regex used to parse content-disposition headers */ + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern + .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); - private Helpers() { - } + private Helpers() { + } - /* - * Parse the Content-Disposition HTTP Header. The format of the header is - * defined here: http://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. + /* + * Parse the Content-Disposition HTTP Header. The format of the header is defined here: + * http://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; - } + 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 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; - } + 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 + /** + * @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; - } + 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()); - } + 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); - } - } + /* 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. - * + /** + * 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 ""; - } - return String.format("%.2f", - (float) overallProgress / (1024.0f * 1024.0f)) - + "MB /" + - String.format("%.2f", (float) overallTotal / - (1024.0f * 1024.0f)) + "MB"; - } + 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 ""; + } + 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"; + } - /** + /** * 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) + ")"; - } + 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 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) { - return String.format("%.2f", bytesPerMillisecond * 1000 / 1024); - } + public static String getSpeedString(float bytesPerMillisecond) { + return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); + } - 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())); - } + 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. - * + /** + * 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"; - } + 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 + /** + * 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 generateSaveFileName(Context c, String fileName) { + String path = getSaveFilePath(c) + File.separator + fileName; + return path; + } - static public String getSaveFilePath(Context c) { - File root = Environment.getExternalStorageDirectory(); - // this makes several issues with Android SDK >= 23 devices. - // https://github.com/danikula/Google-Play-Expansion-File/commit/93a03bd34acad67c6ea34cfb6c3f02c93bdcea85 - // https://issuetracker.google.com/issues/37075181 - //String path = Build.VERSION.SDK_INT >= 23 ? Constants.EXP_PATH_API23 : Constants.EXP_PATH; - String path = Constants.EXP_PATH; - return root.toString() + path + c.getPackageName(); - } + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + 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 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return c.getObbDir().toString(); + } else { + File root = Environment.getExternalStorageDirectory(); + String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); + return path; + } + } - /** - * Helper function to ascertain the existence of a file and return - * true/false appropriately - * + /** + * 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 + * @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 boolean doesFileExist(Context c, String fileName, long fileSize, - boolean deleteFileOnMismatch) { - // the file may have been delivered by Market --- 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; - } + 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. + /** + * 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; - } - } - + 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; + } + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java index b8511a62a0..bae93f633a 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -23,26 +23,26 @@ import android.os.Messenger; * 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_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; + 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; + 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 @@ -53,27 +53,27 @@ public interface IDownloaderClient { * 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_WIFI_DISABLED = 10; + static final int STATE_PAUSED_NEED_WIFI = 11; - static final int STATE_PAUSED_ROAMING = 12; + 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_NETWORK_SETUP_FAILURE = 13; - static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; + 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_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; + 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 @@ -86,13 +86,13 @@ public interface IDownloaderClient { * 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); + 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. @@ -109,18 +109,18 @@ public interface IDownloaderClient { * 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); + 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); + void onDownloadProgress(DownloadProgressInfo progress); } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java index 4789afe19c..a84fb32728 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -31,53 +31,53 @@ import android.os.Messenger; * 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; + 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(); + 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(); + 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(); + void requestContinueDownload(); - /** + /** * Set the flags for this download (e.g. * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). - * + * * @param flags */ - void setDownloadFlags(int flags); + void setDownloadFlags(int flags); - /** + /** * Requests that the download status be sent to the client. */ - void requestDownloadStatus(); + 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); + void onClientUpdated(Messenger clientMessenger); } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java index d5bc3a843e..dcdef1bfcf 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java @@ -33,9 +33,9 @@ import android.os.Messenger; * {@link IDownloaderService#onClientUpdated}. */ public interface IStub { - Messenger getMessenger(); + Messenger getMessenger(); - void connect(Context c); + void connect(Context c); - void disconnect(Context c); + void disconnect(Context c); } 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 12edd97ab2..c5577d4c2a 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 @@ -16,6 +16,7 @@ package com.google.android.vending.expansion.downloader; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; @@ -30,94 +31,96 @@ import android.util.Log; * 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; - } - - 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; - } - - 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) { - /** + 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); - } + mNotificationManager.notify((int)id, notification); + } - public void cancelNotification(long id) { - mNotificationManager.cancel((int) id); - } + public void cancelNotification(long id) { + mNotificationManager.cancel((int)id); + } - public void cancelAllNotifications() { - mNotificationManager.cancelAll(); - } + public void cancelAllNotifications() { + mNotificationManager.cancelAll(); + } - public void startThread(Thread thread) { - thread.start(); - } + public void startThread(Thread thread) { + thread.start(); + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java deleted file mode 100644 index 4667acce67..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java +++ /dev/null @@ -1,536 +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. - */ - -/* - * This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of - * the SSLSessionCache added Froyo devices using reflection. - */ - -package com.google.android.vending.expansion.downloader.impl; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URI; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.scheme.SocketFactory; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.RequestWrapper; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.SSLCertificateSocketFactory; -import android.os.Looper; -import android.util.Log; - -/** - * Subclass of the Apache {@link DefaultHttpClient} that is configured with - * reasonable default settings and registered schemes for Android, and - * also lets the user add {@link HttpRequestInterceptor} classes. - * Don't create this directly, use the {@link #newInstance} factory method. - * - * <p>This client processes cookies but does not retain them by default. - * To retain cookies, simply add a cookie store to the HttpContext:</p> - * - * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> - */ -public final class AndroidHttpClient implements HttpClient { - - static Class<?> sSslSessionCacheClass; - static { - // if we are on Froyo+ devices, we can take advantage of the SSLSessionCache - try { - sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache"); - } catch (Exception e) { - - } - } - - // Gzip of data shorter than this probably won't be worthwhile - public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; - - // Default connection and socket timeout of 60 seconds. Tweak to taste. - private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; - - private static final String TAG = "AndroidHttpClient"; - - - /** Interceptor throws an exception if the executing thread is blocked */ - private static final HttpRequestInterceptor sThreadCheckInterceptor = - new HttpRequestInterceptor() { - public void process(HttpRequest request, HttpContext context) { - // Prevent the HttpRequest from being sent on the main thread - if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { - throw new RuntimeException("This thread forbids HTTP requests"); - } - } - }; - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * - * @param userAgent to report in your HTTP requests - * @param context to use for caching SSL sessions (may be null for no caching) - * @return AndroidHttpClient for you to use for all your requests. - */ - public static AndroidHttpClient newInstance(String userAgent, Context context) { - HttpParams params = new BasicHttpParams(); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - // Don't handle redirects -- return them to the caller. Our code - // often wants to re-POST after a redirect, which we must do ourselves. - HttpClientParams.setRedirecting(params, false); - - Object sessionCache = null; - // Use a session cache for SSL sockets -- Froyo only - if ( null != context && null != sSslSessionCacheClass ) { - Constructor<?> ct; - try { - ct = sSslSessionCacheClass.getConstructor(Context.class); - sessionCache = ct.newInstance(context); - } catch (SecurityException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchMethodException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (InstantiationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalAccessException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (InvocationTargetException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - // Set the specified user agent and register standard protocols. - HttpProtocolParams.setUserAgent(params, userAgent); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80)); - SocketFactory sslCertificateSocketFactory = null; - if ( null != sessionCache ) { - Method getHttpSocketFactoryMethod; - try { - getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass); - sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache); - } catch (SecurityException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchMethodException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalAccessException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (InvocationTargetException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if ( null == sslCertificateSocketFactory ) { - sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory(); - } - schemeRegistry.register(new Scheme("https", - sslCertificateSocketFactory, 443)); - - ClientConnectionManager manager = - new ThreadSafeClientConnManager(params, schemeRegistry); - - // We use a factory method to modify superclass initialization - // parameters without the funny call-a-static-method dance. - return new AndroidHttpClient(manager, params); - } - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * @param userAgent to report in your HTTP requests. - * @return AndroidHttpClient for you to use for all your requests. - */ - public static AndroidHttpClient newInstance(String userAgent) { - return newInstance(userAgent, null /* session cache */); - } - - private final HttpClient delegate; - - private RuntimeException mLeakedException = new IllegalStateException( - "AndroidHttpClient created and never closed"); - - private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { - this.delegate = new DefaultHttpClient(ccm, params) { - @Override - protected BasicHttpProcessor createHttpProcessor() { - // Add interceptor to prevent making requests from main thread. - BasicHttpProcessor processor = super.createHttpProcessor(); - processor.addRequestInterceptor(sThreadCheckInterceptor); - processor.addRequestInterceptor(new CurlLogger()); - - return processor; - } - - @Override - protected HttpContext createHttpContext() { - // Same as DefaultHttpClient.createHttpContext() minus the - // cookie store. - HttpContext context = new BasicHttpContext(); - context.setAttribute( - ClientContext.AUTHSCHEME_REGISTRY, - getAuthSchemes()); - context.setAttribute( - ClientContext.COOKIESPEC_REGISTRY, - getCookieSpecs()); - context.setAttribute( - ClientContext.CREDS_PROVIDER, - getCredentialsProvider()); - return context; - } - }; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (mLeakedException != null) { - Log.e(TAG, "Leak found", mLeakedException); - mLeakedException = null; - } - } - - /** - * Modifies a request to indicate to the server that we would like a - * gzipped response. (Uses the "Accept-Encoding" HTTP header.) - * @param request the request to modify - * @see #getUngzippedContent - */ - public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { - request.addHeader("Accept-Encoding", "gzip"); - } - - /** - * Gets the input stream from a response entity. If the entity is gzipped - * then this will get a stream over the uncompressed data. - * - * @param entity the entity whose content should be read - * @return the input stream to read from - * @throws IOException - */ - public static InputStream getUngzippedContent(HttpEntity entity) - throws IOException { - InputStream responseStream = entity.getContent(); - if (responseStream == null) return responseStream; - Header header = entity.getContentEncoding(); - if (header == null) return responseStream; - String contentEncoding = header.getValue(); - if (contentEncoding == null) return responseStream; - if (contentEncoding.contains("gzip")) responseStream - = new GZIPInputStream(responseStream); - return responseStream; - } - - /** - * Release resources associated with this client. You must call this, - * or significant resources (sockets and memory) may be leaked. - */ - public void close() { - if (mLeakedException != null) { - getConnectionManager().shutdown(); - mLeakedException = null; - } - } - - public HttpParams getParams() { - return delegate.getParams(); - } - - public ClientConnectionManager getConnectionManager() { - return delegate.getConnectionManager(); - } - - public HttpResponse execute(HttpUriRequest request) throws IOException { - return delegate.execute(request); - } - - public HttpResponse execute(HttpUriRequest request, HttpContext context) - throws IOException { - return delegate.execute(request, context); - } - - public HttpResponse execute(HttpHost target, HttpRequest request) - throws IOException { - return delegate.execute(target, request); - } - - public HttpResponse execute(HttpHost target, HttpRequest request, - HttpContext context) throws IOException { - return delegate.execute(target, request, context); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler, context); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler) throws IOException, - ClientProtocolException { - return delegate.execute(target, request, responseHandler); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(target, request, responseHandler, context); - } - - /** - * Compress data to send to server. - * Creates a Http Entity holding the gzipped data. - * The data will not be compressed if it is too short. - * @param data The bytes to compress - * @return Entity holding the data - */ - public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) - throws IOException { - AbstractHttpEntity entity; - if (data.length < getMinGzipSize(resolver)) { - entity = new ByteArrayEntity(data); - } else { - ByteArrayOutputStream arr = new ByteArrayOutputStream(); - OutputStream zipper = new GZIPOutputStream(arr); - zipper.write(data); - zipper.close(); - entity = new ByteArrayEntity(arr.toByteArray()); - entity.setContentEncoding("gzip"); - } - return entity; - } - - /** - * Retrieves the minimum size for compressing data. - * Shorter data will not be compressed. - */ - public static long getMinGzipSize(ContentResolver resolver) { - return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant. - } - - /* cURL logging support. */ - - /** - * Logging tag and level. - */ - private static class LoggingConfiguration { - - private final String tag; - private final int level; - - private LoggingConfiguration(String tag, int level) { - this.tag = tag; - this.level = level; - } - - /** - * Returns true if logging is turned on for this configuration. - */ - private boolean isLoggable() { - return Log.isLoggable(tag, level); - } - - /** - * Prints a message using this configuration. - */ - private void println(String message) { - Log.println(level, tag, message); - } - } - - /** cURL logging configuration. */ - private volatile LoggingConfiguration curlConfiguration; - - /** - * Enables cURL request logging for this client. - * - * @param name to log messages with - * @param level at which to log messages (see {@link android.util.Log}) - */ - public void enableCurlLogging(String name, int level) { - if (name == null) { - throw new NullPointerException("name"); - } - if (level < Log.VERBOSE || level > Log.ASSERT) { - throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); - } - - curlConfiguration = new LoggingConfiguration(name, level); - } - - /** - * Disables cURL logging for this client. - */ - public void disableCurlLogging() { - curlConfiguration = null; - } - - /** - * Logs cURL commands equivalent to requests. - */ - private class CurlLogger implements HttpRequestInterceptor { - public void process(HttpRequest request, HttpContext context) - throws HttpException, IOException { - LoggingConfiguration configuration = curlConfiguration; - if (configuration != null - && configuration.isLoggable() - && request instanceof HttpUriRequest) { - // Never print auth token -- we used to check ro.secure=0 to - // enable that, but can't do that in unbundled code. - configuration.println(toCurl((HttpUriRequest) request, false)); - } - } - } - - /** - * Generates a cURL command equivalent to the given request. - */ - private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException { - StringBuilder builder = new StringBuilder(); - - builder.append("curl "); - - for (Header header: request.getAllHeaders()) { - if (!logAuthToken - && (header.getName().equals("Authorization") || - header.getName().equals("Cookie"))) { - continue; - } - builder.append("--header \""); - builder.append(header.toString().trim()); - builder.append("\" "); - } - - URI uri = request.getURI(); - - // If this is a wrapped request, use the URI from the original - // request instead. getURI() on the wrapper seems to return a - // relative URI. We want an absolute URI. - if (request instanceof RequestWrapper) { - HttpRequest original = ((RequestWrapper) request).getOriginal(); - if (original instanceof HttpUriRequest) { - uri = ((HttpUriRequest) original).getURI(); - } - } - - builder.append("\""); - builder.append(uri); - builder.append("\""); - - if (request instanceof HttpEntityEnclosingRequest) { - HttpEntityEnclosingRequest entityRequest = - (HttpEntityEnclosingRequest) request; - HttpEntity entity = entityRequest.getEntity(); - if (entity != null && entity.isRepeatable()) { - if (entity.getContentLength() < 1024) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - entity.writeTo(stream); - String entityString = stream.toString(); - - // TODO: Check the content type, too. - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); - } else { - builder.append(" [TOO MUCH DATA TO INCLUDE]"); - } - } - } - - return builder.toString(); - } - - /** - * Returns the date of the given HTTP date string. This method can identify - * and parse the date formats emitted by common HTTP servers, such as - * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>, - * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>, - * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>, - * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and - * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI - * C's asctime()</a>. - * - * @return the number of milliseconds since Jan. 1, 1970, midnight GMT. - * @throws IllegalArgumentException if {@code dateString} is not a date or - * of an unsupported format. - */ - public static long parseDate(String dateString) { - return HttpDateTime.parse(dateString); - } -}
\ No newline at end of file diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java index b77af7e085..6346d7703a 100755 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java @@ -32,81 +32,80 @@ import android.util.Log; * 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 = "CancellableIntentService"; - private static final int WHAT_MESSAGE = -10; + 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; - } + public CustomIntentService(String paramString) { + this.mName = paramString; + } - @Override - public IBinder onBind(Intent paramIntent) { - return null; - } + @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 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"); - } + @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 void onHandleIntent(Intent paramIntent); - protected abstract boolean shouldStop(); + 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 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; - } + @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; - } + public void setIntentRedelivery(boolean enabled) { + this.mRedelivery = enabled; + } - private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } + 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"); - } - } - } + @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"); + } + } + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java deleted file mode 100644 index e2673a9dd7..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java +++ /dev/null @@ -1,30 +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; - -/** - * Uses the class-loader model to utilize the updated notification builders in - * Honeycomb while maintaining a compatible version for older devices. - */ -public class CustomNotificationFactory { - static public DownloadNotification.ICustomNotification createCustomNotification() { - if (android.os.Build.VERSION.SDK_INT > 13) - return new V14CustomNotification(); - else - throw new RuntimeException(); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java index 45111b16a3..0e72b7ae77 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java @@ -25,68 +25,68 @@ 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; + 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; + boolean mInitialized; - public int mFuzz; + public int mFuzz; - public DownloadInfo(int index, String fileName, String pkg) { - mFuzz = Helpers.sRandom.nextInt(1001); - mFileName = fileName; - mIndex = index; - } + 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; - } + 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 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); - } + 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); + } } 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 a9f674803c..099e3f05b3 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 @@ -22,11 +22,12 @@ import com.google.android.vending.expansion.downloader.DownloaderClientMarshalle import com.google.android.vending.expansion.downloader.Helpers; import com.google.android.vending.expansion.downloader.IDownloaderClient; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; +import android.os.Build; import android.os.Messenger; +import android.support.v4.app.NotificationCompat; /** * This class handles displaying the notification associated with the download @@ -41,190 +42,183 @@ import android.os.Messenger; */ public class DownloadNotification implements IDownloaderClient { - private int mState; - private final Context mContext; - private final NotificationManager mNotificationManager; - private String mCurrentTitle; - - private IDownloaderClient mClientProxy; - final ICustomNotification mCustomNotification; - private Notification.Builder mNotificationBuilder; - private Notification.Builder mCurrentNotificationBuilder; - private CharSequence mLabel; - private String mCurrentText; - private PendingIntent mContentIntent; - private DownloadProgressInfo mProgressInfo; - - static final String LOGTAG = "DownloadNotification"; - static final int NOTIFICATION_ID = LOGTAG.hashCode(); - - public PendingIntent getClientIntent() { - return mContentIntent; - } - - public void setClientIntent(PendingIntent mClientIntent) { - this.mContentIntent = mClientIntent; - } - - 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.toString(); - mCurrentNotificationBuilder.setTicker(mLabel + ": " + mCurrentText); - mCurrentNotificationBuilder.setSmallIcon(iconResource); - mCurrentNotificationBuilder.setContentTitle(mCurrentTitle); - mCurrentNotificationBuilder.setContentText(mCurrentText); - mCurrentNotificationBuilder.setContentIntent(mContentIntent); - mCurrentNotificationBuilder.setOngoing(ongoingEvent); - mCurrentNotificationBuilder.setAutoCancel(!ongoingEvent); - mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build()); - } - } - - @Override - public void onDownloadProgress(DownloadProgressInfo progress) { - mProgressInfo = progress; - if (null != mClientProxy) { - mClientProxy.onDownloadProgress(progress); - } - if (progress.mOverallTotal <= 0) { - // we just show the text - mNotificationBuilder.setTicker(mCurrentTitle); - mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_download); - mNotificationBuilder.setContentTitle(mCurrentTitle); - mNotificationBuilder.setContentText(mCurrentText); - mNotificationBuilder.setContentIntent(mContentIntent); - mCurrentNotificationBuilder = mNotificationBuilder; - } else { - mCustomNotification.setCurrentBytes(progress.mOverallProgress); - mCustomNotification.setTotalBytes(progress.mOverallTotal); - mCustomNotification.setIcon(android.R.drawable.stat_sys_download); - mCustomNotification.setPendingIntent(mContentIntent); - mCustomNotification.setTicker(mLabel + ": " + mCurrentText); - mCustomNotification.setTitle(mLabel); - mCustomNotification.setTimeRemaining(progress.mTimeRemaining); - mCurrentNotificationBuilder = mCustomNotification.updateNotification(mContext); - } - mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build()); - } - - public interface ICustomNotification { - void setTitle(CharSequence title); - - void setTicker(CharSequence ticker); - - void setPendingIntent(PendingIntent mContentIntent); - - void setTotalBytes(long totalBytes); - - void setCurrentBytes(long currentBytes); - - void setIcon(int iconResource); - - void setTimeRemaining(long timeRemaining); - - Notification.Builder updateNotification(Context c); - } - - /** + 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); - } - } - - /** + 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); - mCustomNotification = CustomNotificationFactory - .createCustomNotification(); - mNotificationBuilder = new Notification.Builder(ctx); - mCurrentNotificationBuilder = mNotificationBuilder; - - } - - @Override - public void onServiceConnected(Messenger m) { - } - + 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) { + } } 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 056d1eca0b..2fa146408b 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * 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. @@ -20,14 +20,7 @@ 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 org.apache.http.Header; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.params.ConnRouteParams; - import android.content.Context; -import android.net.Proxy; import android.os.PowerManager; import android.os.Process; import android.util.Log; @@ -38,8 +31,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.SyncFailedException; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Locale; /** @@ -47,917 +40,794 @@ import java.util.Locale; */ 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(); - } - - /** + 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; - } + 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); - } - } - - /** + 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; - } - - /** + 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; - } - } - - /** + 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; - } - - /** - * Returns the preferred proxy to be used by clients. This is a wrapper - * around {@link android.net.Proxy#getHost()}. Currently no proxy will be - * returned for localhost or if the active network is Wi-Fi. - * - * @param context the context which will be passed to - * {@link android.net.Proxy#getHost()} - * @param url the target URL for the request - * @note Calling this method requires permission - * android.permission.ACCESS_NETWORK_STATE - * @return The preferred proxy to be used by clients, or null if there is no - * proxy. - */ - public HttpHost getPreferredHttpHost(Context context, - String url) { - if (!isLocalHost(url) && !mService.isWiFi()) { - final String proxyHost = Proxy.getHost(context); - if (proxyHost != null) { - return new HttpHost(proxyHost, Proxy.getPort(context), "http"); - } - } - - return null; - } - - static final private boolean isLocalHost(String url) { - if (url == null) { - return false; - } - - try { - final URI uri = URI.create(url); - final String host = uri.getHost(); - if (host != null) { - // TODO: InetAddress.isLoopbackAddress should be used to check - // for localhost. However no public factory methods exist which - // can be used without triggering DNS lookup if host is not - // localhost. - if (host.equalsIgnoreCase("localhost") || - host.equals("127.0.0.1") || - host.equals("[::1]")) { - return true; - } - } - } catch (IllegalArgumentException iex) { - // Ignore (URI.create) - } - - return false; - } - - /** + 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); - AndroidHttpClient client = null; - PowerManager.WakeLock wakeLock = null; - int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; - - try { - PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); - wakeLock.acquire(); - - if (Constants.LOGV) { - Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); - Log.v(Constants.TAG, " at " + mInfo.mUri); - } - - client = AndroidHttpClient.newInstance(userAgent(), mContext); - - 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. - ConnRouteParams.setDefaultProxy(client.getParams(), - getPreferredHttpHost(mContext, state.mRequestUri)); - HttpGet request = new HttpGet(state.mRequestUri); - try { - executeDownload(state, client, request); - finished = true; - } catch (RetryDownload exc) { - // fall through - } finally { - request.abort(); - 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; - } - if (client != null) { - client.close(); - client = null; - } - cleanupDestination(state, finalStatus); - notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, - state.mRedirectCount, state.mGotData, state.mFilename); - } - } - - /** + 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); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); + wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); + + 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, AndroidHttpClient client, HttpGet request) - throws StopRequest, RetryDownload { - InnerState innerState = new InnerState(); - byte data[] = new byte[Constants.BUFFER_SIZE]; + private void executeDownload(State state, HttpURLConnection request) + throws StopRequest, RetryDownload { + InnerState innerState = new InnerState(); + byte data[] = new byte[Constants.BUFFER_SIZE]; - checkPausedOrCanceled(state); + checkPausedOrCanceled(state); - setupDestinationFile(state, innerState); - addRequestHeaders(innerState, request); + setupDestinationFile(state, innerState); + addRequestHeaders(innerState, request); - // check just before sending the request to avoid using an invalid - // connection at all - checkConnectivity(state); + // check just before sending the request to avoid using an invalid + // connection at all + checkConnectivity(state); - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING); - HttpResponse response = sendRequest(state, client, request); - handleExceptionalStatus(state, innerState, response); + 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); - } + if (Constants.LOGV) { + Log.v(Constants.TAG, "received response for " + mInfo.mUri); + } - processResponseHeaders(state, innerState, response); - InputStream entityStream = openResponseEntity(state, response); - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING); - transferData(state, innerState, data, entityStream); - } + 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"); - } - } - - /** + 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); - } - } - - /** + 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"); - } - } - } - - /** + 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; - } - } - - /** + 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); - } - } - } - } - - /** + 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 - } - } - - /** + 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"); - } - } - } - - /** + 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); - } - } - - /** + 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); - } - } - } - - /** + 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; - } - - /** + 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); - } - } - } - - /** + 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, HttpResponse response) - throws StopRequest { - try { - return response.getEntity().getContent(); - } 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")); - } - } - - /** + 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, HttpResponse 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); - } - - /** + 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); - } + 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, HttpResponse response) - throws StopRequest { - Header header = response.getFirstHeader("Content-Disposition"); - if (header != null) { - innerState.mHeaderContentDisposition = header.getValue(); - } - header = response.getFirstHeader("Content-Location"); - if (header != null) { - innerState.mHeaderContentLocation = header.getValue(); - } - header = response.getFirstHeader("ETag"); - if (header != null) { - innerState.mHeaderETag = header.getValue(); - } - String headerTransferEncoding = null; - header = response.getFirstHeader("Transfer-Encoding"); - if (header != null) { - headerTransferEncoding = header.getValue(); - } - String headerContentType = null; - header = response.getFirstHeader("Content-Type"); - if (header != null) { - headerContentType = header.getValue(); - if (!headerContentType.equals("application/vnd.android.obb")) { - throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, - "file delivered with incorrect Mime type"); - } - } - - if (headerTransferEncoding == null) { - header = response.getFirstHeader("Content-Length"); - if (header != null) { - innerState.mHeaderContentLength = header.getValue(); - // this is always set from Market - long contentLength = Long.parseLong(innerState.mHeaderContentLength); - 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 { - // 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"); - } - } - - /** + 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, HttpResponse response) - throws StopRequest, RetryDownload { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { - handleServiceUnavailable(state, response); - } - if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) { - handleRedirect(state, response, statusCode); - } - - int expectedStatus = innerState.mContinuingDownload ? 206 - : DownloaderService.STATUS_SUCCESS; - if (statusCode != expectedStatus) { - handleOtherStatus(state, innerState, statusCode); - } else { - // no longer redirected - state.mRedirectCount = 0; - } - } - - /** + 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); - } - - /** - * Handle a 3xx redirect status. - */ - private void handleRedirect(State state, HttpResponse response, int statusCode) - throws StopRequest, RetryDownload { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP redirect " + statusCode); - } - if (state.mRedirectCount >= Constants.MAX_REDIRECTS) { - throw new StopRequest(DownloaderService.STATUS_TOO_MANY_REDIRECTS, "too many redirects"); - } - Header header = response.getFirstHeader("Location"); - if (header == null) { - return; - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Location :" + header.getValue()); - } - - String newUri; - try { - newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString(); - } catch (URISyntaxException ex) { - if (Constants.LOGV) { - Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue() - + " for " + mInfo.mUri); - } - throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, - "Couldn't resolve redirect URI"); - } - ++state.mRedirectCount; - state.mRequestUri = newUri; - if (statusCode == 301 || statusCode == 303) { - // use the new URI for all future requests (should a retry/resume be - // necessary) - state.mNewUri = newUri; - } - throw new RetryDownload(); - } - - /** + 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, HttpGet request) { - if (innerState.mContinuingDownload) { - if (innerState.mHeaderETag != null) { - request.addHeader("If-Match", innerState.mHeaderETag); - } - request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-"); - } - } - - /** + 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, HttpResponse response) throws StopRequest { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP response code 503"); - } - state.mCountRetry = true; - Header header = response.getFirstHeader("Retry-After"); - if (header != null) { - try { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Retry-After :" + header.getValue()); - } - state.mRetryAfter = Integer.parseInt(header.getValue()); - 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"); - } - - /** + 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 HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request) - throws StopRequest { - try { - return client.execute(request); - } 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; - } - } - - /** + 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); - } - } - - /** + 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); - } - + 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); + } } 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 e83faa2756..25a561ccd4 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 @@ -29,6 +29,7 @@ import com.google.android.vending.licensing.LicenseChecker; import com.google.android.vending.licensing.LicenseCheckerCallback; import com.google.android.vending.licensing.Policy; +import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; @@ -61,82 +62,82 @@ import java.io.File; */ public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { - public DownloaderService() { - super("LVLDownloadService"); - } + public DownloaderService() { + super("LVLDownloadService"); + } - private static final String LOG_TAG = "LVLDL"; + private static final String LOG_TAG = "LVLDL"; - // the following NETWORK_* constants are used to indicates specific reasons - // for disallowing a - // download from using a network, since specific causes can require special - // handling + // the following NETWORK_* constants are used to indicates specific reasons + // for disallowing a + // download from using a network, since specific causes can require special + // handling - /** + /** * The network is usable for the given download. */ - public static final int NETWORK_OK = 1; + public static final int NETWORK_OK = 1; - /** + /** * There is no network connectivity. */ - public static final int NETWORK_NO_CONNECTION = 2; + public static final int NETWORK_NO_CONNECTION = 2; - /** + /** * The download exceeds the maximum size for this network. */ - public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; + public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; - /** + /** * The download exceeds the recommended maximum size for this network, the * user must confirm for this download to proceed without WiFi. */ - public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; + public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; - /** + /** * The current connection is roaming, and the download can't proceed over a * roaming connection. */ - public static final int NETWORK_CANNOT_USE_ROAMING = 5; + public static final int NETWORK_CANNOT_USE_ROAMING = 5; - /** + /** * The app requesting the download specific that it can't use the current * network connection. */ - public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; + public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; - /** + /** * For intents used to notify the user that a download exceeds a size * threshold, if this extra is true, WiFi is required for this download * size; otherwise, it is only recommended. */ - public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; - public static final String EXTRA_FILE_NAME = "downloadId"; + public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; + public static final String EXTRA_FILE_NAME = "downloadId"; - /** + /** * Used with DOWNLOAD_STATUS */ - public static final String EXTRA_STATUS_STATE = "ESS"; - public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS"; - public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS"; - public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP"; - public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP"; + public static final String EXTRA_STATUS_STATE = "ESS"; + public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS"; + public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS"; + public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP"; + public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP"; - public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged"; + public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged"; - /** + /** * Broadcast intent action sent by the download manager when a download * completes. */ - public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE"; + public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE"; - /** + /** * Broadcast intent action sent by the download manager when download status * changes. */ - public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS"; + public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS"; - /* + /* * Lists the states that the download manager can set on a download to * notify applications of the download progress. The codes follow the HTTP * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not @@ -144,503 +145,498 @@ public abstract class DownloaderService extends CustomIntentService implements I * errors */ - /** + /** * Returns whether the status is informational (i.e. 1xx). */ - public static boolean isStatusInformational(int status) { - return (status >= 100 && status < 200); - } + public static boolean isStatusInformational(int status) { + return (status >= 100 && status < 200); + } - /** + /** * Returns whether the status is a success (i.e. 2xx). */ - public static boolean isStatusSuccess(int status) { - return (status >= 200 && status < 300); - } + public static boolean isStatusSuccess(int status) { + return (status >= 200 && status < 300); + } - /** + /** * Returns whether the status is an error (i.e. 4xx or 5xx). */ - public static boolean isStatusError(int status) { - return (status >= 400 && status < 600); - } + public static boolean isStatusError(int status) { + return (status >= 400 && status < 600); + } - /** + /** * Returns whether the status is a client error (i.e. 4xx). */ - public static boolean isStatusClientError(int status) { - return (status >= 400 && status < 500); - } + public static boolean isStatusClientError(int status) { + return (status >= 400 && status < 500); + } - /** + /** * Returns whether the status is a server error (i.e. 5xx). */ - public static boolean isStatusServerError(int status) { - return (status >= 500 && status < 600); - } + public static boolean isStatusServerError(int status) { + return (status >= 500 && status < 600); + } - /** + /** * Returns whether the download has completed (either with success or * error). */ - public static boolean isStatusCompleted(int status) { - return (status >= 200 && status < 300) - || (status >= 400 && status < 600); - } + public static boolean isStatusCompleted(int status) { + return (status >= 200 && status < 300) || (status >= 400 && status < 600); + } - /** + /** * This download hasn't stated yet */ - public static final int STATUS_PENDING = 190; + public static final int STATUS_PENDING = 190; - /** + /** * This download has started */ - public static final int STATUS_RUNNING = 192; + public static final int STATUS_RUNNING = 192; - /** + /** * This download has been paused by the owning app. */ - public static final int STATUS_PAUSED_BY_APP = 193; + public static final int STATUS_PAUSED_BY_APP = 193; - /** + /** * This download encountered some network error and is waiting before * retrying the request. */ - public static final int STATUS_WAITING_TO_RETRY = 194; + public static final int STATUS_WAITING_TO_RETRY = 194; - /** + /** * This download is waiting for network connectivity to proceed. */ - public static final int STATUS_WAITING_FOR_NETWORK = 195; + public static final int STATUS_WAITING_FOR_NETWORK = 195; - /** + /** * This download is waiting for a Wi-Fi connection to proceed or for * permission to download over cellular. */ - public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196; + public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196; - /** + /** * This download is waiting for a Wi-Fi connection to proceed. */ - public static final int STATUS_QUEUED_FOR_WIFI = 197; + public static final int STATUS_QUEUED_FOR_WIFI = 197; - /** + /** * 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. - * + * * @hide */ - public static final int STATUS_SUCCESS = 200; + public static final int STATUS_SUCCESS = 200; - /** + /** * The requested URL is no longer available */ - public static final int STATUS_FORBIDDEN = 403; + public static final int STATUS_FORBIDDEN = 403; - /** + /** * The file was delivered incorrectly */ - public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487; + public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487; - /** + /** * The requested destination file already exists. */ - public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + 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; + public static final int STATUS_CANNOT_RESUME = 489; - /** + /** * This download was canceled - * + * * @hide */ - public static final int STATUS_CANCELED = 490; + 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; + 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. - * + * * @hide */ - public static final int STATUS_FILE_ERROR = 492; + 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. - * + * * @hide */ - public static final int STATUS_UNHANDLED_REDIRECT = 493; + public static final int STATUS_UNHANDLED_REDIRECT = 493; - /** + /** * This download couldn't be completed because of an unspecified unhandled * HTTP code. - * + * * @hide */ - public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + 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. - * + * * @hide */ - public static final int STATUS_HTTP_DATA_ERROR = 495; + public static final int STATUS_HTTP_DATA_ERROR = 495; - /** + /** * This download couldn't be completed because of an HttpException while * setting up the request. - * + * * @hide */ - public static final int STATUS_HTTP_EXCEPTION = 496; + public static final int STATUS_HTTP_EXCEPTION = 496; - /** + /** * This download couldn't be completed because there were too many * redirects. - * + * * @hide */ - public static final int STATUS_TOO_MANY_REDIRECTS = 497; + 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. - * + * * @hide */ - public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + 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. - * + * * @hide */ - public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; - /** + /** * This download is allowed to run. - * + * * @hide */ - public static final int CONTROL_RUN = 0; + public static final int CONTROL_RUN = 0; - /** + /** * This download must pause at the first opportunity. - * + * * @hide */ - public static final int CONTROL_PAUSED = 1; + public static final int CONTROL_PAUSED = 1; - /** + /** * This download is visible but only shows in the notifications while it's * in progress. - * + * * @hide */ - public static final int VISIBILITY_VISIBLE = 0; + public static final int VISIBILITY_VISIBLE = 0; - /** + /** * This download is visible and shows in the notifications while in progress * and after completion. - * + * * @hide */ - public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; + public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; - /** + /** * This download doesn't show in the UI or in the notifications. - * + * * @hide */ - public static final int VISIBILITY_HIDDEN = 2; + public static final int VISIBILITY_HIDDEN = 2; - /** - * Bit flag for {@link #setAllowedNetworkTypes} corresponding to + /** + * Bit flag for setAllowedNetworkTypes corresponding to * {@link ConnectivityManager#TYPE_MOBILE}. */ - public static final int NETWORK_MOBILE = 1 << 0; + public static final int NETWORK_MOBILE = 1 << 0; - /** - * Bit flag for {@link #setAllowedNetworkTypes} corresponding to + /** + * Bit flag for setAllowedNetworkTypes corresponding to * {@link ConnectivityManager#TYPE_WIFI}. */ - public static final int NETWORK_WIFI = 1 << 1; + public static final int NETWORK_WIFI = 1 << 1; - private final static String TEMP_EXT = ".tmp"; + private final static String TEMP_EXT = ".tmp"; - /** + /** * Service thread status */ - private static boolean sIsRunning; + private static boolean sIsRunning; - @Override - public IBinder onBind(Intent paramIntent) { - Log.d(Constants.TAG, "Service Bound"); - return this.mServiceMessenger.getBinder(); - } + @Override + public IBinder onBind(Intent paramIntent) { + Log.d(Constants.TAG, "Service Bound"); + return this.mServiceMessenger.getBinder(); + } - /** + /** * Network state. */ - private boolean mIsConnected; - private boolean mIsFailover; - private boolean mIsCellularConnection; - private boolean mIsRoaming; - private boolean mIsAtLeast3G; - private boolean mIsAtLeast4G; - private boolean mStateChanged; + private boolean mIsConnected; + private boolean mIsFailover; + private boolean mIsCellularConnection; + private boolean mIsRoaming; + private boolean mIsAtLeast3G; + private boolean mIsAtLeast4G; + private boolean mStateChanged; - /** + /** * Download state */ - private int mControl; - private int mStatus; + private int mControl; + private int mStatus; - public boolean isWiFi() { - return mIsConnected && !mIsCellularConnection; - } + public boolean isWiFi() { + return mIsConnected && !mIsCellularConnection; + } - /** + /** * Bindings to important services */ - private ConnectivityManager mConnectivityManager; - private WifiManager mWifiManager; + private ConnectivityManager mConnectivityManager; + private WifiManager mWifiManager; - /** + /** * Package we are downloading for (defaults to package of application) */ - private PackageInfo mPackageInfo; + private PackageInfo mPackageInfo; - /** + /** * Byte counts */ - long mBytesSoFar; - long mTotalLength; - int mFileCount; + long mBytesSoFar; + long mTotalLength; + int mFileCount; - /** + /** * Used for calculating time remaining and speed */ - long mBytesAtSample; - long mMillisecondsAtSample; - float mAverageDownloadSpeed; + long mBytesAtSample; + long mMillisecondsAtSample; + float mAverageDownloadSpeed; - /** + /** * Our binding to the network state broadcasts */ - private BroadcastReceiver mConnReceiver; - final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); - final private Messenger mServiceMessenger = mServiceStub.getMessenger(); - private Messenger mClientMessenger; - private DownloadNotification mNotification; - private PendingIntent mPendingIntent; - private PendingIntent mAlarmIntent; + private BroadcastReceiver mConnReceiver; + final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); + final private Messenger mServiceMessenger = mServiceStub.getMessenger(); + private Messenger mClientMessenger; + private DownloadNotification mNotification; + private PendingIntent mPendingIntent; + private PendingIntent mAlarmIntent; - /** + /** * Updates the network type based upon the type and subtype returned from * the connectivity manager. Subtype is only used for cellular signals. - * + * * @param type * @param subType */ - private void updateNetworkType(int type, int subType) { - switch (type) { - case ConnectivityManager.TYPE_WIFI: - case ConnectivityManager.TYPE_ETHERNET: - case ConnectivityManager.TYPE_BLUETOOTH: - mIsCellularConnection = false; - mIsAtLeast3G = false; - mIsAtLeast4G = false; - break; - case ConnectivityManager.TYPE_WIMAX: - mIsCellularConnection = true; - mIsAtLeast3G = true; - mIsAtLeast4G = true; - break; - case ConnectivityManager.TYPE_MOBILE: - mIsCellularConnection = true; - switch (subType) { - case TelephonyManager.NETWORK_TYPE_1xRTT: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_IDEN: - mIsAtLeast3G = false; - mIsAtLeast4G = false; - break; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_UMTS: - mIsAtLeast3G = true; - mIsAtLeast4G = false; - break; - case TelephonyManager.NETWORK_TYPE_LTE: // 4G - case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop - // with 4G - case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but - // marketed as - // 4G - mIsAtLeast3G = true; - mIsAtLeast4G = true; - break; - default: - mIsCellularConnection = false; - mIsAtLeast3G = false; - mIsAtLeast4G = false; - } - } - } - - private void updateNetworkState(NetworkInfo info) { - boolean isConnected = mIsConnected; - boolean isFailover = mIsFailover; - boolean isCellularConnection = mIsCellularConnection; - boolean isRoaming = mIsRoaming; - boolean isAtLeast3G = mIsAtLeast3G; - if (null != info) { - mIsRoaming = info.isRoaming(); - mIsFailover = info.isFailover(); - mIsConnected = info.isConnected(); - updateNetworkType(info.getType(), info.getSubtype()); - } else { - mIsRoaming = false; - mIsFailover = false; - mIsConnected = false; - updateNetworkType(-1, -1); - } - mStateChanged = (mStateChanged || isConnected != mIsConnected - || isFailover != mIsFailover - || isCellularConnection != mIsCellularConnection - || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G); - if (Constants.LOGVV) { - if (mStateChanged) { - Log.v(LOG_TAG, "Network state changed: "); - Log.v(LOG_TAG, "Starting State: " + - (isConnected ? "Connected " : "Not Connected ") + - (isCellularConnection ? "Cellular " : "WiFi ") + - (isRoaming ? "Roaming " : "Local ") + - (isAtLeast3G ? "3G+ " : "<3G ")); - Log.v(LOG_TAG, "Ending State: " + - (mIsConnected ? "Connected " : "Not Connected ") + - (mIsCellularConnection ? "Cellular " : "WiFi ") + - (mIsRoaming ? "Roaming " : "Local ") + - (mIsAtLeast3G ? "3G+ " : "<3G ")); - - if (isServiceRunning()) { - if (mIsRoaming) { - mStatus = STATUS_WAITING_FOR_NETWORK; - mControl = CONTROL_PAUSED; - } else if (mIsCellularConnection) { - DownloadsDB db = DownloadsDB.getDB(this); - int flags = db.getFlags(); - if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { - mStatus = STATUS_QUEUED_FOR_WIFI; - mControl = CONTROL_PAUSED; - } - } - } - - } - } - } - - /** + private void updateNetworkType(int type, int subType) { + switch (type) { + case ConnectivityManager.TYPE_WIFI: + case ConnectivityManager.TYPE_ETHERNET: + case ConnectivityManager.TYPE_BLUETOOTH: + mIsCellularConnection = false; + mIsAtLeast3G = false; + mIsAtLeast4G = false; + break; + case ConnectivityManager.TYPE_WIMAX: + mIsCellularConnection = true; + mIsAtLeast3G = true; + mIsAtLeast4G = true; + break; + case ConnectivityManager.TYPE_MOBILE: + mIsCellularConnection = true; + switch (subType) { + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_IDEN: + mIsAtLeast3G = false; + mIsAtLeast4G = false; + break; + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_UMTS: + mIsAtLeast3G = true; + mIsAtLeast4G = false; + break; + case TelephonyManager.NETWORK_TYPE_LTE: // 4G + case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop + // with 4G + case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but + // marketed as + // 4G + mIsAtLeast3G = true; + mIsAtLeast4G = true; + break; + default: + mIsCellularConnection = false; + mIsAtLeast3G = false; + mIsAtLeast4G = false; + } + } + } + + private void updateNetworkState(NetworkInfo info) { + boolean isConnected = mIsConnected; + boolean isFailover = mIsFailover; + boolean isCellularConnection = mIsCellularConnection; + boolean isRoaming = mIsRoaming; + boolean isAtLeast3G = mIsAtLeast3G; + if (null != info) { + mIsRoaming = info.isRoaming(); + mIsFailover = info.isFailover(); + mIsConnected = info.isConnected(); + updateNetworkType(info.getType(), info.getSubtype()); + } else { + mIsRoaming = false; + mIsFailover = false; + mIsConnected = false; + updateNetworkType(-1, -1); + } + mStateChanged = (mStateChanged || isConnected != mIsConnected || isFailover != mIsFailover || isCellularConnection != mIsCellularConnection || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G); + if (Constants.LOGVV) { + if (mStateChanged) { + Log.v(LOG_TAG, "Network state changed: "); + Log.v(LOG_TAG, "Starting State: " + + (isConnected ? "Connected " : "Not Connected ") + + (isCellularConnection ? "Cellular " : "WiFi ") + + (isRoaming ? "Roaming " : "Local ") + + (isAtLeast3G ? "3G+ " : "<3G ")); + Log.v(LOG_TAG, "Ending State: " + + (mIsConnected ? "Connected " : "Not Connected ") + + (mIsCellularConnection ? "Cellular " : "WiFi ") + + (mIsRoaming ? "Roaming " : "Local ") + + (mIsAtLeast3G ? "3G+ " : "<3G ")); + + if (isServiceRunning()) { + if (mIsRoaming) { + mStatus = STATUS_WAITING_FOR_NETWORK; + mControl = CONTROL_PAUSED; + } else if (mIsCellularConnection) { + DownloadsDB db = DownloadsDB.getDB(this); + int flags = db.getFlags(); + if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { + mStatus = STATUS_QUEUED_FOR_WIFI; + mControl = CONTROL_PAUSED; + } + } + } + } + } + } + + /** * Polls the network state, setting the flags appropriately. */ - void pollNetworkState() { - if (null == mConnectivityManager) { - mConnectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - } - if (null == mWifiManager) { - mWifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - } - if (mConnectivityManager == null) { - Log.w(Constants.TAG, - "couldn't get connectivity manager to poll network state"); - } else { - NetworkInfo activeInfo = mConnectivityManager - .getActiveNetworkInfo(); - updateNetworkState(activeInfo); - } - } - - public static final int NO_DOWNLOAD_REQUIRED = 0; - public static final int LVL_CHECK_REQUIRED = 1; - public static final int DOWNLOAD_REQUIRED = 2; - - public static final String EXTRA_PACKAGE_NAME = "EPN"; - public static final String EXTRA_PENDING_INTENT = "EPI"; - public static final String EXTRA_MESSAGE_HANDLER = "EMH"; - - /** + void pollNetworkState() { + if (null == mConnectivityManager) { + mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + } + if (null == mWifiManager) { + mWifiManager = (WifiManager)getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + if (mConnectivityManager == null) { + Log.w(Constants.TAG, + "couldn't get connectivity manager to poll network state"); + } else { + @SuppressLint("MissingPermission") + NetworkInfo activeInfo = mConnectivityManager + .getActiveNetworkInfo(); + updateNetworkState(activeInfo); + } + } + + public static final int NO_DOWNLOAD_REQUIRED = 0; + public static final int LVL_CHECK_REQUIRED = 1; + public static final int DOWNLOAD_REQUIRED = 2; + + public static final String EXTRA_PACKAGE_NAME = "EPN"; + public static final String EXTRA_PENDING_INTENT = "EPI"; + public static final String EXTRA_MESSAGE_HANDLER = "EMH"; + + /** * Returns true if the LVL check is required - * + * * @param db a downloads DB synchronized with the latest state * @param pi the package info for the project * @return returns true if the filenames need to be returned */ - private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) { - // we need to update the LVL check and get a successful status to - // proceed - if (db.mVersionCode != pi.versionCode) { - return true; - } - return false; - } + private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) { + // we need to update the LVL check and get a successful status to + // proceed + if (db.mVersionCode != pi.versionCode) { + return true; + } + return false; + } - /** + /** * Careful! Only use this internally. - * + * * @return whether we think the service is running */ - private static synchronized boolean isServiceRunning() { - return sIsRunning; - } - - private static synchronized void setServiceRunning(boolean isRunning) { - sIsRunning = isRunning; - } - - public static int startDownloadServiceIfRequired(Context context, - Intent intent, Class<?> serviceClass) throws NameNotFoundException { - final PendingIntent pendingIntent = (PendingIntent) intent - .getParcelableExtra(EXTRA_PENDING_INTENT); - return startDownloadServiceIfRequired(context, pendingIntent, - serviceClass); - } - - public static int startDownloadServiceIfRequired(Context context, - PendingIntent pendingIntent, Class<?> serviceClass) - throws NameNotFoundException - { - String packageName = context.getPackageName(); - String className = serviceClass.getName(); - - return startDownloadServiceIfRequired(context, pendingIntent, - packageName, className); - } - - /** + private static synchronized boolean isServiceRunning() { + return sIsRunning; + } + + private static synchronized void setServiceRunning(boolean isRunning) { + sIsRunning = isRunning; + } + + public static int startDownloadServiceIfRequired(Context context, + Intent intent, Class<?> serviceClass) throws NameNotFoundException { + final PendingIntent pendingIntent = (PendingIntent)intent + .getParcelableExtra(EXTRA_PENDING_INTENT); + return startDownloadServiceIfRequired(context, pendingIntent, + serviceClass); + } + + public static int startDownloadServiceIfRequired(Context context, + PendingIntent pendingIntent, Class<?> serviceClass) + throws NameNotFoundException { + String packageName = context.getPackageName(); + String className = serviceClass.getName(); + + return startDownloadServiceIfRequired(context, pendingIntent, + packageName, className); + } + + /** * 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 @@ -652,690 +648,673 @@ public abstract class DownloaderService extends CustomIntentService implements I * 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 - * @param thisIntent + * @param pendingIntent * @return true if the app should wait for more guidance from the * downloader, false if the app can continue * @throws NameNotFoundException */ - public static int startDownloadServiceIfRequired(Context context, - PendingIntent pendingIntent, String classPackage, String className) - throws NameNotFoundException { - // first: do we need to do an LVL update? - // we begin by getting our APK version from the package manager - final PackageInfo pi = context.getPackageManager().getPackageInfo( - context.getPackageName(), 0); - - int status = NO_DOWNLOAD_REQUIRED; - - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(context); - - // we need to update the LVL check and get a successful status to - // proceed - if (isLVLCheckRequired(db, pi)) { - status = LVL_CHECK_REQUIRED; - } - // we don't have to update LVL. do we still have a download to start? - if (db.mStatus == 0) { - DownloadInfo[] infos = db.getDownloads(); - if (null != infos) { - for (DownloadInfo info : infos) { - if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) { - status = DOWNLOAD_REQUIRED; - db.updateStatus(-1); - break; - } - } - } - } else { - status = DOWNLOAD_REQUIRED; - } - switch (status) { - case DOWNLOAD_REQUIRED: - case LVL_CHECK_REQUIRED: - Intent fileIntent = new Intent(); - fileIntent.setClassName(classPackage, className); - fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); - context.startService(fileIntent); - break; - } - return status; - } - - @Override - public void requestAbortDownload() { - mControl = CONTROL_PAUSED; - mStatus = STATUS_CANCELED; - } - - @Override - public void requestPauseDownload() { - mControl = CONTROL_PAUSED; - mStatus = STATUS_PAUSED_BY_APP; - } - - @Override - public void setDownloadFlags(int flags) { - DownloadsDB.getDB(this).updateFlags(flags); - } - - @Override - public void requestContinueDownload() { - if (mControl == CONTROL_PAUSED) { - mControl = CONTROL_RUN; - } - Intent fileIntent = new Intent(this, this.getClass()); - fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - this.startService(fileIntent); - } - - public abstract String getPublicKey(); - - public abstract byte[] getSALT(); - - public abstract String getAlarmReceiverClassName(); - - private class LVLRunnable implements Runnable { - LVLRunnable(Context context, PendingIntent intent) { - mContext = context; - mPendingIntent = intent; - } - - final Context mContext; - - @Override - public void run() { - setServiceRunning(true); - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL); - String deviceId = Secure.getString(mContext.getContentResolver(), - Secure.ANDROID_ID); - - final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, - new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); - - // reset our policy back to the start of the world to force a - // re-check - aep.resetPolicy(); - - // let's try and get the OBB file from LVL first - // Construct the LicenseChecker with a Policy. - final LicenseChecker checker = new LicenseChecker(mContext, aep, - getPublicKey() // Your public licensing key. - ); - checker.checkAccess(new LicenseCheckerCallback() { - - @Override - public void allow(int reason) { - try { - int count = aep.getExpansionURLCount(); - DownloadsDB db = DownloadsDB.getDB(mContext); - int status = 0; - if (count != 0) { - for (int i = 0; i < count; i++) { - String currentFileName = aep - .getExpansionFileName(i); - if (null != currentFileName) { - DownloadInfo di = new DownloadInfo(i, - currentFileName, mContext.getPackageName()); - - long fileSize = aep.getExpansionFileSize(i); - if (handleFileUpdated(db, i, currentFileName, - fileSize)) { - status |= -1; - di.resetDownload(); - di.mUri = aep.getExpansionURL(i); - di.mTotalBytes = fileSize; - di.mStatus = status; - db.updateDownload(di); - } else { - // we need to read the download - // information - // from - // the database - DownloadInfo dbdi = db - .getDownloadInfoByFileName(di.mFileName); - if (null == dbdi) { - // the file exists already and is - // the - // correct size - // was delivered by Market or - // through - // another mechanism - Log.d(LOG_TAG, "file " + di.mFileName - + " found. Not downloading."); - di.mStatus = STATUS_SUCCESS; - di.mTotalBytes = fileSize; - di.mCurrentBytes = fileSize; - di.mUri = aep.getExpansionURL(i); - db.updateDownload(di); - } else if (dbdi.mStatus != STATUS_SUCCESS) { - // we just update the URL - dbdi.mUri = aep.getExpansionURL(i); - db.updateDownload(dbdi); - status |= -1; - } - } - } - } - } - // first: do we need to do an LVL update? - // we begin by getting our APK version from the package - // manager - PackageInfo pi; - try { - pi = mContext.getPackageManager().getPackageInfo( - mContext.getPackageName(), 0); - db.updateMetadata(pi.versionCode, status); - Class<?> serviceClass = DownloaderService.this.getClass(); - switch (startDownloadServiceIfRequired(mContext, mPendingIntent, - serviceClass)) { - case NO_DOWNLOAD_REQUIRED: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); - break; - case LVL_CHECK_REQUIRED: - // DANGER WILL ROBINSON! - Log.e(LOG_TAG, "In LVL checking loop!"); - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); - throw new RuntimeException( - "Error with LVL checking and database integrity"); - case DOWNLOAD_REQUIRED: - // do nothing. the download will notify the - // application - // when things are done - break; - } - } catch (NameNotFoundException e1) { - e1.printStackTrace(); - throw new RuntimeException( - "Error with getting information from package name"); - } - } finally { - setServiceRunning(false); - } - } - - @Override - public void dontAllow(int reason) { - try - { - switch (reason) { - case Policy.NOT_LICENSED: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); - break; - case Policy.RETRY: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); - break; - } - } finally { - setServiceRunning(false); - } - - } - - @Override - public void applicationError(int errorCode) { - try { - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); - } finally { - setServiceRunning(false); - } - } - - }); - - } - - }; - - /** + public static int startDownloadServiceIfRequired(Context context, + PendingIntent pendingIntent, String classPackage, String className) + throws NameNotFoundException { + // first: do we need to do an LVL update? + // we begin by getting our APK version from the package manager + final PackageInfo pi = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + + int status = NO_DOWNLOAD_REQUIRED; + + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(context); + + // we need to update the LVL check and get a successful status to + // proceed + if (isLVLCheckRequired(db, pi)) { + status = LVL_CHECK_REQUIRED; + } + // we don't have to update LVL. do we still have a download to start? + if (db.mStatus == 0) { + DownloadInfo[] infos = db.getDownloads(); + if (null != infos) { + for (DownloadInfo info : infos) { + if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) { + status = DOWNLOAD_REQUIRED; + db.updateStatus(-1); + break; + } + } + } + } else { + status = DOWNLOAD_REQUIRED; + } + switch (status) { + case DOWNLOAD_REQUIRED: + case LVL_CHECK_REQUIRED: + Intent fileIntent = new Intent(); + fileIntent.setClassName(classPackage, className); + fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); + context.startService(fileIntent); + break; + } + return status; + } + + @Override + public void requestAbortDownload() { + mControl = CONTROL_PAUSED; + mStatus = STATUS_CANCELED; + } + + @Override + public void requestPauseDownload() { + mControl = CONTROL_PAUSED; + mStatus = STATUS_PAUSED_BY_APP; + } + + @Override + public void setDownloadFlags(int flags) { + DownloadsDB.getDB(this).updateFlags(flags); + } + + @Override + public void requestContinueDownload() { + if (mControl == CONTROL_PAUSED) { + mControl = CONTROL_RUN; + } + Intent fileIntent = new Intent(this, this.getClass()); + fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + this.startService(fileIntent); + } + + public abstract String getPublicKey(); + + public abstract byte[] getSALT(); + + public abstract String getAlarmReceiverClassName(); + + private class LVLRunnable implements Runnable { + LVLRunnable(Context context, PendingIntent intent) { + mContext = context; + mPendingIntent = intent; + } + + final Context mContext; + + @Override + public void run() { + setServiceRunning(true); + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL); + String deviceId = Secure.ANDROID_ID; + + final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, + new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); + + // reset our policy back to the start of the world to force a + // re-check + aep.resetPolicy(); + + // let's try and get the OBB file from LVL first + // Construct the LicenseChecker with a Policy. + final LicenseChecker checker = new LicenseChecker(mContext, aep, + getPublicKey() // Your public licensing key. + ); + checker.checkAccess(new LicenseCheckerCallback() { + @Override + public void allow(int reason) { + try { + int count = aep.getExpansionURLCount(); + DownloadsDB db = DownloadsDB.getDB(mContext); + int status = 0; + if (count != 0) { + for (int i = 0; i < count; i++) { + String currentFileName = aep + .getExpansionFileName(i); + if (null != currentFileName) { + DownloadInfo di = new DownloadInfo(i, + currentFileName, mContext.getPackageName()); + + long fileSize = aep.getExpansionFileSize(i); + if (handleFileUpdated(db, i, currentFileName, + fileSize)) { + status |= -1; + di.resetDownload(); + di.mUri = aep.getExpansionURL(i); + di.mTotalBytes = fileSize; + di.mStatus = status; + db.updateDownload(di); + } else { + // we need to read the download + // information + // from + // the database + DownloadInfo dbdi = db + .getDownloadInfoByFileName(di.mFileName); + if (null == dbdi) { + // the file exists already and is + // the + // correct size + // was delivered by Market or + // through + // another mechanism + Log.d(LOG_TAG, "file " + di.mFileName + " found. Not downloading."); + di.mStatus = STATUS_SUCCESS; + di.mTotalBytes = fileSize; + di.mCurrentBytes = fileSize; + di.mUri = aep.getExpansionURL(i); + db.updateDownload(di); + } else if (dbdi.mStatus != STATUS_SUCCESS) { + // we just update the URL + dbdi.mUri = aep.getExpansionURL(i); + db.updateDownload(dbdi); + status |= -1; + } + } + } + } + } + // first: do we need to do an LVL update? + // we begin by getting our APK version from the package + // manager + PackageInfo pi; + try { + pi = mContext.getPackageManager().getPackageInfo( + mContext.getPackageName(), 0); + db.updateMetadata(pi.versionCode, status); + Class<?> serviceClass = DownloaderService.this.getClass(); + switch (startDownloadServiceIfRequired(mContext, mPendingIntent, + serviceClass)) { + case NO_DOWNLOAD_REQUIRED: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); + break; + case LVL_CHECK_REQUIRED: + // DANGER WILL ROBINSON! + Log.e(LOG_TAG, "In LVL checking loop!"); + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); + throw new RuntimeException( + "Error with LVL checking and database integrity"); + case DOWNLOAD_REQUIRED: + // do nothing. the download will notify the + // application + // when things are done + break; + } + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + throw new RuntimeException( + "Error with getting information from package name"); + } + } finally { + setServiceRunning(false); + } + } + + @Override + public void dontAllow(int reason) { + try { + switch (reason) { + case Policy.NOT_LICENSED: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); + break; + case Policy.RETRY: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); + break; + } + } finally { + setServiceRunning(false); + } + } + + @Override + public void applicationError(int errorCode) { + try { + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); + } finally { + setServiceRunning(false); + } + } + }); + } + }; + + /** * Updates the LVL information from the server. - * + * * @param context */ - public void updateLVL(final Context context) { - Context c = context.getApplicationContext(); - Handler h = new Handler(c.getMainLooper()); - h.post(new LVLRunnable(c, mPendingIntent)); - } + public void updateLVL(final Context context) { + Context c = context.getApplicationContext(); + Handler h = new Handler(c.getMainLooper()); + h.post(new LVLRunnable(c, mPendingIntent)); + } - /** + /** * The APK has been updated and a filename has been sent down from the * Market call. If the file has the same name as the previous file, we do * nothing as the file is guaranteed to be the same. If the file does not * have the same name, we download it if it hasn't already been delivered by * Market. - * + * * @param index the index of the file from market (0 = main, 1 = patch) * @param filename the name of the new file * @param fileSize the size of the new file * @return */ - public boolean handleFileUpdated(DownloadsDB db, int index, - String filename, long fileSize) { - DownloadInfo di = db.getDownloadInfoByFileName(filename); - if (null != di) { - String oldFile = di.mFileName; - // cleanup - if (null != oldFile) { - if (filename.equals(oldFile)) { - return false; - } - - // remove partially downloaded file if it is there - String deleteFile = Helpers.generateSaveFileName(this, oldFile); - File f = new File(deleteFile); - if (f.exists()) - f.delete(); - } - } - return !Helpers.doesFileExist(this, filename, fileSize, true); - } - - private void scheduleAlarm(long wakeUp) { - AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarms == null) { - Log.e(Constants.TAG, "couldn't get alarm manager"); - return; - } - - if (Constants.LOGV) { - Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); - } - - String className = getAlarmReceiverClassName(); - Intent intent = new Intent(Constants.ACTION_RETRY); - intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - intent.setClassName(this.getPackageName(), - className); - mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, - PendingIntent.FLAG_ONE_SHOT); - alarms.set( - AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + wakeUp, mAlarmIntent - ); - } - - private void cancelAlarms() { - if (null != mAlarmIntent) { - AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarms == null) { - Log.e(Constants.TAG, "couldn't get alarm manager"); - return; - } - alarms.cancel(mAlarmIntent); - mAlarmIntent = null; - } - } - - /** + public boolean handleFileUpdated(DownloadsDB db, int index, + String filename, long fileSize) { + DownloadInfo di = db.getDownloadInfoByFileName(filename); + if (null != di) { + String oldFile = di.mFileName; + // cleanup + if (null != oldFile) { + if (filename.equals(oldFile)) { + return false; + } + + // remove partially downloaded file if it is there + String deleteFile = Helpers.generateSaveFileName(this, oldFile); + File f = new File(deleteFile); + if (f.exists()) + f.delete(); + } + } + return !Helpers.doesFileExist(this, filename, fileSize, true); + } + + private void scheduleAlarm(long wakeUp) { + AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + if (alarms == null) { + Log.e(Constants.TAG, "couldn't get alarm manager"); + return; + } + + if (Constants.LOGV) { + Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); + } + + String className = getAlarmReceiverClassName(); + Intent intent = new Intent(Constants.ACTION_RETRY); + intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + intent.setClassName(this.getPackageName(), + className); + mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, + PendingIntent.FLAG_ONE_SHOT); + alarms.set( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + wakeUp, mAlarmIntent); + } + + private void cancelAlarms() { + if (null != mAlarmIntent) { + AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + if (alarms == null) { + Log.e(Constants.TAG, "couldn't get alarm manager"); + return; + } + alarms.cancel(mAlarmIntent); + mAlarmIntent = null; + } + } + + /** * We use this to track network state, such as when WiFi, Cellular, etc. is * enabled when downloads are paused or in progress. */ - private class InnerBroadcastReceiver extends BroadcastReceiver { - final Service mService; - - InnerBroadcastReceiver(Service service) { - mService = service; - } - - @Override - public void onReceive(Context context, Intent intent) { - pollNetworkState(); - if (mStateChanged - && !isServiceRunning()) { - Log.d(Constants.TAG, "InnerBroadcastReceiver Called"); - Intent fileIntent = new Intent(context, mService.getClass()); - fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - // send a new intent to the service - context.startService(fileIntent); - } - } - }; - - /** + private class InnerBroadcastReceiver extends BroadcastReceiver { + final Service mService; + + InnerBroadcastReceiver(Service service) { + mService = service; + } + + @Override + public void onReceive(Context context, Intent intent) { + pollNetworkState(); + if (mStateChanged && !isServiceRunning()) { + Log.d(Constants.TAG, "InnerBroadcastReceiver Called"); + Intent fileIntent = new Intent(context, mService.getClass()); + fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + // send a new intent to the service + context.startService(fileIntent); + } + } + }; + + /** * This is the main thread for the Downloader. This thread is responsible * for queuing up downloads and other goodness. */ - @Override - protected void onHandleIntent(Intent intent) { - setServiceRunning(true); - try { - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(this); - final PendingIntent pendingIntent = (PendingIntent) intent - .getParcelableExtra(EXTRA_PENDING_INTENT); - - if (null != pendingIntent) - { - mNotification.setClientIntent(pendingIntent); - mPendingIntent = pendingIntent; - } else if (null != mPendingIntent) { - mNotification.setClientIntent(mPendingIntent); - } else { - Log.e(LOG_TAG, "Downloader started in bad state without notification intent."); - return; - } - - // when the LVL check completes, a successful response will update - // the service - if (isLVLCheckRequired(db, mPackageInfo)) { - updateLVL(this); - return; - } - - // get each download - DownloadInfo[] infos = db.getDownloads(); - mBytesSoFar = 0; - mTotalLength = 0; - mFileCount = infos.length; - for (DownloadInfo info : infos) { - // We do an (simple) integrity check on each file, just to make - // sure - if (info.mStatus == STATUS_SUCCESS) { - // verify that the file matches the state - if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) { - info.mStatus = 0; - info.mCurrentBytes = 0; - } - } - // get aggregate data - mTotalLength += info.mTotalBytes; - mBytesSoFar += info.mCurrentBytes; - } - - // loop through all downloads and fetch them - pollNetworkState(); - if (null == mConnReceiver) { - - /** + @Override + protected void onHandleIntent(Intent intent) { + setServiceRunning(true); + try { + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(this); + final PendingIntent pendingIntent = (PendingIntent)intent + .getParcelableExtra(EXTRA_PENDING_INTENT); + + if (null != pendingIntent) { + mNotification.setClientIntent(pendingIntent); + mPendingIntent = pendingIntent; + } else if (null != mPendingIntent) { + mNotification.setClientIntent(mPendingIntent); + } else { + Log.e(LOG_TAG, "Downloader started in bad state without notification intent."); + return; + } + + // when the LVL check completes, a successful response will update + // the service + if (isLVLCheckRequired(db, mPackageInfo)) { + updateLVL(this); + return; + } + + // get each download + DownloadInfo[] infos = db.getDownloads(); + mBytesSoFar = 0; + mTotalLength = 0; + mFileCount = infos.length; + for (DownloadInfo info : infos) { + // We do an (simple) integrity check on each file, just to make + // sure + if (info.mStatus == STATUS_SUCCESS) { + // verify that the file matches the state + if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) { + info.mStatus = 0; + info.mCurrentBytes = 0; + } + } + // get aggregate data + mTotalLength += info.mTotalBytes; + mBytesSoFar += info.mCurrentBytes; + } + + // loop through all downloads and fetch them + pollNetworkState(); + if (null == mConnReceiver) { + + /** * We use this to track network state, such as when WiFi, * Cellular, etc. is enabled when downloads are paused or in * progress. */ - mConnReceiver = new InnerBroadcastReceiver(this); - IntentFilter intentFilter = new IntentFilter( - ConnectivityManager.CONNECTIVITY_ACTION); - intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); - registerReceiver(mConnReceiver, intentFilter); - } - - for (DownloadInfo info : infos) { - long startingCount = info.mCurrentBytes; - - if (info.mStatus != STATUS_SUCCESS) { - DownloadThread dt = new DownloadThread(info, this, mNotification); - cancelAlarms(); - scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); - dt.run(); - cancelAlarms(); - } - db.updateFromDb(info); - boolean setWakeWatchdog = false; - int notifyStatus; - switch (info.mStatus) { - case STATUS_FORBIDDEN: - // the URL is out of date - updateLVL(this); - return; - case STATUS_SUCCESS: - mBytesSoFar += info.mCurrentBytes - startingCount; - db.updateMetadata(mPackageInfo.versionCode, 0); - continue; - case STATUS_FILE_DELIVERED_INCORRECTLY: - // we may be on a network that is returning us a web - // page on redirect - notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE; - info.mCurrentBytes = 0; - db.updateDownload(info); - setWakeWatchdog = true; - break; - case STATUS_PAUSED_BY_APP: - notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST; - break; - case STATUS_WAITING_FOR_NETWORK: - case STATUS_WAITING_TO_RETRY: - notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE; - setWakeWatchdog = true; - break; - case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION: - case STATUS_QUEUED_FOR_WIFI: - // look for more detail here - if (null != mWifiManager) { - if (!mWifiManager.isWifiEnabled()) { - notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION; - setWakeWatchdog = true; - break; - } - } - notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION; - setWakeWatchdog = true; - break; - case STATUS_CANCELED: - notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED; - setWakeWatchdog = true; - break; - - case STATUS_INSUFFICIENT_SPACE_ERROR: - notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL; - setWakeWatchdog = true; - break; - - case STATUS_DEVICE_NOT_FOUND_ERROR: - notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE; - setWakeWatchdog = true; - break; - - default: - notifyStatus = IDownloaderClient.STATE_FAILED; - break; - } - if (setWakeWatchdog) { - scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); - } else { - cancelAlarms(); - } - // failure or pause state - mNotification.onDownloadStateChanged(notifyStatus); - return; - } - - // all downloads complete - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); - } finally { - setServiceRunning(false); - } - } - - @Override - public void onDestroy() { - if (null != mConnReceiver) { - unregisterReceiver(mConnReceiver); - mConnReceiver = null; - } - mServiceStub.disconnect(this); - super.onDestroy(); - } - - public int getNetworkAvailabilityState(DownloadsDB db) { - if (mIsConnected) { - if (!mIsCellularConnection) - return NETWORK_OK; - int flags = db.mFlags; - if (mIsRoaming) - return NETWORK_CANNOT_USE_ROAMING; - if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { - return NETWORK_OK; - } else { - return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; - } - } - return NETWORK_NO_CONNECTION; - } - - @Override - public void onCreate() { - super.onCreate(); - try { - mPackageInfo = getPackageManager().getPackageInfo( - getPackageName(), 0); - ApplicationInfo ai = getApplicationInfo(); - CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai); - mNotification = new DownloadNotification(this, applicationLabel); - - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - } - - /** + mConnReceiver = new InnerBroadcastReceiver(this); + IntentFilter intentFilter = new IntentFilter( + ConnectivityManager.CONNECTIVITY_ACTION); + intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + registerReceiver(mConnReceiver, intentFilter); + } + + for (DownloadInfo info : infos) { + long startingCount = info.mCurrentBytes; + + if (info.mStatus != STATUS_SUCCESS) { + DownloadThread dt = new DownloadThread(info, this, mNotification); + cancelAlarms(); + scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); + dt.run(); + cancelAlarms(); + } + db.updateFromDb(info); + boolean setWakeWatchdog = false; + int notifyStatus; + switch (info.mStatus) { + case STATUS_FORBIDDEN: + // the URL is out of date + updateLVL(this); + return; + case STATUS_SUCCESS: + mBytesSoFar += info.mCurrentBytes - startingCount; + db.updateMetadata(mPackageInfo.versionCode, 0); + continue; + case STATUS_FILE_DELIVERED_INCORRECTLY: + // we may be on a network that is returning us a web + // page on redirect + notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE; + info.mCurrentBytes = 0; + db.updateDownload(info); + setWakeWatchdog = true; + break; + case STATUS_PAUSED_BY_APP: + notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST; + break; + case STATUS_WAITING_FOR_NETWORK: + case STATUS_WAITING_TO_RETRY: + notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE; + setWakeWatchdog = true; + break; + case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION: + case STATUS_QUEUED_FOR_WIFI: + // look for more detail here + if (null != mWifiManager) { + if (!mWifiManager.isWifiEnabled()) { + notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION; + setWakeWatchdog = true; + break; + } + } + notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION; + setWakeWatchdog = true; + break; + case STATUS_CANCELED: + notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED; + setWakeWatchdog = true; + break; + + case STATUS_INSUFFICIENT_SPACE_ERROR: + notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL; + setWakeWatchdog = true; + break; + + case STATUS_DEVICE_NOT_FOUND_ERROR: + notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE; + setWakeWatchdog = true; + break; + + default: + notifyStatus = IDownloaderClient.STATE_FAILED; + break; + } + if (setWakeWatchdog) { + scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); + } else { + cancelAlarms(); + } + // failure or pause state + mNotification.onDownloadStateChanged(notifyStatus); + return; + } + + // all downloads complete + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); + } finally { + setServiceRunning(false); + } + } + + @Override + public void onDestroy() { + if (null != mConnReceiver) { + unregisterReceiver(mConnReceiver); + mConnReceiver = null; + } + mServiceStub.disconnect(this); + super.onDestroy(); + } + + public int getNetworkAvailabilityState(DownloadsDB db) { + if (mIsConnected) { + if (!mIsCellularConnection) + return NETWORK_OK; + int flags = db.mFlags; + if (mIsRoaming) + return NETWORK_CANNOT_USE_ROAMING; + if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { + return NETWORK_OK; + } else { + return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; + } + } + return NETWORK_NO_CONNECTION; + } + + @Override + public void onCreate() { + super.onCreate(); + try { + mPackageInfo = getPackageManager().getPackageInfo( + getPackageName(), 0); + ApplicationInfo ai = getApplicationInfo(); + CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai); + mNotification = new DownloadNotification(this, applicationLabel); + + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + } + + /** * Exception thrown from methods called by generateSaveFile() for any fatal * error. */ - public static class GenerateSaveFileError extends Exception { - private static final long serialVersionUID = 3465966015408936540L; - int mStatus; - String mMessage; + public static class GenerateSaveFileError extends Exception { + private static final long serialVersionUID = 3465966015408936540L; + int mStatus; + String mMessage; - public GenerateSaveFileError(int status, String message) { - mStatus = status; - mMessage = message; - } - } + public GenerateSaveFileError(int status, String message) { + mStatus = status; + mMessage = message; + } + } - /** + /** * Returns the filename (where the file should be saved) from info about a * download */ - public String generateTempSaveFileName(String fileName) { - String path = Helpers.getSaveFilePath(this) - + File.separator + fileName + TEMP_EXT; - return path; - } + public String generateTempSaveFileName(String fileName) { + String path = Helpers.getSaveFilePath(this) + File.separator + fileName + TEMP_EXT; + return path; + } - /** + /** * Creates a filename (where the file should be saved) from info about a * download. */ - public String generateSaveFile(String filename, long filesize) - throws GenerateSaveFileError { - String path = generateTempSaveFileName(filename); - File expPath = new File(path); - if (!Helpers.isExternalMediaMounted()) { - Log.d(Constants.TAG, "External media not mounted: " + path); - throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR, - "external media is not yet mounted"); - - } - if (expPath.exists()) { - Log.d(Constants.TAG, "File already exists: " + path); - throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR, - "requested destination file already exists"); - } - if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) { - throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR, - "insufficient space on external storage"); - } - return path; - } - - /** + public String generateSaveFile(String filename, long filesize) + throws GenerateSaveFileError { + String path = generateTempSaveFileName(filename); + File expPath = new File(path); + if (!Helpers.isExternalMediaMounted()) { + Log.d(Constants.TAG, "External media not mounted: " + path); + throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR, + "external media is not yet mounted"); + } + if (expPath.exists()) { + Log.d(Constants.TAG, "File already exists: " + path); + throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR, + "requested destination file already exists"); + } + if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) { + throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR, + "insufficient space on external storage"); + } + return path; + } + + /** * @return a non-localized string appropriate for logging corresponding to * one of the NETWORK_* constants. */ - public String getLogMessageForNetworkError(int networkError) { - switch (networkError) { - case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds recommended limit for mobile network"; + public String getLogMessageForNetworkError(int networkError) { + switch (networkError) { + case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: + return "download size exceeds recommended limit for mobile network"; - case NETWORK_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds limit for mobile network"; + case NETWORK_UNUSABLE_DUE_TO_SIZE: + return "download size exceeds limit for mobile network"; - case NETWORK_NO_CONNECTION: - return "no network connection available"; + case NETWORK_NO_CONNECTION: + return "no network connection available"; - case NETWORK_CANNOT_USE_ROAMING: - return "download cannot use the current network connection because it is roaming"; + case NETWORK_CANNOT_USE_ROAMING: + return "download cannot use the current network connection because it is roaming"; - case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: - return "download was requested to not use the current network type"; + case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: + return "download was requested to not use the current network type"; - default: - return "unknown error with network connectivity"; - } - } + default: + return "unknown error with network connectivity"; + } + } - public int getControl() { - return mControl; - } + public int getControl() { + return mControl; + } - public int getStatus() { - return mStatus; - } + public int getStatus() { + return mStatus; + } - /** + /** * Calculating a moving average for the speed so we don't get jumpy * calculations for time etc. */ - static private final float SMOOTHING_FACTOR = 0.005f; - - public void notifyUpdateBytes(long totalBytesSoFar) { - long timeRemaining; - long currentTime = SystemClock.uptimeMillis(); - if (0 != mMillisecondsAtSample) { - // we have a sample. - long timePassed = currentTime - mMillisecondsAtSample; - long bytesInSample = totalBytesSoFar - mBytesAtSample; - float currentSpeedSample = (float) bytesInSample / (float) timePassed; - if (0 != mAverageDownloadSpeed) { - mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample - + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed; - } else { - mAverageDownloadSpeed = currentSpeedSample; - } - timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed); - } else { - timeRemaining = -1; - } - mMillisecondsAtSample = currentTime; - mBytesAtSample = totalBytesSoFar; - mNotification.onDownloadProgress( - new DownloadProgressInfo(mTotalLength, - totalBytesSoFar, - timeRemaining, - mAverageDownloadSpeed) - ); - - } - - @Override - protected boolean shouldStop() { - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(this); - if (db.mStatus == 0) { - return true; - } - return false; - } - - @Override - public void requestDownloadStatus() { - mNotification.resendState(); - } - - @Override - public void onClientUpdated(Messenger clientMessenger) { - this.mClientMessenger = clientMessenger; - mNotification.setMessenger(mClientMessenger); - } - + static private final float SMOOTHING_FACTOR = 0.005f; + + public void notifyUpdateBytes(long totalBytesSoFar) { + long timeRemaining; + long currentTime = SystemClock.uptimeMillis(); + if (0 != mMillisecondsAtSample) { + // we have a sample. + long timePassed = currentTime - mMillisecondsAtSample; + long bytesInSample = totalBytesSoFar - mBytesAtSample; + float currentSpeedSample = (float)bytesInSample / (float)timePassed; + if (0 != mAverageDownloadSpeed) { + mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed; + } else { + mAverageDownloadSpeed = currentSpeedSample; + } + timeRemaining = (long)((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed); + } else { + timeRemaining = -1; + } + mMillisecondsAtSample = currentTime; + mBytesAtSample = totalBytesSoFar; + mNotification.onDownloadProgress( + new DownloadProgressInfo(mTotalLength, + totalBytesSoFar, + timeRemaining, + mAverageDownloadSpeed)); + } + + @Override + protected boolean shouldStop() { + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(this); + if (db.mStatus == 0) { + return true; + } + return false; + } + + @Override + public void requestDownloadStatus() { + mNotification.resendState(); + } + + @Override + public void onClientUpdated(Messenger clientMessenger) { + this.mClientMessenger = clientMessenger; + mNotification.setMessenger(mClientMessenger); + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java index 250299c400..5d8dce0bac 100755 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java @@ -27,484 +27,443 @@ 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(','); - } - } - - /** + 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[][][] sSchemas = { + DownloadColumns.SCHEMA, MetadataColumns.SCHEMA + }; - static final private String[] sTables = { - DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME - }; + 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(); - } - } - } - - /** + 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; - - /** + 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(); - } - } - } - + 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(); + } + } + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java index 3f440e9893..02bd1f27f6 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java +++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java @@ -27,7 +27,7 @@ import java.util.regex.Pattern; */ 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 @@ -37,164 +37,155 @@ public final class HttpDateTime { * (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_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})"; + 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'); - } - } - - /* + 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); - } + 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); + } } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java deleted file mode 100644 index 56b2331e31..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java +++ /dev/null @@ -1,101 +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.godot.game.R; -import com.google.android.vending.expansion.downloader.Helpers; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; - -public class V14CustomNotification implements DownloadNotification.ICustomNotification { - - CharSequence mTitle; - CharSequence mTicker; - int mIcon; - long mTotalKB = -1; - long mCurrentKB = -1; - long mTimeRemaining; - PendingIntent mPendingIntent; - - @Override - public void setIcon(int icon) { - mIcon = icon; - } - - @Override - public void setTitle(CharSequence title) { - mTitle = title; - } - - @Override - public void setTotalBytes(long totalBytes) { - mTotalKB = totalBytes; - } - - @Override - public void setCurrentBytes(long currentBytes) { - mCurrentKB = currentBytes; - } - - void setProgress(Notification.Builder builder) { - - } - - @Override - public Notification.Builder updateNotification(Context c) { - Notification.Builder builder = new Notification.Builder(c); - builder.setContentTitle(mTitle); - if (mTotalKB > 0 && -1 != mCurrentKB) { - builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false); - } else { - builder.setProgress(0, 0, true); - } - builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB)); - builder.setContentInfo(c.getString(R.string.time_remaining_notification, - Helpers.getTimeRemaining(mTimeRemaining))); - if (mIcon != 0) { - builder.setSmallIcon(mIcon); - } else { - int iconResource = android.R.drawable.stat_sys_download; - builder.setSmallIcon(iconResource); - } - builder.setOngoing(true); - builder.setTicker(mTicker); - builder.setContentIntent(mPendingIntent); - builder.setOnlyAlertOnce(true); - - return builder; - } - - @Override - public void setPendingIntent(PendingIntent contentIntent) { - mPendingIntent = contentIntent; - } - - @Override - public void setTicker(CharSequence ticker) { - mTicker = ticker; - } - - @Override - public void setTimeRemaining(long timeRemaining) { - mTimeRemaining = timeRemaining; - } - -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java new file mode 100644 index 0000000000..feba3034c3 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * An Obfuscator that uses AES to encrypt data. + */ +public class AESObfuscator implements Obfuscator { + private static final String UTF8 = "UTF-8"; + private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final byte[] IV = { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; + private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; + + private Cipher mEncryptor; + private Cipher mDecryptor; + + /** + * @param salt an array of random bytes to use for each (un)obfuscation + * @param applicationId application identifier, e.g. the package name + * @param deviceId device identifier. Use as many sources as possible to + * create this unique identifier. + */ + public AESObfuscator(byte[] salt, String applicationId, String deviceId) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); + KeySpec keySpec = + new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); + SecretKey tmp = factory.generateSecret(keySpec); + SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); + mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); + mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); + } catch (GeneralSecurityException e) { + // This can't happen on a compatible Android device. + throw new RuntimeException("Invalid environment", e); + } + } + + public String obfuscate(String original, String key) { + if (original == null) { + return null; + } + try { + // Header is appended as an integrity check + return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid environment", e); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Invalid environment", e); + } + } + + public String unobfuscate(String obfuscated, String key) throws ValidationException { + if (obfuscated == null) { + return null; + } + try { + String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); + // Check for presence of header. This serves as a final integrity check, for cases + // where the block size is correct during decryption. + int headerIndex = result.indexOf(header + key); + if (headerIndex != 0) { + throw new ValidationException("Header not found (invalid data or key)" + + ":" + + obfuscated); + } + return result.substring(header.length() + key.length(), result.length()); + } catch (Base64DecoderException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (IllegalBlockSizeException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (BadPaddingException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid environment", e); + } + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000000..2c60e7e4b8 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -0,0 +1,413 @@ + +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; + } +} diff --git a/platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java b/platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java index e5c5e2d7ca..2384b8b82f 100644 --- a/platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java +++ b/platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java @@ -37,11 +37,11 @@ package com.google.android.vending.licensing; */ 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); + int isDeviceAllowed(String userId); } diff --git a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl index c816558afc..c816558afc 100644 --- a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java new file mode 100644 index 0000000000..89edeae1b4 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/* + * This file is auto-generated. DO NOT MODIFY. + * Original file: aidl/ILicenseResultListener.aidl + */ +package com.google.android.vending.licensing; +import java.lang.String; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Binder; +import android.os.Parcel; +public interface ILicenseResultListener extends android.os.IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener { + private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + /** + * Cast an IBinder object into an ILicenseResultListener interface, + * generating a proxy if needed. + */ + public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) { + if ((obj == null)) { + return null; + } + android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { + return ((com.google.android.vending.licensing.ILicenseResultListener)iin); + } + return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); + } + public android.os.IBinder asBinder() { + return this; + } + public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_verifyLicense: { + data.enforceInterface(DESCRIPTOR); + int _arg0; + _arg0 = data.readInt(); + java.lang.String _arg1; + _arg1 = data.readString(); + java.lang.String _arg2; + _arg2 = data.readString(); + this.verifyLicense(_arg0, _arg1, _arg2); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) { + mRemote = remote; + } + public android.os.IBinder asBinder() { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(responseCode); + _data.writeString(signedData); + _data.writeString(signature); + mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); + } finally { + _data.recycle(); + } + } + } + static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); + } + public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; +} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl index 664510ce0c..664510ce0c 100644 --- a/platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java new file mode 100644 index 0000000000..8b7cc83541 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/* + * This file is auto-generated. DO NOT MODIFY. + * Original file: aidl/ILicensingService.aidl + */ +package com.google.android.vending.licensing; +import java.lang.String; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Binder; +import android.os.Parcel; +public interface ILicensingService extends android.os.IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService { + private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + /** + * Cast an IBinder object into an ILicensingService interface, + * generating a proxy if needed. + */ + public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) { + if ((obj == null)) { + return null; + } + android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicensingService))) { + return ((com.google.android.vending.licensing.ILicensingService)iin); + } + return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); + } + public android.os.IBinder asBinder() { + return this; + } + public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_checkLicense: { + data.enforceInterface(DESCRIPTOR); + long _arg0; + _arg0 = data.readLong(); + java.lang.String _arg1; + _arg1 = data.readString(); + com.google.android.vending.licensing.ILicenseResultListener _arg2; + _arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); + this.checkLicense(_arg0, _arg1, _arg2); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + private static class Proxy implements com.google.android.vending.licensing.ILicensingService { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) { + mRemote = remote; + } + public android.os.IBinder asBinder() { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeLong(nonce); + _data.writeString(packageName); + _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null))); + mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); + } finally { + _data.recycle(); + } + } + } + static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); + } + public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..38aab9f4f5 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,387 @@ +/* + * 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.google.android.vending.licensing.ILicenseResultListener; +import com.google.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.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 ""; + } + } +} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java index b250a7147b..dc2c2d70bf 100644 --- a/platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java +++ b/platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -34,34 +34,34 @@ package com.google.android.vending.licensing; */ 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); + 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); + 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; + /** 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); + public void applicationError(int errorCode); } diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java new file mode 100644 index 0000000000..77f7dc7295 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java @@ -0,0 +1,232 @@ +/* + * 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); + } +} diff --git a/platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java b/platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java index d87af3153f..a43e454228 100644 --- a/platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java +++ b/platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -26,7 +26,7 @@ package com.google.android.vending.licensing; */ public class NullDeviceLimiter implements DeviceLimiter { - public int isDeviceAllowed(String userId) { - return Policy.LICENSED; - } + public int isDeviceAllowed(String userId) { + return Policy.LICENSED; + } } diff --git a/platform/android/java/src/com/android/vending/licensing/Obfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java index 88891728e6..8731d03aa6 100644 --- a/platform/android/java/src/com/android/vending/licensing/Obfuscator.java +++ b/platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java @@ -20,29 +20,29 @@ 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 + * Any transformation scheme must be reversable. 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); + String obfuscate(String original, String key); - /** + /** * Undo the transformation applied to data by the obfuscate() method. * - * @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. + * @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; + String unobfuscate(String obfuscated, String key) throws ValidationException; } diff --git a/platform/android/java/src/com/android/vending/licensing/Policy.java b/platform/android/java/src/com/google/android/vending/licensing/Policy.java index fa267fc71a..65202aceb9 100644 --- a/platform/android/java/src/com/android/vending/licensing/Policy.java +++ b/platform/android/java/src/com/google/android/vending/licensing/Policy.java @@ -22,38 +22,44 @@ package com.google.android.vending.licensing; */ 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; - /** + 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; - /** + 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; + 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); + void processServerResponse(int response, ResponseData rawData); - /** + /** * Check if the user should be allowed access to the application. */ - boolean allowAccess(); + 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(); } 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 new file mode 100644 index 0000000000..099bb1c48b --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java @@ -0,0 +1,78 @@ +/* + * 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(); + mEditor.apply(); + } + 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; + } + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java new file mode 100644 index 0000000000..1c802f8e45 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.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 }); + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000000..b9a50c1104 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -0,0 +1,299 @@ +/* + * 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; + } +} diff --git a/platform/android/java/src/com/android/vending/licensing/StrictPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java index d8d83b4e4b..9849730c38 100644 --- a/platform/android/java/src/com/android/vending/licensing/StrictPolicy.java +++ b/platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java @@ -16,6 +16,13 @@ package com.google.android.vending.licensing; +import android.util.Log; +import com.google.android.vending.licensing.util.URIQueryDecoder; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + /** * Non-caching policy. All requests will be sent to the licensing service, * and no local caching is performed. @@ -26,38 +33,67 @@ package com.google.android.vending.licensing; * weigh the risks of using this Policy over one which implements caching, * such as ServerManagedPolicy. * <p> - * Access to the application is only allowed if a LICESNED response is. + * Access to the application is only allowed if a LICENSED response is. * received. All other responses (including RETRY) will deny access. */ public class StrictPolicy implements Policy { - private int mLastResponse; + private static final String TAG = "StrictPolicy"; + + private int mLastResponse; + private String mLicensingUrl; - public StrictPolicy() { - // Set default policy. This will force the application to check the policy on launch. - mLastResponse = Policy.RETRY; - } + public StrictPolicy() { + // Set default policy. This will force the application to check the policy on launch. + mLastResponse = Policy.RETRY; + mLicensingUrl = null; + } - /** + /** * Process a new response from the license server. Since we aren't * performing any caching, this equates to reading the LicenseResponse. - * Any ResponseData provided is ignored. + * Any cache-related ResponseData is ignored, but the licensing URL + * extra is still extracted in cases where the app is unlicensed. * * @param response the result from validating the server response * @param rawData the raw server response data */ - public void processServerResponse(int response, ResponseData rawData) { - mLastResponse = response; - } + public void processServerResponse(int response, ResponseData rawData) { + mLastResponse = response; + + if (response == Policy.NOT_LICENSED) { + Map<String, String> extras = decodeExtras(rawData); + mLicensingUrl = extras.get("LU"); + } + } - /** + /** * {@inheritDoc} * * This implementation allows access if and only if a LICENSED response * was received the last time the server was contacted. */ - public boolean allowAccess() { - return (mLastResponse == Policy.LICENSED); - } + public boolean allowAccess() { + return (mLastResponse == Policy.LICENSED); + } + + public String getLicensingUrl() { + return mLicensingUrl; + } + + private Map<String, String> decodeExtras( + com.google.android.vending.licensing.ResponseData rawData) { + Map<String, String> results = new HashMap<String, String>(); + if (rawData == null) { + return results; + } + try { + URI rawExtras = new URI("?" + rawData.extra); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } } diff --git a/platform/android/java/src/com/android/vending/licensing/ValidationException.java b/platform/android/java/src/com/google/android/vending/licensing/ValidationException.java index ee4df47c68..79b70e6804 100644 --- a/platform/android/java/src/com/android/vending/licensing/ValidationException.java +++ b/platform/android/java/src/com/google/android/vending/licensing/ValidationException.java @@ -21,13 +21,13 @@ package com.google.android.vending.licensing; * {@link Obfuscator}.} */ public class ValidationException extends Exception { - public ValidationException() { - super(); - } + public ValidationException() { + super(); + } - public ValidationException(String s) { - super(s); - } + public ValidationException(String s) { + super(s); + } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; } 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 new file mode 100644 index 0000000000..bd711aadf5 --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java @@ -0,0 +1,556 @@ +// Portions copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.vending.licensing.util; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + * <p> + * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> + * periodically to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +import com.godot.game.BuildConfig; + +/** + * Base64 converter class. This code is not a full-blown MIME encoder; + * it simply converts binary data to base64 data and back. + * + * <p>Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { + /** Specify encoding (value is {@code true}). */ + public final static boolean ENCODE = true; + + /** Specify decoding (value is {@code false}). */ + public final static boolean DECODE = false; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', + (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', + (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', + (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', + (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', + (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', + (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', + (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', + (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', + (byte)'9', (byte)'+', (byte)'/' }; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', + (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', + (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', + (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', + (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', + (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', + (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', + (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', + (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', + (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', + (byte)'9', (byte)'-', (byte)'_' }; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /** The web safe decodabet */ + private final static byte[] WEBSAFE_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to three bytes of the array <var>source</var> + * and writes the resulting four Base64 bytes to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 3 for + * the <var>source</var> array or <var>destOffset</var> + 4 for + * the <var>destination</var> array. + * The actual number of significant bytes in your array is + * given by <var>numSigBytes</var>. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param alphabet is the encoding alphabet + * @return the <var>destination</var> array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff)&0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. + * Equivalent to calling + * {@code encodeBytes(source, 0, source.length)} + * + * @param source The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source The data to convert + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param alphabet is the encoding alphabet + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, + boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while (doPadding == false && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param alphabet is the encoding alphabet + * @param maxLineLength maximum length of one line. + * @return the BASE64-encoded byte array + */ + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, + int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = + ((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff)&0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + if (BuildConfig.DEBUG && e != outBuff.length) + throw new RuntimeException(); + return outBuff; + } + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array <var>source</var> + * and writes the resulting bytes (up to three of them) + * to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 4 for + * the <var>source</var> array or <var>destOffset</var> + 3 for + * the <var>destination</var> array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte)(outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte)(outBuff >>> 16); + destination[destOffset + 1] = (byte)(outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte)(outBuff >> 16); + destination[destOffset + 1] = (byte)(outBuff >> 8); + destination[destOffset + 2] = (byte)(outBuff); + return 3; + } + } // end decodeToBytes + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded data. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded byte array. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = 0; i < len; i++) { + sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte)(source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)"); + } + } + + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + } + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } +} diff --git a/platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java index 1aef1b54b8..50724a9b05 100644 --- a/platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java +++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -20,13 +20,13 @@ package com.google.android.vending.licensing.util; * @author nelson */ public class Base64DecoderException extends Exception { - public Base64DecoderException() { - super(); - } + public Base64DecoderException() { + super(); + } - public Base64DecoderException(String s) { - super(s); - } + public Base64DecoderException(String s) { + super(s); + } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; } diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java b/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000000..4f908b472c --- /dev/null +++ b/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing.util; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Map; +import java.util.Scanner; + +public class URIQueryDecoder { + private static final String TAG = "URIQueryDecoder"; + + /** + * Decodes the query portion of the passed-in URI. + * + * @param encodedURI the URI containing the query to decode + * @param results a map containing all query parameters. Query parameters that do not have a + * value will map to a null string + */ + static public void DecodeQuery(URI encodedURI, Map<String, String> results) { + Scanner scanner = new Scanner(encodedURI.getRawQuery()); + scanner.useDelimiter("&"); + try { + while (scanner.hasNext()) { + String param = scanner.next(); + String[] valuePair = param.split("="); + String name, value; + if (valuePair.length == 1) { + value = null; + } else if (valuePair.length == 2) { + value = URLDecoder.decode(valuePair[1], "UTF-8"); + } else { + throw new IllegalArgumentException("query parameter invalid"); + } + name = URLDecoder.decode(valuePair[0], "UTF-8"); + results.put(name, value); + } + } catch (UnsupportedEncodingException e) { + // This should never happen. + Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error."); + } + } +} diff --git a/platform/android/java/src/org/godotengine/godot/Godot.java b/platform/android/java/src/org/godotengine/godot/Godot.java index 88194f00d1..e3878754a0 100644 --- a/platform/android/java/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/src/org/godotengine/godot/Godot.java @@ -30,59 +30,48 @@ package org.godotengine.godot; -import android.R; +//import android.R; + import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.content.pm.ConfigurationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; +import android.os.Messenger; +import android.provider.Settings.Secure; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; -import android.widget.LinearLayout; import android.widget.TextView; -import android.view.ViewGroup.LayoutParams; -import android.app.*; -import android.content.*; -import android.content.SharedPreferences.Editor; -import android.view.*; -import android.view.inputmethod.InputMethodManager; -import android.os.*; -import android.util.Log; -import android.graphics.*; -import android.text.method.*; -import android.text.*; -import android.media.*; -import android.hardware.*; -import android.content.*; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.Uri; -import android.media.MediaPlayer; - -import android.content.ClipboardManager; -import android.content.ClipData; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.ArrayList; - -import org.godotengine.godot.payments.PaymentsManager; -import java.io.IOException; - -import android.provider.Settings.Secure; -import android.widget.FrameLayout; - -import org.godotengine.godot.input.*; - -import java.io.InputStream; -import javax.microedition.khronos.opengles.GL10; -import java.security.MessageDigest; -import java.io.File; -import java.io.FileInputStream; -import java.util.LinkedList; - -import com.google.android.vending.expansion.downloader.Constants; import com.google.android.vending.expansion.downloader.DownloadProgressInfo; import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; @@ -91,9 +80,20 @@ import com.google.android.vending.expansion.downloader.IDownloaderClient; import com.google.android.vending.expansion.downloader.IDownloaderService; import com.google.android.vending.expansion.downloader.IStub; -import android.os.Bundle; -import android.os.Messenger; -import android.os.SystemClock; +import org.godotengine.godot.input.GodotEditText; +import org.godotengine.godot.payments.PaymentsManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import javax.microedition.khronos.opengles.GL10; public class Godot extends Activity implements SensorEventListener, IDownloaderClient { @@ -222,9 +222,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC private Sensor mGyroscope; public FrameLayout layout; - public RelativeLayout adLayout; - static public GodotIO io; + public static GodotIO io; public static void setWindowTitle(String title) { //setTitle(title); @@ -263,24 +262,23 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC }; public void onVideoInit() { - boolean use_gl3 = getGLESVersionCode() >= 0x00030000; //mView = new GodotView(getApplication(),io,use_gl3); //setContentView(mView); layout = new FrameLayout(this); - layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setContentView(layout); // GodotEditText layout GodotEditText edittext = new GodotEditText(this); - edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); + edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); // ...add to FrameLayout layout.addView(edittext); mView = new GodotView(getApplication(), io, use_gl3, use_32_bits, use_debug_opengl, this); - layout.addView(mView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + layout.addView(mView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); edittext.setView(mView); io.setEdit(edittext); @@ -298,11 +296,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } }); - // Ad layout - adLayout = new RelativeLayout(this); - adLayout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); - layout.addView(adLayout); - final String[] current_command_line = command_line; final GodotView view = mView; mView.queueEvent(new Runnable() { @@ -332,10 +325,11 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } public void alert(final String message, final String title) { + final Activity activity = this; runOnUiThread(new Runnable() { @Override public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(getInstance()); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setMessage(message).setTitle(title); builder.setPositiveButton( "OK", @@ -350,14 +344,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC }); } - private static Godot _self; - - public static Godot getInstance() { - return Godot._self; - } - public int getGLESVersionCode() { - ActivityManager am = (ActivityManager)Godot.getInstance().getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager am = (ActivityManager)this.getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo(); return deviceInfo.reqGlEsVersion; } @@ -421,7 +409,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } io = new GodotIO(this); - io.unique_id = Secure.getString(getContentResolver(), Secure.ANDROID_ID); + io.unique_id = Secure.ANDROID_ID; GodotLib.io = io; mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); @@ -452,7 +440,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC protected void onCreate(Bundle icicle) { super.onCreate(icicle); - _self = this; Window window = getWindow(); //window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); @@ -476,7 +463,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC use_debug_opengl = true; } else if (command_line[i].equals("--use_immersive")) { use_immersive = true; - if (Build.VERSION.SDK_INT >= 19.0) { // check if the application runs on an android 4.4+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | @@ -498,7 +485,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC Editor editor = prefs.edit(); editor.putString("store_public_key", main_pack_key); - editor.commit(); + editor.apply(); i++; } else if (command_line[i].trim().length() != 0) { new_args.add(command_line[i]); @@ -665,7 +652,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - if (use_immersive && Build.VERSION.SDK_INT >= 19.0) { // check if the application runs on an android 4.4+ + if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ Window window = getWindow(); window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | @@ -688,13 +675,15 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC @Override public void onSystemUiVisibilityChange(int visibility) { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } } } }); @@ -1024,12 +1013,9 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC mTimeRemaining.setText(getString(com.godot.game.R.string.time_remaining, Helpers.getTimeRemaining(progress.mTimeRemaining))); - progress.mOverallTotal = progress.mOverallTotal; mPB.setMax((int)(progress.mOverallTotal >> 8)); mPB.setProgress((int)(progress.mOverallProgress >> 8)); - mProgressPercent.setText(Long.toString(progress.mOverallProgress * 100 / - progress.mOverallTotal) + - "%"); + mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); } diff --git a/platform/android/java/src/org/godotengine/godot/GodotIO.java b/platform/android/java/src/org/godotengine/godot/GodotIO.java index a95c508d21..75d67831d4 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/src/org/godotengine/godot/GodotIO.java @@ -38,6 +38,7 @@ import java.io.InputStream; import java.io.IOException; import android.app.*; import android.content.*; +import android.util.SparseArray; import android.view.*; import android.view.inputmethod.InputMethodManager; import android.os.*; @@ -61,7 +62,6 @@ public class GodotIO { Godot activity; GodotEditText edit; - Context applicationContext; MediaPlayer mediaPlayer; final int SCREEN_LANDSCAPE = 0; @@ -87,7 +87,7 @@ public class GodotIO { public int pos; } - HashMap<Integer, AssetData> streams; + SparseArray<AssetData> streams; public int file_open(String path, boolean write) { @@ -125,7 +125,7 @@ public class GodotIO { } public int file_get_size(int id) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_get_size: Invalid file id: %d\n", id); return -1; } @@ -134,7 +134,7 @@ public class GodotIO { } public void file_seek(int id, int bytes) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_get_size: Invalid file id: %d\n", id); return; } @@ -174,7 +174,7 @@ public class GodotIO { public int file_tell(int id) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_read: Can't tell eof for invalid file id: %d\n", id); return 0; } @@ -184,7 +184,7 @@ public class GodotIO { } public boolean file_eof(int id) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_read: Can't check eof for invalid file id: %d\n", id); return false; } @@ -195,7 +195,7 @@ public class GodotIO { public byte[] file_read(int id, int bytes) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_read: Can't read invalid file id: %d\n", id); return new byte[0]; } @@ -243,7 +243,7 @@ public class GodotIO { public void file_close(int id) { - if (!streams.containsKey(id)) { + if (streams.get(id) == null) { System.out.printf("file_close: Can't close invalid file id: %d\n", id); return; } @@ -264,7 +264,7 @@ public class GodotIO { public int last_dir_id = 1; - HashMap<Integer, AssetDir> dirs; + SparseArray<AssetDir> dirs; public int dir_open(String path) { @@ -293,7 +293,7 @@ public class GodotIO { } public boolean dir_is_dir(int id) { - if (!dirs.containsKey(id)) { + if (dirs.get(id) == null) { System.out.printf("dir_next: invalid dir id: %d\n", id); return false; } @@ -320,7 +320,7 @@ public class GodotIO { public String dir_next(int id) { - if (!dirs.containsKey(id)) { + if (dirs.get(id) == null) { System.out.printf("dir_next: invalid dir id: %d\n", id); return ""; } @@ -339,7 +339,7 @@ public class GodotIO { public void dir_close(int id) { - if (!dirs.containsKey(id)) { + if (dirs.get(id) == null) { System.out.printf("dir_close: invalid dir id: %d\n", id); return; } @@ -351,9 +351,9 @@ public class GodotIO { am = p_activity.getAssets(); activity = p_activity; - streams = new HashMap<Integer, AssetData>(); - dirs = new HashMap<Integer, AssetDir>(); - applicationContext = activity.getApplicationContext(); + //streams = new HashMap<Integer, AssetData>(); + streams = new SparseArray<AssetData>(); + dirs = new SparseArray<AssetDir>(); } ///////////////////////// @@ -365,7 +365,7 @@ public class GodotIO { private AudioTrack mAudioTrack; public Object audioInit(int sampleRate, int desiredFrames) { - int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO; + int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int frameSize = 4; @@ -496,13 +496,13 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = applicationContext.getResources().getDisplayMetrics(); + DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); return (int)(metrics.density * 160f); } public boolean needsReloadHooks() { - return android.os.Build.VERSION.SDK_INT < 11; + return false; } public void showKeyboard(String p_existing_text) { @@ -564,7 +564,7 @@ public class GodotIO { try { mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(applicationContext, filePath); + mediaPlayer.setDataSource(activity.getApplicationContext(), filePath); mediaPlayer.prepare(); mediaPlayer.start(); } catch (IOException e) { diff --git a/platform/android/java/src/org/godotengine/godot/GodotView.java b/platform/android/java/src/org/godotengine/godot/GodotView.java index 4cb4db33de..da5a8b11e2 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotView.java +++ b/platform/android/java/src/org/godotengine/godot/GodotView.java @@ -29,6 +29,7 @@ /*************************************************************************/ package org.godotengine.godot; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; import android.opengl.GLSurfaceView; @@ -75,9 +76,9 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { private static String TAG = "GodotView"; private static final boolean DEBUG = false; - private static Context ctx; + private Context ctx; - private static GodotIO io; + private GodotIO io; private static boolean firsttime = true; private static boolean use_gl3 = false; private static boolean use_32 = false; @@ -105,20 +106,26 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { init(false, 16, 0); } + public GodotView(Context context) { + super(context); + ctx = context; + } + public GodotView(Context context, boolean translucent, int depth, int stencil) { super(context); init(translucent, depth, stencil); } + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); return activity.gotTouchEvent(event); - }; + } public int get_godot_button(int keyCode) { - int button = 0; + int button; switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B button = 0; @@ -178,7 +185,7 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { default: button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20; break; - }; + } return button; }; @@ -440,6 +447,10 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { private static class ContextFactory implements GLSurfaceView.EGLContextFactory { private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + String driver_name = GodotLib.getGlobal("rendering/quality/driver/driver_name"); + if (use_gl3 && !driver_name.equals("GLES3")) { + use_gl3 = false; + } if (use_gl3) Log.w(TAG, "creating OpenGL ES 3.0 context :"); else @@ -508,26 +519,24 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { * perform actual matching in chooseConfig() below. */ private static int EGL_OPENGL_ES2_BIT = 4; - private static int[] s_configAttribs2 = - { - EGL10.EGL_RED_SIZE, 4, - EGL10.EGL_GREEN_SIZE, 4, - EGL10.EGL_BLUE_SIZE, 4, - // EGL10.EGL_DEPTH_SIZE, 16, - // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL10.EGL_NONE - }; - private static int[] s_configAttribs3 = - { - EGL10.EGL_RED_SIZE, 4, - EGL10.EGL_GREEN_SIZE, 4, - EGL10.EGL_BLUE_SIZE, 4, - // EGL10.EGL_DEPTH_SIZE, 16, - // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT - EGL10.EGL_NONE - }; + private static int[] s_configAttribs2 = { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + // EGL10.EGL_DEPTH_SIZE, 16, + // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + private static int[] s_configAttribs3 = { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + // EGL10.EGL_DEPTH_SIZE, 16, + // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT + EGL10.EGL_NONE + }; public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { diff --git a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java index 53fcf5ef70..b2ac2c0d67 100644 --- a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java @@ -39,6 +39,8 @@ import android.os.Message; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.EditorInfo; +import java.lang.ref.WeakReference; + public class GodotEditText extends EditText { // =========================================================== // Constants @@ -51,9 +53,24 @@ public class GodotEditText extends EditText { // =========================================================== private GodotView mView; private GodotTextInputWrapper mInputWrapper; - private static Handler sHandler; + private EditHandler sHandler = new EditHandler(this); private String mOriginText; + private static class EditHandler extends Handler { + private final WeakReference<GodotEditText> mEdit; + public EditHandler(GodotEditText edit) { + mEdit = new WeakReference<>(edit); + } + + @Override + public void handleMessage(Message msg) { + GodotEditText edit = mEdit.get(); + if (edit != null) { + edit.handleMessage(msg); + } + } + } + // =========================================================== // Constructors // =========================================================== @@ -75,36 +92,33 @@ public class GodotEditText extends EditText { protected void initView() { this.setPadding(0, 0, 0, 0); this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + } - sHandler = new Handler() { - @Override - public void handleMessage(final Message msg) { - switch (msg.what) { - case HANDLER_OPEN_IME_KEYBOARD: { - GodotEditText edit = (GodotEditText)msg.obj; - String text = edit.mOriginText; - if (edit.requestFocus()) { - edit.removeTextChangedListener(edit.mInputWrapper); - edit.setText(""); - edit.append(text); - edit.mInputWrapper.setOriginText(text); - edit.addTextChangedListener(edit.mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(edit, 0); - } - } break; - - case HANDLER_CLOSE_IME_KEYBOARD: { - GodotEditText edit = (GodotEditText)msg.obj; - - edit.removeTextChangedListener(mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); - edit.mView.requestFocus(); - } break; + private void handleMessage(final Message msg) { + switch (msg.what) { + case HANDLER_OPEN_IME_KEYBOARD: { + GodotEditText edit = (GodotEditText)msg.obj; + String text = edit.mOriginText; + if (edit.requestFocus()) { + edit.removeTextChangedListener(edit.mInputWrapper); + edit.setText(""); + edit.append(text); + edit.mInputWrapper.setOriginText(text); + edit.addTextChangedListener(edit.mInputWrapper); + final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(edit, 0); } - } - }; + } break; + + case HANDLER_CLOSE_IME_KEYBOARD: { + GodotEditText edit = (GodotEditText)msg.obj; + + edit.removeTextChangedListener(mInputWrapper); + final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); + edit.mView.requestFocus(); + } break; + } } // =========================================================== diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java index 0a876d2b7f..4042c42e9d 100644 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java +++ b/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java @@ -17,7 +17,6 @@ package org.godotengine.godot.input; import android.content.Context; -import android.os.Build; import android.os.Handler; import android.view.InputDevice; import android.view.MotionEvent; @@ -130,11 +129,7 @@ public interface InputManagerCompat { * @return a compatible implementation of InputManager */ public static InputManagerCompat getInputManager(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return new InputManagerV16(context); - } else { - return new InputManagerV9(); - } + return new InputManagerV16(context); } } } diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java b/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java deleted file mode 100644 index a1418c5899..0000000000 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.util.Log; -import android.util.SparseArray; -import android.view.InputDevice; -import android.view.MotionEvent; - -import java.lang.ref.WeakReference; -import java.util.ArrayDeque; -import java.util.HashMap; -import java.util.Map; -import java.util.Queue; - -public class InputManagerV9 implements InputManagerCompat { - private static final String LOG_TAG = "InputManagerV9"; - private static final int MESSAGE_TEST_FOR_DISCONNECT = 101; - private static final long CHECK_ELAPSED_TIME = 3000L; - - private static final int ON_DEVICE_ADDED = 0; - private static final int ON_DEVICE_CHANGED = 1; - private static final int ON_DEVICE_REMOVED = 2; - - private final SparseArray<long[]> mDevices; - private final Map<InputDeviceListener, Handler> mListeners; - private final Handler mDefaultHandler; - - private static class PollingMessageHandler extends Handler { - private final WeakReference<InputManagerV9> mInputManager; - - PollingMessageHandler(InputManagerV9 im) { - mInputManager = new WeakReference<InputManagerV9>(im); - } - - @Override - public void handleMessage(Message msg) { - super.handleMessage(msg); - switch (msg.what) { - case MESSAGE_TEST_FOR_DISCONNECT: - InputManagerV9 imv = mInputManager.get(); - if (null != imv) { - long time = SystemClock.elapsedRealtime(); - int size = imv.mDevices.size(); - for (int i = 0; i < size; i++) { - long[] lastContact = imv.mDevices.valueAt(i); - if (null != lastContact) { - if (time - lastContact[0] > CHECK_ELAPSED_TIME) { - // check to see if the device has been - // disconnected - int id = imv.mDevices.keyAt(i); - if (null == InputDevice.getDevice(id)) { - // disconnected! - imv.notifyListeners(ON_DEVICE_REMOVED, id); - imv.mDevices.remove(id); - } else { - lastContact[0] = time; - } - } - } - } - sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, - CHECK_ELAPSED_TIME); - } - break; - } - } - } - - public InputManagerV9() { - mDevices = new SparseArray<long[]>(); - mListeners = new HashMap<InputDeviceListener, Handler>(); - mDefaultHandler = new PollingMessageHandler(this); - // as a side-effect, populates our collection of watched - // input devices - getInputDeviceIds(); - } - - @Override - public InputDevice getInputDevice(int id) { - return InputDevice.getDevice(id); - } - - @Override - public int[] getInputDeviceIds() { - // add any hitherto unknown devices to our - // collection of watched input devices - int[] activeDevices = InputDevice.getDeviceIds(); - long time = SystemClock.elapsedRealtime(); - for (int id : activeDevices) { - long[] lastContact = mDevices.get(id); - if (null == lastContact) { - // we have a new device - mDevices.put(id, new long[] { time }); - } - } - return activeDevices; - } - - @Override - public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { - mListeners.remove(listener); - if (handler == null) { - handler = mDefaultHandler; - } - mListeners.put(listener, handler); - } - - @Override - public void unregisterInputDeviceListener(InputDeviceListener listener) { - mListeners.remove(listener); - } - - private void notifyListeners(int why, int deviceId) { - // the state of some device has changed - if (!mListeners.isEmpty()) { - // yes... this will cause an object to get created... hopefully - // it won't happen very often - for (InputDeviceListener listener : mListeners.keySet()) { - Handler handler = mListeners.get(listener); - DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener); - handler.post(odc); - } - } - } - - private static class DeviceEvent implements Runnable { - private int mMessageType; - private int mId; - private InputDeviceListener mListener; - private static Queue<DeviceEvent> sEventQueue = new ArrayDeque<DeviceEvent>(); - - private DeviceEvent() { - } - - static DeviceEvent getDeviceEvent(int messageType, int id, - InputDeviceListener listener) { - DeviceEvent curChanged = sEventQueue.poll(); - if (null == curChanged) { - curChanged = new DeviceEvent(); - } - curChanged.mMessageType = messageType; - curChanged.mId = id; - curChanged.mListener = listener; - return curChanged; - } - - @Override - public void run() { - switch (mMessageType) { - case ON_DEVICE_ADDED: - mListener.onInputDeviceAdded(mId); - break; - case ON_DEVICE_CHANGED: - mListener.onInputDeviceChanged(mId); - break; - case ON_DEVICE_REMOVED: - mListener.onInputDeviceRemoved(mId); - break; - default: - Log.e(LOG_TAG, "Unknown Message Type"); - break; - } - // dump this runnable back in the queue - sEventQueue.offer(this); - } - } - - @Override - public void onGenericMotionEvent(MotionEvent event) { - // detect new devices - int id = event.getDeviceId(); - long[] timeArray = mDevices.get(id); - if (null == timeArray) { - notifyListeners(ON_DEVICE_ADDED, id); - timeArray = new long[1]; - mDevices.put(id, timeArray); - } - long time = SystemClock.elapsedRealtime(); - timeArray[0] = time; - } - - @Override - public void onPause() { - mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); - } - - @Override - public void onResume() { - mDefaultHandler.sendEmptyMessage(MESSAGE_TEST_FOR_DISCONNECT); - } -} diff --git a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java b/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java index 5d94e77cd7..fe67f42f19 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java +++ b/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java @@ -37,59 +37,81 @@ import android.os.AsyncTask; import android.os.RemoteException; import android.util.Log; +import java.lang.ref.WeakReference; + abstract public class ConsumeTask { private Context context; - private IInAppBillingService mService; + + private String mSku; + private String mToken; + + private static class ConsumeAsyncTask extends AsyncTask<String, String, String> { + + private WeakReference<ConsumeTask> mTask; + + ConsumeAsyncTask(ConsumeTask consume) { + mTask = new WeakReference<>(consume); + } + + @Override + protected String doInBackground(String... strings) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(strings); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + consume.onPostExecute(param); + } + } + } + public ConsumeTask(IInAppBillingService mService, Context context) { this.context = context; this.mService = mService; } public void consume(final String sku) { - //Log.d("XXX", "Consuming product " + sku); + mSku = sku; PaymentsCache pc = new PaymentsCache(context); Boolean isBlocked = pc.getConsumableFlag("block", sku); - String _token = pc.getConsumableValue("token", sku); - //Log.d("XXX", "token " + _token); - if (!isBlocked && _token == null) { - //_token = "inapp:"+context.getPackageName()+":android.test.purchased"; - //Log.d("XXX", "Consuming product " + sku + " with token " + _token); + mToken = pc.getConsumableValue("token", sku); + if (!isBlocked && mToken == null) { + // Consuming task is processing } else if (!isBlocked) { - //Log.d("XXX", "It is not blocked ¿?"); return; - } else if (_token == null) { - //Log.d("XXX", "No token available"); + } else if (mToken == null) { this.error("No token for sku:" + sku); return; } - final String token = _token; - new AsyncTask<String, String, String>() { - @Override - protected String doInBackground(String... params) { - try { - //Log.d("XXX", "Requesting to release item."); - int response = mService.consumePurchase(3, context.getPackageName(), token); - //Log.d("XXX", "release response code: " + response); - if (response == 0 || response == 8) { - return null; - } - } catch (RemoteException e) { - return e.getMessage(); - } - return "Some error"; - } + new ConsumeAsyncTask(this).execute(); + } - protected void onPostExecute(String param) { - if (param == null) { - success(new PaymentsCache(context).getConsumableValue("ticket", sku)); - } else { - error(param); - } + private String doInBackground(String... params) { + try { + int response = mService.consumePurchase(3, context.getPackageName(), mToken); + if (response == 0 || response == 8) { + return null; } + } catch (RemoteException e) { + return e.getMessage(); + } + return "Some error"; + } + + private void onPostExecute(String param) { + if (param == null) { + success(new PaymentsCache(context).getConsumableValue("ticket", mSku)); + } else { + error(param); } - .execute(); } abstract protected void success(String ticket); diff --git a/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java b/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java deleted file mode 100644 index 8b48193ae2..0000000000 --- a/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java +++ /dev/null @@ -1,79 +0,0 @@ -/*************************************************************************/ -/* GenericConsumeTask.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -package org.godotengine.godot.payments; - -import com.android.vending.billing.IInAppBillingService; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.RemoteException; -import android.util.Log; - -abstract public class GenericConsumeTask extends AsyncTask<String, String, String> { - - private Context context; - private IInAppBillingService mService; - - public GenericConsumeTask(Context context, IInAppBillingService mService, String sku, String receipt, String signature, String token) { - this.context = context; - this.mService = mService; - this.sku = sku; - this.receipt = receipt; - this.signature = signature; - this.token = token; - } - - private String sku; - private String receipt; - private String signature; - private String token; - - @Override - protected String doInBackground(String... params) { - try { - //Log.d("godot", "Requesting to consume an item with token ." + token); - int response = mService.consumePurchase(3, context.getPackageName(), token); - //Log.d("godot", "consumePurchase response: " + response); - if (response == 0 || response == 8) { - return null; - } - } catch (Exception e) { - Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); - } - return null; - } - - protected void onPostExecute(String sarasa) { - onSuccess(sku, receipt, signature, token); - } - - abstract public void onSuccess(String sku, String receipt, String signature, String token); -} diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java index 40cdeea72e..715f6e6f93 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java +++ b/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java @@ -46,7 +46,7 @@ public class PaymentsCache { SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPref.edit(); editor.putBoolean(sku, flag); - editor.commit(); + editor.apply(); } public boolean getConsumableFlag(String set, String sku) { @@ -60,7 +60,7 @@ public class PaymentsCache { SharedPreferences.Editor editor = sharedPref.edit(); editor.putString(sku, value); //Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku); - editor.commit(); + editor.apply(); } public String getConsumableValue(String set, String sku) { diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java index d4c7380424..b3b27ec1b9 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java @@ -112,7 +112,7 @@ public class PaymentsManager { }; public void requestPurchase(final String sku, String transactionId) { - new PurchaseTask(mService, Godot.getInstance()) { + new PurchaseTask(mService, activity) { @Override protected void error(String message) { godotPaymentV3.callbackFail(message); @@ -159,7 +159,7 @@ public class PaymentsManager { public void requestPurchased() { try { - PaymentsCache pc = new PaymentsCache(Godot.getInstance()); + PaymentsCache pc = new PaymentsCache(activity); String continueToken = null; diff --git a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java b/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java index eccc6f671b..fd6fff3fa9 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java +++ b/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java @@ -30,26 +30,59 @@ package org.godotengine.godot.payments; -import java.util.ArrayList; - -import org.json.JSONException; -import org.json.JSONObject; - -import org.godotengine.godot.Dictionary; -import org.godotengine.godot.Godot; -import com.android.vending.billing.IInAppBillingService; - import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import android.os.RemoteException; import android.util.Log; +import com.android.vending.billing.IInAppBillingService; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + abstract public class ReleaseAllConsumablesTask { private Context context; private IInAppBillingService mService; + private static class ReleaseAllConsumablesAsyncTask extends AsyncTask<String, String, String> { + + private WeakReference<ReleaseAllConsumablesTask> mTask; + private String mSku; + private String mReceipt; + private String mSignature; + private String mToken; + + ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) { + mTask = new WeakReference<ReleaseAllConsumablesTask>(task); + + mSku = sku; + mReceipt = receipt; + mSignature = signature; + mToken = token; + } + + @Override + protected String doInBackground(String... params) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(mToken); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + consume.success(mSku, mReceipt, mSignature, mToken); + } + } + } + public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) { this.context = context; this.mService = mService; @@ -60,12 +93,6 @@ abstract public class ReleaseAllConsumablesTask { //Log.d("godot", "consumeItall for " + context.getPackageName()); Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null); - for (String key : bundle.keySet()) { - Object value = bundle.get(key); - //Log.d("godot", String.format("%s %s (%s)", key, - //value.toString(), value.getClass().getName())); - } - if (bundle.getInt("RESPONSE_CODE") == 0) { final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); @@ -87,14 +114,7 @@ abstract public class ReleaseAllConsumablesTask { String token = inappPurchaseData.getString("purchaseToken"); String signature = mySignatures.get(i); //Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt); - new GenericConsumeTask(context, mService, sku, receipt, signature, token) { - @Override - public void onSuccess(String sku, String receipt, String signature, String token) { - ReleaseAllConsumablesTask.this.success(sku, receipt, signature, token); - } - } - .execute(); - + new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute(); } catch (JSONException e) { } } @@ -104,6 +124,20 @@ abstract public class ReleaseAllConsumablesTask { } } + private String doInBackground(String token) { + try { + //Log.d("godot", "Requesting to consume an item with token ." + token); + int response = mService.consumePurchase(3, context.getPackageName(), token); + //Log.d("godot", "consumePurchase response: " + response); + if (response == 0 || response == 8) { + return null; + } + } catch (Exception e) { + Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); + } + return null; + } + abstract protected void success(String sku, String receipt, String signature, String token); abstract protected void error(String message); abstract protected void notRequired(); diff --git a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java index 0626e50bb1..2d1c467235 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java +++ b/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java @@ -52,69 +52,104 @@ import android.os.Bundle; import android.os.RemoteException; import android.util.Log; +import java.lang.ref.WeakReference; + abstract public class ValidateTask { private Activity context; private GodotPaymentV3 godotPaymentsV3; + private ProgressDialog dialog; + private String mSku; + + private static class ValidateAsyncTask extends AsyncTask<String, String, String> { + private WeakReference<ValidateTask> mTask; + + ValidateAsyncTask(ValidateTask task) { + mTask = new WeakReference<>(task); + } + + @Override + protected void onPreExecute() { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPreExecute(); + } + } + + @Override + protected String doInBackground(String... params) { + ValidateTask task = mTask.get(); + if (task != null) { + return task.doInBackground(params); + } + return null; + } + + @Override + protected void onPostExecute(String response) { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPostExecute(response); + } + } + } + public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) { this.context = context; this.godotPaymentsV3 = godotPaymentsV3; } public void validatePurchase(final String sku) { - new AsyncTask<String, String, String>() { - private ProgressDialog dialog; + mSku = sku; + new ValidateAsyncTask(this).execute(); + } - @Override - protected void onPreExecute() { - dialog = ProgressDialog.show(context, null, "Please wait..."); - } + private void onPreExecute() { + dialog = ProgressDialog.show(context, null, "Please wait..."); + } - @Override - protected String doInBackground(String... params) { - PaymentsCache pc = new PaymentsCache(context); - String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); - RequestParams param = new RequestParams(); - param.setUrl(url); - param.put("ticket", pc.getConsumableValue("ticket", sku)); - param.put("purchaseToken", pc.getConsumableValue("token", sku)); - param.put("sku", sku); - //Log.d("XXX", "Haciendo request a " + url); - //Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); - //Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); - //Log.d("XXX", "sku: " + sku); - param.put("package", context.getApplicationContext().getPackageName()); - HttpRequester requester = new HttpRequester(); - String jsonResponse = requester.post(param); - //Log.d("XXX", "Validation response:\n"+jsonResponse); - return jsonResponse; - } + private String doInBackground(String... params) { + PaymentsCache pc = new PaymentsCache(context); + String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); + RequestParams param = new RequestParams(); + param.setUrl(url); + param.put("ticket", pc.getConsumableValue("ticket", mSku)); + param.put("purchaseToken", pc.getConsumableValue("token", mSku)); + param.put("sku", mSku); + //Log.d("XXX", "Haciendo request a " + url); + //Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); + //Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); + //Log.d("XXX", "sku: " + sku); + param.put("package", context.getApplicationContext().getPackageName()); + HttpRequester requester = new HttpRequester(); + String jsonResponse = requester.post(param); + //Log.d("XXX", "Validation response:\n"+jsonResponse); + return jsonResponse; + } - @Override - protected void onPostExecute(String response) { - if (dialog != null) { - dialog.dismiss(); - } - JSONObject j; - try { - j = new JSONObject(response); - if (j.getString("status").equals("OK")) { - success(); - return; - } else if (j.getString("status") != null) { - error(j.getString("message")); - } else { - error("Connection error"); - } - } catch (JSONException e) { - error(e.getMessage()); - } catch (Exception e) { - error(e.getMessage()); - } + private void onPostExecute(String response) { + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + JSONObject j; + try { + j = new JSONObject(response); + if (j.getString("status").equals("OK")) { + success(); + return; + } else if (j.getString("status") != null) { + error(j.getString("message")); + } else { + error("Connection error"); } + } catch (JSONException e) { + error(e.getMessage()); + } catch (Exception e) { + error(e.getMessage()); } - .execute(); } + abstract protected void success(); abstract protected void error(String message); abstract protected void canceled(); diff --git a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java b/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java index cfe9c4fef0..e1958390a5 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java +++ b/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java @@ -105,7 +105,7 @@ public class HttpRequester { long timeInit = new Date().getTime(); response = request(httpget); long delay = new Date().getTime() - timeInit; - Log.d("com.app11tt.android.utils.HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds"); + Log.d("HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds"); if (response == null || response.length() == 0) { response = ""; } else { @@ -200,7 +200,7 @@ public class HttpRequester { SharedPreferences.Editor editor = sharedPref.edit(); editor.putString("request_" + Crypt.md5(request), response); editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl()); - editor.commit(); + editor.apply(); } public String getResponseFromCache(String request) { diff --git a/platform/iphone/gl_view.mm b/platform/iphone/gl_view.mm index 054c066224..92fba7224f 100644 --- a/platform/iphone/gl_view.mm +++ b/platform/iphone/gl_view.mm @@ -583,7 +583,7 @@ static void clear_touches() { character.parse_utf8([p_text UTF8String]); keyboard_text = keyboard_text + character; OSIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true); - printf("inserting text with character %i\n", character[0]); + printf("inserting text with character %lc\n", (CharType)character[0]); }; - (void)audioRouteChangeListenerCallback:(NSNotification *)notification { diff --git a/platform/server/os_server.cpp b/platform/server/os_server.cpp index 60f20d6009..5d67e2113f 100644 --- a/platform/server/os_server.cpp +++ b/platform/server/os_server.cpp @@ -99,7 +99,7 @@ Error OS_Server::initialize(const VideoMode &p_desired, int p_video_driver, int _ensure_user_data_dir(); - resource_loader_dummy = memnew(ResourceFormatDummyTexture); + resource_loader_dummy.instance(); ResourceLoader::add_resource_format_loader(resource_loader_dummy); return OK; @@ -118,7 +118,8 @@ void OS_Server::finalize() { memdelete(power_manager); - memdelete(resource_loader_dummy); + ResourceLoader::remove_resource_format_loader(resource_loader_dummy); + resource_loader_dummy.unref(); args.clear(); } diff --git a/platform/server/os_server.h b/platform/server/os_server.h index 0367ec3db9..b5dc7900e6 100644 --- a/platform/server/os_server.h +++ b/platform/server/os_server.h @@ -77,7 +77,7 @@ class OS_Server : public OS_Unix { int video_driver_index; - ResourceFormatDummyTexture *resource_loader_dummy; + Ref<ResourceFormatDummyTexture> resource_loader_dummy; protected: virtual int get_video_driver_count() const; diff --git a/platform/x11/os_x11.cpp b/platform/x11/os_x11.cpp index 7b30e7a064..34ec8709c4 100644 --- a/platform/x11/os_x11.cpp +++ b/platform/x11/os_x11.cpp @@ -2080,14 +2080,14 @@ void OS_X11::process_xevents() { mb.instance(); get_key_modifier_state(event.xbutton.state, mb); + mb->set_button_index(event.xbutton.button); if (mb->get_button_index() == 2) mb->set_button_index(3); else if (mb->get_button_index() == 3) mb->set_button_index(2); - mb->set_button_mask(get_mouse_button_state(event.xbutton.button, event.xbutton.type)); + mb->set_button_mask(get_mouse_button_state(mb->get_button_index(), event.xbutton.type)); mb->set_position(Vector2(event.xbutton.x, event.xbutton.y)); mb->set_global_position(mb->get_position()); - mb->set_button_index(event.xbutton.button); mb->set_pressed((event.type == ButtonPress)); diff --git a/scene/2d/tile_map.cpp b/scene/2d/tile_map.cpp index 641cb161ca..ee76a90019 100644 --- a/scene/2d/tile_map.cpp +++ b/scene/2d/tile_map.cpp @@ -327,7 +327,8 @@ void TileMap::update_dirty_quadrants() { Ref<ShaderMaterial> mat = tile_set->tile_get_material(c.id); int z_index = tile_set->tile_get_z_index(c.id); - if (tile_set->tile_get_tile_mode(c.id) == TileSet::AUTO_TILE) { + if (tile_set->tile_get_tile_mode(c.id) == TileSet::AUTO_TILE || + tile_set->tile_get_tile_mode(c.id) == TileSet::ATLAS_TILE) { z_index += tile_set->autotile_get_z_index(c.id, Vector2(c.autotile_coord_x, c.autotile_coord_y)); } diff --git a/scene/animation/animation_node_state_machine.cpp b/scene/animation/animation_node_state_machine.cpp index a7af3f687b..f67ef8164a 100644 --- a/scene/animation/animation_node_state_machine.cpp +++ b/scene/animation/animation_node_state_machine.cpp @@ -480,10 +480,9 @@ float AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *sm, } //compute time left for transitions by using the end node - if (sm->end_node != StringName() && sm->end_node != current) { - rem = sm->blend_node(current, sm->states[sm->end_node].node, 0, true, 0, AnimationNode::FILTER_IGNORE, false); + rem = sm->blend_node(sm->end_node, sm->states[sm->end_node].node, 0, true, 0, AnimationNode::FILTER_IGNORE, false); } return rem; diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index a7f88514e0..14e8c34d5f 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -244,7 +244,7 @@ void Label::_notification(int p_what) { CharType n = xl_text[i + pos + 1]; if (uppercase) { c = String::char_uppercase(c); - n = String::char_uppercase(c); + n = String::char_uppercase(n); } float move = font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_color_shadow, false); @@ -265,7 +265,7 @@ void Label::_notification(int p_what) { CharType n = xl_text[i + pos + 1]; if (uppercase) { c = String::char_uppercase(c); - n = String::char_uppercase(c); + n = String::char_uppercase(n); } x_ofs += drawer.draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_color); @@ -395,7 +395,7 @@ void Label::regenerate_word_cache() { for (int i = 0; i <= xl_text.length(); i++) { - CharType current = i < xl_text.length() ? xl_text[i] : ' '; //always a space at the end, so the algo works + CharType current = i < xl_text.length() ? xl_text[i] : L' '; //always a space at the end, so the algo works if (uppercase) current = String::char_uppercase(current); diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index f621522102..82d6e6480e 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -453,7 +453,6 @@ void PopupMenu::_notification(int p_what) { Color icon_color(1, 1, 1, items[i].disabled ? 0.5 : 1); - item_ofs.x += items[i].h_ofs; if (!items[i].icon.is_null()) { icon_size = items[i].icon->get_size(); @@ -470,6 +469,7 @@ void PopupMenu::_notification(int p_what) { String text = items[i].shortcut.is_valid() ? String(tr(items[i].shortcut->get_name())) : items[i].xl_text; + item_ofs.x += items[i].h_ofs; if (items[i].separator) { int sep_h = separator->get_center_size().height + separator->get_minimum_size().height; diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index d7750c91ef..ae6211a3df 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -210,18 +210,18 @@ #include "scene/resources/physics_material.h" #endif -static ResourceFormatSaverText *resource_saver_text = NULL; -static ResourceFormatLoaderText *resource_loader_text = NULL; +static Ref<ResourceFormatSaverText> resource_saver_text; +static Ref<ResourceFormatLoaderText> resource_loader_text; -static ResourceFormatLoaderDynamicFont *resource_loader_dynamic_font = NULL; +static Ref<ResourceFormatLoaderDynamicFont> resource_loader_dynamic_font; -static ResourceFormatLoaderStreamTexture *resource_loader_stream_texture = NULL; -static ResourceFormatLoaderTextureLayered *resource_loader_texture_layered = NULL; +static Ref<ResourceFormatLoaderStreamTexture> resource_loader_stream_texture; +static Ref<ResourceFormatLoaderTextureLayered> resource_loader_texture_layered; -static ResourceFormatLoaderBMFont *resource_loader_bmfont = NULL; +static Ref<ResourceFormatLoaderBMFont> resource_loader_bmfont; -static ResourceFormatSaverShader *resource_saver_shader = NULL; -static ResourceFormatLoaderShader *resource_loader_shader = NULL; +static Ref<ResourceFormatSaverShader> resource_saver_shader; +static Ref<ResourceFormatLoaderShader> resource_loader_shader; void register_scene_types() { @@ -231,28 +231,28 @@ void register_scene_types() { Node::init_node_hrcr(); - resource_loader_dynamic_font = memnew(ResourceFormatLoaderDynamicFont); + resource_loader_dynamic_font.instance(); ResourceLoader::add_resource_format_loader(resource_loader_dynamic_font); - resource_loader_stream_texture = memnew(ResourceFormatLoaderStreamTexture); + resource_loader_stream_texture.instance(); ResourceLoader::add_resource_format_loader(resource_loader_stream_texture); - resource_loader_texture_layered = memnew(ResourceFormatLoaderTextureLayered); + resource_loader_texture_layered.instance(); ResourceLoader::add_resource_format_loader(resource_loader_texture_layered); - resource_saver_text = memnew(ResourceFormatSaverText); + resource_saver_text.instance(); ResourceSaver::add_resource_format_saver(resource_saver_text, true); - resource_loader_text = memnew(ResourceFormatLoaderText); + resource_loader_text.instance(); ResourceLoader::add_resource_format_loader(resource_loader_text, true); - resource_saver_shader = memnew(ResourceFormatSaverShader); + resource_saver_shader.instance(); ResourceSaver::add_resource_format_saver(resource_saver_shader, true); - resource_loader_shader = memnew(ResourceFormatLoaderShader); + resource_loader_shader.instance(); ResourceLoader::add_resource_format_loader(resource_loader_shader, true); - resource_loader_bmfont = memnew(ResourceFormatLoaderBMFont); + resource_loader_bmfont.instance(); ResourceLoader::add_resource_format_loader(resource_loader_bmfont, true); OS::get_singleton()->yield(); //may take time to init @@ -735,27 +735,37 @@ void unregister_scene_types() { clear_default_theme(); - memdelete(resource_loader_dynamic_font); - memdelete(resource_loader_stream_texture); - memdelete(resource_loader_texture_layered); + ResourceLoader::remove_resource_format_loader(resource_loader_dynamic_font); + resource_loader_dynamic_font.unref(); + + ResourceLoader::remove_resource_format_loader(resource_loader_texture_layered); + resource_loader_texture_layered.unref(); + + ResourceLoader::remove_resource_format_loader(resource_loader_stream_texture); + resource_loader_stream_texture.unref(); DynamicFont::finish_dynamic_fonts(); - if (resource_saver_text) { - memdelete(resource_saver_text); + if (resource_saver_text.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_text); + resource_saver_text.unref(); } - if (resource_loader_text) { - memdelete(resource_loader_text); + if (resource_loader_text.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_text); + resource_loader_text.unref(); } - if (resource_saver_shader) { - memdelete(resource_saver_shader); + if (resource_saver_shader.is_valid()) { + ResourceSaver::remove_resource_format_saver(resource_saver_shader); + resource_saver_shader.unref(); } - if (resource_loader_shader) { - memdelete(resource_loader_shader); + if (resource_loader_shader.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_shader); + resource_loader_shader.unref(); } - if (resource_loader_bmfont) { - memdelete(resource_loader_bmfont); + if (resource_loader_bmfont.is_valid()) { + ResourceLoader::remove_resource_format_loader(resource_loader_bmfont); + resource_loader_bmfont.unref(); } SpatialMaterial::finish_shaders(); diff --git a/scene/resources/bit_mask.cpp b/scene/resources/bit_mask.cpp index 56b236d03b..587ab73dce 100644 --- a/scene/resources/bit_mask.cpp +++ b/scene/resources/bit_mask.cpp @@ -432,8 +432,8 @@ static void fill_bits(const BitMap *p_src, Ref<BitMap> &p_map, const Point2i &p_ int stack_size = 0; Point2i pos = p_pos; - int next_i; - int next_j; + int next_i = 0; + int next_j = 0; bool reenter = true; bool popped = false; diff --git a/scene/resources/default_theme/default_theme.cpp b/scene/resources/default_theme/default_theme.cpp index b0c1dcde9a..7d7bf8a750 100644 --- a/scene/resources/default_theme/default_theme.cpp +++ b/scene/resources/default_theme/default_theme.cpp @@ -600,6 +600,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const theme->set_icon("resizer", "GraphNode", make_icon(window_resizer_png)); theme->set_font("title_font", "GraphNode", default_font); theme->set_color("title_color", "GraphNode", Color(0, 0, 0, 1)); + theme->set_color("close_color", "GraphNode", Color(0, 0, 0, 1)); theme->set_constant("title_offset", "GraphNode", 20 * scale); theme->set_constant("close_offset", "GraphNode", 18 * scale); theme->set_constant("port_offset", "GraphNode", 3 * scale); diff --git a/scene/resources/dynamic_font.h b/scene/resources/dynamic_font.h index 96437e8982..9c41c948bd 100644 --- a/scene/resources/dynamic_font.h +++ b/scene/resources/dynamic_font.h @@ -303,6 +303,7 @@ VARIANT_ENUM_CAST(DynamicFont::SpacingType); ///////////// class ResourceFormatLoaderDynamicFont : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderDynamicFont, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/scene/resources/dynamic_font_stb.h b/scene/resources/dynamic_font_stb.h index e1ef72ea4f..c83cc1b01f 100644 --- a/scene/resources/dynamic_font_stb.h +++ b/scene/resources/dynamic_font_stb.h @@ -180,6 +180,7 @@ public: ///////////// class ResourceFormatLoaderDynamicFont : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderDynamicFont, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index 90552ebb47..9754ba736e 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -61,6 +61,25 @@ void Environment::set_sky_custom_fov(float p_scale) { bg_sky_custom_fov = p_scale; VS::get_singleton()->environment_set_sky_custom_fov(environment, p_scale); } +void Environment::set_sky_orientation(const Basis &p_orientation) { + + bg_sky_orientation = p_orientation; + _change_notify("background_sky_rotation"); + _change_notify("background_sky_rotation_degrees"); + VS::get_singleton()->environment_set_sky_orientation(environment, bg_sky_orientation); +} +void Environment::set_sky_rotation(const Vector3 &p_euler_rad) { + + bg_sky_orientation.set_euler(p_euler_rad); + _change_notify("background_sky_orientation"); + _change_notify("background_sky_rotation_degrees"); + VS::get_singleton()->environment_set_sky_orientation(environment, bg_sky_orientation); +} +void Environment::set_sky_rotation_degrees(const Vector3 &p_euler_deg) { + + set_sky_rotation(p_euler_deg * Math_PI / 180.0); + _change_notify("background_sky_rotation"); +} void Environment::set_bg_color(const Color &p_color) { bg_color = p_color; @@ -106,6 +125,22 @@ float Environment::get_sky_custom_fov() const { return bg_sky_custom_fov; } +Basis Environment::get_sky_orientation() const { + + return bg_sky_orientation; +} + +Vector3 Environment::get_sky_rotation() const { + + // should we cache this? maybe overkill + return bg_sky_orientation.get_euler(); +} + +Vector3 Environment::get_sky_rotation_degrees() const { + + return get_sky_rotation() * 180.0 / Math_PI; +} + Color Environment::get_bg_color() const { return bg_color; @@ -268,7 +303,7 @@ Ref<Texture> Environment::get_adjustment_color_correction() const { void Environment::_validate_property(PropertyInfo &property) const { - if (property.name == "background_sky" || property.name == "background_sky_custom_fov" || property.name == "ambient_light/sky_contribution") { + if (property.name == "background_sky" || property.name == "background_sky_custom_fov" || property.name == "background_sky_orientation" || property.name == "ambient_light/sky_contribution") { if (bg_mode != BG_SKY && bg_mode != BG_COLOR_SKY) { property.usage = PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL; } @@ -875,6 +910,9 @@ void Environment::_bind_methods() { ClassDB::bind_method(D_METHOD("set_background", "mode"), &Environment::set_background); ClassDB::bind_method(D_METHOD("set_sky", "sky"), &Environment::set_sky); ClassDB::bind_method(D_METHOD("set_sky_custom_fov", "scale"), &Environment::set_sky_custom_fov); + ClassDB::bind_method(D_METHOD("set_sky_orientation", "orientation"), &Environment::set_sky_orientation); + ClassDB::bind_method(D_METHOD("set_sky_rotation", "euler_radians"), &Environment::set_sky_rotation); + ClassDB::bind_method(D_METHOD("set_sky_rotation_degrees", "euler_degrees"), &Environment::set_sky_rotation_degrees); ClassDB::bind_method(D_METHOD("set_bg_color", "color"), &Environment::set_bg_color); ClassDB::bind_method(D_METHOD("set_bg_energy", "energy"), &Environment::set_bg_energy); ClassDB::bind_method(D_METHOD("set_canvas_max_layer", "layer"), &Environment::set_canvas_max_layer); @@ -885,6 +923,9 @@ void Environment::_bind_methods() { ClassDB::bind_method(D_METHOD("get_background"), &Environment::get_background); ClassDB::bind_method(D_METHOD("get_sky"), &Environment::get_sky); ClassDB::bind_method(D_METHOD("get_sky_custom_fov"), &Environment::get_sky_custom_fov); + ClassDB::bind_method(D_METHOD("get_sky_orientation"), &Environment::get_sky_orientation); + ClassDB::bind_method(D_METHOD("get_sky_rotation"), &Environment::get_sky_rotation); + ClassDB::bind_method(D_METHOD("get_sky_rotation_degrees"), &Environment::get_sky_rotation_degrees); ClassDB::bind_method(D_METHOD("get_bg_color"), &Environment::get_bg_color); ClassDB::bind_method(D_METHOD("get_bg_energy"), &Environment::get_bg_energy); ClassDB::bind_method(D_METHOD("get_canvas_max_layer"), &Environment::get_canvas_max_layer); @@ -896,6 +937,9 @@ void Environment::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "background_mode", PROPERTY_HINT_ENUM, "Clear Color,Custom Color,Sky,Color+Sky,Canvas,Keep"), "set_background", "get_background"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "background_sky", PROPERTY_HINT_RESOURCE_TYPE, "Sky"), "set_sky", "get_sky"); ADD_PROPERTY(PropertyInfo(Variant::REAL, "background_sky_custom_fov", PROPERTY_HINT_RANGE, "0,180,0.1"), "set_sky_custom_fov", "get_sky_custom_fov"); + ADD_PROPERTY(PropertyInfo(Variant::BASIS, "background_sky_orientation"), "set_sky_orientation", "get_sky_orientation"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "background_sky_rotation", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_sky_rotation", "get_sky_rotation"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "background_sky_rotation_degrees", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_sky_rotation_degrees", "get_sky_rotation_degrees"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "background_color"), "set_bg_color", "get_bg_color"); ADD_PROPERTY(PropertyInfo(Variant::REAL, "background_energy", PROPERTY_HINT_RANGE, "0,16,0.01"), "set_bg_energy", "get_bg_energy"); ADD_PROPERTY(PropertyInfo(Variant::INT, "background_canvas_max_layer", PROPERTY_HINT_RANGE, "-1000,1000,1"), "set_canvas_max_layer", "get_canvas_max_layer"); @@ -1226,6 +1270,7 @@ Environment::Environment() { bg_mode = BG_CLEAR_COLOR; bg_sky_custom_fov = 0; + bg_sky_orientation = Basis(); bg_energy = 1.0; bg_canvas_max_layer = 0; ambient_energy = 1.0; diff --git a/scene/resources/environment.h b/scene/resources/environment.h index 55d96bc5bd..8d6436487b 100644 --- a/scene/resources/environment.h +++ b/scene/resources/environment.h @@ -91,6 +91,7 @@ private: BGMode bg_mode; Ref<Sky> bg_sky; float bg_sky_custom_fov; + Basis bg_sky_orientation; Color bg_color; float bg_energy; int bg_canvas_max_layer; @@ -182,6 +183,9 @@ public: void set_background(BGMode p_bg); void set_sky(const Ref<Sky> &p_sky); void set_sky_custom_fov(float p_scale); + void set_sky_orientation(const Basis &p_orientation); + void set_sky_rotation(const Vector3 &p_euler_rad); + void set_sky_rotation_degrees(const Vector3 &p_euler_deg); void set_bg_color(const Color &p_color); void set_bg_energy(float p_energy); void set_canvas_max_layer(int p_max_layer); @@ -192,6 +196,9 @@ public: BGMode get_background() const; Ref<Sky> get_sky() const; float get_sky_custom_fov() const; + Basis get_sky_orientation() const; + Vector3 get_sky_rotation() const; + Vector3 get_sky_rotation_degrees() const; Color get_bg_color() const; float get_bg_energy() const; int get_canvas_max_layer() const; diff --git a/scene/resources/font.h b/scene/resources/font.h index 39e66a822d..3c5e650758 100644 --- a/scene/resources/font.h +++ b/scene/resources/font.h @@ -201,6 +201,7 @@ public: }; class ResourceFormatLoaderBMFont : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderBMFont, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; diff --git a/scene/resources/scene_format_text.h b/scene/resources/scene_format_text.h index 8d1af2bbb2..e5ee6ae760 100644 --- a/scene/resources/scene_format_text.h +++ b/scene/resources/scene_format_text.h @@ -128,7 +128,7 @@ public: }; class ResourceFormatLoaderText : public ResourceFormatLoader { - + GDCLASS(ResourceFormatLoaderText, ResourceFormatLoader) public: static ResourceFormatLoaderText *singleton; virtual Ref<ResourceInteractiveLoader> load_interactive(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); @@ -170,6 +170,7 @@ public: }; class ResourceFormatSaverText : public ResourceFormatSaver { + GDCLASS(ResourceFormatSaverText, ResourceFormatSaver) public: static ResourceFormatSaverText *singleton; virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); diff --git a/scene/resources/shader.h b/scene/resources/shader.h index c2c205237f..6804832fb9 100644 --- a/scene/resources/shader.h +++ b/scene/resources/shader.h @@ -100,6 +100,7 @@ public: VARIANT_ENUM_CAST(Shader::Mode); class ResourceFormatLoaderShader : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderShader, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -108,6 +109,7 @@ public: }; class ResourceFormatSaverShader : public ResourceFormatSaver { + GDCLASS(ResourceFormatSaverShader, ResourceFormatSaver) public: virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0); virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const; diff --git a/scene/resources/texture.h b/scene/resources/texture.h index e9b69e9cb1..845a075015 100644 --- a/scene/resources/texture.h +++ b/scene/resources/texture.h @@ -236,6 +236,7 @@ public: }; class ResourceFormatLoaderStreamTexture : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderStreamTexture, ResourceFormatLoader) public: virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL); virtual void get_recognized_extensions(List<String> *p_extensions) const; @@ -489,6 +490,7 @@ public: }; class ResourceFormatLoaderTextureLayered : public ResourceFormatLoader { + GDCLASS(ResourceFormatLoaderTextureLayered, ResourceFormatLoader) public: enum Compression { COMPRESSION_LOSSLESS, diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp index c2c2c0ff32..8a24d4bab1 100644 --- a/scene/resources/tile_set.cpp +++ b/scene/resources/tile_set.cpp @@ -314,6 +314,7 @@ void TileSet::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::INT, pre + "autotile/spacing", PROPERTY_HINT_RANGE, "0,256,1", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/occluder_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/navpoly_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::ARRAY, pre + "autotile/z_index_map", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL)); } p_list->push_back(PropertyInfo(Variant::VECTOR2, pre + "occluder_offset")); p_list->push_back(PropertyInfo(Variant::OBJECT, pre + "occluder", PROPERTY_HINT_RESOURCE_TYPE, "OccluderPolygon2D")); diff --git a/servers/visual/rasterizer.h b/servers/visual/rasterizer.h index f78b4aaf5f..2c0feffebb 100644 --- a/servers/visual/rasterizer.h +++ b/servers/visual/rasterizer.h @@ -55,6 +55,7 @@ public: virtual void environment_set_background(RID p_env, VS::EnvironmentBG p_bg) = 0; virtual void environment_set_sky(RID p_env, RID p_sky) = 0; virtual void environment_set_sky_custom_fov(RID p_env, float p_scale) = 0; + virtual void environment_set_sky_orientation(RID p_env, const Basis &p_orientation) = 0; virtual void environment_set_bg_color(RID p_env, const Color &p_color) = 0; virtual void environment_set_bg_energy(RID p_env, float p_energy) = 0; virtual void environment_set_canvas_max_layer(RID p_env, int p_max_layer) = 0; diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index f3a442be99..2d5e974657 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -495,6 +495,7 @@ public: BIND2(environment_set_background, RID, EnvironmentBG) BIND2(environment_set_sky, RID, RID) BIND2(environment_set_sky_custom_fov, RID, float) + BIND2(environment_set_sky_orientation, RID, const Basis &) BIND2(environment_set_bg_color, RID, const Color &) BIND2(environment_set_bg_energy, RID, float) BIND2(environment_set_canvas_max_layer, RID, int) diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 37f6323b8f..87542b03d9 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -421,6 +421,7 @@ public: FUNC2(environment_set_background, RID, EnvironmentBG) FUNC2(environment_set_sky, RID, RID) FUNC2(environment_set_sky_custom_fov, RID, float) + FUNC2(environment_set_sky_orientation, RID, const Basis &) FUNC2(environment_set_bg_color, RID, const Color &) FUNC2(environment_set_bg_energy, RID, float) FUNC2(environment_set_canvas_max_layer, RID, int) diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp index 34cc1cbd66..a92e4efcd6 100644 --- a/servers/visual_server.cpp +++ b/servers/visual_server.cpp @@ -1903,6 +1903,7 @@ void VisualServer::_bind_methods() { ClassDB::bind_method(D_METHOD("environment_set_background", "env", "bg"), &VisualServer::environment_set_background); ClassDB::bind_method(D_METHOD("environment_set_sky", "env", "sky"), &VisualServer::environment_set_sky); ClassDB::bind_method(D_METHOD("environment_set_sky_custom_fov", "env", "scale"), &VisualServer::environment_set_sky_custom_fov); + ClassDB::bind_method(D_METHOD("environment_set_sky_orientation", "env", "orientation"), &VisualServer::environment_set_sky_orientation); ClassDB::bind_method(D_METHOD("environment_set_bg_color", "env", "color"), &VisualServer::environment_set_bg_color); ClassDB::bind_method(D_METHOD("environment_set_bg_energy", "env", "energy"), &VisualServer::environment_set_bg_energy); ClassDB::bind_method(D_METHOD("environment_set_canvas_max_layer", "env", "max_layer"), &VisualServer::environment_set_canvas_max_layer); diff --git a/servers/visual_server.h b/servers/visual_server.h index ad2819a95a..7314c801ef 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -709,6 +709,7 @@ public: virtual void environment_set_background(RID p_env, EnvironmentBG p_bg) = 0; virtual void environment_set_sky(RID p_env, RID p_sky) = 0; virtual void environment_set_sky_custom_fov(RID p_env, float p_scale) = 0; + virtual void environment_set_sky_orientation(RID p_env, const Basis &p_orientation) = 0; virtual void environment_set_bg_color(RID p_env, const Color &p_color) = 0; virtual void environment_set_bg_energy(RID p_env, float p_energy) = 0; virtual void environment_set_canvas_max_layer(RID p_env, int p_max_layer) = 0; |