diff options
author | Juan Linietsky <reduzio@gmail.com> | 2014-03-13 22:57:24 -0300 |
---|---|---|
committer | Juan Linietsky <reduzio@gmail.com> | 2014-03-13 22:57:24 -0300 |
commit | 31ce3c5fd0300aac1e86bced1efc5f9ec94bdb6b (patch) | |
tree | b6d3a290333c72940b49ed4c930ff6858a59515e | |
parent | a65edb4caabec21654c56552e11aacf0fd9291de (diff) |
-fix bug in cache for atlas import/export
-fix some menus
-fixed bug in out transition curves
-detect and remove file:/// in collada
-remove multiscript for now
-remove dependencies on mouse in OS, moved to Input
-avoid fscache from screwing up (fix might make it slower, but it works)
-funcref was missing, it's there now
136 files changed, 10782 insertions, 1576 deletions
diff --git a/core/bind/core_bind.cpp b/core/bind/core_bind.cpp index 73f6f753b9..28906354ab 100644 --- a/core/bind/core_bind.cpp +++ b/core/bind/core_bind.cpp @@ -387,6 +387,12 @@ uint32_t _OS::get_ticks_msec() const { return OS::get_singleton()->get_ticks_msec(); } + +bool _OS::can_use_threads() const { + + return OS::get_singleton()->can_use_threads(); +} + bool _OS::can_draw() const { return OS::get_singleton()->can_draw(); @@ -488,6 +494,27 @@ float _OS::get_frames_per_second() const { return OS::get_singleton()->get_frames_per_second(); } +Error _OS::native_video_play(String p_path) { + + return OS::get_singleton()->native_video_play(p_path); +}; + +bool _OS::native_video_is_playing() { + + return OS::get_singleton()->native_video_is_playing(); +}; + +void _OS::native_video_pause() { + + OS::get_singleton()->native_video_pause(); +}; + +void _OS::native_video_stop() { + + OS::get_singleton()->native_video_stop(); +}; + + String _OS::get_custom_level() const { return OS::get_singleton()->get_custom_level(); @@ -496,7 +523,7 @@ _OS *_OS::singleton=NULL; void _OS::_bind_methods() { - ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&_OS::get_mouse_pos); + //ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&_OS::get_mouse_pos); //ObjectTypeDB::bind_method(_MD("is_mouse_grab_enabled"),&_OS::is_mouse_grab_enabled); ObjectTypeDB::bind_method(_MD("set_clipboard","clipboard"),&_OS::set_clipboard); @@ -550,7 +577,9 @@ void _OS::_bind_methods() { ObjectTypeDB::bind_method(_MD("get_frames_drawn"),&_OS::get_frames_drawn); ObjectTypeDB::bind_method(_MD("is_stdout_verbose"),&_OS::is_stdout_verbose); - ObjectTypeDB::bind_method(_MD("get_mouse_button_state"),&_OS::get_mouse_button_state); + ObjectTypeDB::bind_method(_MD("can_use_threads"),&_OS::can_use_threads); + + //ObjectTypeDB::bind_method(_MD("get_mouse_button_state"),&_OS::get_mouse_button_state); ObjectTypeDB::bind_method(_MD("dump_memory_to_file","file"),&_OS::dump_memory_to_file); ObjectTypeDB::bind_method(_MD("dump_resources_to_file","file"),&_OS::dump_resources_to_file); @@ -568,6 +597,12 @@ void _OS::_bind_methods() { ObjectTypeDB::bind_method(_MD("print_all_textures_by_size"),&_OS::print_all_textures_by_size); + ObjectTypeDB::bind_method(_MD("native_video_play"),&_OS::native_video_play); + ObjectTypeDB::bind_method(_MD("native_video_is_playing"),&_OS::native_video_is_playing); + ObjectTypeDB::bind_method(_MD("native_video_stop"),&_OS::native_video_stop); + ObjectTypeDB::bind_method(_MD("native_video_pause"),&_OS::native_video_pause); + + BIND_CONSTANT( DAY_SUNDAY ); BIND_CONSTANT( DAY_MONDAY ); BIND_CONSTANT( DAY_TUESDAY ); @@ -983,8 +1018,22 @@ void _File::store_string(const String& p_string){ f->store_string(p_string); } -void _File::store_line(const String& p_string){ +void _File::store_pascal_string(const String& p_string) { + + ERR_FAIL_COND(!f); + + f->store_pascal_string(p_string); +}; + +String _File::get_pascal_string() { + + ERR_FAIL_COND_V(!f, ""); + + return f->get_pascal_string(); +}; + +void _File::store_line(const String& p_string){ ERR_FAIL_COND(!f); f->store_line(p_string); @@ -1083,6 +1132,9 @@ void _File::_bind_methods() { ObjectTypeDB::bind_method(_MD("store_string","string"),&_File::store_string); ObjectTypeDB::bind_method(_MD("store_var","value"),&_File::store_var); + ObjectTypeDB::bind_method(_MD("store_pascal_string","string"),&_File::store_pascal_string); + ObjectTypeDB::bind_method(_MD("get_pascal_string"),&_File::get_pascal_string); + ObjectTypeDB::bind_method(_MD("file_exists","path"),&_File::file_exists); BIND_CONSTANT( READ ); diff --git a/core/bind/core_bind.h b/core/bind/core_bind.h index 9545fc65fb..0c80fb3fc4 100644 --- a/core/bind/core_bind.h +++ b/core/bind/core_bind.h @@ -98,6 +98,11 @@ public: bool is_video_mode_resizable(int p_screen=0) const; Array get_fullscreen_mode_list(int p_screen=0) const; + Error native_video_play(String p_path); + bool native_video_is_playing(); + void native_video_pause(); + void native_video_stop(); + void set_iterations_per_second(int p_ips); int get_iterations_per_second() const; @@ -166,6 +171,7 @@ public: void delay_msec(uint32_t p_msec) const; uint32_t get_ticks_msec() const; + bool can_use_threads() const; bool can_draw() const; @@ -280,6 +286,9 @@ public: void store_string(const String& p_string); void store_line(const String& p_string); + virtual void store_pascal_string(const String& p_string); + virtual String get_pascal_string(); + Vector<String> get_csv_line() const; diff --git a/core/func_ref.cpp b/core/func_ref.cpp new file mode 100644 index 0000000000..0e43112de8 --- /dev/null +++ b/core/func_ref.cpp @@ -0,0 +1,55 @@ +#include "func_ref.h" + +Variant FuncRef::call_func(const Variant** p_args, int p_argcount, Variant::CallError& r_error) { + + if (id==0) { + r_error.error=Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL; + return Variant(); + } + Object* obj = ObjectDB::get_instance(id); + + if (!obj) { + r_error.error=Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL; + return Variant(); + } + + return obj->call(function,p_args,p_argcount,r_error); + +} + +void FuncRef::set_instance(Object *p_obj){ + + ERR_FAIL_NULL(p_obj); + id=p_obj->get_instance_ID(); +} +void FuncRef::set_function(const StringName& p_func){ + + function=p_func; +} + +void FuncRef::_bind_methods() { + + { + MethodInfo mi; + mi.name="call"; + mi.arguments.push_back( PropertyInfo( Variant::STRING, "method")); + Vector<Variant> defargs; + for(int i=0;i<10;i++) { + mi.arguments.push_back( PropertyInfo( Variant::NIL, "arg"+itos(i))); + defargs.push_back(Variant()); + } + ObjectTypeDB::bind_native_method(METHOD_FLAGS_DEFAULT,"call_func",&FuncRef::call_func,mi,defargs); + + } + + ObjectTypeDB::bind_method(_MD("set_instance","instance"),&FuncRef::set_instance); + ObjectTypeDB::bind_method(_MD("set_function","name"),&FuncRef::set_function); + +} + + +FuncRef::FuncRef(){ + + id=0; +} + diff --git a/core/func_ref.h b/core/func_ref.h new file mode 100644 index 0000000000..28d0e737be --- /dev/null +++ b/core/func_ref.h @@ -0,0 +1,23 @@ +#ifndef FUNC_REF_H +#define FUNC_REF_H + +#include "reference.h" + +class FuncRef : public Reference{ + + OBJ_TYPE(FuncRef,Reference); + ObjectID id; + StringName function; + +protected: + + static void _bind_methods(); +public: + + Variant call_func(const Variant** p_args, int p_argcount, Variant::CallError& r_error); + void set_instance(Object *p_obj); + void set_function(const StringName& p_func); + FuncRef(); +}; + +#endif // FUNC_REF_H diff --git a/core/globals.cpp b/core/globals.cpp index 997e2a2d85..7df7680827 100644 --- a/core/globals.cpp +++ b/core/globals.cpp @@ -166,10 +166,9 @@ bool Globals::_get(const StringName& p_name,Variant &r_ret) const { _THREAD_SAFE_METHOD_ - const VariantContainer *v=props.getptr(p_name); - if (!v) + if (!props.has(p_name)) return false; - r_ret=v->variant; + r_ret=props[p_name].variant; return true; } @@ -188,18 +187,17 @@ void Globals::_get_property_list(List<PropertyInfo> *p_list) const { _THREAD_SAFE_METHOD_ - const String *k=NULL; Set<_VCSort> vclist; - while ((k=props.next(k))) { + for(Map<StringName,VariantContainer>::Element *E=props.front();E;E=E->next()) { - const VariantContainer *v=props.getptr(*k); + const VariantContainer *v=&E->get(); if (v->hide_from_editor) continue; _VCSort vc; - vc.name=*k; + vc.name=E->key(); vc.order=v->order; vc.type=v->variant.get_type(); if (vc.name.begins_with("input/") || vc.name.begins_with("import/") || vc.name.begins_with("export/") || vc.name.begins_with("/remap") || vc.name.begins_with("/locale") || vc.name.begins_with("/autoload")) @@ -1138,24 +1136,23 @@ Error Globals::save_custom(const String& p_path,const CustomMap& p_custom,const ERR_FAIL_COND_V(p_path=="",ERR_INVALID_PARAMETER); - const String *k=NULL; Set<_VCSort> vclist; - while ((k=props.next(k))) { + for(Map<StringName,VariantContainer>::Element *G=props.front();G;G=G->next()) { - const VariantContainer *v=props.getptr(*k); + const VariantContainer *v=&G->get(); if (v->hide_from_editor) continue; - if (p_custom.has(*k)) + if (p_custom.has(G->key())) continue; bool discard=false; for(const Set<String>::Element *E=p_ignore_masks.front();E;E=E->next()) { - if ( (*k).match(E->get())) { + if ( String(G->key()).match(E->get())) { discard=true; break; } @@ -1165,7 +1162,7 @@ Error Globals::save_custom(const String& p_path,const CustomMap& p_custom,const continue; _VCSort vc; - vc.name=*k; + vc.name=G->key();//*k; vc.order=v->order; vc.type=v->variant.get_type(); vc.flags=PROPERTY_USAGE_CHECKABLE|PROPERTY_USAGE_EDITOR|PROPERTY_USAGE_STORAGE; diff --git a/core/globals.h b/core/globals.h index 08d9f08088..b8dc3f9367 100644 --- a/core/globals.h +++ b/core/globals.h @@ -65,9 +65,9 @@ protected: }; int last_order; - HashMap<String,VariantContainer> props; + Map<StringName,VariantContainer> props; String resource_path; - HashMap<String,PropertyInfo> custom_prop_info; + Map<StringName,PropertyInfo> custom_prop_info; bool disable_platform_override; bool using_datapack; diff --git a/core/io/file_access_pack.cpp b/core/io/file_access_pack.cpp index edecbb6a3e..45e6990ad2 100644 --- a/core/io/file_access_pack.cpp +++ b/core/io/file_access_pack.cpp @@ -172,7 +172,6 @@ bool PackedSourcePCK::try_open_pack(const String& p_path) { uint64_t size = f->get_64(); uint8_t md5[16]; f->get_buffer(md5,16); - PackedData::get_singleton()->add_path(p_path, path, ofs, size, md5,this); }; diff --git a/core/io/marshalls.cpp b/core/io/marshalls.cpp index f5f9d34439..df951c759a 100644 --- a/core/io/marshalls.cpp +++ b/core/io/marshalls.cpp @@ -264,26 +264,94 @@ Error decode_variant(Variant& r_variant,const uint8_t *p_buffer, int p_len,int * } r_variant=img; - if (r_len) + if (r_len) { + if (datalen%4) + (*r_len)+=4-datalen%4; + (*r_len)+=4*5+datalen; + } } break; case Variant::NODE_PATH: { - ERR_FAIL_COND_V(len<4,ERR_INVALID_DATA); + ERR_FAIL_COND_V(len<4,ERR_INVALID_DATA); uint32_t strlen = decode_uint32(buf); - buf+=4; - len-=4; - ERR_FAIL_COND_V((int)strlen>len,ERR_INVALID_DATA); + if (strlen&0x80000000) { + //new format + ERR_FAIL_COND_V(len<12,ERR_INVALID_DATA); + Vector<StringName> names; + Vector<StringName> subnames; + bool absolute; + StringName prop; - String str; - str.parse_utf8((const char*)buf,strlen); + int i=0; + uint32_t namecount=strlen&=0x7FFFFFFF; + uint32_t subnamecount = decode_uint32(buf+4); + uint32_t flags = decode_uint32(buf+8); - r_variant=NodePath(str); + len-=12; + buf+=12; - if (r_len) - (*r_len)+=4+strlen; + int total=namecount+subnamecount; + if (flags&2) + total++; + + if (r_len) + (*r_len)+=12; + + + for(int i=0;i<total;i++) { + + ERR_FAIL_COND_V((int)len<4,ERR_INVALID_DATA); + strlen = decode_uint32(buf); + + int pad=0; + + if (strlen%4) + pad+=4-strlen%4; + + buf+=4; + len-=4; + ERR_FAIL_COND_V((int)strlen+pad>len,ERR_INVALID_DATA); + + String str; + str.parse_utf8((const char*)buf,strlen); + + + if (i<namecount) + names.push_back(str); + else if (i<namecount+subnamecount) + subnames.push_back(str); + else + prop=str; + + buf+=strlen+pad; + len-=strlen+pad; + + if (r_len) + (*r_len)+=4+strlen+pad; + + } + + r_variant=NodePath(names,subnames,flags&1,prop); + + } else { + //old format, just a string + + buf+=4; + len-=4; + ERR_FAIL_COND_V((int)strlen>len,ERR_INVALID_DATA); + + + String str; + str.parse_utf8((const char*)buf,strlen); + + r_variant=NodePath(str); + + if (r_len) + (*r_len)+=4+strlen; + } } break; /*case Variant::RESOURCE: { @@ -713,7 +781,59 @@ Error encode_variant(const Variant& p_variant, uint8_t *r_buffer, int &r_len) { r_len+=4; } break; - case Variant::NODE_PATH: + case Variant::NODE_PATH: { + + NodePath np=p_variant; + if (buf) { + encode_uint32(uint32_t(np.get_name_count())|0x80000000,buf); //for compatibility with the old format + encode_uint32(np.get_subname_count(),buf+4); + uint32_t flags=0; + if (np.is_absolute()) + flags|=1; + if (np.get_property()!=StringName()) + flags|=2; + + encode_uint32(flags,buf+8); + + buf+=12; + } + + r_len+=12; + + int total = np.get_name_count()+np.get_subname_count(); + if (np.get_property()!=StringName()) + total++; + + for(int i=0;i<total;i++) { + + String str; + + if (i<np.get_name_count()) + str=np.get_name(i); + else if (i<np.get_name_count()+np.get_subname_count()) + str=np.get_subname(i-np.get_subname_count()); + else + str=np.get_property(); + + CharString utf8 = str.utf8(); + + int pad = 0; + + if (utf8.length()%4) + pad=4-utf8.length()%4; + + if (buf) { + encode_uint32(utf8.length(),buf); + buf+=4; + copymem(buf,utf8.get_data(),utf8.length()); + buf+=pad+utf8.length(); + } + + + r_len+=4+utf8.length()+pad; + } + + } break; case Variant::STRING: { @@ -879,7 +999,11 @@ Error encode_variant(const Variant& p_variant, uint8_t *r_buffer, int &r_len) { copymem(&buf[20],&r[0],ds); } - r_len+=data.size()+5*4; + int pad=0; + if (data.size()%4) + pad=4-data.size()%4; + + r_len+=data.size()+5*4+pad; } break; /*case Variant::RESOURCE: { diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index c54398935e..47f278596b 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -647,7 +647,7 @@ Error ResourceInteractiveLoaderBinary::poll(){ } stage++; - return OK; + return error; } s-=external_resources.size(); @@ -804,7 +804,12 @@ void ResourceInteractiveLoaderBinary::get_dependencies(FileAccess *p_f,List<Stri for(int i=0;i<external_resources.size();i++) { - p_dependencies->push_back(external_resources[i].path); + String dep=external_resources[i].path; + if (dep.ends_with("*")) { + dep=ResourceLoader::guess_full_filename(dep,external_resources[i].type); + } + + p_dependencies->push_back(dep); } } @@ -892,6 +897,19 @@ void ResourceInteractiveLoaderBinary::open(FileAccess *p_f) { } + //see if the exporter has different set of external resources for more efficient loading + String preload_depts = "deps/"+res_path.md5_text(); + if (Globals::get_singleton()->has(preload_depts)) { + external_resources.clear(); + //ignore external resources and use these + NodePath depts=Globals::get_singleton()->get(preload_depts); + external_resources.resize(depts.get_name_count()); + for(int i=0;i<depts.get_name_count();i++) { + external_resources[i].path=depts.get_name(i); + } + print_line(res_path+" - EXTERNAL RESOURCES: "+itos(external_resources.size())); + } + print_bl("ext resources: "+itos(ext_resources_size)); uint32_t int_resources_size=f->get_32(); @@ -1412,8 +1430,6 @@ void ResourceFormatSaverBinaryInstance::write_variant(const Variant& p_property, f->store_32(OBJECT_EXTERNAL_RESOURCE); save_unicode_string(res->get_save_type()); String path=relative_paths?local_path.path_to_file(res->get_path()):res->get_path(); - if (no_extensions) - path=path.basename()+".*"; save_unicode_string(path); } else { @@ -1439,7 +1455,7 @@ void ResourceFormatSaverBinaryInstance::write_variant(const Variant& p_property, f->store_32(VARIANT_DICTIONARY); Dictionary d = p_property; - f->store_32(uint32_t(d.size())|(d.is_shared()?0x80000000:0)); + f->store_32(uint32_t(d.size())|(d.is_shared()?0x80000000:0)); List<Variant> keys; d.get_key_list(&keys); @@ -1734,7 +1750,7 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_ skip_editor=p_flags&ResourceSaver::FLAG_OMIT_EDITOR_PROPERTIES; bundle_resources=p_flags&ResourceSaver::FLAG_BUNDLE_RESOURCES; big_endian=p_flags&ResourceSaver::FLAG_SAVE_BIG_ENDIAN; - no_extensions=p_flags&ResourceSaver::FLAG_NO_EXTENSION; + local_path=p_path.get_base_dir(); //bin_meta_idx = get_string_index("__bin_meta__"); //is often used, so create @@ -1816,8 +1832,6 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_ save_unicode_string(E->get()->get_save_type()); String path = E->get()->get_path(); - if (no_extensions) - path=path.basename()+".*"; save_unicode_string(path); } // save internal resource table @@ -1861,6 +1875,7 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_ } f->seek_end(); + print_line("SAVING: "+p_path); if (p_resource->get_import_metadata().is_valid()) { uint64_t md_pos = f->get_pos(); Ref<ResourceImportMetadata> imd=p_resource->get_import_metadata(); @@ -1869,6 +1884,8 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_ for(int i=0;i<imd->get_source_count();i++) { save_unicode_string(imd->get_source_path(i)); save_unicode_string(imd->get_source_md5(i)); + print_line("SAVE PATH: "+imd->get_source_path(i)); + print_line("SAVE MD5: "+imd->get_source_md5(i)); } List<String> options; imd->get_options(&options); diff --git a/core/io/resource_format_binary.h b/core/io/resource_format_binary.h index 006148f5a8..bd33fee82c 100644 --- a/core/io/resource_format_binary.h +++ b/core/io/resource_format_binary.h @@ -120,7 +120,7 @@ class ResourceFormatSaverBinaryInstance { String local_path; - bool no_extensions; + bool relative_paths; bool bundle_resources; bool skip_editor; diff --git a/core/io/resource_format_xml.cpp b/core/io/resource_format_xml.cpp index fc5aecfd99..f3c0f1cb8b 100644 --- a/core/io/resource_format_xml.cpp +++ b/core/io/resource_format_xml.cpp @@ -1357,6 +1357,31 @@ Error ResourceInteractiveLoaderXML::poll() { if (error!=OK) return error; + if (ext_resources.size()) { + + error=ERR_FILE_CORRUPT; + String path=ext_resources.front()->get(); + + RES res = ResourceLoader::load(path); + + if (res.is_null()) { + + if (ResourceLoader::get_abort_on_missing_resources()) { + ERR_EXPLAIN(local_path+":"+itos(get_current_line())+": editor exported unexisting resource at: "+path); + ERR_FAIL_V(error); + } else { + ResourceLoader::notify_load_error("Resource Not Found: "+path); + } + } else { + + resource_cache.push_back(res); + } + + error=OK; + ext_resources.pop_front(); + resource_current++; + return error; + } bool exit; Tag *tag = parse_tag(&exit); @@ -1528,7 +1553,7 @@ int ResourceInteractiveLoaderXML::get_stage() const { } int ResourceInteractiveLoaderXML::get_stage_count() const { - return resources_total; + return resources_total+ext_resources.size(); } ResourceInteractiveLoaderXML::~ResourceInteractiveLoaderXML() { @@ -1573,6 +1598,12 @@ void ResourceInteractiveLoaderXML::get_dependencies(FileAccess *f,List<String> * path=Globals::get_singleton()->localize_path(local_path.get_base_dir()+"/"+path); } + if (path.ends_with("*")) { + ERR_FAIL_COND(!tag->args.has("type")); + String type = tag->args["type"]; + path = ResourceLoader::guess_full_filename(path,type); + } + p_dependencies->push_back(path); Error err = close_tag("ext_resource"); @@ -1642,6 +1673,19 @@ void ResourceInteractiveLoaderXML::open(FileAccess *p_f) { } + String preload_depts = "deps/"+local_path.md5_text(); + if (Globals::get_singleton()->has(preload_depts)) { + ext_resources.clear(); + //ignore external resources and use these + NodePath depts=Globals::get_singleton()->get(preload_depts); + + for(int i=0;i<depts.get_name_count();i++) { + ext_resources.push_back(depts.get_name(i)); + } + print_line(local_path+" - EXTERNAL RESOURCES: "+itos(ext_resources.size())); + } + + } String ResourceInteractiveLoaderXML::recognize(FileAccess *p_f) { @@ -1969,8 +2013,6 @@ void ResourceFormatSaverXMLInstance::write_property(const String& p_name,const V if (res->get_path().length() && res->get_path().find("::")==-1) { //external resource String path=relative_paths?local_path.path_to_file(res->get_path()):res->get_path(); - if (no_extension) - path=path.basename()+".*"; escape(path); params+=" path=\""+path+"\""; } else { @@ -2458,7 +2500,6 @@ Error ResourceFormatSaverXMLInstance::save(const String &p_path,const RES& p_res relative_paths=p_flags&ResourceSaver::FLAG_RELATIVE_PATHS; skip_editor=p_flags&ResourceSaver::FLAG_OMIT_EDITOR_PROPERTIES; bundle_resources=p_flags&ResourceSaver::FLAG_BUNDLE_RESOURCES; - no_extension=p_flags&ResourceSaver::FLAG_NO_EXTENSION; depth=0; // save resources @@ -2475,8 +2516,6 @@ Error ResourceFormatSaverXMLInstance::save(const String &p_path,const RES& p_res write_tabs(); String p = E->get()->get_path(); - if (no_extension) - p=p.basename()+".*"; enter_tag("ext_resource","path=\""+p+"\" type=\""+E->get()->get_save_type()+"\""); //bundled exit_tag("ext_resource"); //bundled diff --git a/core/io/resource_format_xml.h b/core/io/resource_format_xml.h index 05313ffbd7..7874431a38 100644 --- a/core/io/resource_format_xml.h +++ b/core/io/resource_format_xml.h @@ -50,6 +50,10 @@ class ResourceInteractiveLoaderXML : public ResourceInteractiveLoader { _FORCE_INLINE_ Error _parse_array_element(Vector<char> &buff,bool p_number_only,FileAccess *f,bool *end); + + + List<StringName> ext_resources; + int resources_total; int resource_current; String resource_type; @@ -113,7 +117,6 @@ class ResourceFormatSaverXMLInstance { - bool no_extension; bool relative_paths; bool bundle_resources; bool skip_editor; diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index 5ee48bae25..d2610d5d4f 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -166,7 +166,7 @@ RES ResourceLoader::load(const String &p_path,const String& p_type_hint,bool p_n String remapped_path = PathRemap::get_singleton()->get_remap(local_path); if (OS::get_singleton()->is_stdout_verbose()) - print_line("load resource: "); + print_line("load resource: "+remapped_path); String extension=remapped_path.extension(); bool found=false; @@ -233,6 +233,10 @@ Ref<ResourceImportMetadata> ResourceLoader::load_import_metadata(const String &p String ResourceLoader::find_complete_path(const String& p_path,const String& p_type) { + //this is an old vestige when the engine saved files without extension. + //remains here for compatibility with old projects and only because it + //can be sometimes nice to open files using .* from a script and have it guess + //the right extension. String local_path = p_path; if (local_path.ends_with("*")) { @@ -353,6 +357,13 @@ void ResourceLoader::get_dependencies(const String& p_path,List<String> *p_depen } } +String ResourceLoader::guess_full_filename(const String &p_path,const String& p_type) { + + String local_path = Globals::get_singleton()->localize_path(p_path); + + return find_complete_path(local_path,p_type); + +} String ResourceLoader::get_resource_type(const String &p_path) { diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index 70b1a79582..ab23158785 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -102,6 +102,7 @@ public: static String get_resource_type(const String &p_path); static void get_dependencies(const String& p_path,List<String> *p_dependencies); + static String guess_full_filename(const String &p_path,const String& p_type); static void set_timestamp_on_load(bool p_timestamp) { timestamp_on_load=p_timestamp; } diff --git a/core/io/resource_saver.h b/core/io/resource_saver.h index 4b794247e0..fd4575c872 100644 --- a/core/io/resource_saver.h +++ b/core/io/resource_saver.h @@ -74,9 +74,6 @@ public: FLAG_OMIT_EDITOR_PROPERTIES=8, FLAG_SAVE_BIG_ENDIAN=16, FLAG_COMPRESS=32, - FLAG_NO_EXTENSION=64, - - }; diff --git a/core/math/math_funcs.cpp b/core/math/math_funcs.cpp index 5d3887d72c..92236a374f 100644 --- a/core/math/math_funcs.cpp +++ b/core/math/math_funcs.cpp @@ -220,9 +220,16 @@ int Math::decimals(double p_step) { double Math::ease(double p_x, double p_c) { + if (p_x<0) + p_x=0; + else if (p_x>1.0) + p_x=1.0; if (p_c>0) { - - return Math::pow(p_x,p_c); + if (p_c<1.0) { + return 1.0-Math::pow(1.0-p_x,1.0/p_c); + } else { + return Math::pow(p_x,p_c); + } } else if (p_c<0) { //inout ease diff --git a/core/os/file_access.cpp b/core/os/file_access.cpp index 23250a7345..31e7d19bae 100644 --- a/core/os/file_access.cpp +++ b/core/os/file_access.cpp @@ -428,8 +428,30 @@ void FileAccess::store_string(const String& p_string) { CharString cs=p_string.utf8(); store_buffer((uint8_t*)&cs[0],cs.length()); - } + +void FileAccess::store_pascal_string(const String& p_string) { + + CharString cs = p_string.utf8(); + store_32(cs.length()); + store_buffer((uint8_t*)&cs[0], cs.length()); +}; + +String FileAccess::get_pascal_string() { + + uint32_t sl = get_32(); + CharString cs; + cs.resize(sl+1); + get_buffer((uint8_t*)cs.ptr(),sl); + cs[sl]=0; + + String ret; + ret.parse_utf8(cs.ptr()); + + return ret; +}; + + void FileAccess::store_line(const String& p_line) { store_string(p_line); diff --git a/core/os/file_access.h b/core/os/file_access.h index bcdae61487..793e971a4c 100644 --- a/core/os/file_access.h +++ b/core/os/file_access.h @@ -125,6 +125,9 @@ public: virtual void store_string(const String& p_string); virtual void store_line(const String& p_string); + virtual void store_pascal_string(const String& p_string); + virtual String get_pascal_string(); + virtual void store_buffer(const uint8_t *p_src,int p_length); ///< store an array of bytes virtual bool file_exists(const String& p_name)=0; ///< return true if a file exists diff --git a/core/os/input.cpp b/core/os/input.cpp index 3266e4cc10..e732eac323 100644 --- a/core/os/input.cpp +++ b/core/os/input.cpp @@ -56,6 +56,7 @@ void Input::_bind_methods() { ObjectTypeDB::bind_method(_MD("get_accelerometer"),&Input::get_accelerometer); ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&Input::get_mouse_pos); ObjectTypeDB::bind_method(_MD("get_mouse_speed"),&Input::get_mouse_speed); + ObjectTypeDB::bind_method(_MD("get_mouse_button_mask"),&Input::get_mouse_button_mask); ObjectTypeDB::bind_method(_MD("set_mouse_mode","mode"),&Input::set_mouse_mode); ObjectTypeDB::bind_method(_MD("get_mouse_mode"),&Input::get_mouse_mode); @@ -280,6 +281,12 @@ Point2 InputDefault::get_mouse_speed() const { return mouse_speed_track.speed; } +int InputDefault::get_mouse_button_mask() const { + + OS::get_singleton()->get_mouse_button_state(); +} + + void InputDefault::iteration(float p_step) { diff --git a/core/os/input.h b/core/os/input.h index 5987d6ef6c..cc51dbf42f 100644 --- a/core/os/input.h +++ b/core/os/input.h @@ -64,6 +64,7 @@ public: virtual Point2 get_mouse_pos() const=0; virtual Point2 get_mouse_speed() const=0; + virtual int get_mouse_button_mask() const=0; virtual Vector3 get_accelerometer()=0; @@ -120,6 +121,7 @@ public: virtual Point2 get_mouse_pos() const; virtual Point2 get_mouse_speed() const; + virtual int get_mouse_button_mask() const; void parse_input_event(const InputEvent& p_event); void set_accelerometer(const Vector3& p_accel); diff --git a/core/os/mutex.h b/core/os/mutex.h index 512180d6c7..241d70e232 100644 --- a/core/os/mutex.h +++ b/core/os/mutex.h @@ -50,7 +50,7 @@ public: virtual void lock()=0; ///< Lock the mutex, block if locked by someone else virtual void unlock()=0; ///< Unlock the mutex, let other threads continue - virtual Error try_lock()=0; ///< Attempt to lock the mutex, true on success, false means it can't lock. + virtual Error try_lock()=0; ///< Attempt to lock the mutex, OK on success, ERROR means it can't lock. static Mutex * create(bool p_recursive=true); ///< Create a mutex diff --git a/core/os/os.cpp b/core/os/os.cpp index 141d5f2b58..c9a5cb1af8 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -430,7 +430,7 @@ Error OS::native_video_play(String p_path) { return FAILED; }; -bool OS::native_video_is_playing() { +bool OS::native_video_is_playing() const { return false; }; @@ -447,6 +447,15 @@ void OS::set_mouse_mode(MouseMode p_mode) { } +bool OS::can_use_threads() const { + +#ifdef NO_THREADS + return false; +#else + return true; +#endif +} + OS::MouseMode OS::get_mouse_mode() const{ return MOUSE_MODE_VISIBLE; diff --git a/core/os/os.h b/core/os/os.h index d77d9bee7f..c790b38635 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -316,10 +316,12 @@ public: virtual String get_unique_ID() const; virtual Error native_video_play(String p_path); - virtual bool native_video_is_playing(); + virtual bool native_video_is_playing() const; virtual void native_video_pause(); virtual void native_video_stop(); + virtual bool can_use_threads() const; + virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, Object* p_obj, String p_callback); virtual Error dialog_input_text(String p_title, String p_description, String p_partial, Object* p_obj, String p_callback); diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 6d107f97e7..7e7ce3479f 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -49,6 +49,7 @@ #include "core/io/xml_parser.h" #include "io/http_client.h" #include "packed_data_container.h" +#include "func_ref.h" #ifdef XML_ENABLED static ResourceFormatSaverXML *resource_saver_xml=NULL; @@ -135,6 +136,7 @@ void register_core_types() { ObjectTypeDB::register_type<Reference>(); ObjectTypeDB::register_type<ResourceImportMetadata>(); ObjectTypeDB::register_type<Resource>(); + ObjectTypeDB::register_type<FuncRef>(); ObjectTypeDB::register_virtual_type<StreamPeer>(); ObjectTypeDB::register_create_type<StreamPeerTCP>(); ObjectTypeDB::register_create_type<TCP_Server>(); diff --git a/core/ustring.cpp b/core/ustring.cpp index b0f06c6ab6..f53829fe21 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -31,6 +31,7 @@ #include "os/memory.h" #include "print_string.h" #include "math_funcs.h" +#include "io/md5.h" #include "ucaps.h" #include "color.h" #define MAX_DIGITS 6 @@ -2264,6 +2265,15 @@ uint64_t String::hash64() const { } +String String::md5_text() const { + + CharString cs=utf8(); + MD5_CTX ctx; + MD5Init(&ctx); + MD5Update(&ctx,(unsigned char*)cs.ptr(),cs.length()); + MD5Final(&ctx); + return String::md5(ctx.digest); +} String String::insert(int p_at_pos,String p_string) const { diff --git a/core/ustring.h b/core/ustring.h index 007565c825..13db00f07f 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -181,7 +181,8 @@ public: static uint32_t hash(const char* p_cstr,int p_len); /* hash the string */ static uint32_t hash(const char* p_cstr); /* hash the string */ uint32_t hash() const; /* hash the string */ - uint64_t hash64() const; /* hash the string */ + uint64_t hash64() const; /* hash the string */ + String md5_text() const; inline bool empty() const { return length() == 0; } diff --git a/drivers/mpc/audio_stream_mpc.cpp b/drivers/mpc/audio_stream_mpc.cpp index 91912c8948..e1f9aacf5f 100644 --- a/drivers/mpc/audio_stream_mpc.cpp +++ b/drivers/mpc/audio_stream_mpc.cpp @@ -140,7 +140,7 @@ mpc_bool_t AudioStreamMPC::_mpc_canseek(mpc_reader *p_reader) { bool AudioStreamMPC::_can_mix() const { - return active && !paused; + return /*active &&*/ !paused; } diff --git a/drivers/openssl/stream_peer_ssl.cpp b/drivers/openssl/stream_peer_ssl.cpp new file mode 100644 index 0000000000..aaedd7dde9 --- /dev/null +++ b/drivers/openssl/stream_peer_ssl.cpp @@ -0,0 +1,111 @@ +#include "stream_peer_ssl.h" + + +int StreamPeerSSL::bio_create( BIO *b ) { + b->init = 1; + b->num = 0; + b->ptr = NULL; + b->flags = 0; + return 1; +} + +int StreamPeerSSL::bio_destroy( BIO *b ) { + + if ( b == NULL ) return 0; + b->ptr = NULL; /* sb_tls_remove() will free it */ + b->init = 0; + b->flags = 0; + return 1; +} + +int StreamPeerSSL::bio_read( BIO *b, char *buf, int len ) { + + if ( buf == NULL || len <= 0 ) return 0; + + StreamPeerSSL * sp = (StreamPeerSSL*)b->ptr; + + if (sp->base.is_null()) + return 0; + + + + BIO_clear_retry_flags( b ); + + Error err; + int ret=0; + if (sp->block) { + err = sp->base->get_data((const uint8_t*)buf,len); + if (err==OK) + ret=len; + } else { + + err = sp->base->get_partial_data((const uint8_t*)buf,len,ret); + if (err==OK && ret!=len) { + BIO_set_retry_write( b ); + } + + } + + return ret; +} + +int StreamPeerSSL::bio_write( BIO *b, const char *buf, int len ) { + + if ( buf == NULL || len <= 0 ) return 0; + + StreamPeerSSL * sp = (StreamPeerSSL*)b->ptr; + + if (sp->base.is_null()) + return 0; + + BIO_clear_retry_flags( b ); + + Error err; + int wrote=0; + if (sp->block) { + err = sp->base->put_data((const uint8_t*)buf,len); + if (err==OK) + wrote=len; + } else { + + err = sp->base->put_partial_data((const uint8_t*)buf,len,wrote); + if (err==OK && wrote!=len) { + BIO_set_retry_write( b ); + } + + } + + return wrote; +} + +long StreamPeerSSL::bio_ctrl( BIO *b, int cmd, long num, void *ptr ) { + if ( cmd == BIO_CTRL_FLUSH ) { + /* The OpenSSL library needs this */ + return 1; + } + return 0; +} + +int StreamPeerSSL::bio_gets( BIO *b, char *buf, int len ) { + return -1; +} + +int StreamPeerSSL::bio_puts( BIO *b, const char *str ) { + return StreamPeerSSL::bio_write( b, str, strlen( str ) ); +} + +BIO_METHOD StreamPeerSSL::bio_methods = +{ + ( 100 | 0x400 ), /* it's a source/sink BIO */ + "sockbuf glue", + StreamPeerSSL::bio_write, + StreamPeerSSL::bio_read, + StreamPeerSSL::bio_puts, + StreamPeerSSL::bio_gets, + StreamPeerSSL::bio_ctrl, + StreamPeerSSL::bio_create, + StreamPeerSSL::bio_destroy +}; + +StreamPeerSSL::StreamPeerSSL() { +} diff --git a/drivers/openssl/stream_peer_ssl.h b/drivers/openssl/stream_peer_ssl.h new file mode 100644 index 0000000000..a126f6122c --- /dev/null +++ b/drivers/openssl/stream_peer_ssl.h @@ -0,0 +1,26 @@ +#ifndef STREAM_PEER_SSL_H +#define STREAM_PEER_SSL_H + +#include "io/stream_peer.h" + +class StreamPeerSSL : public StreamPeer { + + OBJ_TYPE(StreamPeerSSL,StreamPeer); + + Ref<StreamPeer> base; + bool block; + static BIO_METHOD bio_methods; + + static int bio_create( BIO *b ); + static int bio_destroy( BIO *b ); + static int bio_read( BIO *b, char *buf, int len ); + static int bio_write( BIO *b, const char *buf, int len ); + static long bio_ctrl( BIO *b, int cmd, long num, void *ptr ); + static int bio_gets( BIO *b, char *buf, int len ); + static int bio_puts( BIO *b, const char *str ); + +public: + StreamPeerSSL(); +}; + +#endif // STREAM_PEER_SSL_H diff --git a/drivers/vorbis/audio_stream_ogg_vorbis.cpp b/drivers/vorbis/audio_stream_ogg_vorbis.cpp index 0964a22c94..d9b7b1d161 100644 --- a/drivers/vorbis/audio_stream_ogg_vorbis.cpp +++ b/drivers/vorbis/audio_stream_ogg_vorbis.cpp @@ -97,7 +97,7 @@ long AudioStreamOGGVorbis::_ov_tell_func(void *_f) { bool AudioStreamOGGVorbis::_can_mix() const { - return playing && !paused; + return /*playing &&*/ !paused; } @@ -125,6 +125,8 @@ void AudioStreamOGGVorbis::update() { if (ret<0) { playing = false; + setting_up=false; + ERR_EXPLAIN("Error reading OGG Vorbis File: "+file); ERR_BREAK(ret<0); } else if (ret==0) { // end of song, reload? @@ -135,7 +137,8 @@ void AudioStreamOGGVorbis::update() { if (!has_loop()) { - playing=false; + playing=false; + setting_up=false; repeats=1; return; } @@ -145,6 +148,7 @@ void AudioStreamOGGVorbis::update() { int errv = ov_open_callbacks(f,&vf,NULL,0,_ov_callbacks); if (errv!=0) { playing=false; + setting_up=false; return; // :( } @@ -179,6 +183,8 @@ void AudioStreamOGGVorbis::play() { playing=false; setting_up=true; update(); + if (!setting_up) + return; setting_up=false; playing=true; } diff --git a/modules/gdscript/gd_functions.cpp b/modules/gdscript/gd_functions.cpp index 2930d9322c..c099c3f33c 100644 --- a/modules/gdscript/gd_functions.cpp +++ b/modules/gdscript/gd_functions.cpp @@ -31,6 +31,7 @@ #include "object_type_db.h" #include "reference.h" #include "gd_script.h" +#include "func_ref.h" #include "os/os.h" const char *GDFunctions::get_func_name(Function p_func) { @@ -80,6 +81,7 @@ const char *GDFunctions::get_func_name(Function p_func) { "clamp", "nearest_po2", "weakref", + "funcref", "convert", "typeof", "str", @@ -452,6 +454,36 @@ void GDFunctions::call(Function p_func,const Variant **p_args,int p_arg_count,Va } break; + case FUNC_FUNCREF: { + VALIDATE_ARG_COUNT(2); + if (p_args[0]->get_type()!=Variant::OBJECT) { + + r_error.error=Variant::CallError::CALL_ERROR_INVALID_ARGUMENT; + r_error.argument=0; + r_error.expected=Variant::OBJECT; + r_ret=Variant(); + return; + + } + if (p_args[1]->get_type()!=Variant::STRING && p_args[1]->get_type()!=Variant::NODE_PATH) { + + r_error.error=Variant::CallError::CALL_ERROR_INVALID_ARGUMENT; + r_error.argument=1; + r_error.expected=Variant::STRING; + r_ret=Variant(); + return; + + } + + Ref<FuncRef> fr = memnew( FuncRef); + + Object *obj = *p_args[0]; + fr->set_instance(*p_args[0]); + fr->set_function(*p_args[1]); + + r_ret=fr; + + } break; case TYPE_CONVERT: { VALIDATE_ARG_COUNT(2); VALIDATE_ARG_NUM(1); @@ -678,7 +710,7 @@ void GDFunctions::call(Function p_func,const Variant **p_args,int p_arg_count,Va } r_ret=ResourceLoader::load(*p_args[0]); - } + } break; case INST2DICT: { VALIDATE_ARG_COUNT(1); @@ -1130,6 +1162,13 @@ MethodInfo GDFunctions::get_info(Function p_func) { return mi; } break; + case FUNC_FUNCREF: { + + MethodInfo mi("funcref",PropertyInfo(Variant::OBJECT,"instance"),PropertyInfo(Variant::STRING,"funcname")); + mi.return_val.type=Variant::OBJECT; + return mi; + + } break; case TYPE_CONVERT: { MethodInfo mi("convert",PropertyInfo(Variant::NIL,"what"),PropertyInfo(Variant::INT,"type")); diff --git a/modules/gdscript/gd_functions.h b/modules/gdscript/gd_functions.h index 2ab397d18a..9255e5e2c5 100644 --- a/modules/gdscript/gd_functions.h +++ b/modules/gdscript/gd_functions.h @@ -77,6 +77,7 @@ public: LOGIC_CLAMP, LOGIC_NEAREST_PO2, OBJ_WEAKREF, + FUNC_FUNCREF, TYPE_CONVERT, TYPE_OF, TEXT_STR, diff --git a/modules/multiscript/SCsub b/modules/multiscript/SCsub deleted file mode 100644 index d20da72b72..0000000000 --- a/modules/multiscript/SCsub +++ /dev/null @@ -1,7 +0,0 @@ -Import('env') - -env.add_source_files(env.modules_sources,"*.cpp") - -Export('env') - - diff --git a/modules/multiscript/config.py b/modules/multiscript/config.py deleted file mode 100644 index f9bd7da08d..0000000000 --- a/modules/multiscript/config.py +++ /dev/null @@ -1,11 +0,0 @@ - - -def can_build(platform): - return True - - -def configure(env): - pass - - - diff --git a/modules/multiscript/multi_script.cpp b/modules/multiscript/multi_script.cpp deleted file mode 100644 index 1924cf2a6e..0000000000 --- a/modules/multiscript/multi_script.cpp +++ /dev/null @@ -1,498 +0,0 @@ -/*************************************************************************/ -/* multi_script.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* http://www.godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ -#include "multi_script.h" - -bool MultiScriptInstance::set(const StringName& p_name, const Variant& p_value) { - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - bool found = sarr[i]->set(p_name,p_value); - if (found) - return true; - } - - if (String(p_name).begins_with("script_")) { - bool valid; - owner->set(p_name,p_value,&valid); - return valid; - } - return false; - -} - -bool MultiScriptInstance::get(const StringName& p_name, Variant &r_ret) const{ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - bool found = sarr[i]->get(p_name,r_ret); - if (found) - return true; - } - if (String(p_name).begins_with("script_")) { - bool valid; - r_ret=owner->get(p_name,&valid); - return valid; - } - return false; - -} -void MultiScriptInstance::get_property_list(List<PropertyInfo> *p_properties) const{ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - - Set<String> existing; - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - List<PropertyInfo> pl; - sarr[i]->get_property_list(&pl); - - for(List<PropertyInfo>::Element *E=pl.front();E;E=E->next()) { - - if (existing.has(E->get().name)) - continue; - - p_properties->push_back(E->get()); - existing.insert(E->get().name); - } - } - - p_properties->push_back( PropertyInfo(Variant::NIL,"Scripts",PROPERTY_HINT_NONE,String(),PROPERTY_USAGE_CATEGORY) ); - - for(int i=0;i<owner->scripts.size();i++) { - - p_properties->push_back( PropertyInfo(Variant::OBJECT,"script_"+String::chr('a'+i),PROPERTY_HINT_RESOURCE_TYPE,"Script",PROPERTY_USAGE_EDITOR) ); - - } - - if (owner->scripts.size()<25) { - - p_properties->push_back( PropertyInfo(Variant::OBJECT,"script_"+String::chr('a'+(owner->scripts.size())),PROPERTY_HINT_RESOURCE_TYPE,"Script",PROPERTY_USAGE_EDITOR) ); - } - -} - -void MultiScriptInstance::get_method_list(List<MethodInfo> *p_list) const{ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - - Set<StringName> existing; - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - List<MethodInfo> ml; - sarr[i]->get_method_list(&ml); - - for(List<MethodInfo>::Element *E=ml.front();E;E=E->next()) { - - if (existing.has(E->get().name)) - continue; - - p_list->push_back(E->get()); - existing.insert(E->get().name); - } - } - -} -bool MultiScriptInstance::has_method(const StringName& p_method) const{ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - if (sarr[i]->has_method(p_method)) - return true; - } - - return false; - -} - -Variant MultiScriptInstance::call(const StringName& p_method,const Variant** p_args,int p_argcount,Variant::CallError &r_error) { - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - Variant r = sarr[i]->call(p_method,p_args,p_argcount,r_error); - if (r_error.error==Variant::CallError::CALL_OK) - return r; - else if (r_error.error!=Variant::CallError::CALL_ERROR_INVALID_METHOD) - return r; - } - - r_error.error=Variant::CallError::CALL_ERROR_INVALID_METHOD; - return Variant(); - -} - -void MultiScriptInstance::call_multilevel(const StringName& p_method,const Variant** p_args,int p_argcount){ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - sarr[i]->call_multilevel(p_method,p_args,p_argcount); - } - - -} -void MultiScriptInstance::notification(int p_notification){ - - ScriptInstance **sarr = instances.ptr(); - int sc = instances.size(); - - for(int i=0;i<sc;i++) { - - if (!sarr[i]) - continue; - - sarr[i]->notification(p_notification); - } - -} - - -Ref<Script> MultiScriptInstance::get_script() const { - - return owner; -} - -ScriptLanguage *MultiScriptInstance::get_language() { - - return MultiScriptLanguage::get_singleton(); -} - -MultiScriptInstance::~MultiScriptInstance() { - - owner->remove_instance(object); -} - - -/////////////////// - - -bool MultiScript::is_tool() const { - - for(int i=0;i<scripts.size();i++) { - - if (scripts[i]->is_tool()) - return true; - } - - return false; -} - -bool MultiScript::_set(const StringName& p_name, const Variant& p_value) { - - _THREAD_SAFE_METHOD_ - - String s = String(p_name); - if (s.begins_with("script_")) { - - int idx = s[7]; - if (idx==0) - return false; - idx-='a'; - - ERR_FAIL_COND_V(idx<0,false); - - Ref<Script> s = p_value; - - if (idx<scripts.size()) { - - - if (s.is_null()) - remove_script(idx); - else - set_script(idx,s); - } else if (idx==scripts.size()) { - if (s.is_null()) - return false; - add_script(s); - } else - return false; - - return true; - } - - return false; -} - -bool MultiScript::_get(const StringName& p_name,Variant &r_ret) const{ - - _THREAD_SAFE_METHOD_ - - String s = String(p_name); - if (s.begins_with("script_")) { - - int idx = s[7]; - if (idx==0) - return false; - idx-='a'; - - ERR_FAIL_COND_V(idx<0,false); - - if (idx<scripts.size()) { - - r_ret=get_script(idx); - return true; - } else if (idx==scripts.size()) { - r_ret=Ref<Script>(); - return true; - } - } - - return false; -} -void MultiScript::_get_property_list( List<PropertyInfo> *p_list) const{ - - _THREAD_SAFE_METHOD_ - - for(int i=0;i<scripts.size();i++) { - - p_list->push_back( PropertyInfo(Variant::OBJECT,"script_"+String::chr('a'+i),PROPERTY_HINT_RESOURCE_TYPE,"Script") ); - - } - - if (scripts.size()<25) { - - p_list->push_back( PropertyInfo(Variant::OBJECT,"script_"+String::chr('a'+(scripts.size())),PROPERTY_HINT_RESOURCE_TYPE,"Script") ); - } -} - -void MultiScript::set_script(int p_idx,const Ref<Script>& p_script ) { - - _THREAD_SAFE_METHOD_ - - ERR_FAIL_INDEX(p_idx,scripts.size()); - ERR_FAIL_COND( p_script.is_null() ); - - scripts[p_idx]=p_script; - Ref<Script> s=p_script; - - for (Map<Object*,MultiScriptInstance*>::Element *E=instances.front();E;E=E->next()) { - - - MultiScriptInstance*msi=E->get(); - ScriptInstance *si = msi->instances[p_idx]; - if (si) { - msi->instances[p_idx]=NULL; - memdelete(si); - } - - if (p_script->can_instance()) - msi->instances[p_idx]=s->instance_create(msi->object); - - } - - -} - - -Ref<Script> MultiScript::get_script(int p_idx) const{ - - _THREAD_SAFE_METHOD_ - - ERR_FAIL_INDEX_V(p_idx,scripts.size(),Ref<Script>()); - - return scripts[p_idx]; - -} -void MultiScript::add_script(const Ref<Script>& p_script){ - - _THREAD_SAFE_METHOD_ - ERR_FAIL_COND( p_script.is_null() ); - scripts.push_back(p_script); - Ref<Script> s=p_script; - - for (Map<Object*,MultiScriptInstance*>::Element *E=instances.front();E;E=E->next()) { - - - MultiScriptInstance*msi=E->get(); - - if (p_script->can_instance()) - msi->instances.push_back( s->instance_create(msi->object) ); - else - msi->instances.push_back(NULL); - - msi->object->_change_notify(); - - } - - - _change_notify(); -} - - -void MultiScript::remove_script(int p_idx) { - - _THREAD_SAFE_METHOD_ - - ERR_FAIL_INDEX(p_idx,scripts.size()); - - scripts.remove(p_idx); - - for (Map<Object*,MultiScriptInstance*>::Element *E=instances.front();E;E=E->next()) { - - - MultiScriptInstance*msi=E->get(); - ScriptInstance *si = msi->instances[p_idx]; - msi->instances.remove(p_idx); - if (si) { - memdelete(si); - } - - msi->object->_change_notify(); - } - - -} - - -void MultiScript::remove_instance(Object *p_object) { - - _THREAD_SAFE_METHOD_ - instances.erase(p_object); -} - -bool MultiScript::can_instance() const { - - return true; -} - -StringName MultiScript::get_instance_base_type() const { - - return StringName(); -} -ScriptInstance* MultiScript::instance_create(Object *p_this) { - - _THREAD_SAFE_METHOD_ - MultiScriptInstance *msi = memnew( MultiScriptInstance ); - msi->object=p_this; - msi->owner=this; - for(int i=0;i<scripts.size();i++) { - - ScriptInstance *si; - - if (scripts[i]->can_instance()) - si = scripts[i]->instance_create(p_this); - else - si=NULL; - - msi->instances.push_back(si); - } - - instances[p_this]=msi; - p_this->_change_notify(); - return msi; -} -bool MultiScript::instance_has(const Object *p_this) const { - - _THREAD_SAFE_METHOD_ - return instances.has((Object*)p_this); -} - -bool MultiScript::has_source_code() const { - - return false; -} -String MultiScript::get_source_code() const { - - return ""; -} -void MultiScript::set_source_code(const String& p_code) { - - -} -Error MultiScript::reload() { - - for(int i=0;i<scripts.size();i++) - scripts[i]->reload(); - - return OK; -} - -String MultiScript::get_node_type() const { - - return ""; -} - -void MultiScript::_bind_methods() { - - -} - -ScriptLanguage *MultiScript::get_language() const { - - return MultiScriptLanguage::get_singleton(); -} - - -/////////////// - -MultiScript::MultiScript() { -} - - -MultiScriptLanguage *MultiScriptLanguage::singleton=NULL; diff --git a/modules/multiscript/multi_script.h b/modules/multiscript/multi_script.h deleted file mode 100644 index 87d4b4e4c8..0000000000 --- a/modules/multiscript/multi_script.h +++ /dev/null @@ -1,158 +0,0 @@ -/*************************************************************************/ -/* multi_script.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* http://www.godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ -#ifndef MULTI_SCRIPT_H -#define MULTI_SCRIPT_H - -#include "script_language.h" -#include "os/thread_safe.h" - -class MultiScript; - -class MultiScriptInstance : public ScriptInstance { -friend class MultiScript; - mutable Vector<ScriptInstance*> instances; - Object *object; - mutable MultiScript *owner; - -public: - virtual bool set(const StringName& p_name, const Variant& p_value); - virtual bool get(const StringName& p_name, Variant &r_ret) const; - virtual void get_property_list(List<PropertyInfo> *p_properties) const; - - virtual void get_method_list(List<MethodInfo> *p_list) const; - virtual bool has_method(const StringName& p_method) const; - virtual Variant call(const StringName& p_method,const Variant** p_args,int p_argcount,Variant::CallError &r_error); - virtual void call_multilevel(const StringName& p_method,const Variant** p_args,int p_argcount); - virtual void notification(int p_notification); - - - virtual Ref<Script> get_script() const; - - virtual ScriptLanguage *get_language(); - virtual ~MultiScriptInstance(); -}; - - -class MultiScript : public Script { - - _THREAD_SAFE_CLASS_ -friend class MultiScriptInstance; - OBJ_TYPE( MultiScript,Script); - - Vector<Ref<Script> > scripts; - - Map<Object*,MultiScriptInstance*> instances; -protected: - - bool _set(const StringName& p_name, const Variant& p_value); - bool _get(const StringName& p_name,Variant &r_ret) const; - void _get_property_list( List<PropertyInfo> *p_list) const; - - static void _bind_methods(); - -public: - - void remove_instance(Object *p_object); - virtual bool can_instance() const; - - virtual StringName get_instance_base_type() const; - virtual ScriptInstance* instance_create(Object *p_this); - virtual bool instance_has(const Object *p_this) const; - - virtual bool has_source_code() const; - virtual String get_source_code() const; - virtual void set_source_code(const String& p_code); - virtual Error reload(); - - virtual bool is_tool() const; - - virtual String get_node_type() const; - - - void set_script(int p_idx,const Ref<Script>& p_script ); - Ref<Script> get_script(int p_idx) const; - void remove_script(int p_idx); - void add_script(const Ref<Script>& p_script); - - virtual ScriptLanguage *get_language() const; - - MultiScript(); -}; - - -class MultiScriptLanguage : public ScriptLanguage { - - static MultiScriptLanguage *singleton; -public: - - static _FORCE_INLINE_ MultiScriptLanguage *get_singleton() { return singleton; } - virtual String get_name() const { return "MultiScript"; } - - /* LANGUAGE FUNCTIONS */ - virtual void init() {} - virtual String get_type() const { return "MultiScript"; } - virtual String get_extension() const { return ""; } - virtual Error execute_file(const String& p_path) { return OK; } - virtual void finish() {} - - /* EDITOR FUNCTIONS */ - virtual void get_reserved_words(List<String> *p_words) const {} - virtual void get_comment_delimiters(List<String> *p_delimiters) const {} - virtual void get_string_delimiters(List<String> *p_delimiters) const {} - virtual String get_template(const String& p_class_name, const String& p_base_class_name) const { return ""; } - virtual bool validate(const String& p_script, int &r_line_error,int &r_col_error,String& r_test_error,const String& p_path="",List<String>* r_fn=NULL) const { return true; } - virtual Script *create_script() const { return memnew( MultiScript ); } - virtual bool has_named_classes() const { return false; } - virtual int find_function(const String& p_function,const String& p_code) const { return -1; } - virtual String make_function(const String& p_class,const String& p_name,const StringArray& p_args) const { return ""; } - - /* DEBUGGER FUNCTIONS */ - - virtual String debug_get_error() const { return ""; } - virtual int debug_get_stack_level_count() const { return 0; } - virtual int debug_get_stack_level_line(int p_level) const { return 0; } - virtual String debug_get_stack_level_function(int p_level) const { return ""; } - virtual String debug_get_stack_level_source(int p_level) const { return ""; } - virtual void debug_get_stack_level_locals(int p_level,List<String> *p_locals, List<Variant> *p_values, int p_max_subitems=-1,int p_max_depth=-1) {} - virtual void debug_get_stack_level_members(int p_level,List<String> *p_members, List<Variant> *p_values, int p_max_subitems=-1,int p_max_depth=-1) {} - virtual void debug_get_globals(List<String> *p_locals, List<Variant> *p_values, int p_max_subitems=-1,int p_max_depth=-1) {} - virtual String debug_parse_stack_level_expression(int p_level,const String& p_expression,int p_max_subitems=-1,int p_max_depth=-1) { return ""; } - - /* LOADER FUNCTIONS */ - - virtual void get_recognized_extensions(List<String> *p_extensions) const {} - virtual void get_public_functions(List<MethodInfo> *p_functions) const {} - virtual void get_public_constants(List<Pair<String,Variant> > *p_constants) const {} - - MultiScriptLanguage() { singleton=this; } - virtual ~MultiScriptLanguage() {}; -}; - - -#endif // MULTI_SCRIPT_H diff --git a/modules/multiscript/register_types.cpp b/modules/multiscript/register_types.cpp deleted file mode 100644 index 4b25656723..0000000000 --- a/modules/multiscript/register_types.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/*************************************************/ -/* register_script_types.cpp */ -/*************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/*************************************************/ -/* Source code within this file is: */ -/* (c) 2007-2010 Juan Linietsky, Ariel Manzur */ -/* All Rights Reserved. */ -/*************************************************/ - -#include "register_types.h" - -#include "multi_script.h" -#include "io/resource_loader.h" - -static MultiScriptLanguage *script_multi_script=NULL; - -void register_multiscript_types() { - - - script_multi_script = memnew( MultiScriptLanguage ); - ScriptServer::register_language(script_multi_script); - ObjectTypeDB::register_type<MultiScript>(); - - -} -void unregister_multiscript_types() { - - if (script_multi_script); - memdelete(script_multi_script); -} diff --git a/modules/multiscript/register_types.h b/modules/multiscript/register_types.h deleted file mode 100644 index d137b1c63f..0000000000 --- a/modules/multiscript/register_types.h +++ /dev/null @@ -1,30 +0,0 @@ -/*************************************************************************/ -/* register_types.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* http://www.godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ -void register_multiscript_types(); -void unregister_multiscript_types(); diff --git a/platform/android/AndroidManifest.xml.template b/platform/android/AndroidManifest.xml.template index 208d24059e..41be3e9d80 100644 --- a/platform/android/AndroidManifest.xml.template +++ b/platform/android/AndroidManifest.xml.template @@ -35,6 +35,6 @@ $$ADD_APPLICATION_CHUNKS$$ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> - <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/> + <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="15"/> </manifest> diff --git a/platform/android/SCsub b/platform/android/SCsub index 327b1ffe09..5464376c31 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -8,7 +8,7 @@ android_files = [ 'godot_android.cpp', 'file_access_android.cpp', 'dir_access_android.cpp', - 'audio_driver_android.cpp', + 'audio_driver_opensl.cpp', 'file_access_jandroid.cpp', 'dir_access_jandroid.cpp', 'thread_jandroid.cpp', @@ -37,7 +37,9 @@ abspath=env.Dir(".").abspath pp_basein = open(abspath+"/project.properties.template","rb") pp_baseout = open(abspath+"/java/project.properties","wb") pp_baseout.write( pp_basein.read() ) + refcount=1 + for x in env.android_source_modules: pp_baseout.write("android.library.reference."+str(refcount)+"="+x+"\n") refcount+=1 diff --git a/platform/android/audio_driver_android.cpp b/platform/android/audio_driver_opensl.cpp index 4f7b4ee348..857d1a4a54 100644 --- a/platform/android/audio_driver_android.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* audio_driver_android.cpp */ +/* audio_driver_opensl.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -26,9 +26,8 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "audio_driver_android.h" +#include "audio_driver_opensl.h" #include <string.h> -#ifdef ANDROID_NATIVE_ACTIVITY @@ -40,21 +39,32 @@ /* Structure for passing information to callback function */ -void AudioDriverAndroid::_buffer_callback( +void AudioDriverOpenSL::_buffer_callback( SLAndroidSimpleBufferQueueItf queueItf /* SLuint32 eventFlags, const void * pBuffer, SLuint32 bufferSize, SLuint32 dataUsed*/) { + bool mix=true; + if (pause) { + mix=false; + } else if (mutex) { + mix = mutex->try_lock()==OK; + } - if (mutex) - mutex->lock(); + if (mix) { + audio_server_process(buffer_size,mixdown_buffer); + } else { - audio_server_process(buffer_size,mixdown_buffer); + int32_t* src_buff=mixdown_buffer; + for(int i=0;i<buffer_size*2;i++) { + src_buff[i]=0; + } + } - if (mutex) + if (mutex && mix) mutex->unlock(); @@ -87,7 +97,7 @@ void AudioDriverAndroid::_buffer_callback( #endif } -void AudioDriverAndroid::_buffer_callbacks( +void AudioDriverOpenSL::_buffer_callbacks( SLAndroidSimpleBufferQueueItf queueItf, /*SLuint32 eventFlags, const void * pBuffer, @@ -96,7 +106,7 @@ void AudioDriverAndroid::_buffer_callbacks( void *pContext) { - AudioDriverAndroid *ad = (AudioDriverAndroid*)pContext; + AudioDriverOpenSL *ad = (AudioDriverOpenSL*)pContext; // ad->_buffer_callback(queueItf,eventFlags,pBuffer,bufferSize,dataUsed); ad->_buffer_callback(queueItf); @@ -104,17 +114,17 @@ void AudioDriverAndroid::_buffer_callbacks( } -AudioDriverAndroid* AudioDriverAndroid::s_ad=NULL; +AudioDriverOpenSL* AudioDriverOpenSL::s_ad=NULL; -const char* AudioDriverAndroid::get_name() const { +const char* AudioDriverOpenSL::get_name() const { return "Android"; } #if 0 -int AudioDriverAndroid::thread_func(SceSize args, void *argp) { +int AudioDriverOpenSL::thread_func(SceSize args, void *argp) { - AudioDriverAndroid* ad = s_ad; + AudioDriverOpenSL* ad = s_ad; sceAudioOutput2Reserve(AUDIO_OUTPUT_SAMPLE); int half=0; @@ -170,7 +180,7 @@ int AudioDriverAndroid::thread_func(SceSize args, void *argp) { } #endif -Error AudioDriverAndroid::init(){ +Error AudioDriverOpenSL::init(){ SLresult res; @@ -197,7 +207,7 @@ Error AudioDriverAndroid::init(){ return OK; } -void AudioDriverAndroid::start(){ +void AudioDriverOpenSL::start(){ mutex = Mutex::create(); @@ -357,37 +367,44 @@ void AudioDriverAndroid::start(){ active=true; } -int AudioDriverAndroid::get_mix_rate() const { +int AudioDriverOpenSL::get_mix_rate() const { return 44100; } -AudioDriverSW::OutputFormat AudioDriverAndroid::get_output_format() const{ +AudioDriverSW::OutputFormat AudioDriverOpenSL::get_output_format() const{ return OUTPUT_STEREO; } -void AudioDriverAndroid::lock(){ +void AudioDriverOpenSL::lock(){ - //if (active && mutex) - // mutex->lock(); + if (active && mutex) + mutex->lock(); } -void AudioDriverAndroid::unlock() { +void AudioDriverOpenSL::unlock() { - //if (active && mutex) - // mutex->unlock(); + if (active && mutex) + mutex->unlock(); } -void AudioDriverAndroid::finish(){ +void AudioDriverOpenSL::finish(){ (*sl)->Destroy(sl); } +void AudioDriverOpenSL::set_pause(bool p_pause) { -AudioDriverAndroid::AudioDriverAndroid() + pause=p_pause; +} + + +AudioDriverOpenSL::AudioDriverOpenSL() { s_ad=this; - mutex=NULL; + mutex=Mutex::create();//NULL; + pause=false; } -#endif + + diff --git a/platform/android/audio_driver_android.h b/platform/android/audio_driver_opensl.h index 655772c772..d852928ab8 100644 --- a/platform/android/audio_driver_android.h +++ b/platform/android/audio_driver_opensl.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* audio_driver_android.h */ +/* audio_driver_opensl.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -26,16 +26,18 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef AUDIO_DRIVER_ANDROID_H -#define AUDIO_DRIVER_ANDROID_H +#ifndef AUDIO_DRIVER_OPENSL_H +#define AUDIO_DRIVER_OPENSL_H + -#ifdef ANDROID_NATIVE_ACTIVITY #include "servers/audio/audio_server_sw.h" #include "os/mutex.h" #include <SLES/OpenSLES.h> #include "SLES/OpenSLES_Android.h" -class AudioDriverAndroid : public AudioDriverSW { + + +class AudioDriverOpenSL : public AudioDriverSW { bool active; Mutex *mutex; @@ -45,7 +47,7 @@ class AudioDriverAndroid : public AudioDriverSW { BUFFER_COUNT=2 }; - + bool pause; uint32_t buffer_size; @@ -67,7 +69,7 @@ class AudioDriverAndroid : public AudioDriverSW { SLDataLocator_OutputMix locator_outputmix; SLBufferQueueState state; - static AudioDriverAndroid* s_ad; + static AudioDriverOpenSL* s_ad; void _buffer_callback( SLAndroidSimpleBufferQueueItf queueItf @@ -97,9 +99,10 @@ public: virtual void unlock(); virtual void finish(); + virtual void set_pause(bool p_pause); - AudioDriverAndroid(); + AudioDriverOpenSL(); }; #endif // AUDIO_DRIVER_ANDROID_H -#endif + diff --git a/platform/android/detect.py b/platform/android/detect.py index b89024a81a..17e97fae0f 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -14,6 +14,7 @@ def can_build(): import os if (not os.environ.has_key("ANDROID_NDK_ROOT")): return False + return True def get_opts(): @@ -23,7 +24,7 @@ def get_opts(): ('NDK_TOOLCHAIN', 'toolchain to use for the NDK',"arm-eabi-4.4.0"), #android 2.3 ('ndk_platform', 'compile for platform: (2.2,2.3)',"2.2"), - ('NDK_TARGET', 'toolchain to use for the NDK',"arm-linux-androideabi-4.7"), + ('NDK_TARGET', 'toolchain to use for the NDK',"arm-linux-androideabi-4.8"), ('android_stl','enable STL support in android port (for modules)','no'), ('armv6','compile for older phones running arm v6 (instead of v7+neon+smp)','no') @@ -55,13 +56,10 @@ def configure(env): env.Tool('gcc') env['SPAWN'] = methods.win32_spawn + env.android_source_modules.append("../libs/apk_expansion") ndk_platform="" - if (env["ndk_platform"]=="2.2"): - ndk_platform="android-8" - else: - ndk_platform="android-9" - env.Append(CPPFLAGS=["-DANDROID_NATIVE_ACTIVITY"]) + ndk_platform="android-15" print("Godot Android!!!!!") @@ -111,6 +109,7 @@ def configure(env): env['CCFLAGS'] = string.split('-DNO_STATVFS -MMD -MP -MF -fpic -ffunction-sections -funwind-tables -fstack-protector -D__ARM_ARCH_7__ -D__GLIBC__ -Wno-psabi -march=armv6 -mfpu=neon -mfloat-abi=softfp -ftree-vectorize -funsafe-math-optimizations -fno-strict-aliasing -DANDROID -Wa,--noexecstack -DGLES2_ENABLED -DGLES1_ENABLED') env.Append(LDPATH=[ld_path]) + env.Append(LIBS=['OpenSLES']) # env.Append(LIBS=['c','m','stdc++','log','EGL','GLESv1_CM','GLESv2','OpenSLES','supc++','android']) if (env["ndk_platform"]!="2.2"): env.Append(LIBS=['EGL','OpenSLES','android']) diff --git a/platform/android/java/ant.properties b/platform/android/java/ant.properties index 950b8b0199..dc3868516d 100644 --- a/platform/android/java/ant.properties +++ b/platform/android/java/ant.properties @@ -15,8 +15,8 @@ # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. -key.store=my-release-key.keystore -key.alias=mykey +key.store=/home/luis/Downloads/carnavalguachin.keystore +key.alias=momoselacome -key.store.password=123456 -key.alias.password=123456 +key.store.password=12345678 +key.alias.password=12345678 diff --git a/platform/android/java/src/com/android/godot/Godot.java b/platform/android/java/src/com/android/godot/Godot.java index cf1545df82..9868e62957 100644 --- a/platform/android/java/src/com/android/godot/Godot.java +++ b/platform/android/java/src/com/android/godot/Godot.java @@ -49,15 +49,19 @@ import android.media.*; import android.hardware.*; import android.content.*; +import android.net.Uri; +import android.media.MediaPlayer; + import java.lang.reflect.Method; import java.util.List; import java.util.ArrayList; +import com.android.godot.payments.PaymentsManager; +import java.io.IOException; import android.provider.Settings.Secure; public class Godot extends Activity implements SensorEventListener -{ - +{ static public class SingletonBase { protected void registerClass(String p_name, String[] p_methods) { @@ -131,8 +135,12 @@ public class Godot extends Activity implements SensorEventListener }; public ResultCallback result_callback; + private PaymentsManager mPaymentsManager; + @Override protected void onActivityResult (int requestCode, int resultCode, Intent data) { - if (result_callback != null) { + if(requestCode == PaymentsManager.REQUEST_CODE_FOR_PURCHASE){ + mPaymentsManager.processPurchaseResponse(resultCode, data); + }else if (result_callback != null) { result_callback.callback(requestCode, resultCode, data); result_callback = null; }; @@ -152,13 +160,17 @@ public class Godot extends Activity implements SensorEventListener } + private static Godot _self; + + public static Godot getInstance(){ + return Godot._self; + } + @Override 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); @@ -172,12 +184,20 @@ public class Godot extends Activity implements SensorEventListener mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); result_callback = null; - + + mPaymentsManager = PaymentsManager.createManager(this).initService(); + // instanceSingleton( new GodotFacebook(this) ); } + @Override protected void onDestroy(){ + + if(mPaymentsManager != null ) mPaymentsManager.destroy(); + super.onDestroy(); + } + @Override protected void onPause() { super.onPause(); mView.onPause(); @@ -291,7 +311,15 @@ public class Godot extends Activity implements SensorEventListener @Override public boolean onKeyDown(int keyCode, KeyEvent event) { GodotLib.key(event.getUnicodeChar(0), true); return super.onKeyDown(keyCode, event); - }; + } + + public PaymentsManager getPaymentsManager() { + return mPaymentsManager; + } + +// public void setPaymentsManager(PaymentsManager mPaymentsManager) { +// this.mPaymentsManager = mPaymentsManager; +// }; // Audio diff --git a/platform/android/java/src/com/android/godot/GodotIO.java b/platform/android/java/src/com/android/godot/GodotIO.java index 1f3967cb8f..4b6a44335c 100644 --- a/platform/android/java/src/com/android/godot/GodotIO.java +++ b/platform/android/java/src/com/android/godot/GodotIO.java @@ -57,6 +57,9 @@ public class GodotIO { AssetManager am; Activity activity; + Context applicationContext; + MediaPlayer mediaPlayer; + final int SCREEN_LANDSCAPE=0; final int SCREEN_PORTRAIT=1; final int SCREEN_REVERSE_LANDSCAPE=2; @@ -326,7 +329,7 @@ public class GodotIO { activity=p_activity; streams=new HashMap<Integer,AssetData>(); dirs=new HashMap<Integer,AssetDir>(); - + applicationContext = activity.getApplicationContext(); } @@ -502,6 +505,43 @@ public class GodotIO { } }; + public void playVideo(String p_path) + { + Uri filePath = Uri.parse(p_path); + mediaPlayer = new MediaPlayer(); + + try { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setDataSource(applicationContext, filePath); + mediaPlayer.prepare(); + mediaPlayer.start(); + } + catch(IOException e) + { + System.out.println("IOError while playing video"); + } + } + + public boolean isVideoPlaying() { + if (mediaPlayer != null) { + return mediaPlayer.isPlaying(); + } + return false; + } + + public void pauseVideo() { + if (mediaPlayer != null) { + mediaPlayer.pause(); + } + } + + public void stopVideo() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + protected static final String PREFS_FILE = "device_id.xml"; protected static final String PREFS_DEVICE_ID = "device_id"; diff --git a/platform/android/java/src/com/android/godot/GodotLib.java b/platform/android/java/src/com/android/godot/GodotLib.java index f0ec3e97c6..ffa3f306f1 100644 --- a/platform/android/java/src/com/android/godot/GodotLib.java +++ b/platform/android/java/src/com/android/godot/GodotLib.java @@ -51,14 +51,14 @@ public class GodotLib { public static native void step(); public static native void touch(int what,int pointer,int howmany, int[] arr); public static native void accelerometer(float x, float y, float z); - public static native void key(int p_unicode_char, boolean p_pressed); + public static native void key(int p_unicode_char, boolean p_pressed); public static native void focusin(); public static native void focusout(); public static native void audio(); public static native void singleton(String p_name,Object p_object); public static native void method(String p_sname,String p_name,String p_ret,String[] p_params); public static native String getGlobal(String p_key); - public static native void callobject(int p_ID, String p_method, Object[] p_params); - public static native void calldeferred(int p_ID, String p_method, Object[] p_params); + public static native void callobject(int p_ID, String p_method, Object[] p_params); + public static native void calldeferred(int p_ID, String p_method, Object[] p_params); } diff --git a/platform/android/java_glue.cpp b/platform/android/java_glue.cpp index cbe17f2f73..dbd9f2bf06 100644 --- a/platform/android/java_glue.cpp +++ b/platform/android/java_glue.cpp @@ -568,6 +568,11 @@ static jmethodID _hideKeyboard=0; static jmethodID _setScreenOrientation=0; static jmethodID _getUniqueID=0; +static jmethodID _playVideo=0; +static jmethodID _isVideoPlaying=0; +static jmethodID _pauseVideo=0; +static jmethodID _stopVideo=0; + static void _gfx_init_func(void* ud, bool gl2) { @@ -628,6 +633,31 @@ static void _hide_vk() { env->CallVoidMethod(godot_io, _hideKeyboard); }; +// virtual Error native_video_play(String p_path); +// virtual bool native_video_is_playing(); +// virtual void native_video_pause(); +// virtual void native_video_stop(); + +static void _play_video(const String& p_path) { + +} + +static bool _is_video_playing() { + JNIEnv* env = ThreadAndroid::get_env(); + return env->CallBooleanMethod(godot_io, _isVideoPlaying); + //return false; +} + +static void _pause_video() { + JNIEnv* env = ThreadAndroid::get_env(); + env->CallVoidMethod(godot_io, _pauseVideo); +} + +static void _stop_video() { + JNIEnv* env = ThreadAndroid::get_env(); + env->CallVoidMethod(godot_io, _stopVideo); +} + JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env, jobject obj, jobject activity,jboolean p_need_reload_hook) { __android_log_print(ANDROID_LOG_INFO,"godot","**INIT EVENT! - %p\n",env); @@ -675,6 +705,11 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env, _showKeyboard = env->GetMethodID(c,"showKeyboard","(Ljava/lang/String;)V"); _hideKeyboard = env->GetMethodID(c,"hideKeyboard","()V"); _setScreenOrientation = env->GetMethodID(c,"setScreenOrientation","(I)V"); + + _playVideo = env->GetMethodID(c,"playVideo","(Ljava/lang/String;)V"); + _isVideoPlaying = env->GetMethodID(c,"isVideoPlaying","()Z"); + _pauseVideo = env->GetMethodID(c,"pauseVideo","()V"); + _stopVideo = env->GetMethodID(c,"stopVideo","()V"); } ThreadAndroid::make_default(jvm); @@ -685,7 +720,7 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env, - os_android = new OS_Android(_gfx_init_func,env,_open_uri,_get_data_dir,_get_locale, _get_model,_show_vk, _hide_vk,_set_screen_orient,_get_unique_id); + os_android = new OS_Android(_gfx_init_func,env,_open_uri,_get_data_dir,_get_locale, _get_model,_show_vk, _hide_vk,_set_screen_orient,_get_unique_id, _play_video, _is_video_playing, _pause_video, _stop_video); os_android->set_need_reload_hooks(p_need_reload_hook); char wd[500]; @@ -803,6 +838,12 @@ static void _initialize_java_modules() { __android_log_print(ANDROID_LOG_INFO,"godot","****^*^*?^*^*class data %x",singletonClass); jmethodID initialize = env->GetStaticMethodID(singletonClass, "initialize", "(Landroid/app/Activity;)Lcom/android/godot/Godot$SingletonBase;"); + if (!initialize) { + + ERR_EXPLAIN("Couldn't find proper initialize function 'public static Godot.SingletonBase Class::initialize(Activity p_activity)' initializer for singleton class: "+m); + ERR_CONTINUE(!initialize); + + } jobject obj = env->CallStaticObjectMethod(singletonClass,initialize,_godot_instance); __android_log_print(ANDROID_LOG_INFO,"godot","****^*^*?^*^*class instance %x",obj); jobject gob = env->NewGlobalRef(obj); diff --git a/platform/android/libs/apk_expansion/AndroidManifest.xml b/platform/android/libs/apk_expansion/AndroidManifest.xml new file mode 100644 index 0000000000..20b74a2988 --- /dev/null +++ b/platform/android/libs/apk_expansion/AndroidManifest.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.vending.expansion.downloader" + android:versionCode="2" + android:versionName="1.1" > + + <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15"/> + +</manifest>
\ No newline at end of file diff --git a/platform/android/libs/apk_expansion/build.xml b/platform/android/libs/apk_expansion/build.xml new file mode 100644 index 0000000000..5b2f2c590e --- /dev/null +++ b/platform/android/libs/apk_expansion/build.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="apk_expansion" default="help"> + + <!-- The local.properties file is created and updated by the 'android' tool. + It contains the path to the SDK. It should *NOT* be checked into + Version Control Systems. --> + <property file="local.properties" /> + + <!-- The ant.properties file can be created by you. It is only edited by the + 'android' tool to add properties to it. + This is the place to change some Ant specific build properties. + Here are some properties you may want to change/update: + + source.dir + The name of the source directory. Default is 'src'. + out.dir + The name of the output directory. Default is 'bin'. + + For other overridable properties, look at the beginning of the rules + files in the SDK, at tools/ant/build.xml + + Properties related to the SDK location or the project target should + be updated using the 'android' tool with the 'update' action. + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. + + --> + <property file="ant.properties" /> + + <!-- if sdk.dir was not set from one of the property file, then + get it from the ANDROID_HOME env var. + This must be done before we load project.properties since + the proguard config can use sdk.dir --> + <property environment="env" /> + <condition property="sdk.dir" value="${env.ANDROID_HOME}"> + <isset property="env.ANDROID_HOME" /> + </condition> + + <!-- The project.properties file is created and updated by the 'android' + tool, as well as ADT. + + This contains project specific properties such as project target, and library + dependencies. Lower level build properties are stored in ant.properties + (or in .classpath for Eclipse projects). + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. --> + <loadproperties srcFile="project.properties" /> + + <!-- quick check on sdk.dir --> + <fail + message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable." + unless="sdk.dir" + /> + + <!-- + Import per project custom build rules if present at the root of the project. + This is the place to put custom intermediary targets such as: + -pre-build + -pre-compile + -post-compile (This is typically used for code obfuscation. + Compiled code location: ${out.classes.absolute.dir} + If this is not done in place, override ${out.dex.input.absolute.dir}) + -post-package + -post-build + -pre-clean + --> + <import file="custom_rules.xml" optional="true" /> + + <!-- Import the actual build file. + + To customize existing targets, there are two options: + - Customize only one target: + - copy/paste the target into this file, *before* the + <import> task. + - customize it to your needs. + - Customize the whole content of build.xml + - copy/paste the content of the rules files (minus the top node) + into this file, replacing the <import> task. + - customize to your needs. + + *********************** + ****** IMPORTANT ****** + *********************** + In all cases you must update the value of version-tag below to read 'custom' instead of an integer, + in order to avoid having your file be overridden by tools such as "android update project" + --> + <!-- version-tag: 1 --> + <import file="${sdk.dir}/tools/ant/build.xml" /> + +</project> diff --git a/platform/android/libs/apk_expansion/proguard-project.txt b/platform/android/libs/apk_expansion/proguard-project.txt new file mode 100644 index 0000000000..f2fe1559a2 --- /dev/null +++ b/platform/android/libs/apk_expansion/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/platform/android/libs/apk_expansion/project.properties b/platform/android/libs/apk_expansion/project.properties new file mode 100644 index 0000000000..eda83430bf --- /dev/null +++ b/platform/android/libs/apk_expansion/project.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-15 +android.library=true +android.library.reference.1=../play_licensing diff --git a/platform/android/libs/apk_expansion/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/libs/apk_expansion/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..f5b762ecf3 --- /dev/null +++ b/platform/android/libs/apk_expansion/res/drawable-hdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/libs/apk_expansion/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/libs/apk_expansion/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..9ecb8af06c --- /dev/null +++ b/platform/android/libs/apk_expansion/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/libs/apk_expansion/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/libs/apk_expansion/res/layout/status_bar_ongoing_event_progress_bar.xml new file mode 100644 index 0000000000..23bac02294 --- /dev/null +++ b/platform/android/libs/apk_expansion/res/layout/status_bar_ongoing_event_progress_bar.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2008, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<LinearLayout android:layout_width="match_parent" + android:layout_height="match_parent" + android:baselineAligned="false" + android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android"> + + <RelativeLayout + android:layout_width="35dp" + android:layout_height="fill_parent" + android:paddingTop="10dp" + android:paddingBottom="8dp" > + + <ImageView + android:id="@+id/appIcon" + android:layout_width="fill_parent" + android:layout_height="25dp" + android:scaleType="centerInside" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:src="@android:drawable/stat_sys_download" /> + + <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_alignParentBottom="true" + android:layout_gravity="center_horizontal" + android:singleLine="true" + android:gravity="center" /> + </RelativeLayout> + + <RelativeLayout + android:layout_width="0dip" + android:layout_height="match_parent" + android:layout_weight="1.0" + android:clickable="true" + android:focusable="true" + android:paddingTop="10dp" + android:paddingRight="8dp" + android:paddingBottom="8dp" > + + <TextView + android:id="@+id/title" + style="@style/NotificationTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:singleLine="true"/> + + <TextView + android:id="@+id/time_remaining" + style="@style/NotificationText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:singleLine="true"/> + <!-- Only one of progress_bar and paused_text will be visible. --> + + <FrameLayout + android:id="@+id/progress_bar_frame" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" > + + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingRight="25dp" /> + + <TextView + android:id="@+id/description" + style="@style/NotificationTextShadow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingRight="25dp" + android:singleLine="true" /> + </FrameLayout> + + </RelativeLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/platform/android/libs/apk_expansion/res/values-v11/styles.xml b/platform/android/libs/apk_expansion/res/values-v11/styles.xml new file mode 100644 index 0000000000..f2013bc0bf --- /dev/null +++ b/platform/android/libs/apk_expansion/res/values-v11/styles.xml @@ -0,0 +1,6 @@ +<?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/libs/apk_expansion/res/values-v9/styles.xml b/platform/android/libs/apk_expansion/res/values-v9/styles.xml new file mode 100644 index 0000000000..736e77a5d6 --- /dev/null +++ b/platform/android/libs/apk_expansion/res/values-v9/styles.xml @@ -0,0 +1,5 @@ +<?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/libs/apk_expansion/res/values/strings.xml b/platform/android/libs/apk_expansion/res/values/strings.xml new file mode 100644 index 0000000000..b84749faf2 --- /dev/null +++ b/platform/android/libs/apk_expansion/res/values/strings.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download successfully completed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_complete">Download complete</string> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download failed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_failed">Download unsuccessful</string> + + + <string name="state_unknown">Starting..."</string> + <string name="state_idle">Waiting for download to start</string> + <string name="state_fetching_url">Looking for resources to download</string> + <string name="state_connecting">Connecting to the download server</string> + <string name="state_downloading">Downloading resources</string> + <string name="state_completed">Download finished</string> + <string name="state_paused_network_unavailable">Download paused because no network is available</string> + <string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string> + <string name="state_paused_by_request">Download paused</string> + <string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string> + <string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string> + <string name="state_paused_roaming">Download paused because you are roaming</string> + <string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string> + <string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string> + <string name="state_failed_fetching_url">Download failed because the resources could not be found</string> + <string name="state_failed_sdcard_full">Download failed because the external storage is full</string> + <string name="state_failed_cancelled">Download cancelled</string> + <string name="state_failed">Download failed</string> + + <string name="kilobytes_per_second">%1$s KB/s</string> + <string name="time_remaining">Time remaining: %1$s</string> + <string name="time_remaining_notification">%1$s left</string> +</resources>
\ No newline at end of file diff --git a/platform/android/libs/apk_expansion/res/values/styles.xml b/platform/android/libs/apk_expansion/res/values/styles.xml new file mode 100644 index 0000000000..a442f61e7e --- /dev/null +++ b/platform/android/libs/apk_expansion/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="NotificationText"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> + + <style name="NotificationTextShadow" parent="NotificationText"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:shadowColor">@android:color/background_dark</item> + <item name="android:shadowDx">1.0</item> + <item name="android:shadowDy">1.0</item> + <item name="android:shadowRadius">1</item> + </style> + + <style name="NotificationTitle"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textStyle">bold</item> + </style> + + <style name="ButtonBackground"> + <item name="android:background">@android:color/background_dark</item> + </style> + +</resources>
\ No newline at end of file diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Constants.java new file mode 100644 index 0000000000..ff2c6f535a --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Constants.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import java.io.File; + + +/** + * Contains the internal constants that are used in the download manager. + * As a general rule, modifying these constants should be done with care. + */ +public class Constants { + /** Tag used for debugging/logging */ + public static final String TAG = "LVLDL"; + + /** + * Expansion path where we store obb files + */ + public static final String EXP_PATH = File.separator + "Android" + + File.separator + "obb" + File.separator; + + /** The intent that gets sent when the service must wake up for a retry */ + public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; + + /** the intent that gets sent when clicking a successful download */ + public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; + + /** the intent that gets sent when clicking an incomplete/failed download */ + public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; + + /** the intent that gets sent when deleting the notification of a completed download */ + public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; + + /** + * When a number has to be appended to the filename, this string is used to separate the + * base filename from the sequence number + */ + public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; + + /** The default user agent used for downloads */ + public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; + + /** The buffer size used to stream the data */ + public static final int BUFFER_SIZE = 4096; + + /** The minimum amount of progress that has to be done before the progress bar gets updated */ + public static final int MIN_PROGRESS_STEP = 4096; + + /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ + public static final long MIN_PROGRESS_TIME = 1000; + + /** The maximum number of rows in the database (FIFO) */ + public static final int MAX_DOWNLOADS = 1000; + + /** + * The number of times that the download manager will retry its network + * operations when no progress is happening before it gives up. + */ + public static final int MAX_RETRIES = 5; + + /** + * The minimum amount of time that the download manager accepts for + * a Retry-After response header with a parameter in delta-seconds. + */ + public static final int MIN_RETRY_AFTER = 30; // 30s + + /** + * The maximum amount of time that the download manager accepts for + * a Retry-After response header with a parameter in delta-seconds. + */ + public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h + + /** + * The maximum number of redirects. + */ + public static final int MAX_REDIRECTS = 5; // can't be more than 7. + + /** + * The time between a failure and the first retry after an IOException. + * Each subsequent retry grows exponentially, doubling each time. + * The time is in seconds. + */ + public static final int RETRY_FIRST_DELAY = 30; + + /** Enable separate connectivity logging */ + public static final boolean LOGX = true; + + /** Enable verbose logging */ + public static final boolean LOGV = false; + + /** Enable super-verbose logging */ + private static final boolean LOCAL_LOGVV = false; + public static final boolean LOGVV = LOCAL_LOGVV && LOGV; + + /** + * This download has successfully completed. + * Warning: there might be other status values that indicate success + * in the future. + * Use isSucccess() to capture the entire category. + */ + public static final int STATUS_SUCCESS = 200; + + /** + * This request couldn't be parsed. This is also used when processing + * requests with unknown/unsupported URI schemes. + */ + public static final int STATUS_BAD_REQUEST = 400; + + /** + * This download can't be performed because the content type cannot be + * handled. + */ + public static final int STATUS_NOT_ACCEPTABLE = 406; + + /** + * This download cannot be performed because the length cannot be + * determined accurately. This is the code for the HTTP error "Length + * Required", which is typically used when making requests that require + * a content length but don't have one, and it is also used in the + * client when a response is received whose length cannot be determined + * accurately (therefore making it impossible to know when a download + * completes). + */ + public static final int STATUS_LENGTH_REQUIRED = 411; + + /** + * This download was interrupted and cannot be resumed. + * This is the code for the HTTP error "Precondition Failed", and it is + * also used in situations where the client doesn't have an ETag at all. + */ + public static final int STATUS_PRECONDITION_FAILED = 412; + + /** + * The lowest-valued error status that is not an actual HTTP status code. + */ + public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; + + /** + * The requested destination file already exists. + */ + public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + + /** + * Some possibly transient error occurred, but we can't resume the download. + */ + public static final int STATUS_CANNOT_RESUME = 489; + + /** + * This download was canceled + */ + public static final int STATUS_CANCELED = 490; + + /** + * This download has completed with an error. + * Warning: there will be other status values that indicate errors in + * the future. Use isStatusError() to capture the entire category. + */ + public static final int STATUS_UNKNOWN_ERROR = 491; + + /** + * This download couldn't be completed because of a storage issue. + * Typically, that's because the filesystem is missing or full. + * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} + * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. + */ + public static final int STATUS_FILE_ERROR = 492; + + /** + * This download couldn't be completed because of an HTTP + * redirect response that the download manager couldn't + * handle. + */ + public static final int STATUS_UNHANDLED_REDIRECT = 493; + + /** + * This download couldn't be completed because of an + * unspecified unhandled HTTP code. + */ + public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + + /** + * This download couldn't be completed because of an + * error receiving or processing data at the HTTP level. + */ + public static final int STATUS_HTTP_DATA_ERROR = 495; + + /** + * This download couldn't be completed because of an + * HttpException while setting up the request. + */ + public static final int STATUS_HTTP_EXCEPTION = 496; + + /** + * This download couldn't be completed because there were + * too many redirects. + */ + public static final int STATUS_TOO_MANY_REDIRECTS = 497; + + /** + * This download couldn't be completed due to insufficient storage + * space. Typically, this is because the SD card is full. + */ + public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + + /** + * This download couldn't be completed because no external storage + * device was found. Typically, this is because the SD card is not + * mounted. + */ + public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + + /** + * The wake duration to check to see if a download is possible. + */ + public static final long WATCHDOG_WAKE_TIMER = 60*1000; + + /** + * The wake duration to check to see if the process was killed. + */ + public static final long ACTIVE_THREAD_WATCHDOG = 5*1000; + +}
\ No newline at end of file diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java new file mode 100644 index 0000000000..9cb294d721 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * This class contains progress information about the active download(s). + * + * When you build the Activity that initiates a download and tracks the + * progress by implementing the {@link IDownloaderClient} interface, you'll + * receive a DownloadProgressInfo object in each call to the {@link + * IDownloaderClient#onDownloadProgress} method. This allows you to update + * your activity's UI with information about the download progress, such + * as the progress so far, time remaining and current speed. + */ +public class DownloadProgressInfo implements Parcelable { + public long mOverallTotal; + public long mOverallProgress; + public long mTimeRemaining; // time remaining + public float mCurrentSpeed; // speed in KB/S + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int i) { + p.writeLong(mOverallTotal); + p.writeLong(mOverallProgress); + p.writeLong(mTimeRemaining); + p.writeFloat(mCurrentSpeed); + } + + public DownloadProgressInfo(Parcel p) { + mOverallTotal = p.readLong(); + mOverallProgress = p.readLong(); + mTimeRemaining = p.readLong(); + mCurrentSpeed = p.readFloat(); + } + + public DownloadProgressInfo(long overallTotal, long overallProgress, + long timeRemaining, + float currentSpeed) { + this.mOverallTotal = overallTotal; + this.mOverallProgress = overallProgress; + this.mTimeRemaining = timeRemaining; + this.mCurrentSpeed = currentSpeed; + } + + public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { + @Override + public DownloadProgressInfo createFromParcel(Parcel parcel) { + return new DownloadProgressInfo(parcel); + } + + @Override + public DownloadProgressInfo[] newArray(int i) { + return new DownloadProgressInfo[i]; + } + }; + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java new file mode 100644 index 0000000000..2201751254 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + + + +/** + * This class binds the service API to your application client. It contains the IDownloaderClient proxy, + * which is used to call functions in your client as well as the Stub, which is used to call functions + * in the client implementation of IDownloaderClient. + * + * <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method + * should be called whenever the client wants to bind to the service. It opens up a service connection + * that ends up calling the onServiceConnected client API that passes the service messenger + * in. If the client wants to be notified by the service, it is responsible for then passing its + * messenger to the service in a separate call. + * + * <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}. + * + * <p>When your application first starts, you should first check whether your app's expansion files are + * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which + * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method + * returns a value indicating whether download is required or not. + * + * <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through + * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link + * IStub} object that you need in order to receive calls through your {@link IDownloaderClient} + * interface. + */ +public class DownloaderClientMarshaller { + public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; + public static final int MSG_ONDOWNLOADPROGRESS = 11; + public static final int MSG_ONSERVICECONNECTED = 12; + + public static final String PARAM_NEW_STATE = "newState"; + public static final String PARAM_PROGRESS = "progress"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; + public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; + public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; + + private static class Proxy implements IDownloaderClient { + private Messenger mServiceMessenger; + + @Override + public void onDownloadStateChanged(int newState) { + Bundle params = new Bundle(1); + params.putInt(PARAM_NEW_STATE, newState); + send(MSG_ONDOWNLOADSTATE_CHANGED, params); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + Bundle params = new Bundle(1); + params.putParcelable(PARAM_PROGRESS, progress); + send(MSG_ONDOWNLOADPROGRESS, params); + } + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mServiceMessenger.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mServiceMessenger = msg; + } + + @Override + public void onServiceConnected(Messenger m) { + /** + * This is never called through the proxy. + */ + } + } + + private static class Stub implements IStub { + private IDownloaderClient mItf = null; + private Class<?> mDownloaderServiceClass; + private boolean mBound; + private Messenger mServiceMessenger; + private Context mContext; + /** + * Target we publish for clients to send messages to IncomingHandler. + */ + 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; + } + } + }); + + public Stub(IDownloaderClient itf, Class<?> downloaderService) { + mItf = itf; + mDownloaderServiceClass = downloaderService; + } + + /** + * Class for interacting with the main interface of the service. + */ + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + mServiceMessenger = new Messenger(service); + mItf.onServiceConnected( + mServiceMessenger); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + mServiceMessenger = null; + } + }; + + @Override + public void connect(Context c) { + mContext = c; + Intent bindIntent = new Intent(c, mDownloaderServiceClass); + bindIntent.putExtra(PARAM_MESSENGER, mMessenger); + if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) { + if ( Constants.LOGVV ) { + Log.d(Constants.TAG, "Service Unbound"); + } + } else { + mBound = true; + } + + } + + @Override + public void disconnect(Context c) { + if (mBound) { + c.unbindService(mConnection); + mBound = false; + } + mContext = null; + } + + @Override + public Messenger getMessenger() { + return mMessenger; + } + } + + /** + * Returns a proxy that will marshal calls to IDownloaderClient methods + * + * @param msg + * @return + */ + public static IDownloaderClient CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshaled + * {@link IDownloaderClient} methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderClient that will be called + * when remote method calls are unmarshaled. + * @param downloaderService The class for your implementation of {@link + * impl.DownloaderService}. + * @return The {@link IStub} that allows you to connect to the service such that + * your {@link IDownloaderClient} receives status updates. + */ + public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { + return new Stub(itf, downloaderService); + } + + /** + * Starts the download if necessary. This function starts a flow that does ` + * many things. 1) Checks to see if the APK version has been checked and + * the metadata database updated 2) If the APK version does not match, + * checks the new LVL status to see if a new download is required 3) If the + * APK version does match, then checks to see if the download(s) have been + * completed 4) If the downloads have been completed, returns + * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the + * startup of an application to quickly ascertain if the application needs + * to wait to hear about any updated APK expansion files. Note that this does + * mean that the application MUST be run for the first time with a network + * connection, even if Market delivers all of the files. + * + * @param context Your application Context. + * @param notificationClient A PendingIntent to start the Activity in your application + * that shows the download progress and which will also start the application when download + * completes. + * @param serviceClass the class of your {@link imp.DownloaderService} implementation + * @return whether the service was started and the reason for starting the service. + * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link + * #DOWNLOAD_REQUIRED}. + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, + Class<?> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + + /** + * This version assumes that the intent contains the pending intent as a parameter. This + * is used for responding to alarms. + * <p>The pending intent must be in an extra with the key {@link + * impl.DownloaderService#EXTRA_PENDING_INTENT}. + * + * @param context + * @param notificationClient + * @param serviceClass the class of the service to start + * @return + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, + Class<?> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java new file mode 100644 index 0000000000..054eaa9895 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + + + +/** + * This class is used by the client activity to proxy requests to the Downloader + * Service. + * + * Most importantly, you must call {@link #CreateProxy} during the {@link + * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate + * an {@link IDownloaderService} object that you can then use to issue commands to the {@link + * DownloaderService} (such as to pause and resume downloads). + */ +public class DownloaderServiceMarshaller { + + public static final int MSG_REQUEST_ABORT_DOWNLOAD = + 1; + public static final int MSG_REQUEST_PAUSE_DOWNLOAD = + 2; + public static final int MSG_SET_DOWNLOAD_FLAGS = + 3; + public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = + 4; + public static final int MSG_REQUEST_DOWNLOAD_STATE = + 5; + public static final int MSG_REQUEST_CLIENT_UPDATE = + 6; + + public static final String PARAMS_FLAGS = "flags"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + private static class Proxy implements IDownloaderService { + private Messenger mMsg; + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mMsg.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mMsg = msg; + } + + @Override + public void requestAbortDownload() { + send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); + } + + @Override + public void requestPauseDownload() { + send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); + } + + @Override + public void setDownloadFlags(int flags) { + Bundle params = new Bundle(); + params.putInt(PARAMS_FLAGS, flags); + send(MSG_SET_DOWNLOAD_FLAGS, params); + } + + @Override + public void requestContinueDownload() { + send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); + } + + @Override + public void requestDownloadStatus() { + send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); + } + + @Override + public void onClientUpdated(Messenger clientMessenger) { + Bundle bundle = new Bundle(1); + bundle.putParcelable(PARAM_MESSENGER, clientMessenger); + send(MSG_REQUEST_CLIENT_UPDATE, bundle); + } + } + + private static class Stub implements IStub { + private IDownloaderService mItf = null; + 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) { + + } + } + + /** + * Returns a proxy that will marshall calls to IDownloaderService methods + * + * @param ctx + * @return + */ + public static IDownloaderService CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshalled + * IDownloaderService methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderService that will be called + * when remote method calls are unmarshalled. + * @return + */ + public static IStub CreateStub(IDownloaderService itf) { + return new Stub(itf); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java new file mode 100644 index 0000000000..1e84e54a0f --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.android.vending.expansion.downloader.R; + +import android.content.Context; +import android.os.Environment; +import android.os.StatFs; +import android.os.SystemClock; +import android.util.Log; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Some helper functions for the download manager + */ +public class Helpers { + + public static Random sRandom = new Random(SystemClock.uptimeMillis()); + + /** Regex used to parse content-disposition headers */ + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern + .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + + private Helpers() { + } + + /* + * Parse the Content-Disposition HTTP Header. The format of the header is + * defined here: 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; + } + + /** + * @return the root of the filesystem containing the given path + */ + public static File getFilesystemRoot(String path) { + File cache = Environment.getDownloadCacheDirectory(); + if (path.startsWith(cache.getPath())) { + return cache; + } + File external = Environment.getExternalStorageDirectory(); + if (path.startsWith(external.getPath())) { + return external; + } + throw new IllegalArgumentException( + "Cannot determine filesystem root for " + path); + } + + public static boolean isExternalMediaMounted() { + if (!Environment.getExternalStorageState().equals( + Environment.MEDIA_MOUNTED)) { + // No SD card found. + if ( Constants.LOGVV ) { + Log.d(Constants.TAG, "no external storage"); + } + return false; + } + return true; + } + + /** + * @return the number of bytes available on the filesystem rooted at the + * given File + */ + public static long getAvailableBytes(File root) { + StatFs stat = new StatFs(root.getPath()); + // put a bit of margin (in case creating the file grows the system by a + // few blocks) + long availableBlocks = (long) stat.getAvailableBlocks() - 4; + return stat.getBlockSize() * availableBlocks; + } + + /** + * Checks whether the filename looks legitimate + */ + public static boolean isFilenameValid(String filename) { + filename = filename.replaceFirst("/+", "/"); // normalize leading + // slashes + return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) + || filename.startsWith(Environment.getExternalStorageDirectory().toString()); + } + + /* + * Delete the given file from device + */ + /* package */static void deleteFile(String path) { + try { + File file = new File(path); + file.delete(); + } catch (Exception e) { + Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); + } + } + + /** + * Showing progress in MB here. It would be nice to choose the unit (KB, MB, + * GB) based on total file size, but given what we know about the expected + * ranges of file sizes for APK expansion files, it's probably not necessary. + * + * @param overallProgress + * @param overallTotal + * @return + */ + + static public String getDownloadProgressString(long overallProgress, long overallTotal) { + if (overallTotal == 0) { + if ( Constants.LOGVV ) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return String.format("%.2f", + (float) overallProgress / (1024.0f * 1024.0f)) + + "MB /" + + String.format("%.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) + ")"; + } + + 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 getTimeRemaining(long durationInMilliseconds) { + SimpleDateFormat sdf; + if (durationInMilliseconds > 1000 * 60 * 60) { + sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); + } else { + sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); + } + return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); + } + + /** + * Returns the file name (without full path) for an Expansion APK file from + * the given context. + * + * @param c the context + * @param mainFile true for main file, false for patch file + * @param versionCode the version of the file + * @return String the file name of the expansion file + */ + public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { + return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; + } + + /** + * Returns the filename (where the file should be saved) from info about a + * download + */ + static public String generateSaveFileName(Context c, String fileName) { + String path = getSaveFilePath(c) + + File.separator + fileName; + return path; + } + + static public String getSaveFilePath(Context c) { + 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 + * + * @param c the app/activity/service context + * @param fileName the name (sans path) of the file to query + * @param fileSize the size that the file must match + * @param deleteFileOnMismatch if the file sizes do not match, delete the + * file + * @return true if it does exist, false otherwise + */ + static public boolean doesFileExist(Context c, String fileName, long fileSize, + boolean deleteFileOnMismatch) { + // the file may have been delivered by 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; + } + + /** + * 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; + } + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java new file mode 100644 index 0000000000..b8511a62a0 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.os.Messenger; + +/** + * This interface should be implemented by the client activity for the + * downloader. It is used to pass status from the service to the client. + */ +public interface IDownloaderClient { + static final int STATE_IDLE = 1; + static final int STATE_FETCHING_URL = 2; + static final int STATE_CONNECTING = 3; + static final int STATE_DOWNLOADING = 4; + static final int STATE_COMPLETED = 5; + + static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; + static final int STATE_PAUSED_BY_REQUEST = 7; + + /** + * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and + * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and + * cellular permission will restart the service. Wi-Fi disabled means that + * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the + * other case Wi-Fi is enabled but not available. + */ + static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; + static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; + + /** + * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that + * Wi-Fi is unavailable and cellular permission will NOT restart the + * service. Wi-Fi disabled means that the Wi-Fi manager is returning that + * Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not + * available. + * <p> + * The service does not return these values. We recommend that app + * developers with very large payloads do not allow these payloads to be + * downloaded over cellular connections. + */ + static final int STATE_PAUSED_WIFI_DISABLED = 10; + static final int STATE_PAUSED_NEED_WIFI = 11; + + static final int STATE_PAUSED_ROAMING = 12; + + /** + * Scary case. We were on a network that redirected us to another website + * that delivered us the wrong file. + */ + static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; + + static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; + + static final int STATE_FAILED_UNLICENSED = 15; + static final int STATE_FAILED_FETCHING_URL = 16; + static final int STATE_FAILED_SDCARD_FULL = 17; + static final int STATE_FAILED_CANCELED = 18; + + static final int STATE_FAILED = 19; + + /** + * Called internally by the stub when the service is bound to the client. + * <p> + * Critical implementation detail. In onServiceConnected we create the + * remote service and marshaler. This is how we pass the client information + * back to the service so the client can be properly notified of changes. We + * must do this every time we reconnect to the service. + * <p> + * That is, when you receive this callback, you should call + * {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member + * instance of {@link IDownloaderService}, then call + * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved + * from your {@link IStub} proxy object. + * + * @param m the service Messenger. This Messenger is used to call the + * service API from the client. + */ + void onServiceConnected(Messenger m); + + /** + * Called when the download state changes. Depending on the state, there may + * be user requests. The service is free to change the download state in the + * middle of a user request, so the client should be able to handle this. + * <p> + * The Downloader Library includes a collection of string resources that + * correspond to each of the states, which you can use to provide users a + * useful message based on the state provided in this callback. To fetch the + * appropriate string for a state, call + * {@link Helpers#getDownloaderStringResourceIDFromState}. + * <p> + * What this means to the developer: The application has gotten a message + * that the download has paused due to lack of WiFi. The developer should + * then show UI asking the user if they want to enable downloading over + * cellular connections with appropriate warnings. If the application + * suddenly starts downloading, the application should revert to showing the + * progress again, rather than leaving up the download over cellular UI up. + * + * @param newState one of the STATE_* values defined in IDownloaderClient + */ + void onDownloadStateChanged(int newState); + + /** + * Shows the download progress. This is intended to be used to fill out a + * client UI. This progress should only be shown in a few states such as + * STATE_DOWNLOADING. + * + * @param progress the DownloadProgressInfo object containing the current + * progress of all downloads. + */ + void onDownloadProgress(DownloadProgressInfo progress); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java new file mode 100644 index 0000000000..4789afe19c --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; +import android.os.Messenger; + +/** + * This interface is implemented by the DownloaderService and by the + * DownloaderServiceMarshaller. It contains functions to control the service. + * When a client binds to the service, it must call the onClientUpdated + * function. + * <p> + * You can acquire a proxy that implements this interface for your service by + * calling {@link DownloaderServiceMarshaller#CreateProxy} during the + * {@link IDownloaderClient#onServiceConnected} callback. At which point, you + * should immediately call {@link #onClientUpdated}. + */ +public interface IDownloaderService { + /** + * Set this flag in response to the + * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then + * call RequestContinueDownload to resume a download + */ + public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; + + /** + * Request that the service abort the current download. The service should + * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. + */ + void requestAbortDownload(); + + /** + * Request that the service pause the current download. The service should + * respond by changing the state to + * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. + */ + void requestPauseDownload(); + + /** + * Request that the service continue a paused download, when in any paused + * or failed state, including + * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. + */ + void requestContinueDownload(); + + /** + * Set the flags for this download (e.g. + * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). + * + * @param flags + */ + void setDownloadFlags(int flags); + + /** + * Requests that the download status be sent to the client. + */ + void requestDownloadStatus(); + + /** + * Call this when you get {@link + * IDownloaderClient.onServiceConnected(Messenger m)} from the + * DownloaderClient to register the client with the service. It will + * automatically send the current status to the client. + * + * @param clientMessenger + */ + void onClientUpdated(Messenger clientMessenger); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java new file mode 100644 index 0000000000..d5bc3a843e --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.content.Context; +import android.os.Messenger; + +/** + * This is the interface that is used to connect/disconnect from the downloader + * service. + * <p> + * You should get a proxy object that implements this interface by calling + * {@link DownloaderClientMarshaller#CreateStub} in your activity when the + * downloader service starts. Then, call {@link #connect} during your activity's + * onResume() and call {@link #disconnect} during onStop(). + * <p> + * Then during the {@link IDownloaderClient#onServiceConnected} callback, you + * should call {@link #getMessenger} to pass the stub's Messenger object to + * {@link IDownloaderService#onClientUpdated}. + */ +public interface IStub { + Messenger getMessenger(); + + void connect(Context c); + + void disconnect(Context c); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java new file mode 100644 index 0000000000..12edd97ab2 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; +import android.util.Log; + +/** + * 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) { + /** + * TODO: The system notification manager takes ints, not longs, as IDs, + * but the download manager uses IDs take straight from the database, + * which are longs. This will have to be dealt with at some point. + */ + mNotificationManager.notify((int) id, notification); + } + + public void cancelNotification(long id) { + mNotificationManager.cancel((int) id); + } + + public void cancelAllNotifications() { + mNotificationManager.cancelAll(); + } + + public void startThread(Thread thread) { + thread.start(); + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java new file mode 100644 index 0000000000..4667acce67 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java @@ -0,0 +1,536 @@ +/* + * 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/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java new file mode 100755 index 0000000000..b77af7e085 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +/** + * This service differs from IntentService in a few minor ways/ It will not + * auto-stop itself after the intent is handled unless the target returns "true" + * in should stop. Since the goal of this service is to handle a single kind of + * intent, it does not queue up batches of intents of the same type. + */ +public abstract class CustomIntentService extends Service { + private String mName; + private boolean mRedelivery; + private volatile ServiceHandler mServiceHandler; + private volatile Looper mServiceLooper; + private static final String LOG_TAG = "CancellableIntentService"; + private static final int WHAT_MESSAGE = -10; + + public CustomIntentService(String paramString) { + this.mName = paramString; + } + + @Override + public IBinder onBind(Intent paramIntent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + HandlerThread localHandlerThread = new HandlerThread("IntentService[" + + this.mName + "]"); + localHandlerThread.start(); + this.mServiceLooper = localHandlerThread.getLooper(); + this.mServiceHandler = new ServiceHandler(this.mServiceLooper); + } + + @Override + public void onDestroy() { + Thread localThread = this.mServiceLooper.getThread(); + if ((localThread != null) && (localThread.isAlive())) { + localThread.interrupt(); + } + this.mServiceLooper.quit(); + Log.d(LOG_TAG, "onDestroy"); + } + + protected abstract void onHandleIntent(Intent paramIntent); + + protected abstract boolean shouldStop(); + + @Override + public void onStart(Intent paramIntent, int startId) { + if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { + Message localMessage = this.mServiceHandler.obtainMessage(); + localMessage.arg1 = startId; + localMessage.obj = paramIntent; + localMessage.what = WHAT_MESSAGE; + this.mServiceHandler.sendMessage(localMessage); + } + } + + @Override + public int onStartCommand(Intent paramIntent, int flags, int startId) { + onStart(paramIntent, startId); + return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; + } + + public void setIntentRedelivery(boolean enabled) { + this.mRedelivery = enabled; + } + + private final class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message paramMessage) { + CustomIntentService.this + .onHandleIntent((Intent) paramMessage.obj); + if (shouldStop()) { + Log.d(LOG_TAG, "stopSelf"); + CustomIntentService.this.stopSelf(paramMessage.arg1); + Log.d(LOG_TAG, "afterStopSelf"); + } + } + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java new file mode 100644 index 0000000000..9a0ca02122 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java @@ -0,0 +1,30 @@ +/* + * 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 + return new V3CustomNotification(); + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java new file mode 100644 index 0000000000..45111b16a3 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; + +import android.util.Log; + +/** + * Representation of information about an individual download from the database. + */ +public class DownloadInfo { + public String mUri; + public final int mIndex; + public final String mFileName; + public String mETag; + public long mTotalBytes; + public long mCurrentBytes; + public long mLastMod; + public int mStatus; + public int mControl; + public int mNumFailed; + public int mRetryAfter; + public int mRedirectCount; + + boolean mInitialized; + + public int mFuzz; + + public DownloadInfo(int index, String fileName, String pkg) { + mFuzz = Helpers.sRandom.nextInt(1001); + mFileName = fileName; + mIndex = index; + } + + public void resetDownload() { + mCurrentBytes = 0; + mETag = ""; + mLastMod = 0; + mStatus = 0; + mControl = 0; + mNumFailed = 0; + mRetryAfter = 0; + mRedirectCount = 0; + } + + /** + * Returns the time when a download should be restarted. + */ + public long restartTime(long now) { + if (mNumFailed == 0) { + return now; + } + if (mRetryAfter > 0) { + return mLastMod + mRetryAfter; + } + return mLastMod + + Constants.RETRY_FIRST_DELAY * + (1000 + mFuzz) * (1 << (mNumFailed - 1)); + } + + public void logVerboseInfo() { + Log.v(Constants.TAG, "Service adding new entry"); + Log.v(Constants.TAG, "FILENAME: " + mFileName); + Log.v(Constants.TAG, "URI : " + mUri); + Log.v(Constants.TAG, "FILENAME: " + mFileName); + Log.v(Constants.TAG, "CONTROL : " + mControl); + Log.v(Constants.TAG, "STATUS : " + mStatus); + Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); + Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); + Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); + Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); + Log.v(Constants.TAG, "TOTAL : " + mTotalBytes); + Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); + Log.v(Constants.TAG, "ETAG : " + mETag); + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java new file mode 100644 index 0000000000..eef205d7b7 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java @@ -0,0 +1,231 @@ +/* + * 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.android.vending.expansion.downloader.R; +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Messenger; + +/** + * This class handles displaying the notification associated with the download + * queue going on in the download manager. It handles multiple status types; + * Some require user interaction and some do not. Some of the user interactions + * may be transient. (for example: the user is queried to continue the download + * on 3G when it started on WiFi, but then the phone locks onto WiFi again so + * the prompt automatically goes away) + * <p/> + * The application interface for the downloader also needs to understand and + * handle these transient states. + */ +public class DownloadNotification implements IDownloaderClient { + + private int mState; + private final Context mContext; + private final NotificationManager mNotificationManager; + private String mCurrentTitle; + + private IDownloaderClient mClientProxy; + final ICustomNotification mCustomNotification; + private Notification mNotification; + private Notification mCurrentNotification; + 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(); + mCurrentNotification.tickerText = mLabel + ": " + mCurrentText; + mCurrentNotification.icon = iconResource; + mCurrentNotification.setLatestEventInfo(mContext, mCurrentTitle, mCurrentText, + mContentIntent); + if (ongoingEvent) { + mCurrentNotification.flags |= Notification.FLAG_ONGOING_EVENT; + } else { + mCurrentNotification.flags &= ~Notification.FLAG_ONGOING_EVENT; + mCurrentNotification.flags |= Notification.FLAG_AUTO_CANCEL; + } + mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification); + } + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + mProgressInfo = progress; + if (null != mClientProxy) { + mClientProxy.onDownloadProgress(progress); + } + if (progress.mOverallTotal <= 0) { + // we just show the text + mNotification.tickerText = mCurrentTitle; + mNotification.icon = android.R.drawable.stat_sys_download; + mNotification.setLatestEventInfo(mContext, mLabel, mCurrentText, mContentIntent); + mCurrentNotification = mNotification; + } 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); + mCurrentNotification = mCustomNotification.updateNotification(mContext); + } + mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification); + } + + 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 updateNotification(Context c); + } + + /** + * Called in response to onClientUpdated. Creates a new proxy and notifies + * it of the current state. + * + * @param msg the client Messenger to notify + */ + public void setMessenger(Messenger msg) { + mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); + if (null != mProgressInfo) { + mClientProxy.onDownloadProgress(mProgressInfo); + } + if (mState != -1) { + mClientProxy.onDownloadStateChanged(mState); + } + } + + /** + * Constructor + * + * @param ctx The context to use to obtain access to the Notification + * Service + */ + DownloadNotification(Context ctx, CharSequence applicationLabel) { + mState = -1; + mContext = ctx; + mLabel = applicationLabel; + mNotificationManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mCustomNotification = CustomNotificationFactory + .createCustomNotification(); + mNotification = new Notification(); + mCurrentNotification = mNotification; + + } + + @Override + public void onServiceConnected(Messenger m) { + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java new file mode 100644 index 0000000000..056d1eca0b --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; +import 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; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SyncFailedException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; + +/** + * Runs an actual download + */ +public class DownloadThread { + + private Context mContext; + private DownloadInfo mInfo; + private DownloaderService mService; + private final DownloadsDB mDB; + private final DownloadNotification mNotification; + private String mUserAgent; + + public DownloadThread(DownloadInfo info, DownloaderService service, + DownloadNotification notification) { + mContext = service; + mInfo = info; + mService = service; + mNotification = notification; + mDB = DownloadsDB.getDB(service); + mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" + + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" + + android.os.Build.ID + ")" + + service.getPackageName(); + } + + /** + * Returns the default user agent + */ + private String userAgent() { + return mUserAgent; + } + + /** + * State for the entire run() method. + */ + private static class State { + public String mFilename; + public FileOutputStream mStream; + public boolean mCountRetry = false; + public int mRetryAfter = 0; + public int mRedirectCount = 0; + public String mNewUri; + public boolean mGotData = false; + public String mRequestUri; + + public State(DownloadInfo info, DownloaderService service) { + mRedirectCount = info.mRedirectCount; + mRequestUri = info.mUri; + mFilename = service.generateTempSaveFileName(info.mFileName); + } + } + + /** + * State within executeDownload() + */ + private static class InnerState { + public int mBytesSoFar = 0; + public int mBytesThisSession = 0; + public String mHeaderETag; + public boolean mContinuingDownload = false; + public String mHeaderContentLength; + public String mHeaderContentDisposition; + public String mHeaderContentLocation; + public int mBytesNotified = 0; + public long mTimeLastNotification = 0; + } + + /** + * Raised from methods called by run() to indicate that the current request + * should be stopped immediately. Note the message passed to this exception + * will be logged and therefore must be guaranteed not to contain any PII, + * meaning it generally can't include any information about the request URI, + * headers, or destination filename. + */ + private class StopRequest extends Throwable { + /** + * + */ + private static final long serialVersionUID = 6338592678988347973L; + public int mFinalStatus; + + public StopRequest(int finalStatus, String message) { + super(message); + mFinalStatus = finalStatus; + } + + public StopRequest(int finalStatus, String message, Throwable throwable) { + super(message, throwable); + mFinalStatus = finalStatus; + } + } + + /** + * Raised from methods called by executeDownload() to indicate that the + * download should be retried immediately. + */ + private class RetryDownload extends Throwable { + + /** + * + */ + private static final long serialVersionUID = 6196036036517540229L; + } + + /** + * 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; + } + + /** + * 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); + } + } + + /** + * 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]; + + checkPausedOrCanceled(state); + + setupDestinationFile(state, innerState); + addRequestHeaders(innerState, request); + + // check just before sending the request to avoid using an invalid + // connection at all + checkConnectivity(state); + + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING); + HttpResponse response = sendRequest(state, client, request); + handleExceptionalStatus(state, innerState, response); + + 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); + } + + /** + * Check if current connectivity is valid for this request. + */ + private void checkConnectivity(State state) throws StopRequest { + switch (mService.getNetworkAvailabilityState(mDB)) { + case DownloaderService.NETWORK_OK: + return; + case DownloaderService.NETWORK_NO_CONNECTION: + throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, + "waiting for network to return"); + case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: + throw new StopRequest( + DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION, + "waiting for wifi or for download over cellular to be authorized"); + case DownloaderService.NETWORK_CANNOT_USE_ROAMING: + throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, + "roaming is not allowed"); + case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE: + throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi"); + } + } + + /** + * Transfer as much data as possible from the HTTP response to the + * destination file. + * + * @param data buffer to use to read data + * @param entityStream stream for reading the HTTP response entity + */ + private void transferData(State state, InnerState innerState, byte[] data, + InputStream entityStream) throws StopRequest { + for (;;) { + int bytesRead = readFromResponse(state, innerState, data, entityStream); + if (bytesRead == -1) { // success, end of stream already reached + handleEndOfStream(state, innerState); + return; + } + + state.mGotData = true; + writeDataToDestination(state, data, bytesRead); + innerState.mBytesSoFar += bytesRead; + innerState.mBytesThisSession += bytesRead; + reportProgress(state, innerState); + + checkPausedOrCanceled(state); + } + } + + /** + * Called after a successful completion to take any necessary action on the + * downloaded file. + */ + private void finalizeDestinationFile(State state) throws StopRequest { + syncDestination(state); + String tempFilename = state.mFilename; + String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName); + if (!state.mFilename.equals(finalFilename)) { + File startFile = new File(tempFilename); + File destFile = new File(finalFilename); + if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) { + if (!startFile.renameTo(destFile)) { + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "unable to finalize destination file"); + } + } else { + throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, + "file delivered with incorrect size. probably due to network not browser configured"); + } + } + } + + /** + * Called just before the thread finishes, regardless of status, to take any + * necessary action on the downloaded file. + */ + private void cleanupDestination(State state, int finalStatus) { + closeDestination(state); + if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) { + new File(state.mFilename).delete(); + state.mFilename = null; + } + } + + /** + * Sync the destination file to storage. + */ + private void syncDestination(State state) { + FileOutputStream downloadedFileStream = null; + try { + downloadedFileStream = new FileOutputStream(state.mFilename, true); + downloadedFileStream.getFD().sync(); + } catch (FileNotFoundException ex) { + Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); + } catch (SyncFailedException ex) { + Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); + } catch (IOException ex) { + Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); + } catch (RuntimeException ex) { + Log.w(Constants.TAG, "exception while syncing file: ", ex); + } finally { + if (downloadedFileStream != null) { + try { + downloadedFileStream.close(); + } catch (IOException ex) { + Log.w(Constants.TAG, "IOException while closing synced file: ", ex); + } catch (RuntimeException ex) { + Log.w(Constants.TAG, "exception while closing file: ", ex); + } + } + } + } + + /** + * Close the destination output stream. + */ + private void closeDestination(State state) { + try { + // close the file + if (state.mStream != null) { + state.mStream.close(); + state.mStream = null; + } + } catch (IOException ex) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "exception when closing the file after download : " + ex); + } + // nothing can really be done if the file can't be closed + } + } + + /** + * Check if the download has been paused or canceled, stopping the request + * appropriately if it has been. + */ + private void checkPausedOrCanceled(State state) throws StopRequest { + if (mService.getControl() == DownloaderService.CONTROL_PAUSED) { + int status = mService.getStatus(); + switch (status) { + case DownloaderService.STATUS_PAUSED_BY_APP: + throw new StopRequest(mService.getStatus(), + "download paused"); + } + } + } + + /** + * Report download progress through the database if necessary. + */ + private void reportProgress(State state, InnerState innerState) { + long now = System.currentTimeMillis(); + if (innerState.mBytesSoFar - innerState.mBytesNotified + > Constants.MIN_PROGRESS_STEP + && now - innerState.mTimeLastNotification + > Constants.MIN_PROGRESS_TIME) { + // we store progress updates to the database here + mInfo.mCurrentBytes = innerState.mBytesSoFar; + mDB.updateDownloadCurrentBytes(mInfo); + + innerState.mBytesNotified = innerState.mBytesSoFar; + innerState.mTimeLastNotification = now; + + long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar; + + if (Constants.LOGVV) { + Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " + + mInfo.mTotalBytes); + Log.v(Constants.TAG, " total " + totalBytesSoFar + " out of " + + mService.mTotalLength); + } + + mService.notifyUpdateBytes(totalBytesSoFar); + } + } + + /** + * Write a data buffer to the destination file. + * + * @param data buffer containing the data to write + * @param bytesRead how many bytes to write from the buffer + */ + private void writeDataToDestination(State state, byte[] data, int bytesRead) + throws StopRequest { + for (;;) { + try { + if (state.mStream == null) { + state.mStream = new FileOutputStream(state.mFilename, true); + } + state.mStream.write(data, 0, bytesRead); + // we close after every write --- this may be too inefficient + closeDestination(state); + return; + } catch (IOException ex) { + if (!Helpers.isExternalMediaMounted()) { + throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR, + "external media not mounted while writing destination file"); + } + + long availableBytes = + Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename)); + if (availableBytes < bytesRead) { + throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR, + "insufficient space while writing destination file", ex); + } + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "while writing destination file: " + ex.toString(), ex); + } + } + } + + /** + * Called when we've reached the end of the HTTP response stream, to update + * the database and check for consistency. + */ + private void handleEndOfStream(State state, InnerState innerState) throws StopRequest { + mInfo.mCurrentBytes = innerState.mBytesSoFar; + // this should always be set from the market + // if ( innerState.mHeaderContentLength == null ) { + // mInfo.mTotalBytes = innerState.mBytesSoFar; + // } + mDB.updateDownload(mInfo); + + boolean lengthMismatched = (innerState.mHeaderContentLength != null) + && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength)); + if (lengthMismatched) { + if (cannotResume(innerState)) { + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + "mismatched content length"); + } else { + throw new StopRequest(getFinalStatusForHttpError(state), + "closed socket before end of file"); + } + } + } + + private boolean cannotResume(InnerState innerState) { + return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null; + } + + /** + * Read some data from the HTTP response stream, handling I/O errors. + * + * @param data buffer to use to read data + * @param entityStream stream for reading the HTTP response entity + * @return the number of bytes actually read or -1 if the end of the stream + * has been reached + */ + private int readFromResponse(State state, InnerState innerState, byte[] data, + InputStream entityStream) throws StopRequest { + try { + return entityStream.read(data); + } catch (IOException ex) { + logNetworkState(); + mInfo.mCurrentBytes = innerState.mBytesSoFar; + mDB.updateDownload(mInfo); + if (cannotResume(innerState)) { + String message = "while reading response: " + ex.toString() + + ", can't resume interrupted download with no ETag"; + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + message, ex); + } else { + throw new StopRequest(getFinalStatusForHttpError(state), + "while reading response: " + ex.toString(), ex); + } + } + } + + /** + * Open a stream for the HTTP response entity, handling I/O errors. + * + * @return an InputStream to read the response entity + */ + private InputStream openResponseEntity(State state, 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")); + } + } + + /** + * 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); + } + + /** + * Update necessary database fields based on values of HTTP response headers + * that have been read. + */ + private void updateDatabaseFromHeaders(State state, InnerState innerState) { + mInfo.mETag = innerState.mHeaderETag; + mDB.updateDownload(mInfo); + } + + /** + * Read headers from the HTTP response and store them into local state. + */ + private void readResponseHeaders(State state, InnerState innerState, 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"); + } + } + + /** + * 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; + } + } + + /** + * 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(); + } + + /** + * 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 + "-"); + } + } + + /** + * 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"); + } + + /** + * 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; + } + } + + /** + * Prepare the destination file to receive data. If the file already exists, + * we'll set up appropriately for resumption. + */ + private void setupDestinationFile(State state, InnerState innerState) + throws StopRequest { + if (state.mFilename != null) { // only true if we've already run a + // thread for this download + if (!Helpers.isFilenameValid(state.mFilename)) { + // this should never happen + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "found invalid internal destination filename"); + } + // We're resuming a download that got interrupted + File f = new File(state.mFilename); + if (f.exists()) { + long fileLength = f.length(); + if (fileLength == 0) { + // The download hadn't actually started, we can restart from + // scratch + f.delete(); + state.mFilename = null; + } else if (mInfo.mETag == null) { + // This should've been caught upon failure + f.delete(); + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + "Trying to resume a download that can't be resumed"); + } else { + // All right, we'll be able to resume this download + try { + state.mStream = new FileOutputStream(state.mFilename, true); + } catch (FileNotFoundException exc) { + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "while opening destination for resuming: " + exc.toString(), exc); + } + innerState.mBytesSoFar = (int) fileLength; + if (mInfo.mTotalBytes != -1) { + innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); + } + innerState.mHeaderETag = mInfo.mETag; + innerState.mContinuingDownload = true; + } + } + } + + if (state.mStream != null) { + closeDestination(state); + } + } + + /** + * Stores information about the completed download, and notifies the + * initiating application. + */ + private void notifyDownloadCompleted( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename) { + updateDownloadDatabase( + status, countRetry, retryAfter, redirectCount, gotData, filename); + if (DownloaderService.isStatusCompleted(status)) { + // TBD: send status update? + } + } + + private void updateDownloadDatabase( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename) { + mInfo.mStatus = status; + mInfo.mRetryAfter = retryAfter; + mInfo.mRedirectCount = redirectCount; + mInfo.mLastMod = System.currentTimeMillis(); + if (!countRetry) { + mInfo.mNumFailed = 0; + } else if (gotData) { + mInfo.mNumFailed = 1; + } else { + mInfo.mNumFailed++; + } + mDB.updateDownload(mInfo); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java new file mode 100644 index 0000000000..627bf3eedd --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java @@ -0,0 +1,1341 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; +import com.google.android.vending.expansion.downloader.IDownloaderService; +import com.google.android.vending.expansion.downloader.IStub; +import com.google.android.vending.licensing.AESObfuscator; +import com.google.android.vending.licensing.APKExpansionPolicy; +import com.google.android.vending.licensing.LicenseChecker; +import com.google.android.vending.licensing.LicenseCheckerCallback; +import com.google.android.vending.licensing.Policy; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Messenger; +import android.os.SystemClock; +import android.provider.Settings.Secure; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.io.File; + +/** + * Performs the background downloads requested by applications that use the + * Downloads provider. This service does not run as a foreground task, so + * Android may kill it off at will, but it will try to restart itself if it can. + * Note that Android by default will kill off any process that has an open file + * handle on the shared (SD Card) partition if the partition is unmounted. + */ +public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { + + public DownloaderService() { + super("LVLDownloadService"); + } + + 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 network is usable for the given download. + */ + public static final int NETWORK_OK = 1; + + /** + * There is no network connectivity. + */ + 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; + + /** + * 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; + + /** + * The current connection is roaming, and the download can't proceed over a + * roaming connection. + */ + 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; + + /** + * 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"; + + /** + * 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 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"; + + /** + * Broadcast intent action sent by the download manager when download status + * changes. + */ + 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 + * used by the download manager)<br> 4xx: client errors<br> 5xx: server + * errors + */ + + /** + * Returns whether the status is informational (i.e. 1xx). + */ + 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); + } + + /** + * Returns whether the status is an error (i.e. 4xx or 5xx). + */ + 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); + } + + /** + * Returns whether the status is a server error (i.e. 5xx). + */ + 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); + } + + /** + * This download hasn't stated yet + */ + public static final int STATUS_PENDING = 190; + + /** + * This download has started + */ + 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; + + /** + * This download encountered some network error and is waiting before + * retrying the request. + */ + 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; + + /** + * 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; + + /** + * This download is waiting for a Wi-Fi connection to proceed. + */ + 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; + + /** + * The requested URL is no longer available + */ + public static final int STATUS_FORBIDDEN = 403; + + /** + * The file was delivered incorrectly + */ + 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; + + /** + * Some possibly transient error occurred, but we can't resume the download. + */ + public static final int STATUS_CANNOT_RESUME = 489; + + /** + * This download was canceled + * + * @hide + */ + public static final int STATUS_CANCELED = 490; + + /** + * This download has completed with an error. Warning: there will be other + * status values that indicate errors in the future. Use isStatusError() to + * capture the entire category. + */ + public static final int STATUS_UNKNOWN_ERROR = 491; + + /** + * This download couldn't be completed because of a storage issue. + * Typically, that's because the filesystem is missing or full. Use the more + * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and + * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. + * + * @hide + */ + 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; + + /** + * This download couldn't be completed because of an unspecified unhandled + * HTTP code. + * + * @hide + */ + 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; + + /** + * This download couldn't be completed because of an HttpException while + * setting up the request. + * + * @hide + */ + 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; + + /** + * 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; + + /** + * 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; + + /** + * This download is allowed to run. + * + * @hide + */ + public static final int CONTROL_RUN = 0; + + /** + * This download must pause at the first opportunity. + * + * @hide + */ + 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; + + /** + * 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; + + /** + * This download doesn't show in the UI or in the notifications. + * + * @hide + */ + public static final int VISIBILITY_HIDDEN = 2; + + /** + * Bit flag for {@link #setAllowedNetworkTypes} corresponding to + * {@link ConnectivityManager#TYPE_MOBILE}. + */ + public static final int NETWORK_MOBILE = 1 << 0; + + /** + * Bit flag for {@link #setAllowedNetworkTypes} corresponding to + * {@link ConnectivityManager#TYPE_WIFI}. + */ + public static final int NETWORK_WIFI = 1 << 1; + + private final static String TEMP_EXT = ".tmp"; + + /** + * Service thread status + */ + private static boolean sIsRunning; + + @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; + + /** + * Download state + */ + private int mControl; + private int mStatus; + + public boolean isWiFi() { + return mIsConnected && !mIsCellularConnection; + } + + /** + * Bindings to important services + */ + private ConnectivityManager mConnectivityManager; + private WifiManager mWifiManager; + + /** + * Package we are downloading for (defaults to package of application) + */ + private PackageInfo mPackageInfo; + + /** + * Byte counts + */ + long mBytesSoFar; + long mTotalLength; + int mFileCount; + + /** + * Used for calculating time remaining and speed + */ + 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; + + /** + * 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; + } + } + } + + } + } + } + + /** + * Polls the network state, setting the flags appropriately. + */ + void pollNetworkState() { + if (null == mConnectivityManager) { + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + } + if (null == mWifiManager) { + mWifiManager = (WifiManager) 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"; + + /** + * 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; + } + + /** + * 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); + } + + /** + * Starts the download if necessary. This function starts a flow that does ` + * many things. 1) Checks to see if the APK version has been checked and the + * metadata database updated 2) If the APK version does not match, checks + * the new LVL status to see if a new download is required 3) If the APK + * version does match, then checks to see if the download(s) have been + * completed 4) If the downloads have been completed, returns + * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the + * startup of an application to quickly ascertain if the application needs + * to wait to hear about any updated APK expansion files. Note that this + * does mean that the application MUST be run for the first time with a + * network connection, even if Market delivers all of the files. + * + * @param context + * @param thisIntent + * @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); + } + } + + }); + + } + + }; + + /** + * 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)); + } + + /** + * 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; + } + } + + /** + * 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); + } + } + }; + + /** + * 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) { + + /** + * 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(); + } + } + + /** + * 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 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; + } + + /** + * 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; + } + + /** + * @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"; + + 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_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"; + + default: + return "unknown error with network connectivity"; + } + } + + public int getControl() { + return mControl; + } + + 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); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java new file mode 100755 index 0000000000..250299c400 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.provider.BaseColumns; +import android.util.Log; + +public class DownloadsDB { + private static final String DATABASE_NAME = "DownloadsDB"; + private static final int DATABASE_VERSION = 7; + public static final String LOG_TAG = DownloadsDB.class.getName(); + final SQLiteOpenHelper mHelper; + SQLiteStatement mGetDownloadByIndex; + SQLiteStatement mUpdateCurrentBytes; + private static DownloadsDB mDownloadsDB; + long mMetadataRowID = -1; + int mVersionCode = -1; + int mStatus = -1; + int mFlags; + + static public synchronized DownloadsDB getDB(Context paramContext) { + if (null == mDownloadsDB) { + return new DownloadsDB(paramContext); + } + return mDownloadsDB; + } + + private SQLiteStatement getDownloadByIndexStatement() { + if (null == mGetDownloadByIndex) { + mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( + "SELECT " + BaseColumns._ID + " FROM " + + DownloadColumns.TABLE_NAME + " WHERE " + + DownloadColumns.INDEX + " = ?"); + } + return mGetDownloadByIndex; + } + + private SQLiteStatement getUpdateCurrentBytesStatement() { + if (null == mUpdateCurrentBytes) { + mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( + "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + + " = ?" + + " WHERE " + DownloadColumns.INDEX + " = ?"); + } + return mUpdateCurrentBytes; + } + + private DownloadsDB(Context paramContext) { + this.mHelper = new DownloadsContentDBHelper(paramContext); + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + // Query for the version code, the row ID of the metadata (for future + // updating) the status and the flags + Cursor cur = sqldb.rawQuery("SELECT " + + MetadataColumns.APKVERSION + "," + + BaseColumns._ID + "," + + MetadataColumns.DOWNLOAD_STATUS + "," + + MetadataColumns.FLAGS + + " FROM " + + MetadataColumns.TABLE_NAME + " LIMIT 1", null); + if (null != cur && cur.moveToFirst()) { + mVersionCode = cur.getInt(0); + mMetadataRowID = cur.getLong(1); + mStatus = cur.getInt(2); + mFlags = cur.getInt(3); + cur.close(); + } + mDownloadsDB = this; + } + + protected DownloadInfo getDownloadInfoByFileName(String fileName) { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor itemcur = null; + try { + itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, + DownloadColumns.FILENAME + " = ?", + new String[] { + fileName + }, null, null, null); + if (null != itemcur && itemcur.moveToFirst()) { + return getDownloadInfoFromCursor(itemcur); + } + } finally { + if (null != itemcur) + itemcur.close(); + } + return null; + } + + public long getIDForDownloadInfo(final DownloadInfo di) { + return getIDByIndex(di.mIndex); + } + + public long getIDByIndex(int index) { + SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); + downloadByIndex.clearBindings(); + downloadByIndex.bindLong(1, index); + try { + return downloadByIndex.simpleQueryForLong(); + } catch (SQLiteDoneException e) { + return -1; + } + } + + public void updateDownloadCurrentBytes(final DownloadInfo di) { + SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); + downloadCurrentBytes.clearBindings(); + downloadCurrentBytes.bindLong(1, di.mCurrentBytes); + downloadCurrentBytes.bindLong(2, di.mIndex); + downloadCurrentBytes.execute(); + } + + public void close() { + this.mHelper.close(); + } + + protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { + DownloadsContentDBHelper(Context paramContext) { + super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); + } + + private String createTableQueryFromArray(String paramString, + String[][] paramArrayOfString) { + StringBuilder localStringBuilder = new StringBuilder(); + localStringBuilder.append("CREATE TABLE "); + localStringBuilder.append(paramString); + localStringBuilder.append(" ("); + int i = paramArrayOfString.length; + for (int j = 0;; j++) { + if (j >= i) { + localStringBuilder + .setLength(localStringBuilder.length() - 1); + localStringBuilder.append(");"); + return localStringBuilder.toString(); + } + String[] arrayOfString = paramArrayOfString[j]; + localStringBuilder.append(' '); + localStringBuilder.append(arrayOfString[0]); + localStringBuilder.append(' '); + localStringBuilder.append(arrayOfString[1]); + localStringBuilder.append(','); + } + } + + /** + * These two arrays must match and have the same order. For every Schema + * there must be a corresponding table name. + */ + static final private String[][][] sSchemas = { + DownloadColumns.SCHEMA, MetadataColumns.SCHEMA + }; + + static final private String[] sTables = { + DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME + }; + + /** + * Goes through all of the tables in sTables and drops each table if it + * exists. Altered to no longer make use of reflection. + */ + private void dropTables(SQLiteDatabase paramSQLiteDatabase) { + for (String table : sTables) { + try { + paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); + } catch (Exception localException) { + localException.printStackTrace(); + } + } + } + + /** + * Goes through all of the tables in sTables and creates a database with + * the corresponding schema described in sSchemas. Altered to no longer + * make use of reflection. + */ + public void onCreate(SQLiteDatabase paramSQLiteDatabase) { + int numSchemas = sSchemas.length; + for (int i = 0; i < numSchemas; i++) { + try { + String[][] schema = (String[][]) sSchemas[i]; + paramSQLiteDatabase.execSQL(createTableQueryFromArray( + sTables[i], schema)); + } catch (Exception localException) { + while (true) + localException.printStackTrace(); + } + } + } + + public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, + int paramInt1, int paramInt2) { + Log.w(DownloadsContentDBHelper.class.getName(), + "Upgrading database from version " + paramInt1 + " to " + + paramInt2 + ", which will destroy all old data"); + dropTables(paramSQLiteDatabase); + onCreate(paramSQLiteDatabase); + } + } + + public static class MetadataColumns implements BaseColumns { + public static final String APKVERSION = "APKVERSION"; + public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; + public static final String FLAGS = "DOWNLOADFLAGS"; + + public static final String[][] SCHEMA = { + { + BaseColumns._ID, "INTEGER PRIMARY KEY" + }, + { + APKVERSION, "INTEGER" + }, { + DOWNLOAD_STATUS, "INTEGER" + }, + { + FLAGS, "INTEGER" + } + }; + public static final String TABLE_NAME = "MetadataColumns"; + public static final String _ID = "MetadataColumns._id"; + } + + public static class DownloadColumns implements BaseColumns { + public static final String INDEX = "FILEIDX"; + public static final String URI = "URI"; + public static final String FILENAME = "FN"; + public static final String ETAG = "ETAG"; + + public static final String TOTALBYTES = "TOTALBYTES"; + public static final String CURRENTBYTES = "CURRENTBYTES"; + public static final String LASTMOD = "LASTMOD"; + + public static final String STATUS = "STATUS"; + public static final String CONTROL = "CONTROL"; + public static final String NUM_FAILED = "FAILCOUNT"; + public static final String RETRY_AFTER = "RETRYAFTER"; + public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; + + public static final String[][] SCHEMA = { + { + BaseColumns._ID, "INTEGER PRIMARY KEY" + }, + { + INDEX, "INTEGER UNIQUE" + }, { + URI, "TEXT" + }, + { + FILENAME, "TEXT UNIQUE" + }, { + ETAG, "TEXT" + }, + { + TOTALBYTES, "INTEGER" + }, { + CURRENTBYTES, "INTEGER" + }, + { + LASTMOD, "INTEGER" + }, { + STATUS, "INTEGER" + }, + { + CONTROL, "INTEGER" + }, { + NUM_FAILED, "INTEGER" + }, + { + RETRY_AFTER, "INTEGER" + }, { + REDIRECT_COUNT, "INTEGER" + } + }; + public static final String TABLE_NAME = "DownloadColumns"; + public static final String _ID = "DownloadColumns._id"; + } + + private static final String[] DC_PROJECTION = { + DownloadColumns.FILENAME, + DownloadColumns.URI, DownloadColumns.ETAG, + DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, + DownloadColumns.LASTMOD, DownloadColumns.STATUS, + DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, + DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, + DownloadColumns.INDEX + }; + + private static final int FILENAME_IDX = 0; + private static final int URI_IDX = 1; + private static final int ETAG_IDX = 2; + private static final int TOTALBYTES_IDX = 3; + private static final int CURRENTBYTES_IDX = 4; + private static final int LASTMOD_IDX = 5; + private static final int STATUS_IDX = 6; + private static final int CONTROL_IDX = 7; + private static final int NUM_FAILED_IDX = 8; + private static final int RETRY_AFTER_IDX = 9; + private static final int REDIRECT_COUNT_IDX = 10; + private static final int INDEX_IDX = 11; + + /** + * This function will add a new file to the database if it does not exist. + * + * @param di DownloadInfo that we wish to store + * @return the row id of the record to be updated/inserted, or -1 + */ + public boolean updateDownload(DownloadInfo di) { + ContentValues cv = new ContentValues(); + cv.put(DownloadColumns.INDEX, di.mIndex); + cv.put(DownloadColumns.FILENAME, di.mFileName); + cv.put(DownloadColumns.URI, di.mUri); + cv.put(DownloadColumns.ETAG, di.mETag); + cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); + cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); + cv.put(DownloadColumns.LASTMOD, di.mLastMod); + cv.put(DownloadColumns.STATUS, di.mStatus); + cv.put(DownloadColumns.CONTROL, di.mControl); + cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); + cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); + cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); + return updateDownload(di, cv); + } + + public boolean updateDownload(DownloadInfo di, ContentValues cv) { + long id = di == null ? -1 : getIDForDownloadInfo(di); + try { + final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); + if (id != -1) { + if (1 != sqldb.update(DownloadColumns.TABLE_NAME, + cv, DownloadColumns._ID + " = " + id, null)) { + return false; + } + } else { + return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, + DownloadColumns.URI, cv); + } + } catch (android.database.sqlite.SQLiteException ex) { + ex.printStackTrace(); + } + return false; + } + + public int getLastCheckedVersionCode() { + return mVersionCode; + } + + public boolean isDownloadRequired() { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + + DownloadColumns.TABLE_NAME + " WHERE " + + DownloadColumns.STATUS + " <> 0", null); + try { + if (null != cur && cur.moveToFirst()) { + return 0 == cur.getInt(0); + } + } finally { + if (null != cur) + cur.close(); + } + return true; + } + + public int getFlags() { + return mFlags; + } + + public boolean updateFlags(int flags) { + if (mFlags != flags) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.FLAGS, flags); + if (updateMetadata(cv)) { + mFlags = flags; + return true; + } else { + return false; + } + } else { + return true; + } + }; + + public boolean updateStatus(int status) { + if (mStatus != status) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.DOWNLOAD_STATUS, status); + if (updateMetadata(cv)) { + mStatus = status; + return true; + } else { + return false; + } + } else { + return true; + } + }; + + public boolean updateMetadata(ContentValues cv) { + final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); + if (-1 == this.mMetadataRowID) { + long newID = sqldb.insert(MetadataColumns.TABLE_NAME, + MetadataColumns.APKVERSION, cv); + if (-1 == newID) + return false; + mMetadataRowID = newID; + } else { + if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, + BaseColumns._ID + " = " + mMetadataRowID, null)) + return false; + } + return true; + } + + public boolean updateMetadata(int apkVersion, int downloadStatus) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.APKVERSION, apkVersion); + cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); + if (updateMetadata(cv)) { + mVersionCode = apkVersion; + mStatus = downloadStatus; + return true; + } else { + return false; + } + }; + + public boolean updateFromDb(DownloadInfo di) { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = null; + try { + cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, + DownloadColumns.FILENAME + "= ?", + new String[] { + di.mFileName + }, null, null, null); + if (null != cur && cur.moveToFirst()) { + setDownloadInfoFromCursor(di, cur); + return true; + } + return false; + } finally { + if (null != cur) { + cur.close(); + } + } + } + + public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { + di.mUri = cur.getString(URI_IDX); + di.mETag = cur.getString(ETAG_IDX); + di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); + di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); + di.mLastMod = cur.getLong(LASTMOD_IDX); + di.mStatus = cur.getInt(STATUS_IDX); + di.mControl = cur.getInt(CONTROL_IDX); + di.mNumFailed = cur.getInt(NUM_FAILED_IDX); + di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); + di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); + } + + public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { + DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), + cur.getString(FILENAME_IDX), this.getClass().getPackage() + .getName()); + setDownloadInfoFromCursor(di, cur); + return di; + } + + public DownloadInfo[] getDownloads() { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = null; + try { + cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, + null, null, null, null); + if (null != cur && cur.moveToFirst()) { + DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; + int idx = 0; + do { + DownloadInfo di = getDownloadInfoFromCursor(cur); + retInfos[idx++] = di; + } while (cur.moveToNext()); + return retInfos; + } + return null; + } finally { + if (null != cur) { + cur.close(); + } + } + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java new file mode 100644 index 0000000000..3f440e9893 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.text.format.Time; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper for parsing an HTTP date. + */ +public final class HttpDateTime { + + /* + * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT + * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850, + * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format + * with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon + * YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS + * GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon + * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first + * digit is zero. Mon can be the full name of the month. + */ + private static final String HTTP_DATE_RFC_REGEXP = + "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; + + private static final String HTTP_DATE_ANSIC_REGEXP = + "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + /** + * The compiled version of the HTTP-date regular expressions. + */ + private static final Pattern HTTP_DATE_RFC_PATTERN = + Pattern.compile(HTTP_DATE_RFC_REGEXP); + private static final Pattern HTTP_DATE_ANSIC_PATTERN = + Pattern.compile(HTTP_DATE_ANSIC_REGEXP); + + private static class TimeOfDay { + TimeOfDay(int h, int m, int s) { + this.hour = h; + this.minute = m; + this.second = s; + } + + int hour; + int minute; + int second; + } + + public static long parse(String timeString) + throws IllegalArgumentException { + + int date = 1; + int month = Calendar.JANUARY; + int year = 1970; + TimeOfDay timeOfDay; + + Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); + if (rfcMatcher.find()) { + date = getDate(rfcMatcher.group(1)); + month = getMonth(rfcMatcher.group(2)); + year = getYear(rfcMatcher.group(3)); + timeOfDay = getTime(rfcMatcher.group(4)); + } else { + Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); + if (ansicMatcher.find()) { + month = getMonth(ansicMatcher.group(1)); + date = getDate(ansicMatcher.group(2)); + timeOfDay = getTime(ansicMatcher.group(3)); + year = getYear(ansicMatcher.group(4)); + } else { + throw new IllegalArgumentException(); + } + } + + // FIXME: Y2038 BUG! + if (year >= 2038) { + year = 2038; + month = Calendar.JANUARY; + date = 1; + } + + Time time = new Time(Time.TIMEZONE_UTC); + time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, + month, year); + return time.toMillis(false /* use isDst */); + } + + private static int getDate(String dateString) { + if (dateString.length() == 2) { + return (dateString.charAt(0) - '0') * 10 + + (dateString.charAt(1) - '0'); + } else { + return (dateString.charAt(0) - '0'); + } + } + + /* + * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0 + * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20 + * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19 + * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9 + */ + private static int getMonth(String monthString) { + int hash = Character.toLowerCase(monthString.charAt(0)) + + Character.toLowerCase(monthString.charAt(1)) + + Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; + switch (hash) { + case 22: + return Calendar.JANUARY; + case 10: + return Calendar.FEBRUARY; + case 29: + return Calendar.MARCH; + case 32: + return Calendar.APRIL; + case 36: + return Calendar.MAY; + case 42: + return Calendar.JUNE; + case 40: + return Calendar.JULY; + case 26: + return Calendar.AUGUST; + case 37: + return Calendar.SEPTEMBER; + case 35: + return Calendar.OCTOBER; + case 48: + return Calendar.NOVEMBER; + case 9: + return Calendar.DECEMBER; + default: + throw new IllegalArgumentException(); + } + } + + private static int getYear(String yearString) { + if (yearString.length() == 2) { + int year = (yearString.charAt(0) - '0') * 10 + + (yearString.charAt(1) - '0'); + if (year >= 70) { + return year + 1900; + } else { + return year + 2000; + } + } else if (yearString.length() == 3) { + // According to RFC 2822, three digit years should be added to 1900. + int year = (yearString.charAt(0) - '0') * 100 + + (yearString.charAt(1) - '0') * 10 + + (yearString.charAt(2) - '0'); + return year + 1900; + } else if (yearString.length() == 4) { + return (yearString.charAt(0) - '0') * 1000 + + (yearString.charAt(1) - '0') * 100 + + (yearString.charAt(2) - '0') * 10 + + (yearString.charAt(3) - '0'); + } else { + return 1970; + } + } + + private static TimeOfDay getTime(String timeString) { + // HH might be H + int i = 0; + int hour = timeString.charAt(i++) - '0'; + if (timeString.charAt(i) != ':') + hour = hour * 10 + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int minute = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int second = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + + return new TimeOfDay(hour, minute, second); + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java new file mode 100644 index 0000000000..e736603e2a --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java @@ -0,0 +1,101 @@ +/* + * 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.android.vending.expansion.downloader.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 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.getNotification(); + } + + @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/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V3CustomNotification.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V3CustomNotification.java new file mode 100644 index 0000000000..e3666e05b9 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V3CustomNotification.java @@ -0,0 +1,116 @@ +/* + * 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.android.vending.expansion.downloader.R; +import com.google.android.vending.expansion.downloader.Helpers; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.BitmapFactory; +import android.view.View; +import android.widget.RemoteViews; + +public class V3CustomNotification implements DownloadNotification.ICustomNotification { + + CharSequence mTitle; + CharSequence mTicker; + int mIcon; + long mTotalBytes = -1; + long mCurrentBytes = -1; + long mTimeRemaining; + PendingIntent mPendingIntent; + Notification mNotification = new Notification(); + + @Override + public void setIcon(int icon) { + mIcon = icon; + } + + @Override + public void setTitle(CharSequence title) { + mTitle = title; + } + + @Override + public void setTotalBytes(long totalBytes) { + mTotalBytes = totalBytes; + } + + @Override + public void setCurrentBytes(long currentBytes) { + mCurrentBytes = currentBytes; + } + + @Override + public Notification updateNotification(Context c) { + Notification n = mNotification; + + n.icon = mIcon; + + n.flags |= Notification.FLAG_ONGOING_EVENT; + + if (android.os.Build.VERSION.SDK_INT > 10) { + n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; // only matters for + // Honeycomb + } + + // Build the RemoteView object + RemoteViews expandedView = new RemoteViews( + c.getPackageName(), + R.layout.status_bar_ongoing_event_progress_bar); + + expandedView.setTextViewText(R.id.title, mTitle); + // look at strings + expandedView.setViewVisibility(R.id.description, View.VISIBLE); + expandedView.setTextViewText(R.id.description, + Helpers.getDownloadProgressString(mCurrentBytes, mTotalBytes)); + expandedView.setViewVisibility(R.id.progress_bar_frame, View.VISIBLE); + expandedView.setProgressBar(R.id.progress_bar, + (int) (mTotalBytes >> 8), + (int) (mCurrentBytes >> 8), + mTotalBytes <= 0); + expandedView.setViewVisibility(R.id.time_remaining, View.VISIBLE); + expandedView.setTextViewText( + R.id.time_remaining, + c.getString(R.string.time_remaining_notification, + Helpers.getTimeRemaining(mTimeRemaining))); + expandedView.setTextViewText(R.id.progress_text, + Helpers.getDownloadProgressPercent(mCurrentBytes, mTotalBytes)); + expandedView.setImageViewResource(R.id.appIcon, mIcon); + n.contentView = expandedView; + n.contentIntent = mPendingIntent; + return n; + } + + @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/libs/play_licensing/AndroidManifest.xml b/platform/android/libs/play_licensing/AndroidManifest.xml new file mode 100644 index 0000000000..c7849130c3 --- /dev/null +++ b/platform/android/libs/play_licensing/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.vending.licensing" + android:versionCode="2" + android:versionName="1.5"> + <!-- Devices >= 3 have version of Android Market that supports licensing. --> + <uses-sdk android:minSdkVersion="3" android:targetSdkVersion="15" /> + <!-- Required permission to check licensing. --> + <uses-permission android:name="com.android.vending.CHECK_LICENSE" /> +</manifest> diff --git a/platform/android/libs/play_licensing/aidl/ILicenseResultListener.aidl b/platform/android/libs/play_licensing/aidl/ILicenseResultListener.aidl new file mode 100644 index 0000000000..c816558afc --- /dev/null +++ b/platform/android/libs/play_licensing/aidl/ILicenseResultListener.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.licensing; + +// Android library projects do not yet support AIDL, so this has been +// precompiled into the src directory. +oneway interface ILicenseResultListener { + void verifyLicense(int responseCode, String signedData, String signature); +} diff --git a/platform/android/libs/play_licensing/aidl/ILicensingService.aidl b/platform/android/libs/play_licensing/aidl/ILicensingService.aidl new file mode 100644 index 0000000000..664510ce0c --- /dev/null +++ b/platform/android/libs/play_licensing/aidl/ILicensingService.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.licensing; + +import com.android.vending.licensing.ILicenseResultListener; + +// Android library projects do not yet support AIDL, so this has been +// precompiled into the src directory. +oneway interface ILicensingService { + void checkLicense(long nonce, String packageName, in ILicenseResultListener listener); +} diff --git a/platform/android/libs/play_licensing/build.xml b/platform/android/libs/play_licensing/build.xml new file mode 100644 index 0000000000..0e800d6b9b --- /dev/null +++ b/platform/android/libs/play_licensing/build.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="play_licensing" default="help"> + + <!-- The local.properties file is created and updated by the 'android' tool. + It contains the path to the SDK. It should *NOT* be checked into + Version Control Systems. --> + <property file="local.properties" /> + + <!-- The ant.properties file can be created by you. It is only edited by the + 'android' tool to add properties to it. + This is the place to change some Ant specific build properties. + Here are some properties you may want to change/update: + + source.dir + The name of the source directory. Default is 'src'. + out.dir + The name of the output directory. Default is 'bin'. + + For other overridable properties, look at the beginning of the rules + files in the SDK, at tools/ant/build.xml + + Properties related to the SDK location or the project target should + be updated using the 'android' tool with the 'update' action. + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. + + --> + <property file="ant.properties" /> + + <!-- if sdk.dir was not set from one of the property file, then + get it from the ANDROID_HOME env var. + This must be done before we load project.properties since + the proguard config can use sdk.dir --> + <property environment="env" /> + <condition property="sdk.dir" value="${env.ANDROID_HOME}"> + <isset property="env.ANDROID_HOME" /> + </condition> + + <!-- The project.properties file is created and updated by the 'android' + tool, as well as ADT. + + This contains project specific properties such as project target, and library + dependencies. Lower level build properties are stored in ant.properties + (or in .classpath for Eclipse projects). + + This file is an integral part of the build system for your + application and should be checked into Version Control Systems. --> + <loadproperties srcFile="project.properties" /> + + <!-- quick check on sdk.dir --> + <fail + message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable." + unless="sdk.dir" + /> + + <!-- + Import per project custom build rules if present at the root of the project. + This is the place to put custom intermediary targets such as: + -pre-build + -pre-compile + -post-compile (This is typically used for code obfuscation. + Compiled code location: ${out.classes.absolute.dir} + If this is not done in place, override ${out.dex.input.absolute.dir}) + -post-package + -post-build + -pre-clean + --> + <import file="custom_rules.xml" optional="true" /> + + <!-- Import the actual build file. + + To customize existing targets, there are two options: + - Customize only one target: + - copy/paste the target into this file, *before* the + <import> task. + - customize it to your needs. + - Customize the whole content of build.xml + - copy/paste the content of the rules files (minus the top node) + into this file, replacing the <import> task. + - customize to your needs. + + *********************** + ****** IMPORTANT ****** + *********************** + In all cases you must update the value of version-tag below to read 'custom' instead of an integer, + in order to avoid having your file be overridden by tools such as "android update project" + --> + <!-- version-tag: 1 --> + <import file="${sdk.dir}/tools/ant/build.xml" /> + +</project> diff --git a/platform/android/libs/play_licensing/proguard-project.txt b/platform/android/libs/play_licensing/proguard-project.txt new file mode 100644 index 0000000000..f2fe1559a2 --- /dev/null +++ b/platform/android/libs/play_licensing/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/platform/android/libs/play_licensing/project.properties b/platform/android/libs/play_licensing/project.properties new file mode 100644 index 0000000000..f28bc833e1 --- /dev/null +++ b/platform/android/libs/play_licensing/project.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +android.library=true +# Project target. +target=android-15 diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/AESObfuscator.java new file mode 100644 index 0000000000..ee12c68deb --- /dev/null +++ b/platform/android/libs/play_licensing/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.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/libs/play_licensing/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000000..17cc7a7cfd --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -0,0 +1,397 @@ + +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/libs/play_licensing/src/com/google/android/vending/licensing/DeviceLimiter.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/DeviceLimiter.java new file mode 100644 index 0000000000..e5c5e2d7ca --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/DeviceLimiter.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Allows the developer to limit the number of devices using a single license. + * <p> + * The LICENSED response from the server contains a user identifier unique to + * the <application, user> pair. The developer can send this identifier + * to their own server along with some device identifier (a random number + * generated and stored once per application installation, + * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId}, + * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc). + * The more sources used to identify the device, the harder it will be for an + * attacker to spoof. + * <p> + * The server can look at the <application, user, device id> tuple and + * restrict a user's application license to run on at most 10 different devices + * in a week (for example). We recommend not being too restrictive because a + * user might legitimately have multiple devices or be in the process of + * changing phones. This will catch egregious violations of multiple people + * sharing one license. + */ +public interface DeviceLimiter { + + /** + * Checks if this device is allowed to use the given user's license. + * + * @param userId the user whose license the server responded with + * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs + */ + int isDeviceAllowed(String userId); +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java new file mode 100644 index 0000000000..d90d6eac7b --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java @@ -0,0 +1,99 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java new file mode 100644 index 0000000000..95599544e4 --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java @@ -0,0 +1,99 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..0b1c4b6cca --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,351 @@ +/* + * 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 { + boolean bindResult = mContext + .bindService( + new Intent( + new String( + Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))), + 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/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java new file mode 100644 index 0000000000..b250a7147b --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Callback for the license checker library. + * <p> + * Upon checking with the Market server and conferring with the {@link Policy}, + * the library calls the appropriate callback method to communicate the result. + * <p> + * <b>The callback does not occur in the original checking thread.</b> Your + * application should post to the appropriate handling thread or lock + * accordingly. + * <p> + * The reason that is passed back with allow/dontAllow is the base status handed + * to the policy for allowed/disallowing the license. Policy.RETRY will call + * allow or dontAllow depending on other statistics associated with the policy, + * while in most cases Policy.NOT_LICENSED will call dontAllow and + * Policy.LICENSED will Allow. + */ +public interface LicenseCheckerCallback { + + /** + * Allow use. App should proceed as normal. + * + * @param reason Policy.LICENSED or Policy.RETRY typically. (although in + * theory the policy can return Policy.NOT_LICENSED here as well) + */ + public void allow(int reason); + + /** + * Don't allow use. App should inform user and take appropriate action. + * + * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory + * the policy can return Policy.LICENSED here as well --- + * perhaps the call to the LVL took too long, for example) + */ + public void dontAllow(int reason); + + /** Application error codes. */ + public static final int ERROR_INVALID_PACKAGE_NAME = 1; + public static final int ERROR_NON_MATCHING_UID = 2; + public static final int ERROR_NOT_MARKET_MANAGED = 3; + public static final int ERROR_CHECK_IN_PROGRESS = 4; + public static final int ERROR_INVALID_PUBLIC_KEY = 5; + public static final int ERROR_MISSING_PERMISSION = 6; + + /** + * Error in application code. Caller did not call or set up license checker + * correctly. Should be considered fatal. + */ + public void applicationError(int errorCode); +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java new file mode 100644 index 0000000000..61d3c7e79e --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java @@ -0,0 +1,224 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java new file mode 100644 index 0000000000..d87af3153f --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * A DeviceLimiter that doesn't limit the number of devices that can use a + * given user's license. + * <p> + * Unless you have reason to believe that your application is being pirated + * by multiple users using the same license (signing in to Market as the same + * user), we recommend you use this implementation. + */ +public class NullDeviceLimiter implements DeviceLimiter { + + public int isDeviceAllowed(String userId) { + return Policy.LICENSED; + } +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java new file mode 100644 index 0000000000..b5d510d72d --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Interface used as part of a {@link Policy} to allow application authors to obfuscate + * licensing data that will be stored into a SharedPreferences file. + * <p> + * Any transformation scheme must be 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); + + /** + * 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. + * @throws ValidationException Optionally thrown if a data integrity check fails. + */ + String unobfuscate(String obfuscated, String key) throws ValidationException; +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java new file mode 100644 index 0000000000..fa267fc71a --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Policy used by {@link LicenseChecker} to determine whether a user should have + * access to the application. + */ +public interface Policy { + + /** + * Change these values to make it more difficult for tools to automatically + * strip LVL protection from your APK. + */ + + /** + * LICENSED means that the server returned back a valid license response + */ + public static final int LICENSED = 0x0100; + /** + * NOT_LICENSED means that the server returned back a valid license response + * that indicated that the user definitively is not licensed + */ + public static final int NOT_LICENSED = 0x0231; + /** + * RETRY means that the license response was unable to be determined --- + * perhaps as a result of faulty networking + */ + public static final int RETRY = 0x0123; + + /** + * Provide results from contact with the license server. Retry counts are + * incremented if the current value of response is RETRY. Results will be + * used for any future policy decisions. + * + * @param response the result from validating the server response + * @param rawData the raw server response data, can be null for RETRY + */ + void processServerResponse(int response, ResponseData rawData); + + /** + * Check if the user should be allowed access to the application. + */ + boolean allowAccess(); +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java new file mode 100644 index 0000000000..7c42bfc28a --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java @@ -0,0 +1,77 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java new file mode 100644 index 0000000000..2adef3709e --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java @@ -0,0 +1,79 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000000..fbf8cf6d00 --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -0,0 +1,276 @@ +/* + * 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/libs/play_licensing/src/com/google/android/vending/licensing/StrictPolicy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/StrictPolicy.java new file mode 100644 index 0000000000..d8d83b4e4b --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/StrictPolicy.java @@ -0,0 +1,63 @@ +/* + * 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; + +/** + * Non-caching policy. All requests will be sent to the licensing service, + * and no local caching is performed. + * <p> + * Using a non-caching policy ensures that there is no local preference data + * for malicious users to tamper with. As a side effect, applications + * will not be permitted to run while offline. Developers should carefully + * weigh the risks of using this Policy over one which implements caching, + * such as ServerManagedPolicy. + * <p> + * Access to the application is only allowed if a LICESNED response is. + * received. All other responses (including RETRY) will deny access. + */ +public class StrictPolicy implements Policy { + + private int mLastResponse; + + public StrictPolicy() { + // Set default policy. This will force the application to check the policy on launch. + mLastResponse = Policy.RETRY; + } + + /** + * 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. + * + * @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; + } + + /** + * {@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); + } + +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java new file mode 100644 index 0000000000..ee4df47c68 --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Indicates that an error occurred while validating the integrity of data managed by an + * {@link Obfuscator}.} + */ +public class ValidationException extends Exception { + public ValidationException() { + super(); + } + + public ValidationException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java new file mode 100644 index 0000000000..a0d2779af2 --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java @@ -0,0 +1,570 @@ +// 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/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java new file mode 100644 index 0000000000..1aef1b54b8 --- /dev/null +++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.vending.licensing.util; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 9d9da3750e..3a101515da 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -34,7 +34,7 @@ #include "drivers/unix/dir_access_unix.h" #include "servers/visual/visual_server_raster.h" - +#include "servers/visual/visual_server_wrap_mt.h" #include "main/main.h" #include "core/globals.h" @@ -128,6 +128,10 @@ void OS_Android::initialize(const VideoMode& p_desired,int p_video_driver,int p_ } visual_server = memnew( VisualServerRaster(rasterizer) ); + if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { + + visual_server = memnew(VisualServerWrapMT(visual_server, false)); + }; visual_server->init(); visual_server->cursor_set_visible(false, 0); @@ -672,8 +676,29 @@ String OS_Android::get_unique_ID() const { return OS::get_unique_ID(); } +Error OS_Android::native_video_play(String p_path) { + if (video_play_func) + video_play_func(p_path); + return OK; +} + +bool OS_Android::native_video_is_playing() { + if (video_is_playing_func) + return video_is_playing_func(); + return false; +} + +void OS_Android::native_video_pause() { + if (video_pause_func) + video_pause_func(); +} + +void OS_Android::native_video_stop() { + if (video_stop_func) + video_stop_func(); +} -OS_Android::OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id) { +OS_Android::OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id, VideoPlayFunc p_video_play_func, VideoIsPlayingFunc p_video_is_playing_func, VideoPauseFunc p_video_pause_func, VideoStopFunc p_video_stop_func) { default_videomode.width=800; diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 93c672927e..76139c8a2d 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -41,16 +41,19 @@ #ifdef ANDROID_NATIVE_ACTIVITY -#include "audio_driver_android.h" + #include <android/sensor.h> #include <android/log.h> #include <android_native_app_glue.h> #else -#include "audio_driver_jandroid.h" + #endif +#include "audio_driver_jandroid.h" +#include "audio_driver_opensl.h" + typedef void (*GFXInitFunc)(void *ud,bool gl2); typedef int (*OpenURIFunc)(const String&); typedef String (*GetDataDirFunc)(); @@ -61,6 +64,11 @@ typedef void (*ShowVirtualKeyboardFunc)(const String&); typedef void (*HideVirtualKeyboardFunc)(); typedef void (*SetScreenOrientationFunc)(int); +typedef void (*VideoPlayFunc)(const String&); +typedef bool (*VideoIsPlayingFunc)(); +typedef void (*VideoPauseFunc)(); +typedef void (*VideoStopFunc)(); + class OS_Android : public OS_Unix { public: @@ -89,7 +97,12 @@ private: SpatialSound2DServerSW *spatial_sound_2d_server; PhysicsServer *physics_server; Physics2DServer *physics_2d_server; +#if 0 AudioDriverAndroid audio_driver_android; +#else + AudioDriverOpenSL audio_driver_android; +#endif + const char* gl_extensions; InputDefault *input; @@ -105,6 +118,11 @@ private: SetScreenOrientationFunc set_screen_orientation_func; GetUniqueIDFunc get_unique_id_func; + VideoPlayFunc video_play_func; + VideoIsPlayingFunc video_is_playing_func; + VideoPauseFunc video_pause_func; + VideoStopFunc video_stop_func; + public: // functions used by main to initialize/deintialize the OS @@ -189,7 +207,13 @@ public: void process_touch(int p_what,int p_pointer, const Vector<TouchPos>& p_points); void process_event(InputEvent p_event); void init_video_mode(int p_video_width,int p_video_height); - OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id); + + virtual Error native_video_play(String p_path); + virtual bool native_video_is_playing(); + virtual void native_video_pause(); + virtual void native_video_stop(); + + OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id, VideoPlayFunc p_video_play_func, VideoIsPlayingFunc p_video_is_playing_func, VideoPauseFunc p_video_pause_func, VideoStopFunc p_video_stop_func); ~OS_Android(); }; diff --git a/platform/android/project.properties.template b/platform/android/project.properties.template index 72bea9659c..d81b72b525 100644 --- a/platform/android/project.properties.template +++ b/platform/android/project.properties.template @@ -12,4 +12,4 @@ # Project target. #android.library=true -target=android-13 +target=android-15 diff --git a/platform/android/sign.sh b/platform/android/sign.sh index 830da05a37..8f760e6312 100755 --- a/platform/android/sign.sh +++ b/platform/android/sign.sh @@ -1,6 +1,6 @@ #!/bin/bash -jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore my-release-key.keystore "$1" reduz +jarsigner -digestalg SHA1 -sigalg MD5withRSA -verbose -keystore /home/luis/Downloads/carnavalguachin.keystore -storepass 12345678 "$1" momoselacome echo "" echo "" diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index ed935839d8..61ec87ccc1 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -82,7 +82,8 @@ def configure(env): '-framework', 'AudioToolbox', '-framework', 'SystemConfiguration', '-framework', 'Security', - '-framework', 'AdSupport', + #'-framework', 'AdSupport', + '-framework', 'MediaPlayer', ]) if env['game_center'] == 'yes': diff --git a/platform/iphone/game_center.h b/platform/iphone/game_center.h index 58a040eb6d..df1d980c04 100644 --- a/platform/iphone/game_center.h +++ b/platform/iphone/game_center.h @@ -50,7 +50,6 @@ public: bool is_connected(); Error post_score(Variant p_score); - Error award_achievement(Variant p_params); int get_pending_event_count(); diff --git a/platform/iphone/gl_view.h b/platform/iphone/gl_view.h index 0d95af9ab1..3e6181ab90 100755 --- a/platform/iphone/gl_view.h +++ b/platform/iphone/gl_view.h @@ -31,6 +31,7 @@ #import <OpenGLES/EAGL.h> #import <OpenGLES/ES1/gl.h> #import <OpenGLES/ES1/glext.h> +#import <MediaPlayer/MediaPlayer.h> @protocol GLViewDelegate; @@ -65,6 +66,9 @@ @property(nonatomic, assign) id<GLViewDelegate> delegate; +@property(strong, nonatomic) MPMoviePlayerController *moviePlayerController; +@property(strong, nonatomic) UIWindow *backgroundWindow; + -(void)startAnimation; -(void)stopAnimation; -(void)drawView; diff --git a/platform/iphone/gl_view.mm b/platform/iphone/gl_view.mm index 07f5b39fac..a603e8dbf3 100755 --- a/platform/iphone/gl_view.mm +++ b/platform/iphone/gl_view.mm @@ -31,6 +31,7 @@ #import <OpenGLES/EAGLDrawable.h> #include "os_iphone.h" #include "core/os/keyboard.h" +#include "core/globals.h" #import "gl_view.h" @@ -59,6 +60,48 @@ void _hide_keyboard() { keyboard_text = ""; }; +bool _play_video(String p_path) { + + p_path = Globals::get_singleton()->globalize_path(p_path); + + // NSString *file_path = [NSString stringWithCString:p_path.utf8().get_data() encoding:NSASCIIStringEncoding]; + NSString* file_path = [[[NSString alloc] initWithUTF8String:p_path.utf8().get_data()] autorelease]; + NSURL *file_url = [NSURL fileURLWithPath:file_path]; + + _instance.moviePlayerController = [[MPMoviePlayerController alloc] initWithContentURL:file_url]; + _instance.moviePlayerController.controlStyle = MPMovieControlStyleNone; + [_instance.moviePlayerController setScalingMode:MPMovieScalingModeAspectFit]; + // [_instance.moviePlayerController setScalingMode:MPMovieScalingModeAspectFill]; + + [[NSNotificationCenter defaultCenter] addObserver:_instance + selector:@selector(moviePlayBackDidFinish:) + name:MPMoviePlayerPlaybackDidFinishNotification + object:_instance.moviePlayerController]; + + [[_instance window] makeKeyAndVisible]; + _instance.backgroundWindow = [[UIApplication sharedApplication] keyWindow]; + [_instance.moviePlayerController.view setFrame:_instance.backgroundWindow.frame]; + _instance.moviePlayerController.view.userInteractionEnabled = NO; + [_instance.backgroundWindow addSubview:_instance.moviePlayerController.view]; + [_instance.moviePlayerController play]; + + return true; +} + +bool _is_video_playing() { + NSInteger playback_state = _instance.moviePlayerController.playbackState; + return (playback_state == MPMoviePlaybackStatePlaying); +} + +void _pause_video() { + [_instance.moviePlayerController pause]; +} + +void _stop_video() { + [_instance.moviePlayerController stop]; + [_instance.moviePlayerController.view removeFromSuperview]; +} + @implementation GLView @synthesize animationInterval; @@ -189,8 +232,10 @@ static void clear_touches() { // If our view is resized, we'll be asked to layout subviews. // This is the perfect opportunity to also update the framebuffer so that it is // the same size as our display area. + -(void)layoutSubviews { + printf("HERE\n"); [EAGLContext setCurrentContext:context]; [self destroyFramebuffer]; [self createFramebuffer]; @@ -201,6 +246,7 @@ static void clear_touches() { { // Generate IDs for a framebuffer object and a color renderbuffer UIScreen* mainscr = [UIScreen mainScreen]; + printf("******** screen size %i, %i\n", (int)mainscr.currentMode.size.width, (int)mainscr.currentMode.size.height); if (mainscr.currentMode.size.width == 640 || mainscr.currentMode.size.width == 960) // modern iphone, can go to 640x960 self.contentScaleFactor = 2.0; @@ -454,4 +500,18 @@ static void clear_touches() { [super dealloc]; } +- (void)moviePlayBackDidFinish:(NSNotification*)notification { + MPMoviePlayerController *player = [notification object]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:MPMoviePlayerPlaybackDidFinishNotification + object:player]; + + _stop_video(); +} + +- (void)handleTap:(UITapGestureRecognizer *)gesture { + NSLog(@"Gesture\n"); +} + @end diff --git a/platform/iphone/godot_iphone.cpp b/platform/iphone/godot_iphone.cpp index eb3f17736a..b86af007f8 100644 --- a/platform/iphone/godot_iphone.cpp +++ b/platform/iphone/godot_iphone.cpp @@ -37,6 +37,7 @@ static OSIPhone* os = NULL; extern "C" { int add_path(int p_argc, char** p_args); +int add_cmdline(int p_argc, char** p_args); }; int iphone_main(int width, int height, int argc, char** argv) { @@ -67,6 +68,7 @@ int iphone_main(int width, int height, int argc, char** argv) { }; fargv[argc] = NULL; argc = add_path(argc, fargv); + argc = add_cmdline(argc, fargv); printf("os created\n"); Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); diff --git a/platform/iphone/os_iphone.cpp b/platform/iphone/os_iphone.cpp index 3b3b30cb86..756e8b575e 100644 --- a/platform/iphone/os_iphone.cpp +++ b/platform/iphone/os_iphone.cpp @@ -422,6 +422,8 @@ void OSIPhone::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) c bool OSIPhone::can_draw() const { + if (native_video_is_playing()) + return false; return true; }; @@ -483,6 +485,31 @@ String OSIPhone::get_locale() const { return locale_code; } +extern bool _play_video(String p_path); +extern bool _is_video_playing(); +extern void _pause_video(); +extern void _stop_video(); + +Error OSIPhone::native_video_play(String p_path) { + if ( _play_video(p_path) ) + return OK; + return FAILED; +} + +bool OSIPhone::native_video_is_playing() const { + return _is_video_playing(); +} + +void OSIPhone::native_video_pause() { + if (native_video_is_playing()) + _pause_video(); +} + +void OSIPhone::native_video_stop() { + if (native_video_is_playing()) + _stop_video(); +} + OSIPhone::OSIPhone(int width, int height) { rasterizer_gles22 = NULL; diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index a8ccba02b5..643bf2b5e3 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -184,6 +184,11 @@ public: void set_unique_ID(String p_ID); String get_unique_ID() const; + virtual Error native_video_play(String p_path); + virtual bool native_video_is_playing() const; + virtual void native_video_pause(); + virtual void native_video_stop(); + OSIPhone(int width, int height); ~OSIPhone(); }; diff --git a/platform/iphone/view_controller.h b/platform/iphone/view_controller.h index ac94d7ed3a..7c4a107bb8 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -37,4 +37,6 @@ - (void)didReceiveMemoryWarning; +- (BOOL)prefersStatusBarHidden; + @end diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 8af20358b9..eb331a61de 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -46,6 +46,26 @@ int add_path(int p_argc, char** p_args) { return p_argc; }; +int add_cmdline(int p_argc, char** p_args) { + + NSArray* arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"]; + if (!arr) + return p_argc; + + for (int i=0; i < [arr count]; i++) { + + NSString* str = [arr objectAtIndex:i]; + if (!str) + continue; + [str retain]; // @todo delete these at some point + p_args[p_argc++] = (char*)[str cString]; + }; + + p_args[p_argc] = NULL; + + return p_argc; +}; + }; @interface ViewController () @@ -104,4 +124,10 @@ int add_path(int p_argc, char** p_args) { } }; + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + @end diff --git a/platform/isim/detect.py b/platform/isim/detect.py index 196d269deb..48f71bbef5 100644 --- a/platform/isim/detect.py +++ b/platform/isim/detect.py @@ -74,6 +74,7 @@ def configure(env): '-framework', 'OpenGLES', '-framework', 'QuartzCore', '-framework', 'AudioToolbox', + '-framework', 'MediaPlayer', '-F$ISIMSDK', ]) diff --git a/scene/2d/area_2d.cpp b/scene/2d/area_2d.cpp index 70b5a652d5..ad228662e4 100644 --- a/scene/2d/area_2d.cpp +++ b/scene/2d/area_2d.cpp @@ -317,6 +317,7 @@ Area2D::Area2D() : CollisionObject2D(Physics2DServer::get_singleton()->area_crea density=0.1; priority=0; monitoring=false; + set_enable_monitoring(true); } diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index acae3e62c4..fe6c9637e0 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -26,483 +26,501 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "camera_2d.h"
-#include "scene/scene_string_names.h"
-#include "servers/visual_server.h"
-
-void Camera2D::_update_scroll() {
-
-
- if (!is_inside_scene())
- return;
-
- if (get_scene()->is_editor_hint()) {
- update(); //will just be drawn
- return;
- }
-
- if (current) {
- Matrix32 xform = get_camera_transform();
-
- RID vp = viewport->get_viewport();
- if (viewport) {
- viewport->set_canvas_transform( xform );
- }
- get_scene()->call_group(SceneMainLoop::GROUP_CALL_REALTIME,group_name,"_camera_moved",xform);
- };
-
-}
-
-void Camera2D::set_zoom(const Vector2 &p_zoom) {
-
- zoom = p_zoom;
- _update_scroll();
-};
-
-Vector2 Camera2D::get_zoom() const {
-
- return zoom;
-};
-
-
-Matrix32 Camera2D::get_camera_transform() {
-
- if (!get_scene())
- return Matrix32();
-
- Size2 screen_size = get_viewport_rect().size;
- screen_size=get_viewport_rect().size;
-
-
- Point2 new_camera_pos = get_global_transform().get_origin();
- Point2 ret_camera_pos;
-
- if (!first) {
-
-
- if (centered) {
-
- if (h_drag_enabled) {
- camera_pos.x = MIN( camera_pos.x, (new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_RIGHT]));
- camera_pos.x = MAX( camera_pos.x, (new_camera_pos.x - screen_size.x * 0.5 * drag_margin[MARGIN_LEFT]));
- } else {
-
- if (h_ofs<0) {
- camera_pos.x = new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_RIGHT] * h_ofs;
- } else {
- camera_pos.x = new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_LEFT] * h_ofs;
- }
- }
-
- if (v_drag_enabled) {
-
- camera_pos.y = MIN( camera_pos.y, (new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_BOTTOM]));
- camera_pos.y = MAX( camera_pos.y, (new_camera_pos.y - screen_size.y * 0.5 * drag_margin[MARGIN_TOP]));
-
- } else {
-
- if (v_ofs<0) {
- camera_pos.y = new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_TOP] * v_ofs;
- } else {
- camera_pos.y = new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_BOTTOM] * v_ofs;
- }
- }
-
- }
-
-
- if (smoothing>0.0) {
-
- float c = smoothing*get_fixed_process_delta_time();
- smoothed_camera_pos = ((new_camera_pos-smoothed_camera_pos)*c)+smoothed_camera_pos;
- ret_camera_pos=smoothed_camera_pos;
-// camera_pos=camera_pos*(1.0-smoothing)+new_camera_pos*smoothing;
- } else {
-
- ret_camera_pos=smoothed_camera_pos=camera_pos;
-
- }
-
-
-
- } else {
- ret_camera_pos=smoothed_camera_pos=camera_pos=new_camera_pos;
- first=false;
- }
-
-
- Point2 screen_offset = (centered ? (screen_size * 0.5 * zoom) : Point2());;
- screen_offset+=offset;
-
- float angle = get_global_transform().get_rotation();
- if(rotating){
- screen_offset = screen_offset.rotated(angle);
- }
-
- Rect2 screen_rect(-screen_offset+ret_camera_pos,screen_size);
- if (screen_rect.pos.x + screen_rect.size.x > limit[MARGIN_RIGHT])
- screen_rect.pos.x = limit[MARGIN_RIGHT] - screen_rect.size.x;
-
- if (screen_rect.pos.y + screen_rect.size.y > limit[MARGIN_BOTTOM])
- screen_rect.pos.y = limit[MARGIN_BOTTOM] - screen_rect.size.y;
-
-
- if (screen_rect.pos.x < limit[MARGIN_LEFT])
- screen_rect.pos.x=limit[MARGIN_LEFT];
-
- if (screen_rect.pos.y < limit[MARGIN_TOP])
- screen_rect.pos.y =limit[MARGIN_TOP];
-
- camera_screen_center=screen_rect.pos+screen_rect.size*0.5;
-
- Matrix32 xform;
- if(rotating){
- xform.set_rotation(angle);
- }
- xform.scale_basis(zoom);
- xform.set_origin(screen_rect.pos/*.floor()*/);
-
-
-/*
- if (0) {
-
- xform = get_global_transform() * xform;
- } else {
-
- xform.elements[2]+=get_global_transform().get_origin();
- }
-*/
-
-
- return (xform).affine_inverse();
-}
-
-
-
-void Camera2D::_notification(int p_what) {
-
- switch(p_what) {
-
- case NOTIFICATION_FIXED_PROCESS: {
-
- _update_scroll();
-
- } break;
- case NOTIFICATION_TRANSFORM_CHANGED: {
-
-
- if (!is_fixed_processing())
- _update_scroll();
-
- } break;
- case NOTIFICATION_ENTER_SCENE: {
-
- viewport = NULL;
- Node *n=this;
- while(n){
-
- viewport = n->cast_to<Viewport>();
- if (viewport)
- break;
- n=n->get_parent();
- }
-
- canvas = get_canvas();
-
- RID vp = viewport->get_viewport();
-
- group_name = "__cameras_"+itos(vp.get_id());
- canvas_group_name ="__cameras_c"+itos(canvas.get_id());
- add_to_group(group_name);
- add_to_group(canvas_group_name);
-
- _update_scroll();
- first=true;
-
-
- } break;
- case NOTIFICATION_EXIT_SCENE: {
-
- if (is_current()) {
- if (viewport) {
- viewport->set_canvas_transform( Matrix32() );
- }
- }
- remove_from_group(group_name);
- remove_from_group(canvas_group_name);
- viewport=NULL;
-
- } break;
- }
-}
-
-void Camera2D::set_offset(const Vector2& p_offset) {
-
- offset=p_offset;
- _update_scroll();
-
-}
-
-Vector2 Camera2D::get_offset() const{
-
- return offset;
-}
-
-void Camera2D::set_centered(bool p_centered){
-
- centered=p_centered;
- _update_scroll();
-}
-
-bool Camera2D::is_centered() const {
-
- return centered;
-}
-
-void Camera2D::set_rotating(bool p_rotating){
-
- rotating=p_rotating;
- _update_scroll();
-}
-
-bool Camera2D::is_rotating() const {
-
- return rotating;
-}
-
-
-void Camera2D::_make_current(Object *p_which) {
-
- if (p_which==this) {
-
- current=true;
- _update_scroll();
- } else {
- current=false;
- }
-}
-
-
-void Camera2D::_set_current(bool p_current) {
-
- if (p_current)
- make_current();
-
- current=p_current;
-}
-
-bool Camera2D::is_current() const {
-
- return current;
-}
-
-void Camera2D::make_current() {
-
- if (!is_inside_scene()) {
- current=true;
- } else {
- get_scene()->call_group(SceneMainLoop::GROUP_CALL_REALTIME,group_name,"_make_current",this);
- }
-}
-
-void Camera2D::set_limit(Margin p_margin,int p_limit) {
-
- ERR_FAIL_INDEX(p_margin,4);
- limit[p_margin]=p_limit;
-}
-
-int Camera2D::get_limit(Margin p_margin) const{
-
- ERR_FAIL_INDEX_V(p_margin,4,0);
- return limit[p_margin];
-
-}
-
-void Camera2D::set_drag_margin(Margin p_margin,float p_drag_margin) {
-
- ERR_FAIL_INDEX(p_margin,4);
- drag_margin[p_margin]=p_drag_margin;
-}
-
-float Camera2D::get_drag_margin(Margin p_margin) const{
-
- ERR_FAIL_INDEX_V(p_margin,4,0);
- return drag_margin[p_margin];
-
-}
-
-
-Vector2 Camera2D::get_camera_pos() const {
-
-
- return camera_pos;
-}
-
-void Camera2D::force_update_scroll() {
-
-
- _update_scroll();
-}
-
-
-void Camera2D::set_follow_smoothing(float p_speed) {
-
- smoothing=p_speed;
- if (smoothing>0)
- set_fixed_process(true);
- else
- set_fixed_process(false);
-}
-
-float Camera2D::get_follow_smoothing() const{
-
- return smoothing;
-}
-
-Point2 Camera2D::get_camera_screen_center() const {
-
- return camera_screen_center;
-}
-
-
-void Camera2D::set_h_drag_enabled(bool p_enabled) {
-
- h_drag_enabled=p_enabled;
-}
-
-bool Camera2D::is_h_drag_enabled() const{
-
- return h_drag_enabled;
-}
-
-void Camera2D::set_v_drag_enabled(bool p_enabled){
-
- v_drag_enabled=p_enabled;
-}
-
-bool Camera2D::is_v_drag_enabled() const{
-
- return v_drag_enabled;
-}
-
-void Camera2D::set_v_offset(float p_offset) {
-
- v_ofs=p_offset;
-}
-
-float Camera2D::get_v_offset() const{
-
- return v_ofs;
-}
-
-void Camera2D::set_h_offset(float p_offset){
-
- h_ofs=p_offset;
-}
-float Camera2D::get_h_offset() const{
-
- return h_ofs;
-}
-
-
-void Camera2D::_bind_methods() {
-
- ObjectTypeDB::bind_method(_MD("set_offset","offset"),&Camera2D::set_offset);
- ObjectTypeDB::bind_method(_MD("get_offset"),&Camera2D::get_offset);
-
- ObjectTypeDB::bind_method(_MD("set_centered","centered"),&Camera2D::set_centered);
- ObjectTypeDB::bind_method(_MD("is_centered"),&Camera2D::is_centered);
-
- ObjectTypeDB::bind_method(_MD("set_rotating","rotating"),&Camera2D::set_rotating);
- ObjectTypeDB::bind_method(_MD("is_rotating"),&Camera2D::is_rotating);
-
- ObjectTypeDB::bind_method(_MD("make_current"),&Camera2D::make_current);
- ObjectTypeDB::bind_method(_MD("_make_current"),&Camera2D::_make_current);
-
- ObjectTypeDB::bind_method(_MD("_update_scroll"),&Camera2D::_update_scroll);
-
-
- ObjectTypeDB::bind_method(_MD("_set_current","current"),&Camera2D::_set_current);
- ObjectTypeDB::bind_method(_MD("is_current"),&Camera2D::is_current);
-
- ObjectTypeDB::bind_method(_MD("set_limit","margin","limit"),&Camera2D::set_limit);
- ObjectTypeDB::bind_method(_MD("get_limit","margin"),&Camera2D::get_limit);
-
- ObjectTypeDB::bind_method(_MD("set_v_drag_enabled","enabled"),&Camera2D::set_v_drag_enabled);
- ObjectTypeDB::bind_method(_MD("is_v_drag_enabled"),&Camera2D::is_v_drag_enabled);
-
- ObjectTypeDB::bind_method(_MD("set_h_drag_enabled","enabled"),&Camera2D::set_h_drag_enabled);
- ObjectTypeDB::bind_method(_MD("is_h_drag_enabled"),&Camera2D::is_h_drag_enabled);
-
- ObjectTypeDB::bind_method(_MD("set_v_offset","ofs"),&Camera2D::set_v_offset);
- ObjectTypeDB::bind_method(_MD("get_v_offset"),&Camera2D::get_v_offset);
-
- ObjectTypeDB::bind_method(_MD("set_h_offset","ofs"),&Camera2D::set_h_offset);
- ObjectTypeDB::bind_method(_MD("get_h_offset"),&Camera2D::get_h_offset);
-
- ObjectTypeDB::bind_method(_MD("set_drag_margin","margin","drag_margin"),&Camera2D::set_drag_margin);
- ObjectTypeDB::bind_method(_MD("get_drag_margin","margin"),&Camera2D::get_drag_margin);
-
- ObjectTypeDB::bind_method(_MD("get_camera_pos"),&Camera2D::get_camera_pos);
- ObjectTypeDB::bind_method(_MD("get_camera_screen_center"),&Camera2D::get_camera_screen_center);
-
- ObjectTypeDB::bind_method(_MD("set_zoom"),&Camera2D::set_zoom);
- ObjectTypeDB::bind_method(_MD("get_zoom"),&Camera2D::get_zoom);
-
-
- ObjectTypeDB::bind_method(_MD("set_follow_smoothing","follow_smoothing"),&Camera2D::set_follow_smoothing);
- ObjectTypeDB::bind_method(_MD("get_follow_smoothing"),&Camera2D::get_follow_smoothing);
-
- ObjectTypeDB::bind_method(_MD("force_update_scroll"),&Camera2D::force_update_scroll);
-
-
- ADD_PROPERTYNZ( PropertyInfo(Variant::VECTOR2,"offset"),_SCS("set_offset"),_SCS("get_offset"));
- ADD_PROPERTY( PropertyInfo(Variant::BOOL,"centered"),_SCS("set_centered"),_SCS("is_centered"));
- ADD_PROPERTY( PropertyInfo(Variant::BOOL,"rotating"),_SCS("set_rotating"),_SCS("is_rotating"));
- ADD_PROPERTY( PropertyInfo(Variant::BOOL,"current"),_SCS("_set_current"),_SCS("is_current"));
- ADD_PROPERTY( PropertyInfo(Variant::REAL,"smoothing"),_SCS("set_follow_smoothing"),_SCS("get_follow_smoothing") );
- ADD_PROPERTY( PropertyInfo(Variant::VECTOR2,"zoom"),_SCS("set_zoom"),_SCS("get_zoom") );
-
- ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/left"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_LEFT);
- ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/top"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_TOP);
- ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/right"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_RIGHT);
- ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/bottom"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_BOTTOM);
-
- ADD_PROPERTY( PropertyInfo(Variant::BOOL,"drag_margin/h_enabled"),_SCS("set_h_drag_enabled"),_SCS("is_h_drag_enabled") );
- ADD_PROPERTY( PropertyInfo(Variant::BOOL,"drag_margin/v_enabled"),_SCS("set_v_drag_enabled"),_SCS("is_v_drag_enabled") );
-
- ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/left",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_LEFT);
- ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/top",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_TOP);
- ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/right",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_RIGHT);
- ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/bottom",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_BOTTOM);
-
-
-
-}
-
-Camera2D::Camera2D() {
-
-
-
- centered=true;
- rotating=false;
- current=false;
- limit[MARGIN_LEFT]=-10000000;
- limit[MARGIN_TOP]=-10000000;
- limit[MARGIN_RIGHT]=10000000;
- limit[MARGIN_BOTTOM]=10000000;
- drag_margin[MARGIN_LEFT]=0.2;
- drag_margin[MARGIN_TOP]=0.2;
- drag_margin[MARGIN_RIGHT]=0.2;
- drag_margin[MARGIN_BOTTOM]=0.2;
- camera_pos=Vector2();
-
- smoothing=0.0;
- zoom = Vector2(1, 1);
-
- h_drag_enabled=true;
- v_drag_enabled=true;
- h_ofs=0;
- v_ofs=0;
-
-}
+#include "camera_2d.h" +#include "scene/scene_string_names.h" +#include "servers/visual_server.h" + +void Camera2D::_update_scroll() { + + + if (!is_inside_scene()) + return; + + if (get_scene()->is_editor_hint()) { + update(); //will just be drawn + return; + } + + if (current) { + Matrix32 xform = get_camera_transform(); + + RID vp = viewport->get_viewport(); + if (viewport) { + viewport->set_canvas_transform( xform ); + } + get_scene()->call_group(SceneMainLoop::GROUP_CALL_REALTIME,group_name,"_camera_moved",xform); + }; + +} + +void Camera2D::set_zoom(const Vector2 &p_zoom) { + + zoom = p_zoom; + _update_scroll(); +}; + +Vector2 Camera2D::get_zoom() const { + + return zoom; +}; + + +Matrix32 Camera2D::get_camera_transform() { + + if (!get_scene()) + return Matrix32(); + + Size2 screen_size = get_viewport_rect().size; + screen_size=get_viewport_rect().size; + + + Point2 new_camera_pos = get_global_transform().get_origin(); + Point2 ret_camera_pos; + + if (!first) { + + + if (centered) { + + if (h_drag_enabled) { + camera_pos.x = MIN( camera_pos.x, (new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_RIGHT])); + camera_pos.x = MAX( camera_pos.x, (new_camera_pos.x - screen_size.x * 0.5 * drag_margin[MARGIN_LEFT])); + } else { + + if (h_ofs<0) { + camera_pos.x = new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_RIGHT] * h_ofs; + } else { + camera_pos.x = new_camera_pos.x + screen_size.x * 0.5 * drag_margin[MARGIN_LEFT] * h_ofs; + } + } + + if (v_drag_enabled) { + + camera_pos.y = MIN( camera_pos.y, (new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_BOTTOM])); + camera_pos.y = MAX( camera_pos.y, (new_camera_pos.y - screen_size.y * 0.5 * drag_margin[MARGIN_TOP])); + + } else { + + if (v_ofs<0) { + camera_pos.y = new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_TOP] * v_ofs; + } else { + camera_pos.y = new_camera_pos.y + screen_size.y * 0.5 * drag_margin[MARGIN_BOTTOM] * v_ofs; + } + } + + } + + + if (smoothing>0.0) { + + float c = smoothing*get_fixed_process_delta_time(); + smoothed_camera_pos = ((new_camera_pos-smoothed_camera_pos)*c)+smoothed_camera_pos; + ret_camera_pos=smoothed_camera_pos; +// camera_pos=camera_pos*(1.0-smoothing)+new_camera_pos*smoothing; + } else { + + ret_camera_pos=smoothed_camera_pos=camera_pos; + + } + + + + } else { + ret_camera_pos=smoothed_camera_pos=camera_pos=new_camera_pos; + first=false; + } + + + Point2 screen_offset = (centered ? (screen_size * 0.5 * zoom) : Point2());; + screen_offset; + + float angle = get_global_transform().get_rotation(); + if(rotating){ + screen_offset = screen_offset.rotated(angle); + } + + Rect2 screen_rect(-screen_offset+ret_camera_pos,screen_size); + if (screen_rect.pos.x + screen_rect.size.x > limit[MARGIN_RIGHT]) + screen_rect.pos.x = limit[MARGIN_RIGHT] - screen_rect.size.x; + + if (screen_rect.pos.y + screen_rect.size.y > limit[MARGIN_BOTTOM]) + screen_rect.pos.y = limit[MARGIN_BOTTOM] - screen_rect.size.y; + + + if (screen_rect.pos.x < limit[MARGIN_LEFT]) + screen_rect.pos.x=limit[MARGIN_LEFT]; + + if (screen_rect.pos.y < limit[MARGIN_TOP]) + screen_rect.pos.y =limit[MARGIN_TOP]; + + if (offset!=Vector2()) { + + screen_rect.pos+=offset; + if (screen_rect.pos.x + screen_rect.size.x > limit[MARGIN_RIGHT]) + screen_rect.pos.x = limit[MARGIN_RIGHT] - screen_rect.size.x; + + if (screen_rect.pos.y + screen_rect.size.y > limit[MARGIN_BOTTOM]) + screen_rect.pos.y = limit[MARGIN_BOTTOM] - screen_rect.size.y; + + + if (screen_rect.pos.x < limit[MARGIN_LEFT]) + screen_rect.pos.x=limit[MARGIN_LEFT]; + + if (screen_rect.pos.y < limit[MARGIN_TOP]) + screen_rect.pos.y =limit[MARGIN_TOP]; + + } + + camera_screen_center=screen_rect.pos+screen_rect.size*0.5; + + Matrix32 xform; + if(rotating){ + xform.set_rotation(angle); + } + xform.scale_basis(zoom); + xform.set_origin(screen_rect.pos/*.floor()*/); + + +/* + if (0) { + + xform = get_global_transform() * xform; + } else { + + xform.elements[2]+=get_global_transform().get_origin(); + } +*/ + + + return (xform).affine_inverse(); +} + + + +void Camera2D::_notification(int p_what) { + + switch(p_what) { + + case NOTIFICATION_FIXED_PROCESS: { + + _update_scroll(); + + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { + + + if (!is_fixed_processing()) + _update_scroll(); + + } break; + case NOTIFICATION_ENTER_SCENE: { + + viewport = NULL; + Node *n=this; + while(n){ + + viewport = n->cast_to<Viewport>(); + if (viewport) + break; + n=n->get_parent(); + } + + canvas = get_canvas(); + + RID vp = viewport->get_viewport(); + + group_name = "__cameras_"+itos(vp.get_id()); + canvas_group_name ="__cameras_c"+itos(canvas.get_id()); + add_to_group(group_name); + add_to_group(canvas_group_name); + + _update_scroll(); + first=true; + + + } break; + case NOTIFICATION_EXIT_SCENE: { + + if (is_current()) { + if (viewport) { + viewport->set_canvas_transform( Matrix32() ); + } + } + remove_from_group(group_name); + remove_from_group(canvas_group_name); + viewport=NULL; + + } break; + } +} + +void Camera2D::set_offset(const Vector2& p_offset) { + + offset=p_offset; + _update_scroll(); + +} + +Vector2 Camera2D::get_offset() const{ + + return offset; +} + +void Camera2D::set_centered(bool p_centered){ + + centered=p_centered; + _update_scroll(); +} + +bool Camera2D::is_centered() const { + + return centered; +} + +void Camera2D::set_rotating(bool p_rotating){ + + rotating=p_rotating; + _update_scroll(); +} + +bool Camera2D::is_rotating() const { + + return rotating; +} + + +void Camera2D::_make_current(Object *p_which) { + + if (p_which==this) { + + current=true; + _update_scroll(); + } else { + current=false; + } +} + + +void Camera2D::_set_current(bool p_current) { + + if (p_current) + make_current(); + + current=p_current; +} + +bool Camera2D::is_current() const { + + return current; +} + +void Camera2D::make_current() { + + if (!is_inside_scene()) { + current=true; + } else { + get_scene()->call_group(SceneMainLoop::GROUP_CALL_REALTIME,group_name,"_make_current",this); + } +} + +void Camera2D::set_limit(Margin p_margin,int p_limit) { + + ERR_FAIL_INDEX(p_margin,4); + limit[p_margin]=p_limit; +} + +int Camera2D::get_limit(Margin p_margin) const{ + + ERR_FAIL_INDEX_V(p_margin,4,0); + return limit[p_margin]; + +} + +void Camera2D::set_drag_margin(Margin p_margin,float p_drag_margin) { + + ERR_FAIL_INDEX(p_margin,4); + drag_margin[p_margin]=p_drag_margin; +} + +float Camera2D::get_drag_margin(Margin p_margin) const{ + + ERR_FAIL_INDEX_V(p_margin,4,0); + return drag_margin[p_margin]; + +} + + +Vector2 Camera2D::get_camera_pos() const { + + + return camera_pos; +} + +void Camera2D::force_update_scroll() { + + + _update_scroll(); +} + + +void Camera2D::set_follow_smoothing(float p_speed) { + + smoothing=p_speed; + if (smoothing>0) + set_fixed_process(true); + else + set_fixed_process(false); +} + +float Camera2D::get_follow_smoothing() const{ + + return smoothing; +} + +Point2 Camera2D::get_camera_screen_center() const { + + return camera_screen_center; +} + + +void Camera2D::set_h_drag_enabled(bool p_enabled) { + + h_drag_enabled=p_enabled; +} + +bool Camera2D::is_h_drag_enabled() const{ + + return h_drag_enabled; +} + +void Camera2D::set_v_drag_enabled(bool p_enabled){ + + v_drag_enabled=p_enabled; +} + +bool Camera2D::is_v_drag_enabled() const{ + + return v_drag_enabled; +} + +void Camera2D::set_v_offset(float p_offset) { + + v_ofs=p_offset; +} + +float Camera2D::get_v_offset() const{ + + return v_ofs; +} + +void Camera2D::set_h_offset(float p_offset){ + + h_ofs=p_offset; +} +float Camera2D::get_h_offset() const{ + + return h_ofs; +} + + +void Camera2D::_bind_methods() { + + ObjectTypeDB::bind_method(_MD("set_offset","offset"),&Camera2D::set_offset); + ObjectTypeDB::bind_method(_MD("get_offset"),&Camera2D::get_offset); + + ObjectTypeDB::bind_method(_MD("set_centered","centered"),&Camera2D::set_centered); + ObjectTypeDB::bind_method(_MD("is_centered"),&Camera2D::is_centered); + + ObjectTypeDB::bind_method(_MD("set_rotating","rotating"),&Camera2D::set_rotating); + ObjectTypeDB::bind_method(_MD("is_rotating"),&Camera2D::is_rotating); + + ObjectTypeDB::bind_method(_MD("make_current"),&Camera2D::make_current); + ObjectTypeDB::bind_method(_MD("_make_current"),&Camera2D::_make_current); + + ObjectTypeDB::bind_method(_MD("_update_scroll"),&Camera2D::_update_scroll); + + + ObjectTypeDB::bind_method(_MD("_set_current","current"),&Camera2D::_set_current); + ObjectTypeDB::bind_method(_MD("is_current"),&Camera2D::is_current); + + ObjectTypeDB::bind_method(_MD("set_limit","margin","limit"),&Camera2D::set_limit); + ObjectTypeDB::bind_method(_MD("get_limit","margin"),&Camera2D::get_limit); + + ObjectTypeDB::bind_method(_MD("set_v_drag_enabled","enabled"),&Camera2D::set_v_drag_enabled); + ObjectTypeDB::bind_method(_MD("is_v_drag_enabled"),&Camera2D::is_v_drag_enabled); + + ObjectTypeDB::bind_method(_MD("set_h_drag_enabled","enabled"),&Camera2D::set_h_drag_enabled); + ObjectTypeDB::bind_method(_MD("is_h_drag_enabled"),&Camera2D::is_h_drag_enabled); + + ObjectTypeDB::bind_method(_MD("set_v_offset","ofs"),&Camera2D::set_v_offset); + ObjectTypeDB::bind_method(_MD("get_v_offset"),&Camera2D::get_v_offset); + + ObjectTypeDB::bind_method(_MD("set_h_offset","ofs"),&Camera2D::set_h_offset); + ObjectTypeDB::bind_method(_MD("get_h_offset"),&Camera2D::get_h_offset); + + ObjectTypeDB::bind_method(_MD("set_drag_margin","margin","drag_margin"),&Camera2D::set_drag_margin); + ObjectTypeDB::bind_method(_MD("get_drag_margin","margin"),&Camera2D::get_drag_margin); + + ObjectTypeDB::bind_method(_MD("get_camera_pos"),&Camera2D::get_camera_pos); + ObjectTypeDB::bind_method(_MD("get_camera_screen_center"),&Camera2D::get_camera_screen_center); + + ObjectTypeDB::bind_method(_MD("set_zoom"),&Camera2D::set_zoom); + ObjectTypeDB::bind_method(_MD("get_zoom"),&Camera2D::get_zoom); + + + ObjectTypeDB::bind_method(_MD("set_follow_smoothing","follow_smoothing"),&Camera2D::set_follow_smoothing); + ObjectTypeDB::bind_method(_MD("get_follow_smoothing"),&Camera2D::get_follow_smoothing); + + ObjectTypeDB::bind_method(_MD("force_update_scroll"),&Camera2D::force_update_scroll); + + + ADD_PROPERTYNZ( PropertyInfo(Variant::VECTOR2,"offset"),_SCS("set_offset"),_SCS("get_offset")); + ADD_PROPERTY( PropertyInfo(Variant::BOOL,"centered"),_SCS("set_centered"),_SCS("is_centered")); + ADD_PROPERTY( PropertyInfo(Variant::BOOL,"rotating"),_SCS("set_rotating"),_SCS("is_rotating")); + ADD_PROPERTY( PropertyInfo(Variant::BOOL,"current"),_SCS("_set_current"),_SCS("is_current")); + ADD_PROPERTY( PropertyInfo(Variant::REAL,"smoothing"),_SCS("set_follow_smoothing"),_SCS("get_follow_smoothing") ); + ADD_PROPERTY( PropertyInfo(Variant::VECTOR2,"zoom"),_SCS("set_zoom"),_SCS("get_zoom") ); + + ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/left"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_LEFT); + ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/top"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_TOP); + ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/right"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_RIGHT); + ADD_PROPERTYI( PropertyInfo(Variant::INT,"limit/bottom"),_SCS("set_limit"),_SCS("get_limit"),MARGIN_BOTTOM); + + ADD_PROPERTY( PropertyInfo(Variant::BOOL,"drag_margin/h_enabled"),_SCS("set_h_drag_enabled"),_SCS("is_h_drag_enabled") ); + ADD_PROPERTY( PropertyInfo(Variant::BOOL,"drag_margin/v_enabled"),_SCS("set_v_drag_enabled"),_SCS("is_v_drag_enabled") ); + + ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/left",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_LEFT); + ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/top",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_TOP); + ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/right",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_RIGHT); + ADD_PROPERTYI( PropertyInfo(Variant::REAL,"drag_margin/bottom",PROPERTY_HINT_RANGE,"0,1,0.01"),_SCS("set_drag_margin"),_SCS("get_drag_margin"),MARGIN_BOTTOM); + + + +} + +Camera2D::Camera2D() { + + + + centered=true; + rotating=false; + current=false; + limit[MARGIN_LEFT]=-10000000; + limit[MARGIN_TOP]=-10000000; + limit[MARGIN_RIGHT]=10000000; + limit[MARGIN_BOTTOM]=10000000; + drag_margin[MARGIN_LEFT]=0.2; + drag_margin[MARGIN_TOP]=0.2; + drag_margin[MARGIN_RIGHT]=0.2; + drag_margin[MARGIN_BOTTOM]=0.2; + camera_pos=Vector2(); + + smoothing=0.0; + zoom = Vector2(1, 1); + + h_drag_enabled=true; + v_drag_enabled=true; + h_ofs=0; + v_ofs=0; + +} diff --git a/scene/2d/physics_body_2d.cpp b/scene/2d/physics_body_2d.cpp index afdca4a3be..a1e7195b0a 100644 --- a/scene/2d/physics_body_2d.cpp +++ b/scene/2d/physics_body_2d.cpp @@ -1027,7 +1027,7 @@ void KinematicBody2D::_bind_methods() { ObjectTypeDB::bind_method(_MD("get_collision_pos"),&KinematicBody2D::get_collision_pos); ObjectTypeDB::bind_method(_MD("get_collision_normal"),&KinematicBody2D::get_collision_normal); ObjectTypeDB::bind_method(_MD("get_collider_velocity"),&KinematicBody2D::get_collider_velocity); - ObjectTypeDB::bind_method(_MD("get_collider:Object"),&KinematicBody2D::get_collider); + ObjectTypeDB::bind_method(_MD("get_collider:Object"),&KinematicBody2D::_get_collider); ObjectTypeDB::bind_method(_MD("set_collide_with_static_bodies","enable"),&KinematicBody2D::set_collide_with_static_bodies); @@ -1064,7 +1064,7 @@ KinematicBody2D::KinematicBody2D() : PhysicsBody2D(Physics2DServer::BODY_MODE_KI colliding=false; collider=0; - margin=0.01; + margin=0.08; } KinematicBody2D::~KinematicBody2D() { diff --git a/scene/2d/visibility_notifier_2d.cpp b/scene/2d/visibility_notifier_2d.cpp index 323d02df4b..c76c18a595 100644 --- a/scene/2d/visibility_notifier_2d.cpp +++ b/scene/2d/visibility_notifier_2d.cpp @@ -37,11 +37,11 @@ void VisibilityNotifier2D::_enter_viewport(Viewport* p_viewport) { ERR_FAIL_COND(viewports.has(p_viewport)); viewports.insert(p_viewport); + if (viewports.size()==1) { emit_signal(SceneStringNames::get_singleton()->enter_screen); - _screen_enter(); - + _screen_enter(); } emit_signal(SceneStringNames::get_singleton()->enter_viewport,p_viewport); diff --git a/scene/io/resource_format_image.cpp b/scene/io/resource_format_image.cpp index 98453bcabb..b0fcf9717b 100644 --- a/scene/io/resource_format_image.cpp +++ b/scene/io/resource_format_image.cpp @@ -131,11 +131,11 @@ RES ResourceFormatLoaderImage::load(const String &p_path,const String& p_origina uint32_t flags=0; - if (bool(GLOBAL_DEF("texture_import/filter",true))) + if (bool(GLOBAL_DEF("image_loader/filter",true))) flags|=Texture::FLAG_FILTER; - if (bool(GLOBAL_DEF("texture_import/gen_mipmaps",true))) + if (bool(GLOBAL_DEF("image_loader/gen_mipmaps",true))) flags|=Texture::FLAG_MIPMAPS; - if (bool(GLOBAL_DEF("texture_import/repeat",false))) + if (bool(GLOBAL_DEF("image_loader/repeat",false))) flags|=Texture::FLAG_REPEAT; @@ -192,8 +192,8 @@ ResourceFormatLoaderImage::ResourceFormatLoaderImage() { max_texture_size = GLOBAL_DEF("debug/max_texture_size",0); GLOBAL_DEF("debug/max_texture_size_alert",false); debug_load_times=GLOBAL_DEF("debug/image_load_times",false); - GLOBAL_DEF("texture_import/filter",true); - GLOBAL_DEF("texture_import/gen_mipmaps",true); - GLOBAL_DEF("texture_import/repeat",true); + GLOBAL_DEF("image_loader/filter",true); + GLOBAL_DEF("image_loader/gen_mipmaps",true); + GLOBAL_DEF("image_loader/repeat",false); } diff --git a/scene/resources/audio_stream_resampled.cpp b/scene/resources/audio_stream_resampled.cpp index 989e094edf..8e694a6110 100644 --- a/scene/resources/audio_stream_resampled.cpp +++ b/scene/resources/audio_stream_resampled.cpp @@ -180,7 +180,6 @@ bool AudioStreamResampled::mix(int32_t *p_dest, int p_frames) { } int todo = MIN( ((int64_t(rb_todo)<<MIX_FRAC_BITS)/increment)+1, p_frames ); - #if 0 if (int(mix_rate)==get_mix_rate()) { diff --git a/servers/visual/visual_server_wrap_mt.cpp b/servers/visual/visual_server_wrap_mt.cpp index 85f7b8c3cb..919656fe04 100644 --- a/servers/visual/visual_server_wrap_mt.cpp +++ b/servers/visual/visual_server_wrap_mt.cpp @@ -28,7 +28,7 @@ /*************************************************************************/ #include "visual_server_wrap_mt.h" #include "os/os.h" - +#include "globals.h" void VisualServerWrapMT::thread_exit() { exit=true; @@ -160,8 +160,12 @@ void VisualServerWrapMT::finish() { if (thread) { command_queue.push( this, &VisualServerWrapMT::thread_exit); - Thread::wait_to_finish( thread ); + Thread::wait_to_finish( thread ); memdelete(thread); + + + texture_free_cached_ids(); + thread=NULL; } else { visual_server->finish(); @@ -181,6 +185,8 @@ VisualServerWrapMT::VisualServerWrapMT(VisualServer* p_contained,bool p_create_t draw_mutex=NULL; draw_pending=0; draw_thread_up=false; + alloc_mutex=Mutex::create(); + texture_pool_max_size=GLOBAL_DEF("render/thread_textures_prealloc",20); if (!p_create_thread) { server_thread=Thread::get_caller_ID(); } else { @@ -192,6 +198,7 @@ VisualServerWrapMT::VisualServerWrapMT(VisualServer* p_contained,bool p_create_t VisualServerWrapMT::~VisualServerWrapMT() { memdelete(visual_server); + memdelete(alloc_mutex); //finish(); } diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 3d227cfdbc..3a6a3e509a 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -60,65 +60,102 @@ class VisualServerWrapMT : public VisualServer { void thread_exit(); + Mutex*alloc_mutex; + + + int texture_pool_max_size; + List<RID> texture_id_pool; + + public: -#define FUNC0R(m_r,m_func)\ - virtual m_r m_func() { \ +#define FUNC0R(m_r,m_type)\ + virtual m_r m_type() { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,&ret);\ return ret;\ } else {\ - return visual_server->m_func();\ + return visual_server->m_type();\ + }\ + } + +#define FUNCRID(m_type)\ + int m_type##allocn() {\ + for(int i=0;i<m_type##_pool_max_size;i++) {\ + m_type##_id_pool.push_back( visual_server->m_type##_create() );\ + }\ + return 0;\ + }\ + void m_type##_free_cached_ids() {\ + while (m_type##_id_pool.size()) {\ + free(m_type##_id_pool.front()->get());\ + m_type##_id_pool.pop_front();\ + }\ + }\ + virtual RID m_type##_create() { \ + if (Thread::get_caller_ID()!=server_thread) {\ + RID rid;\ + alloc_mutex->lock();\ + if (m_type##_id_pool.size()==0) {\ + int ret;\ + command_queue.push_and_ret( this, &VisualServerWrapMT::m_type##allocn,&ret);\ + }\ + rid=m_type##_id_pool.front()->get();\ + m_type##_id_pool.pop_front();\ + alloc_mutex->unlock();\ + return rid;\ + } else {\ + return visual_server->m_type##_create();\ }\ } -#define FUNC0RC(m_r,m_func)\ - virtual m_r m_func() const { \ +#define FUNC0RC(m_r,m_type)\ + virtual m_r m_type() const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,&ret);\ return ret;\ } else {\ - return visual_server->m_func();\ + return visual_server->m_type();\ }\ } -#define FUNC0(m_func)\ - virtual void m_func() { \ +#define FUNC0(m_type)\ + virtual void m_type() { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func);\ + command_queue.push( visual_server, &VisualServer::m_type);\ } else {\ - visual_server->m_func();\ + visual_server->m_type();\ }\ } -#define FUNC0C(m_func)\ - virtual void m_func() const { \ +#define FUNC0C(m_type)\ + virtual void m_type() const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func);\ + command_queue.push( visual_server, &VisualServer::m_type);\ } else {\ - visual_server->m_func();\ + visual_server->m_type();\ }\ } -#define FUNC0S(m_func)\ - virtual void m_func() { \ +#define FUNC0S(m_type)\ + virtual void m_type() { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type);\ } else {\ - visual_server->m_func();\ + visual_server->m_type();\ }\ } -#define FUNC0SC(m_func)\ - virtual void m_func() const { \ +#define FUNC0SC(m_type)\ + virtual void m_type() const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type);\ } else {\ - visual_server->m_func();\ + visual_server->m_type();\ }\ } @@ -126,448 +163,449 @@ public: /////////////////////////////////////////////// -#define FUNC1R(m_r,m_func,m_arg1)\ - virtual m_r m_func(m_arg1 p1) { \ +#define FUNC1R(m_r,m_type,m_arg1)\ + virtual m_r m_type(m_arg1 p1) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1);\ + return visual_server->m_type(p1);\ }\ } -#define FUNC1RC(m_r,m_func,m_arg1)\ - virtual m_r m_func(m_arg1 p1) const { \ +#define FUNC1RC(m_r,m_type,m_arg1)\ + virtual m_r m_type(m_arg1 p1) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1);\ + return visual_server->m_type(p1);\ }\ } -#define FUNC1S(m_func,m_arg1)\ - virtual void m_func(m_arg1 p1) { \ +#define FUNC1S(m_type,m_arg1)\ + virtual void m_type(m_arg1 p1) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1);\ } else {\ - visual_server->m_func(p1);\ + visual_server->m_type(p1);\ }\ } -#define FUNC1SC(m_func,m_arg1)\ - virtual void m_func(m_arg1 p1) const { \ +#define FUNC1SC(m_type,m_arg1)\ + virtual void m_type(m_arg1 p1) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1);\ } else {\ - visual_server->m_func(p1);\ + visual_server->m_type(p1);\ }\ } -#define FUNC1(m_func,m_arg1)\ - virtual void m_func(m_arg1 p1) { \ +#define FUNC1(m_type,m_arg1)\ + virtual void m_type(m_arg1 p1) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1);\ + command_queue.push( visual_server, &VisualServer::m_type,p1);\ } else {\ - visual_server->m_func(p1);\ + visual_server->m_type(p1);\ }\ } -#define FUNC1C(m_func,m_arg1)\ - virtual void m_func(m_arg1 p1) const { \ +#define FUNC1C(m_type,m_arg1)\ + virtual void m_type(m_arg1 p1) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1);\ + command_queue.push( visual_server, &VisualServer::m_type,p1);\ } else {\ - visual_server->m_func(p1);\ + visual_server->m_type(p1);\ }\ } -#define FUNC2R(m_r,m_func,m_arg1, m_arg2)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2) { \ +#define FUNC2R(m_r,m_type,m_arg1, m_arg2)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2);\ + return visual_server->m_type(p1, p2);\ }\ } -#define FUNC2RC(m_r,m_func,m_arg1, m_arg2)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2) const { \ +#define FUNC2RC(m_r,m_type,m_arg1, m_arg2)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2);\ + return visual_server->m_type(p1, p2);\ }\ } -#define FUNC2S(m_func,m_arg1, m_arg2)\ - virtual void m_func(m_arg1 p1, m_arg2 p2) { \ +#define FUNC2S(m_type,m_arg1, m_arg2)\ + virtual void m_type(m_arg1 p1, m_arg2 p2) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2);\ } else {\ - visual_server->m_func(p1, p2);\ + visual_server->m_type(p1, p2);\ }\ } -#define FUNC2SC(m_func,m_arg1, m_arg2)\ - virtual void m_func(m_arg1 p1, m_arg2 p2) const { \ +#define FUNC2SC(m_type,m_arg1, m_arg2)\ + virtual void m_type(m_arg1 p1, m_arg2 p2) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2);\ } else {\ - visual_server->m_func(p1, p2);\ + visual_server->m_type(p1, p2);\ }\ } -#define FUNC2(m_func,m_arg1, m_arg2)\ - virtual void m_func(m_arg1 p1, m_arg2 p2) { \ +#define FUNC2(m_type,m_arg1, m_arg2)\ + virtual void m_type(m_arg1 p1, m_arg2 p2) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2);\ } else {\ - visual_server->m_func(p1, p2);\ + visual_server->m_type(p1, p2);\ }\ } -#define FUNC2C(m_func,m_arg1, m_arg2)\ - virtual void m_func(m_arg1 p1, m_arg2 p2) const { \ +#define FUNC2C(m_type,m_arg1, m_arg2)\ + virtual void m_type(m_arg1 p1, m_arg2 p2) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2);\ } else {\ - visual_server->m_func(p1, p2);\ + visual_server->m_type(p1, p2);\ }\ } -#define FUNC3R(m_r,m_func,m_arg1, m_arg2, m_arg3)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ +#define FUNC3R(m_r,m_type,m_arg1, m_arg2, m_arg3)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3);\ + return visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC3RC(m_r,m_func,m_arg1, m_arg2, m_arg3)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ +#define FUNC3RC(m_r,m_type,m_arg1, m_arg2, m_arg3)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3);\ + return visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC3S(m_func,m_arg1, m_arg2, m_arg3)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ +#define FUNC3S(m_type,m_arg1, m_arg2, m_arg3)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3);\ } else {\ - visual_server->m_func(p1, p2, p3);\ + visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC3SC(m_func,m_arg1, m_arg2, m_arg3)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ +#define FUNC3SC(m_type,m_arg1, m_arg2, m_arg3)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3);\ } else {\ - visual_server->m_func(p1, p2, p3);\ + visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC3(m_func,m_arg1, m_arg2, m_arg3)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ +#define FUNC3(m_type,m_arg1, m_arg2, m_arg3)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3);\ } else {\ - visual_server->m_func(p1, p2, p3);\ + visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC3C(m_func,m_arg1, m_arg2, m_arg3)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ +#define FUNC3C(m_type,m_arg1, m_arg2, m_arg3)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3);\ } else {\ - visual_server->m_func(p1, p2, p3);\ + visual_server->m_type(p1, p2, p3);\ }\ } -#define FUNC4R(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ +#define FUNC4R(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4);\ + return visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC4RC(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ +#define FUNC4RC(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4);\ + return visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC4S(m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ +#define FUNC4S(m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4);\ } else {\ - visual_server->m_func(p1, p2, p3, p4);\ + visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC4SC(m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ +#define FUNC4SC(m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4);\ } else {\ - visual_server->m_func(p1, p2, p3, p4);\ + visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC4(m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ +#define FUNC4(m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4);\ } else {\ - visual_server->m_func(p1, p2, p3, p4);\ + visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC4C(m_func,m_arg1, m_arg2, m_arg3, m_arg4)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ +#define FUNC4C(m_type,m_arg1, m_arg2, m_arg3, m_arg4)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4);\ } else {\ - visual_server->m_func(p1, p2, p3, p4);\ + visual_server->m_type(p1, p2, p3, p4);\ }\ } -#define FUNC5R(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ +#define FUNC5R(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5);\ + return visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC5RC(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ +#define FUNC5RC(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5);\ + return visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC5S(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ +#define FUNC5S(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5);\ + visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC5SC(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ +#define FUNC5SC(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5);\ + visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC5(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ +#define FUNC5(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5);\ + visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC5C(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ +#define FUNC5C(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5);\ + visual_server->m_type(p1, p2, p3, p4, p5);\ }\ } -#define FUNC6R(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ +#define FUNC6R(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + return visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC6RC(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ +#define FUNC6RC(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + return visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC6S(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ +#define FUNC6S(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC6SC(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ +#define FUNC6SC(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC6(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ +#define FUNC6(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC6C(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ +#define FUNC6C(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6);\ }\ } -#define FUNC7R(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ +#define FUNC7R(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + return visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } -#define FUNC7RC(m_r,m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual m_r m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ +#define FUNC7RC(m_r,m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual m_r m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ if (Thread::get_caller_ID()!=server_thread) {\ m_r ret;\ - command_queue.push_and_ret( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7,&ret);\ + command_queue.push_and_ret( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7,&ret);\ return ret;\ } else {\ - return visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + return visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } -#define FUNC7S(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ +#define FUNC7S(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } -#define FUNC7SC(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ +#define FUNC7SC(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push_and_sync( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7);\ + command_queue.push_and_sync( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } -#define FUNC7(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ +#define FUNC7(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } -#define FUNC7C(m_func,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ - virtual void m_func(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ +#define FUNC7C(m_type,m_arg1, m_arg2, m_arg3, m_arg4, m_arg5, m_arg6, m_arg7)\ + virtual void m_type(m_arg1 p1, m_arg2 p2, m_arg3 p3, m_arg4 p4, m_arg5 p5, m_arg6 p6, m_arg7 p7) const { \ if (Thread::get_caller_ID()!=server_thread) {\ - command_queue.push( visual_server, &VisualServer::m_func,p1, p2, p3, p4, p5, p6, p7);\ + command_queue.push( visual_server, &VisualServer::m_type,p1, p2, p3, p4, p5, p6, p7);\ } else {\ - visual_server->m_func(p1, p2, p3, p4, p5, p6, p7);\ + visual_server->m_type(p1, p2, p3, p4, p5, p6, p7);\ }\ } - FUNC0R(RID,texture_create); + //FUNC0R(RID,texture_create); + FUNCRID(texture); FUNC5(texture_allocate,RID,int,int,Image::Format,uint32_t); FUNC3(texture_set_data,RID,const Image&,CubeMapSide); FUNC2RC(Image,texture_get_data,RID,CubeMapSide); @@ -594,7 +632,7 @@ public: if (Thread::get_caller_ID()!=server_thread) { command_queue.push_and_sync( visual_server, &VisualServer::shader_get_param_list,p_shader,p_param_list); } else { - visual_server->m_func(p1, p2, p3, p4, p5); + visual_server->m_type(p1, p2, p3, p4, p5); } }*/ diff --git a/tools/collada/collada.cpp b/tools/collada/collada.cpp index 66e5e3f0e6..0d02c32d00 100644 --- a/tools/collada/collada.cpp +++ b/tools/collada/collada.cpp @@ -344,6 +344,9 @@ void Collada::_parse_image(XMLParser& parser) { // path is relative to file being loaded, so convert to a resource path path=Globals::get_singleton()->localize_path(state.local_path.get_base_dir()+"/"+path); + } else if (path.find("file:///")==0) { + path=path.replace_first("file:///",""); + path=Globals::get_singleton()->localize_path(path); } image.path=path; diff --git a/tools/editor/editor_file_system.cpp b/tools/editor/editor_file_system.cpp index 52b195b232..861b6a9e3c 100644 --- a/tools/editor/editor_file_system.cpp +++ b/tools/editor/editor_file_system.cpp @@ -227,7 +227,7 @@ EditorFileSystem::DirItem* EditorFileSystem::_scan_dir(DirAccess *da,Set<String> DirCache *dc = dir_cache.getptr(path); - if (dc && dc->modification_time==mtime) { + if (false && dc && dc->modification_time==mtime) { //use the cached files, since directory did not change for (Set<String>::Element *E=dc->subdirs.front();E;E=E->next()) { dirs.push_back(E->get()); @@ -542,6 +542,7 @@ bool EditorFileSystem::_check_meta_sources(EditorFileSystemDirectory::ImportMeta for(int j=0;j<p_meta.sources.size();j++) { + String src = EditorImportPlugin::expand_source_path(p_meta.sources[j].path); if (!FileAccess::exists(src)) { @@ -556,6 +557,7 @@ bool EditorFileSystem::_check_meta_sources(EditorFileSystemDirectory::ImportMeta if (mt!=p_meta.sources[j].modified_time) { //scan String md5 = FileAccess::get_md5(src); + print_line("checking: "+src); print_line("md5: "+md5); print_line("vs: "+p_meta.sources[j].md5); if (md5!=p_meta.sources[j].md5) { diff --git a/tools/editor/editor_import_export.cpp b/tools/editor/editor_import_export.cpp index 1a5dd73040..18c0010904 100644 --- a/tools/editor/editor_import_export.cpp +++ b/tools/editor/editor_import_export.cpp @@ -447,12 +447,64 @@ void EditorExportPlatformPC::_get_property_list( List<PropertyInfo> *p_list) con +static void _exp_add_dep(Map<StringName,List<StringName> > &deps,const StringName& p_path) { + + + if (deps.has(p_path)) + return; //already done + + deps.insert(p_path,List<StringName>()); + + List<StringName> &deplist=deps[p_path]; + Set<StringName> depset; + + List<String> dl; + ResourceLoader::get_dependencies(p_path,&dl); + + //added in order so child dependencies are always added bfore parent dependencies + for (List<String>::Element *E=dl.front();E;E=E->next()) { + + + if (!deps.has(E->get())) + _exp_add_dep(deps,E->get()); + + for(List<StringName>::Element *F=deps[E->get()].front();F;F=F->next()) { + + + if (!depset.has(F->get())) { + depset.insert(F->get()); + deplist.push_back(F->get()); + } + } + + if (!depset.has(E->get())) { + depset.insert(E->get()); + deplist.push_back(E->get()); + } + + } +} + + + Error EditorExportPlatform::export_project_files(EditorExportSaveFunction p_func, void* p_udata,bool p_make_bundles) { /* ALL FILES AND DEPENDENCIES */ Vector<StringName> files=get_dependencies(p_make_bundles); + Map<StringName,List<StringName> > deps; + + if (false) { + for(int i=0;i<files.size();i++) { + + _exp_add_dep(deps,files[i]); + + } + } + + + /* GROUP ATLAS */ @@ -643,7 +695,7 @@ Error EditorExportPlatform::export_project_files(EditorExportSaveFunction p_func for (List<StringName>::Element *F=atlas_images.front();F;F=F->next()) { - imd->add_source(EditorImportPlugin::validate_source_path(F->get())); + imd->add_source(EditorImportPlugin::validate_source_path(F->get()),FileAccess::get_md5(F->get())); } @@ -683,7 +735,6 @@ Error EditorExportPlatform::export_project_files(EditorExportSaveFunction p_func rects.resize(r_rects.size()); for(int i=0;i<r_rects.size();i++) { //get back region and margins - rects[i]=r_rects[i]; } @@ -788,18 +839,49 @@ Error EditorExportPlatform::export_project_files(EditorExportSaveFunction p_func { //make binary engine.cfg config - Map<String,Variant> custom; + if (remap_files.size()) { Vector<String> remapsprop; for(Map<StringName,StringName>::Element *E=remap_files.front();E;E=E->next()) { + print_line("REMAP: "+String(E->key())+" -> "+E->get()); remapsprop.push_back(E->key()); remapsprop.push_back(E->get()); } custom["remap/all"]=remapsprop; } + + //add presaved dependencies + for(Map<StringName,List<StringName> >::Element *E=deps.front();E;E=E->next()) { + + if (E->get().size()==0) + continue; //no deps + String key; + Vector<StringName> deps; + //if bundle continue (when bundles supported obviously) + + if (remap_files.has(E->key())) { + key=remap_files[E->key()]; + } else { + key=E->key(); + } + + deps.resize(E->get().size()); + int i=0; + + for(List<StringName>::Element *F=E->get().front();F;F=F->next()) { + deps[i++]=F->get(); + print_line(" -"+String(F->get())); + } + + NodePath prop(deps,true,String()); //seems best to use this for performance + + custom["deps/"+key.md5_text()]=prop; + + } + String remap_file="engine.cfb"; String engine_cfb =EditorSettings::get_singleton()->get_settings_path()+"/tmp/tmp"+remap_file; Globals::get_singleton()->save_custom(engine_cfb,custom); diff --git a/tools/editor/editor_node.cpp b/tools/editor/editor_node.cpp index d8f9dcc947..af61022af2 100644 --- a/tools/editor/editor_node.cpp +++ b/tools/editor/editor_node.cpp @@ -723,8 +723,6 @@ void EditorNode::_save_scene(String p_file) { flg|=ResourceSaver::FLAG_COMPRESS; if (EditorSettings::get_singleton()->get("on_save/save_paths_as_relative")) flg|=ResourceSaver::FLAG_RELATIVE_PATHS; - if (EditorSettings::get_singleton()->get("on_save/save_paths_without_extension")) - flg|=ResourceSaver::FLAG_NO_EXTENSION; err = ResourceSaver::save(p_file,sdata,flg); @@ -1005,8 +1003,6 @@ void EditorNode::_dialog_action(String p_file) { flg|=ResourceSaver::FLAG_COMPRESS; if (EditorSettings::get_singleton()->get("on_save/save_paths_as_relative")) flg|=ResourceSaver::FLAG_RELATIVE_PATHS; - if (EditorSettings::get_singleton()->get("on_save/save_paths_without_extension")) - flg|=ResourceSaver::FLAG_NO_EXTENSION; err = ResourceSaver::save(p_file,sdata,flg); @@ -3455,7 +3451,7 @@ EditorNode::EditorNode() { p->add_item("Undo",EDIT_UNDO,KEY_MASK_CMD+KEY_Z); p->add_item("Redo",EDIT_REDO,KEY_MASK_CMD+KEY_MASK_SHIFT+KEY_Z); p->add_separator(); - p->add_item("Run Script",FILE_RUN_SCRIPT,KEY_MASK_CMD+KEY_R); + p->add_item("Run Script",FILE_RUN_SCRIPT,KEY_MASK_SHIFT+KEY_MASK_CMD+KEY_R); p->add_separator(); p->add_item("Project Settings",RUN_SETTINGS); p->add_separator(); diff --git a/tools/editor/io_plugins/editor_font_import_plugin.cpp b/tools/editor/io_plugins/editor_font_import_plugin.cpp index 4de68c7f4c..4b3e052907 100644 --- a/tools/editor/io_plugins/editor_font_import_plugin.cpp +++ b/tools/editor/io_plugins/editor_font_import_plugin.cpp @@ -88,6 +88,7 @@ public: Color color; Color gradient_begin; Color gradient_end; + bool color_use_monochrome; String gradient_image; @@ -148,6 +149,8 @@ public: gradient_end=p_value; else if (n=="color/image") gradient_image=p_value; + else if (n=="color/monochrome") + color_use_monochrome=p_value; else if (n=="advanced/round_advance") round_advance=p_value; else @@ -210,6 +213,8 @@ public: r_ret=gradient_end; else if (n=="color/image") r_ret=gradient_image; + else if (n=="color/monochrome") + r_ret=color_use_monochrome; else if (n=="advanced/round_advance") r_ret=round_advance; else @@ -258,6 +263,7 @@ public: if (color_type==COLOR_GRADIENT_IMAGE) { p_list->push_back(PropertyInfo(Variant::STRING,"color/image",PROPERTY_HINT_FILE)); } + p_list->push_back(PropertyInfo(Variant::BOOL,"color/monochrome")); p_list->push_back(PropertyInfo(Variant::BOOL,"advanced/round_advance")); } @@ -270,6 +276,35 @@ public: } + void reset() { + + char_extra_spacing=0; + top_extra_spacing=0; + bottom_extra_spacing=0; + space_extra_spacing=0; + + character_set=CHARSET_LATIN; + + shadow=false; + shadow_radius=2; + shadow_color=Color(0,0,0,0.3); + shadow_transition=1.0; + + shadow2=false; + shadow2_radius=2; + shadow2_color=Color(0,0,0,0.3); + shadow2_transition=1.0; + + color_type=COLOR_WHITE; + color=Color(1,1,1,1); + gradient_begin=Color(1,1,1,1); + gradient_end=Color(0.5,0.5,0.5,1); + color_use_monochrome=false; + + round_advance=true; + + } + _EditorFontImportOptions() { char_extra_spacing=0; @@ -293,6 +328,7 @@ public: color=Color(1,1,1,1); gradient_begin=Color(1,1,1,1); gradient_end=Color(0.5,0.5,0.5,1); + color_use_monochrome=false; round_advance=true; } @@ -503,6 +539,7 @@ public: dest->get_line_edit()->set_text(p_path); List<String> opts; rimd->get_options(&opts); + options->reset(); for(List<String>::Element *E=opts.front();E;E=E->next()) { options->_set(E->get(),rimd->get_option(E->get())); @@ -1200,6 +1237,12 @@ Ref<Font> EditorFontImportPlugin::generate_font(const Ref<ResourceImportMetadata } + + if (from->has_option("color/monochrome") && bool(from->get_option("color/monochrome"))) { + + atlas.convert(Image::FORMAT_GRAYSCALE_ALPHA); + } + if (0) { //debug the texture Ref<ImageTexture> atlast = memnew( ImageTexture ); diff --git a/tools/editor/io_plugins/editor_texture_import_plugin.cpp b/tools/editor/io_plugins/editor_texture_import_plugin.cpp index 90dcbb97e0..916bd59360 100644 --- a/tools/editor/io_plugins/editor_texture_import_plugin.cpp +++ b/tools/editor/io_plugins/editor_texture_import_plugin.cpp @@ -747,8 +747,10 @@ Error EditorTextureImportPlugin::import2(const String& p_path, const Ref<Resourc for(int i=0;i<from->get_source_count();i++) { String path = EditorImportPlugin::expand_source_path(from->get_source_path(i)); + String md5 = FileAccess::get_md5(path); + from->set_source_md5(i,FileAccess::get_md5(path)); ep.step("Loading Image: "+path,i); - print_line("source path: "+path); + print_line("source path: "+path+" md5 "+md5); Image src; Error err = ImageLoader::load_image(path,&src); if (err) { @@ -894,7 +896,7 @@ Error EditorTextureImportPlugin::import2(const String& p_path, const Ref<Resourc EditorNode::add_io_error("Couldn't save atlas image: "+apath); return err; } - from->set_source_md5(i,FileAccess::get_md5(apath)); + //from->set_source_md5(i,FileAccess::get_md5(apath)); } } } @@ -953,7 +955,7 @@ Error EditorTextureImportPlugin::import2(const String& p_path, const Ref<Resourc } else { - print_line("compress..."); + Image image=texture->get_data(); ERR_FAIL_COND_V(image.empty(),ERR_INVALID_DATA); @@ -990,7 +992,6 @@ Error EditorTextureImportPlugin::import2(const String& p_path, const Ref<Resourc } - print_line("COMPRESSED TO: "+itos(image.get_format())); texture->create_from_image(image,tex_flags); @@ -1075,7 +1076,7 @@ Vector<uint8_t> EditorTextureImportPlugin::custom_export(const String& p_path, c rimd->set_option("quality",group_lossy_quality); rimd->set_option("atlas",false); rimd->set_option("shrink",group_shrink); - rimd->add_source(EditorImportPlugin::validate_source_path(p_path)); + rimd->add_source(EditorImportPlugin::validate_source_path(p_path),FileAccess::get_md5(p_path)); } else if (EditorImportExport::get_singleton()->get_image_formats().has(p_path.extension().to_lower()) && EditorImportExport::get_singleton()->get_export_image_action()!=EditorImportExport::IMAGE_ACTION_NONE) { //handled by general image export settings @@ -1102,7 +1103,7 @@ Vector<uint8_t> EditorTextureImportPlugin::custom_export(const String& p_path, c rimd->set_option("flags",flags); rimd->set_option("quality",EditorImportExport::get_singleton()->get_export_image_quality()); rimd->set_option("atlas",false); - rimd->add_source(EditorImportPlugin::validate_source_path(p_path)); + rimd->add_source(EditorImportPlugin::validate_source_path(p_path),FileAccess::get_md5(p_path)); } else { return Vector<uint8_t>(); @@ -1117,7 +1118,7 @@ Vector<uint8_t> EditorTextureImportPlugin::custom_export(const String& p_path, c } uint32_t flags = rimd->get_option("flags"); - uint8_t shrink = rimd->has_option("shrink") ? rimd->get_option("shrink"): Variant(1); + uint8_t shrink = rimd->has_option("shrink") ? rimd->get_option("shrink"): Variant(1); uint8_t format = rimd->get_option("format"); uint8_t comp = (format==EditorTextureImportPlugin::IMAGE_FORMAT_COMPRESS_RAM)?uint8_t(p_platform->get_image_compression()):uint8_t(255); @@ -1214,6 +1215,9 @@ EditorTextureImportPlugin::EditorTextureImportPlugin(EditorNode *p_editor, Mode if (rimd.is_valid()) { if (rimd->get_editor()!="") { + int compression = rimd->get_option("format"); + if (compression!=EditorTextureImportPlugin::IMAGE_FORMAT_COMPRESS_RAM) + return Vector<uint8_t>(); //only useful for RAM compression to reconvert Ref<EditorImportPlugin> pl = EditorImportExport::get_singleton()->get_import_plugin_by_name(rimd->get_editor()); if (pl.is_valid()) { Vector<uint8_t> ce = pl->custom_export(p_path,p_platform); diff --git a/tools/editor/project_export.cpp b/tools/editor/project_export.cpp index eae5dacb27..f571aba434 100644 --- a/tools/editor/project_export.cpp +++ b/tools/editor/project_export.cpp @@ -276,7 +276,7 @@ void ProjectExportDialog::_notification(int p_what) { image_action->select(EditorImportExport::get_singleton()->get_export_image_action()); image_quality->set_val(EditorImportExport::get_singleton()->get_export_image_quality()); - image_shrink->set_val(EditorImportExport::get_singleton()->get_export_image_quality()); + image_shrink->set_val(EditorImportExport::get_singleton()->get_export_image_shrink()); image_quality->connect("value_changed",this,"_quality_edited"); image_shrink->connect("value_changed",this,"_shrink_edited"); image_action->connect("item_selected",this,"_image_export_edited"); @@ -349,7 +349,7 @@ void ProjectExportDialog::_validate_platform() { List<String> pl; EditorFileSystem::get_singleton()->get_changed_sources(&pl); - if (pl.size()) { + if (false && pl.size()) { if (pl.size()==1) platform_error_string->set_text(" -One Resource is pending re-import."); else diff --git a/tools/editor/resources_dock.cpp b/tools/editor/resources_dock.cpp index 37a3469578..eb2e526d71 100644 --- a/tools/editor/resources_dock.cpp +++ b/tools/editor/resources_dock.cpp @@ -156,8 +156,6 @@ void ResourcesDock::save_resource(const String& p_path,const Ref<Resource>& p_re flg|=ResourceSaver::FLAG_COMPRESS; if (EditorSettings::get_singleton()->get("on_save/save_paths_as_relative")) flg|=ResourceSaver::FLAG_RELATIVE_PATHS; - if (EditorSettings::get_singleton()->get("on_save/save_paths_without_extension")) - flg|=ResourceSaver::FLAG_NO_EXTENSION; String path = Globals::get_singleton()->localize_path(p_path); Error err = ResourceSaver::save(path,p_resource,flg); diff --git a/tools/editor/scene_tree_editor.cpp b/tools/editor/scene_tree_editor.cpp index 89b7e54195..e2ae897fe9 100644 --- a/tools/editor/scene_tree_editor.cpp +++ b/tools/editor/scene_tree_editor.cpp @@ -73,6 +73,12 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item,int p_column,int p_id) undo_redo->commit_action(); } else if (n->is_type("CanvasItem")) { + CanvasItem *ci = n->cast_to<CanvasItem>(); + if (!ci->is_visible() && ci->get_parent_item() && !ci->get_parent_item()->is_visible()) { + error->set_text("This item cannot be made visible because the parent is hidden. Unhide the parent first."); + error->popup_centered_minsize(Size2(400,80)); + return; + } bool v = !bool(n->call("is_hidden")); undo_redo->create_action("Toggle CanvasItem Visible"); undo_redo->add_do_method(n,v?"hide":"show"); @@ -663,6 +669,9 @@ SceneTreeEditor::SceneTreeEditor(bool p_label,bool p_can_rename, bool p_can_open tree->connect("button_pressed",this,"_cell_button_pressed"); // tree->connect("item_edited", this,"_renamed",Vector<Variant>(),true); + error = memnew( AcceptDialog ); + add_child(error); + last_hash=0; pending_test_update=false; updating_tree=false; diff --git a/tools/editor/scene_tree_editor.h b/tools/editor/scene_tree_editor.h index 9a2bdb7ef9..edd67a4047 100644 --- a/tools/editor/scene_tree_editor.h +++ b/tools/editor/scene_tree_editor.h @@ -52,6 +52,8 @@ class SceneTreeEditor : public Control { Tree *tree; Node *selected; + AcceptDialog *error; + int blocked; void _compute_hash(Node *p_node,uint64_t &hash); diff --git a/tools/ios_xcode_template/godot_ios.xcodeproj/project.xcworkspace/xcuserdata/punto.xcuserdatad/UserInterfaceState.xcuserstate b/tools/ios_xcode_template/godot_ios.xcodeproj/project.xcworkspace/xcuserdata/punto.xcuserdatad/UserInterfaceState.xcuserstate Binary files differindex bc65cadf59..7c338929ed 100644 --- a/tools/ios_xcode_template/godot_ios.xcodeproj/project.xcworkspace/xcuserdata/punto.xcuserdatad/UserInterfaceState.xcuserstate +++ b/tools/ios_xcode_template/godot_ios.xcodeproj/project.xcworkspace/xcuserdata/punto.xcuserdatad/UserInterfaceState.xcuserstate |