-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
This commit is contained in:
parent
a65edb4caa
commit
31ce3c5fd0
136 changed files with 10784 additions and 1578 deletions
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
55
core/func_ref.cpp
Normal file
55
core/func_ref.cpp
Normal file
|
@ -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;
|
||||
}
|
||||
|
23
core/func_ref.h
Normal file
23
core/func_ref.h
Normal file
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
int i=0;
|
||||
uint32_t namecount=strlen&=0x7FFFFFFF;
|
||||
uint32_t subnamecount = decode_uint32(buf+4);
|
||||
uint32_t flags = decode_uint32(buf+8);
|
||||
|
||||
len-=12;
|
||||
buf+=12;
|
||||
|
||||
int total=namecount+subnamecount;
|
||||
if (flags&2)
|
||||
total++;
|
||||
|
||||
if (r_len)
|
||||
(*r_len)+=12;
|
||||
|
||||
|
||||
String str;
|
||||
str.parse_utf8((const char*)buf,strlen);
|
||||
for(int i=0;i<total;i++) {
|
||||
|
||||
r_variant=NodePath(str);
|
||||
ERR_FAIL_COND_V((int)len<4,ERR_INVALID_DATA);
|
||||
strlen = decode_uint32(buf);
|
||||
|
||||
if (r_len)
|
||||
(*r_len)+=4+strlen;
|
||||
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: {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -120,7 +120,7 @@ class ResourceFormatSaverBinaryInstance {
|
|||
|
||||
String local_path;
|
||||
|
||||
bool no_extensions;
|
||||
|
||||
bool relative_paths;
|
||||
bool bundle_resources;
|
||||
bool skip_editor;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -74,9 +74,6 @@ public:
|
|||
FLAG_OMIT_EDITOR_PROPERTIES=8,
|
||||
FLAG_SAVE_BIG_ENDIAN=16,
|
||||
FLAG_COMPRESS=32,
|
||||
FLAG_NO_EXTENSION=64,
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
111
drivers/openssl/stream_peer_ssl.cpp
Normal file
111
drivers/openssl/stream_peer_ssl.cpp
Normal file
|
@ -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() {
|
||||
}
|
26
drivers/openssl/stream_peer_ssl.h
Normal file
26
drivers/openssl/stream_peer_ssl.h
Normal file
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -451,6 +453,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);
|
||||
|
@ -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);
|
||||
|
@ -1129,6 +1161,13 @@ MethodInfo GDFunctions::get_info(Function p_func) {
|
|||
mi.return_val.type=Variant::OBJECT;
|
||||
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: {
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ public:
|
|||
LOGIC_CLAMP,
|
||||
LOGIC_NEAREST_PO2,
|
||||
OBJ_WEAKREF,
|
||||
FUNC_FUNCREF,
|
||||
TYPE_CONVERT,
|
||||
TYPE_OF,
|
||||
TEXT_STR,
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
Import('env')
|
||||
|
||||
env.add_source_files(env.modules_sources,"*.cpp")
|
||||
|
||||
Export('env')
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
|
||||
def can_build(platform):
|
||||
return True
|
||||
|
||||
|
||||
def configure(env):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -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;
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
s_ad=this;
|
||||
mutex=NULL;
|
||||
pause=p_pause;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
AudioDriverOpenSL::AudioDriverOpenSL()
|
||||
{
|
||||
s_ad=this;
|
||||
mutex=Mutex::create();//NULL;
|
||||
pause=false;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
9
platform/android/libs/apk_expansion/AndroidManifest.xml
Normal file
9
platform/android/libs/apk_expansion/AndroidManifest.xml
Normal file
|
@ -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>
|
92
platform/android/libs/apk_expansion/build.xml
Normal file
92
platform/android/libs/apk_expansion/build.xml
Normal file
|
@ -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>
|
20
platform/android/libs/apk_expansion/proguard-project.txt
Normal file
20
platform/android/libs/apk_expansion/proguard-project.txt
Normal file
|
@ -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 *;
|
||||
#}
|
13
platform/android/libs/apk_expansion/project.properties
Normal file
13
platform/android/libs/apk_expansion/project.properties
Normal file
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
41
platform/android/libs/apk_expansion/res/values/strings.xml
Normal file
41
platform/android/libs/apk_expansion/res/values/strings.xml
Normal file
|
@ -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>
|
25
platform/android/libs/apk_expansion/res/values/styles.xml
Normal file
25
platform/android/libs/apk_expansion/res/values/styles.xml
Normal file
|
@ -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>
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
24
platform/android/libs/play_licensing/AndroidManifest.xml
Normal file
24
platform/android/libs/play_licensing/AndroidManifest.xml
Normal file
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
92
platform/android/libs/play_licensing/build.xml
Normal file
92
platform/android/libs/play_licensing/build.xml
Normal file
|
@ -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>
|
20
platform/android/libs/play_licensing/proguard-project.txt
Normal file
20
platform/android/libs/play_licensing/proguard-project.txt
Normal file
|
@ -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 *;
|
||||
#}
|
12
platform/android/libs/play_licensing/project.properties
Normal file
12
platform/android/libs/play_licensing/project.properties
Normal file
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue