2
0
Эх сурвалжийг харах

-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

Juan Linietsky 11 жил өмнө
parent
commit
31ce3c5fd0
100 өөрчлөгдсөн 9053 нэмэгдсэн , 860 устгасан
  1. 55 3
      core/bind/core_bind.cpp
  2. 9 0
      core/bind/core_bind.h
  3. 55 0
      core/func_ref.cpp
  4. 23 0
      core/func_ref.h
  5. 10 13
      core/globals.cpp
  6. 2 2
      core/globals.h
  7. 0 1
      core/io/file_access_pack.cpp
  8. 136 12
      core/io/marshalls.cpp
  9. 25 8
      core/io/resource_format_binary.cpp
  10. 1 1
      core/io/resource_format_binary.h
  11. 45 6
      core/io/resource_format_xml.cpp
  12. 4 1
      core/io/resource_format_xml.h
  13. 12 1
      core/io/resource_loader.cpp
  14. 1 0
      core/io/resource_loader.h
  15. 0 3
      core/io/resource_saver.h
  16. 9 2
      core/math/math_funcs.cpp
  17. 23 1
      core/os/file_access.cpp
  18. 3 0
      core/os/file_access.h
  19. 7 0
      core/os/input.cpp
  20. 2 0
      core/os/input.h
  21. 1 1
      core/os/mutex.h
  22. 10 1
      core/os/os.cpp
  23. 3 1
      core/os/os.h
  24. 2 0
      core/register_core_types.cpp
  25. 10 0
      core/ustring.cpp
  26. 2 1
      core/ustring.h
  27. 1 1
      drivers/mpc/audio_stream_mpc.cpp
  28. 111 0
      drivers/openssl/stream_peer_ssl.cpp
  29. 26 0
      drivers/openssl/stream_peer_ssl.h
  30. 8 2
      drivers/vorbis/audio_stream_ogg_vorbis.cpp
  31. 40 1
      modules/gdscript/gd_functions.cpp
  32. 1 0
      modules/gdscript/gd_functions.h
  33. 0 7
      modules/multiscript/SCsub
  34. 0 11
      modules/multiscript/config.py
  35. 0 498
      modules/multiscript/multi_script.cpp
  36. 0 158
      modules/multiscript/multi_script.h
  37. 0 32
      modules/multiscript/register_types.cpp
  38. 0 30
      modules/multiscript/register_types.h
  39. 1 1
      platform/android/AndroidManifest.xml.template
  40. 3 1
      platform/android/SCsub
  41. 45 28
      platform/android/audio_driver_opensl.cpp
  42. 12 9
      platform/android/audio_driver_opensl.h
  43. 5 6
      platform/android/detect.py
  44. 4 4
      platform/android/java/ant.properties
  45. 36 8
      platform/android/java/src/com/android/godot/Godot.java
  46. 41 1
      platform/android/java/src/com/android/godot/GodotIO.java
  47. 3 3
      platform/android/java/src/com/android/godot/GodotLib.java
  48. 42 1
      platform/android/java_glue.cpp
  49. 9 0
      platform/android/libs/apk_expansion/AndroidManifest.xml
  50. 92 0
      platform/android/libs/apk_expansion/build.xml
  51. 20 0
      platform/android/libs/apk_expansion/proguard-project.txt
  52. 13 0
      platform/android/libs/apk_expansion/project.properties
  53. BIN
      platform/android/libs/apk_expansion/res/drawable-hdpi/notify_panel_notification_icon_bg.png
  54. BIN
      platform/android/libs/apk_expansion/res/drawable-mdpi/notify_panel_notification_icon_bg.png
  55. 104 0
      platform/android/libs/apk_expansion/res/layout/status_bar_ongoing_event_progress_bar.xml
  56. 6 0
      platform/android/libs/apk_expansion/res/values-v11/styles.xml
  57. 5 0
      platform/android/libs/apk_expansion/res/values-v9/styles.xml
  58. 41 0
      platform/android/libs/apk_expansion/res/values/strings.xml
  59. 25 0
      platform/android/libs/apk_expansion/res/values/styles.xml
  60. 236 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Constants.java
  61. 80 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java
  62. 277 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
  63. 181 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
  64. 306 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java
  65. 126 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java
  66. 83 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java
  67. 41 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java
  68. 123 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java
  69. 536 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java
  70. 112 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java
  71. 30 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java
  72. 92 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java
  73. 231 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
  74. 963 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
  75. 1341 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
  76. 510 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java
  77. 200 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java
  78. 101 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java
  79. 116 0
      platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V3CustomNotification.java
  80. 24 0
      platform/android/libs/play_licensing/AndroidManifest.xml
  81. 23 0
      platform/android/libs/play_licensing/aidl/ILicenseResultListener.aidl
  82. 25 0
      platform/android/libs/play_licensing/aidl/ILicensingService.aidl
  83. 92 0
      platform/android/libs/play_licensing/build.xml
  84. 20 0
      platform/android/libs/play_licensing/proguard-project.txt
  85. 12 0
      platform/android/libs/play_licensing/project.properties
  86. 110 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/AESObfuscator.java
  87. 397 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/APKExpansionPolicy.java
  88. 47 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/DeviceLimiter.java
  89. 99 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java
  90. 99 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java
  91. 351 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java
  92. 67 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java
  93. 224 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java
  94. 32 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java
  95. 48 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java
  96. 59 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java
  97. 77 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java
  98. 79 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java
  99. 276 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java
  100. 63 0
      platform/android/libs/play_licensing/src/com/google/android/vending/licensing/StrictPolicy.java

+ 55 - 3
core/bind/core_bind.cpp

@@ -387,6 +387,12 @@ uint32_t _OS::get_ticks_msec() const {
 	return OS::get_singleton()->get_ticks_msec();
 }
 
+
+bool _OS::can_use_threads() const {
+
+	return OS::get_singleton()->can_use_threads();
+}
+
 bool _OS::can_draw() const {
 
 	return OS::get_singleton()->can_draw();
@@ -488,6 +494,27 @@ float _OS::get_frames_per_second() const {
 	return OS::get_singleton()->get_frames_per_second();
 }
 
+Error _OS::native_video_play(String p_path) {
+
+	return OS::get_singleton()->native_video_play(p_path);
+};
+
+bool _OS::native_video_is_playing() {
+
+	return OS::get_singleton()->native_video_is_playing();
+};
+
+void _OS::native_video_pause() {
+
+	OS::get_singleton()->native_video_pause();
+};
+
+void _OS::native_video_stop() {
+
+	OS::get_singleton()->native_video_stop();
+};
+
+
 String _OS::get_custom_level() const {
 
 	return OS::get_singleton()->get_custom_level();
@@ -496,7 +523,7 @@ _OS *_OS::singleton=NULL;
 
 void _OS::_bind_methods() {
 
-	ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&_OS::get_mouse_pos);
+	//ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&_OS::get_mouse_pos);
 	//ObjectTypeDB::bind_method(_MD("is_mouse_grab_enabled"),&_OS::is_mouse_grab_enabled);
 
 	ObjectTypeDB::bind_method(_MD("set_clipboard","clipboard"),&_OS::set_clipboard);
@@ -550,7 +577,9 @@ void _OS::_bind_methods() {
 	ObjectTypeDB::bind_method(_MD("get_frames_drawn"),&_OS::get_frames_drawn);
 	ObjectTypeDB::bind_method(_MD("is_stdout_verbose"),&_OS::is_stdout_verbose);
 
-	ObjectTypeDB::bind_method(_MD("get_mouse_button_state"),&_OS::get_mouse_button_state);
+	ObjectTypeDB::bind_method(_MD("can_use_threads"),&_OS::can_use_threads);
+
+	//ObjectTypeDB::bind_method(_MD("get_mouse_button_state"),&_OS::get_mouse_button_state);
 
 	ObjectTypeDB::bind_method(_MD("dump_memory_to_file","file"),&_OS::dump_memory_to_file);
 	ObjectTypeDB::bind_method(_MD("dump_resources_to_file","file"),&_OS::dump_resources_to_file);
@@ -568,6 +597,12 @@ void _OS::_bind_methods() {
 
 	ObjectTypeDB::bind_method(_MD("print_all_textures_by_size"),&_OS::print_all_textures_by_size);
 
+	ObjectTypeDB::bind_method(_MD("native_video_play"),&_OS::native_video_play);
+	ObjectTypeDB::bind_method(_MD("native_video_is_playing"),&_OS::native_video_is_playing);
+	ObjectTypeDB::bind_method(_MD("native_video_stop"),&_OS::native_video_stop);
+	ObjectTypeDB::bind_method(_MD("native_video_pause"),&_OS::native_video_pause);
+
+
 	BIND_CONSTANT( DAY_SUNDAY );
 	BIND_CONSTANT( DAY_MONDAY );
 	BIND_CONSTANT( DAY_TUESDAY );
@@ -983,8 +1018,22 @@ void _File::store_string(const String& p_string){
 
 	f->store_string(p_string);
 }
-void _File::store_line(const String& p_string){
 
+void _File::store_pascal_string(const String& p_string) {
+
+	ERR_FAIL_COND(!f);
+
+	f->store_pascal_string(p_string);
+};
+
+String _File::get_pascal_string() {
+
+	ERR_FAIL_COND_V(!f, "");
+
+	return f->get_pascal_string();
+};
+
+void _File::store_line(const String& p_string){
 
 	ERR_FAIL_COND(!f);
 	f->store_line(p_string);
@@ -1083,6 +1132,9 @@ void _File::_bind_methods() {
 	ObjectTypeDB::bind_method(_MD("store_string","string"),&_File::store_string);
 	ObjectTypeDB::bind_method(_MD("store_var","value"),&_File::store_var);
 
+	ObjectTypeDB::bind_method(_MD("store_pascal_string","string"),&_File::store_pascal_string);
+	ObjectTypeDB::bind_method(_MD("get_pascal_string"),&_File::get_pascal_string);
+
 	ObjectTypeDB::bind_method(_MD("file_exists","path"),&_File::file_exists);
 
 	BIND_CONSTANT( READ );

+ 9 - 0
core/bind/core_bind.h

@@ -98,6 +98,11 @@ public:
 	bool is_video_mode_resizable(int p_screen=0) const;
 	Array get_fullscreen_mode_list(int p_screen=0) const;
 
+	Error native_video_play(String p_path);
+	bool native_video_is_playing();
+	void native_video_pause();
+	void native_video_stop();
+
 	void set_iterations_per_second(int p_ips);
 	int get_iterations_per_second() const;
 
@@ -166,6 +171,7 @@ public:
 	void delay_msec(uint32_t p_msec) const;
 	uint32_t get_ticks_msec() const;
 
+	bool can_use_threads() const;
 
 	bool can_draw() const;
 
@@ -280,6 +286,9 @@ public:
 	void store_string(const String& p_string);
 	void store_line(const String& p_string);
 
+	virtual void store_pascal_string(const String& p_string);
+	virtual String get_pascal_string();
+
 	Vector<String> get_csv_line() const;
 
 

+ 55 - 0
core/func_ref.cpp

@@ -0,0 +1,55 @@
+#include "func_ref.h"
+
+Variant FuncRef::call_func(const Variant** p_args, int p_argcount, Variant::CallError& r_error) {
+
+	if (id==0) {
+		r_error.error=Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL;
+		return Variant();
+	}
+	Object* obj = ObjectDB::get_instance(id);
+
+	if (!obj) {
+		r_error.error=Variant::CallError::CALL_ERROR_INSTANCE_IS_NULL;
+		return Variant();
+	}
+
+	return obj->call(function,p_args,p_argcount,r_error);
+
+}
+
+void FuncRef::set_instance(Object *p_obj){
+
+	ERR_FAIL_NULL(p_obj);
+	id=p_obj->get_instance_ID();
+}
+void FuncRef::set_function(const StringName& p_func){
+
+	function=p_func;
+}
+
+void FuncRef::_bind_methods() {
+
+	{
+		MethodInfo mi;
+		mi.name="call";
+		mi.arguments.push_back( PropertyInfo( Variant::STRING, "method"));
+		Vector<Variant> defargs;
+		for(int i=0;i<10;i++) {
+			mi.arguments.push_back( PropertyInfo( Variant::NIL, "arg"+itos(i)));
+			defargs.push_back(Variant());
+		}
+		ObjectTypeDB::bind_native_method(METHOD_FLAGS_DEFAULT,"call_func",&FuncRef::call_func,mi,defargs);
+
+	}
+
+	ObjectTypeDB::bind_method(_MD("set_instance","instance"),&FuncRef::set_instance);
+	ObjectTypeDB::bind_method(_MD("set_function","name"),&FuncRef::set_function);
+
+}
+
+
+FuncRef::FuncRef(){
+
+	id=0;
+}
+

+ 23 - 0
core/func_ref.h

@@ -0,0 +1,23 @@
+#ifndef FUNC_REF_H
+#define FUNC_REF_H
+
+#include "reference.h"
+
+class FuncRef : public Reference{
+
+	OBJ_TYPE(FuncRef,Reference);
+	ObjectID id;
+	StringName function;
+
+protected:
+
+	static void _bind_methods();
+public:
+
+	Variant call_func(const Variant** p_args, int p_argcount, Variant::CallError& r_error);
+	void set_instance(Object *p_obj);
+	void set_function(const StringName& p_func);
+	FuncRef();
+};
+
+#endif // FUNC_REF_H

+ 10 - 13
core/globals.cpp

@@ -166,10 +166,9 @@ bool Globals::_get(const StringName& p_name,Variant &r_ret) const {
 
 	_THREAD_SAFE_METHOD_
 
-	const VariantContainer *v=props.getptr(p_name);
-	if (!v)
+	if (!props.has(p_name))
 		return false;
-	r_ret=v->variant;
+	r_ret=props[p_name].variant;
 	return true;
 	
 }
@@ -188,18 +187,17 @@ void Globals::_get_property_list(List<PropertyInfo> *p_list) const {
 	
 	_THREAD_SAFE_METHOD_
 
-	const String *k=NULL;
 	Set<_VCSort> vclist;
 	
-	while ((k=props.next(k))) {
+	for(Map<StringName,VariantContainer>::Element *E=props.front();E;E=E->next()) {
 		
-		const VariantContainer *v=props.getptr(*k);
+		const VariantContainer *v=&E->get();
 
 		if (v->hide_from_editor)
 			continue;
 
 		_VCSort vc;
-		vc.name=*k;
+		vc.name=E->key();
 		vc.order=v->order;
 		vc.type=v->variant.get_type();
 		if (vc.name.begins_with("input/") || vc.name.begins_with("import/") || vc.name.begins_with("export/") || vc.name.begins_with("/remap") || vc.name.begins_with("/locale") || vc.name.begins_with("/autoload"))
@@ -1138,24 +1136,23 @@ Error Globals::save_custom(const String& p_path,const CustomMap& p_custom,const
 
 	ERR_FAIL_COND_V(p_path=="",ERR_INVALID_PARAMETER);
 
-	const String *k=NULL;
 	Set<_VCSort> vclist;
 
-	while ((k=props.next(k))) {
+	for(Map<StringName,VariantContainer>::Element *G=props.front();G;G=G->next()) {
 
-		const VariantContainer *v=props.getptr(*k);
+		const VariantContainer *v=&G->get();
 
 		if (v->hide_from_editor)
 			continue;
 
-		if (p_custom.has(*k))
+		if (p_custom.has(G->key()))
 			continue;
 
 		bool discard=false;
 
 		for(const Set<String>::Element *E=p_ignore_masks.front();E;E=E->next()) {
 
-			if ( (*k).match(E->get())) {
+			if ( String(G->key()).match(E->get())) {
 				discard=true;
 				break;
 			}
@@ -1165,7 +1162,7 @@ Error Globals::save_custom(const String& p_path,const CustomMap& p_custom,const
 			continue;
 
 		_VCSort vc;
-		vc.name=*k;
+		vc.name=G->key();//*k;
 		vc.order=v->order;
 		vc.type=v->variant.get_type();
 		vc.flags=PROPERTY_USAGE_CHECKABLE|PROPERTY_USAGE_EDITOR|PROPERTY_USAGE_STORAGE;

+ 2 - 2
core/globals.h

@@ -65,9 +65,9 @@ protected:
 	};
 
 	int last_order;
-	HashMap<String,VariantContainer> props;
+	Map<StringName,VariantContainer> props;
 	String resource_path;
-	HashMap<String,PropertyInfo> custom_prop_info;
+	Map<StringName,PropertyInfo> custom_prop_info;
 	bool disable_platform_override;
 	bool using_datapack;
 

+ 0 - 1
core/io/file_access_pack.cpp

@@ -172,7 +172,6 @@ bool PackedSourcePCK::try_open_pack(const String& p_path) {
 		uint64_t size = f->get_64();
 		uint8_t md5[16];
 		f->get_buffer(md5,16);
-
 		PackedData::get_singleton()->add_path(p_path, path, ofs, size, md5,this);
 	};
 

+ 136 - 12
core/io/marshalls.cpp

@@ -264,26 +264,94 @@ Error decode_variant(Variant& r_variant,const uint8_t *p_buffer, int p_len,int *
 			}
 
 			r_variant=img;
-			if (r_len)
+			if (r_len) {
+				if (datalen%4)
+					(*r_len)+=4-datalen%4;
+
 				(*r_len)+=4*5+datalen;
+			}
 
 		} break;
 		case Variant::NODE_PATH: {
 
-			ERR_FAIL_COND_V(len<4,ERR_INVALID_DATA);
+			ERR_FAIL_COND_V(len<4,ERR_INVALID_DATA);			
 			uint32_t strlen = decode_uint32(buf);
-			buf+=4;
-			len-=4;
-			ERR_FAIL_COND_V((int)strlen>len,ERR_INVALID_DATA);
 
+			if (strlen&0x80000000) {
+				//new format
+				ERR_FAIL_COND_V(len<12,ERR_INVALID_DATA);
+				Vector<StringName> names;
+				Vector<StringName> subnames;
+				bool absolute;
+				StringName prop;
 
-			String str;
-			str.parse_utf8((const char*)buf,strlen);
+				int i=0;
+				uint32_t namecount=strlen&=0x7FFFFFFF;
+				uint32_t subnamecount = decode_uint32(buf+4);
+				uint32_t flags = decode_uint32(buf+8);
 
-			r_variant=NodePath(str);
+				len-=12;
+				buf+=12;
 
-			if (r_len)
-				(*r_len)+=4+strlen;
+				int total=namecount+subnamecount;
+				if (flags&2)
+					total++;
+
+				if (r_len)
+					(*r_len)+=12;
+
+
+				for(int i=0;i<total;i++) {
+
+					ERR_FAIL_COND_V((int)len<4,ERR_INVALID_DATA);
+					strlen = decode_uint32(buf);
+
+					int pad=0;
+
+					if (strlen%4)
+						pad+=4-strlen%4;
+
+					buf+=4;
+					len-=4;
+					ERR_FAIL_COND_V((int)strlen+pad>len,ERR_INVALID_DATA);
+
+					String str;
+					str.parse_utf8((const char*)buf,strlen);
+
+
+					if (i<namecount)
+						names.push_back(str);
+					else if (i<namecount+subnamecount)
+						subnames.push_back(str);
+					else
+						prop=str;
+
+					buf+=strlen+pad;
+					len-=strlen+pad;
+
+					if (r_len)
+						(*r_len)+=4+strlen+pad;
+
+				}
+
+				r_variant=NodePath(names,subnames,flags&1,prop);
+
+			} else {
+				//old format, just a string
+
+				buf+=4;
+				len-=4;
+				ERR_FAIL_COND_V((int)strlen>len,ERR_INVALID_DATA);
+
+
+				String str;
+				str.parse_utf8((const char*)buf,strlen);
+
+				r_variant=NodePath(str);
+
+				if (r_len)
+					(*r_len)+=4+strlen;
+			}
 
 		} break;
 		/*case Variant::RESOURCE: {
@@ -713,7 +781,59 @@ Error encode_variant(const Variant& p_variant, uint8_t *r_buffer, int &r_len) {
 			r_len+=4;
 
 		} break;
-		case Variant::NODE_PATH:
+		case Variant::NODE_PATH: {
+
+			NodePath np=p_variant;
+			if (buf) {
+				encode_uint32(uint32_t(np.get_name_count())|0x80000000,buf);	//for compatibility with the old format
+				encode_uint32(np.get_subname_count(),buf+4);
+				uint32_t flags=0;
+				if (np.is_absolute())
+					flags|=1;
+				if (np.get_property()!=StringName())
+					flags|=2;
+
+				encode_uint32(flags,buf+8);
+
+				buf+=12;
+			}
+
+			r_len+=12;
+
+			int total = np.get_name_count()+np.get_subname_count();
+			if (np.get_property()!=StringName())
+				total++;
+
+			for(int i=0;i<total;i++) {
+
+				String str;
+
+				if (i<np.get_name_count())
+					str=np.get_name(i);
+				else if (i<np.get_name_count()+np.get_subname_count())
+					str=np.get_subname(i-np.get_subname_count());
+				else
+					str=np.get_property();
+
+				CharString utf8 = str.utf8();
+
+				int pad = 0;
+
+				if (utf8.length()%4)
+					pad=4-utf8.length()%4;
+
+				if (buf) {
+					encode_uint32(utf8.length(),buf);
+					buf+=4;
+					copymem(buf,utf8.get_data(),utf8.length());
+					buf+=pad+utf8.length();
+				}
+
+
+				r_len+=4+utf8.length()+pad;
+			}
+
+		} break;
 		case Variant::STRING: {
 
 
@@ -879,7 +999,11 @@ Error encode_variant(const Variant& p_variant, uint8_t *r_buffer, int &r_len) {
 				copymem(&buf[20],&r[0],ds);
 			}
 
-			r_len+=data.size()+5*4;
+			int pad=0;
+			if (data.size()%4)
+				pad=4-data.size()%4;
+
+			r_len+=data.size()+5*4+pad;
 
 		} break;		
 		/*case Variant::RESOURCE: {

+ 25 - 8
core/io/resource_format_binary.cpp

@@ -647,7 +647,7 @@ Error ResourceInteractiveLoaderBinary::poll(){
 		}
 
 		stage++;
-		return OK;
+		return error;
 	}
 
 	s-=external_resources.size();
@@ -804,7 +804,12 @@ void ResourceInteractiveLoaderBinary::get_dependencies(FileAccess *p_f,List<Stri
 
 	for(int i=0;i<external_resources.size();i++) {
 
-		p_dependencies->push_back(external_resources[i].path);
+		String dep=external_resources[i].path;
+		if (dep.ends_with("*")) {
+			dep=ResourceLoader::guess_full_filename(dep,external_resources[i].type);
+		}
+
+		p_dependencies->push_back(dep);
 	}
 
 }
@@ -892,6 +897,19 @@ void ResourceInteractiveLoaderBinary::open(FileAccess *p_f) {
 
 	}
 
+	//see if the exporter has different set of external resources for more efficient loading
+	String preload_depts = "deps/"+res_path.md5_text();
+	if (Globals::get_singleton()->has(preload_depts)) {
+		external_resources.clear();
+		//ignore external resources and use these
+		NodePath depts=Globals::get_singleton()->get(preload_depts);
+		external_resources.resize(depts.get_name_count());
+		for(int i=0;i<depts.get_name_count();i++) {
+			external_resources[i].path=depts.get_name(i);
+		}
+		print_line(res_path+" - EXTERNAL RESOURCES: "+itos(external_resources.size()));
+	}
+
 	print_bl("ext resources: "+itos(ext_resources_size));
 	uint32_t int_resources_size=f->get_32();
 
@@ -1412,8 +1430,6 @@ void ResourceFormatSaverBinaryInstance::write_variant(const Variant& p_property,
 				f->store_32(OBJECT_EXTERNAL_RESOURCE);
 				save_unicode_string(res->get_save_type());
 				String path=relative_paths?local_path.path_to_file(res->get_path()):res->get_path();
-				if (no_extensions)
-					path=path.basename()+".*";
 				save_unicode_string(path);
 			} else {
 
@@ -1439,7 +1455,7 @@ void ResourceFormatSaverBinaryInstance::write_variant(const Variant& p_property,
 
 			f->store_32(VARIANT_DICTIONARY);
 			Dictionary d = p_property;
-            f->store_32(uint32_t(d.size())|(d.is_shared()?0x80000000:0));
+			f->store_32(uint32_t(d.size())|(d.is_shared()?0x80000000:0));
 
 			List<Variant> keys;
 			d.get_key_list(&keys);
@@ -1734,7 +1750,7 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_
 	skip_editor=p_flags&ResourceSaver::FLAG_OMIT_EDITOR_PROPERTIES;
 	bundle_resources=p_flags&ResourceSaver::FLAG_BUNDLE_RESOURCES;
 	big_endian=p_flags&ResourceSaver::FLAG_SAVE_BIG_ENDIAN;
-	no_extensions=p_flags&ResourceSaver::FLAG_NO_EXTENSION;
+
 
 	local_path=p_path.get_base_dir();
 	//bin_meta_idx = get_string_index("__bin_meta__"); //is often used, so create
@@ -1816,8 +1832,6 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_
 
 		save_unicode_string(E->get()->get_save_type());
 		String path = E->get()->get_path();
-		if (no_extensions)
-			path=path.basename()+".*";
 		save_unicode_string(path);
 	}
 	// save internal resource table
@@ -1861,6 +1875,7 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_
 	}
 
 	f->seek_end();
+	print_line("SAVING: "+p_path);
 	if (p_resource->get_import_metadata().is_valid()) {
 		uint64_t md_pos = f->get_pos();
 		Ref<ResourceImportMetadata> imd=p_resource->get_import_metadata();
@@ -1869,6 +1884,8 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path,const RES& p_
 		for(int i=0;i<imd->get_source_count();i++) {
 			save_unicode_string(imd->get_source_path(i));
 			save_unicode_string(imd->get_source_md5(i));
+			print_line("SAVE PATH: "+imd->get_source_path(i));
+			print_line("SAVE MD5: "+imd->get_source_md5(i));
 		}
 		List<String> options;
 		imd->get_options(&options);

+ 1 - 1
core/io/resource_format_binary.h

@@ -120,7 +120,7 @@ class ResourceFormatSaverBinaryInstance  {
 
 	String local_path;
 
-	bool no_extensions;
+
 	bool relative_paths;
 	bool bundle_resources;
 	bool skip_editor;

+ 45 - 6
core/io/resource_format_xml.cpp

@@ -1357,6 +1357,31 @@ Error ResourceInteractiveLoaderXML::poll() {
 	if (error!=OK)
 		return error;
 
+	if (ext_resources.size()) {
+
+		error=ERR_FILE_CORRUPT;
+		String path=ext_resources.front()->get();
+
+		RES res = ResourceLoader::load(path);
+
+		if (res.is_null()) {
+
+			if (ResourceLoader::get_abort_on_missing_resources()) {
+				ERR_EXPLAIN(local_path+":"+itos(get_current_line())+": editor exported unexisting resource at: "+path);
+				ERR_FAIL_V(error);
+			} else {
+				ResourceLoader::notify_load_error("Resource Not Found: "+path);
+			}
+		} else {
+
+			resource_cache.push_back(res);
+		}
+
+		error=OK;
+		ext_resources.pop_front();
+		resource_current++;
+		return error;
+	}
 
 	bool exit;
 	Tag *tag = parse_tag(&exit);
@@ -1528,7 +1553,7 @@ int ResourceInteractiveLoaderXML::get_stage() const {
 }
 int ResourceInteractiveLoaderXML::get_stage_count() const {
 
-	return resources_total;
+	return resources_total+ext_resources.size();
 }
 
 ResourceInteractiveLoaderXML::~ResourceInteractiveLoaderXML() {
@@ -1573,6 +1598,12 @@ void ResourceInteractiveLoaderXML::get_dependencies(FileAccess *f,List<String> *
 			path=Globals::get_singleton()->localize_path(local_path.get_base_dir()+"/"+path);
 		}
 
+		if (path.ends_with("*")) {
+			ERR_FAIL_COND(!tag->args.has("type"));
+			String type = tag->args["type"];
+			path = ResourceLoader::guess_full_filename(path,type);
+		}
+
 		p_dependencies->push_back(path);
 
 		Error err = close_tag("ext_resource");
@@ -1642,6 +1673,19 @@ void ResourceInteractiveLoaderXML::open(FileAccess *p_f) {
 
 	}
 
+	String preload_depts = "deps/"+local_path.md5_text();
+	if (Globals::get_singleton()->has(preload_depts)) {
+		ext_resources.clear();
+		//ignore external resources and use these
+		NodePath depts=Globals::get_singleton()->get(preload_depts);
+
+		for(int i=0;i<depts.get_name_count();i++) {
+			ext_resources.push_back(depts.get_name(i));
+		}
+		print_line(local_path+" - EXTERNAL RESOURCES: "+itos(ext_resources.size()));
+	}
+
+
 }
 
 String ResourceInteractiveLoaderXML::recognize(FileAccess *p_f) {
@@ -1969,8 +2013,6 @@ void ResourceFormatSaverXMLInstance::write_property(const String& p_name,const V
 			if (res->get_path().length() && res->get_path().find("::")==-1) {
 				//external resource
 				String path=relative_paths?local_path.path_to_file(res->get_path()):res->get_path();
-				if (no_extension)
-					path=path.basename()+".*";
 				escape(path);
 				params+=" path=\""+path+"\"";
 			} else {
@@ -2458,7 +2500,6 @@ Error ResourceFormatSaverXMLInstance::save(const String &p_path,const RES& p_res
 	relative_paths=p_flags&ResourceSaver::FLAG_RELATIVE_PATHS;
 	skip_editor=p_flags&ResourceSaver::FLAG_OMIT_EDITOR_PROPERTIES;
 	bundle_resources=p_flags&ResourceSaver::FLAG_BUNDLE_RESOURCES;
-	no_extension=p_flags&ResourceSaver::FLAG_NO_EXTENSION;
 	depth=0;
 
 	// save resources
@@ -2475,8 +2516,6 @@ Error ResourceFormatSaverXMLInstance::save(const String &p_path,const RES& p_res
 
 		write_tabs();
 		String p = E->get()->get_path();
-		if (no_extension)
-			p=p.basename()+".*";
 
 		enter_tag("ext_resource","path=\""+p+"\" type=\""+E->get()->get_save_type()+"\""); //bundled
 		exit_tag("ext_resource"); //bundled

+ 4 - 1
core/io/resource_format_xml.h

@@ -50,6 +50,10 @@ class ResourceInteractiveLoaderXML : public ResourceInteractiveLoader {
 
 	_FORCE_INLINE_ Error _parse_array_element(Vector<char> &buff,bool p_number_only,FileAccess *f,bool *end);
 
+
+
+	List<StringName> ext_resources;
+
 	int resources_total;
 	int resource_current;
 	String resource_type;
@@ -113,7 +117,6 @@ class ResourceFormatSaverXMLInstance  {
 
 
 
-	bool no_extension;
 	bool relative_paths;
 	bool bundle_resources;
 	bool skip_editor;

+ 12 - 1
core/io/resource_loader.cpp

@@ -166,7 +166,7 @@ RES ResourceLoader::load(const String &p_path,const String& p_type_hint,bool p_n
 	String remapped_path = PathRemap::get_singleton()->get_remap(local_path);
 
 	if (OS::get_singleton()->is_stdout_verbose())
-		print_line("load resource: ");
+		print_line("load resource: "+remapped_path);
 
 	String extension=remapped_path.extension();
 	bool found=false;
@@ -233,6 +233,10 @@ Ref<ResourceImportMetadata> ResourceLoader::load_import_metadata(const String &p
 
 
 String ResourceLoader::find_complete_path(const String& p_path,const String& p_type) {
+	//this is an old vestige when the engine saved files without extension.
+	//remains here for compatibility with old projects and only because it
+	//can be sometimes nice to open files using .* from a script and have it guess
+	//the right extension.
 
 	String local_path = p_path;
 	if (local_path.ends_with("*")) {
@@ -353,6 +357,13 @@ void ResourceLoader::get_dependencies(const String& p_path,List<String> *p_depen
 	}
 }
 
+String ResourceLoader::guess_full_filename(const String &p_path,const String& p_type) {
+
+	String local_path = Globals::get_singleton()->localize_path(p_path);
+
+	return find_complete_path(local_path,p_type);
+
+}
 
 String ResourceLoader::get_resource_type(const String &p_path) {
 

+ 1 - 0
core/io/resource_loader.h

@@ -102,6 +102,7 @@ public:
 	static String get_resource_type(const String &p_path);
 	static void get_dependencies(const String& p_path,List<String> *p_dependencies);
 
+	static String guess_full_filename(const String &p_path,const String& p_type);
 
 	static void set_timestamp_on_load(bool p_timestamp) { timestamp_on_load=p_timestamp; }
 

+ 0 - 3
core/io/resource_saver.h

@@ -74,9 +74,6 @@ public:
 		FLAG_OMIT_EDITOR_PROPERTIES=8,
 		FLAG_SAVE_BIG_ENDIAN=16,
 		FLAG_COMPRESS=32,
-		FLAG_NO_EXTENSION=64,
-
-
 	};
 
 

+ 9 - 2
core/math/math_funcs.cpp

@@ -220,9 +220,16 @@ int Math::decimals(double p_step) {
 
 double Math::ease(double p_x, double p_c) {
 
+	if (p_x<0)
+		p_x=0;
+	else if (p_x>1.0)
+		p_x=1.0;
 	if (p_c>0) {
-
-		return Math::pow(p_x,p_c);
+		if (p_c<1.0) {
+			return 1.0-Math::pow(1.0-p_x,1.0/p_c);
+		} else {
+			return Math::pow(p_x,p_c);
+		}
 	} else  if (p_c<0) {
 		//inout ease
 

+ 23 - 1
core/os/file_access.cpp

@@ -428,8 +428,30 @@ void FileAccess::store_string(const String& p_string) {
 	CharString cs=p_string.utf8();
 	store_buffer((uint8_t*)&cs[0],cs.length());
 
-
 }
+
+void FileAccess::store_pascal_string(const String& p_string) {
+
+	CharString cs = p_string.utf8();
+	store_32(cs.length());
+	store_buffer((uint8_t*)&cs[0], cs.length());
+};
+
+String FileAccess::get_pascal_string() {
+
+	uint32_t sl = get_32();
+	CharString cs;
+	cs.resize(sl+1);
+	get_buffer((uint8_t*)cs.ptr(),sl);
+	cs[sl]=0;
+
+	String ret;
+	ret.parse_utf8(cs.ptr());
+
+	return ret;
+};
+
+
 void FileAccess::store_line(const String& p_line) {
 
 	store_string(p_line);

+ 3 - 0
core/os/file_access.h

@@ -125,6 +125,9 @@ public:
 	virtual void store_string(const String& p_string);
 	virtual void store_line(const String& p_string);
 
+	virtual void store_pascal_string(const String& p_string);
+	virtual String get_pascal_string();
+
 	virtual void store_buffer(const uint8_t *p_src,int p_length); ///< store an array of bytes 
 	
 	virtual bool file_exists(const String& p_name)=0; ///< return true if a file exists 

+ 7 - 0
core/os/input.cpp

@@ -56,6 +56,7 @@ void Input::_bind_methods() {
 	ObjectTypeDB::bind_method(_MD("get_accelerometer"),&Input::get_accelerometer);
 	ObjectTypeDB::bind_method(_MD("get_mouse_pos"),&Input::get_mouse_pos);
 	ObjectTypeDB::bind_method(_MD("get_mouse_speed"),&Input::get_mouse_speed);
+	ObjectTypeDB::bind_method(_MD("get_mouse_button_mask"),&Input::get_mouse_button_mask);
 	ObjectTypeDB::bind_method(_MD("set_mouse_mode","mode"),&Input::set_mouse_mode);
 	ObjectTypeDB::bind_method(_MD("get_mouse_mode"),&Input::get_mouse_mode);
 
@@ -280,6 +281,12 @@ Point2 InputDefault::get_mouse_speed() const {
 	return mouse_speed_track.speed;
 }
 
+int InputDefault::get_mouse_button_mask() const {
+
+	OS::get_singleton()->get_mouse_button_state();
+}
+
+
 void InputDefault::iteration(float p_step) {
 
 

+ 2 - 0
core/os/input.h

@@ -64,6 +64,7 @@ public:
 
 	virtual Point2 get_mouse_pos() const=0;
 	virtual Point2 get_mouse_speed() const=0;
+	virtual int get_mouse_button_mask() const=0;
 
 	virtual Vector3 get_accelerometer()=0;
 
@@ -120,6 +121,7 @@ public:
 
 	virtual Point2 get_mouse_pos() const;
 	virtual Point2 get_mouse_speed() const;
+	virtual int get_mouse_button_mask() const;
 
 	void parse_input_event(const InputEvent& p_event);
 	void set_accelerometer(const Vector3& p_accel);

+ 1 - 1
core/os/mutex.h

@@ -50,7 +50,7 @@ public:
 
 	virtual void lock()=0; ///< Lock the mutex, block if locked by someone else
 	virtual void unlock()=0; ///< Unlock the mutex, let other threads continue
-	virtual Error try_lock()=0; ///< Attempt to lock the mutex, true on success, false means it can't lock.
+	virtual Error try_lock()=0; ///< Attempt to lock the mutex, OK on success, ERROR means it can't lock.
 
 	static Mutex * create(bool p_recursive=true); ///< Create a mutex
 	

+ 10 - 1
core/os/os.cpp

@@ -430,7 +430,7 @@ Error OS::native_video_play(String p_path) {
 	return FAILED;
 };
 
-bool OS::native_video_is_playing() {
+bool OS::native_video_is_playing() const {
 
 	return false;
 };
@@ -447,6 +447,15 @@ void OS::set_mouse_mode(MouseMode p_mode) {
 
 }
 
+bool OS::can_use_threads() const {
+
+#ifdef NO_THREADS
+	return false;
+#else
+	return true;
+#endif
+}
+
 OS::MouseMode OS::get_mouse_mode() const{
 
 	return MOUSE_MODE_VISIBLE;

+ 3 - 1
core/os/os.h

@@ -316,10 +316,12 @@ public:
 	virtual String get_unique_ID() const;
 
 	virtual Error native_video_play(String p_path);
-	virtual bool native_video_is_playing();
+	virtual bool native_video_is_playing() const;
 	virtual void native_video_pause();
 	virtual void native_video_stop();
 
+	virtual bool can_use_threads() const;
+
 	virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, Object* p_obj, String p_callback);
 	virtual Error dialog_input_text(String p_title, String p_description, String p_partial, Object* p_obj, String p_callback);
 

+ 2 - 0
core/register_core_types.cpp

@@ -49,6 +49,7 @@
 #include "core/io/xml_parser.h"
 #include "io/http_client.h"
 #include "packed_data_container.h"
+#include "func_ref.h"
 
 #ifdef XML_ENABLED
 static ResourceFormatSaverXML *resource_saver_xml=NULL;
@@ -135,6 +136,7 @@ void register_core_types() {
 	ObjectTypeDB::register_type<Reference>();
 	ObjectTypeDB::register_type<ResourceImportMetadata>();
 	ObjectTypeDB::register_type<Resource>();
+	ObjectTypeDB::register_type<FuncRef>();
 	ObjectTypeDB::register_virtual_type<StreamPeer>();
 	ObjectTypeDB::register_create_type<StreamPeerTCP>();
 	ObjectTypeDB::register_create_type<TCP_Server>();

+ 10 - 0
core/ustring.cpp

@@ -31,6 +31,7 @@
 #include "os/memory.h"
 #include "print_string.h"
 #include "math_funcs.h"
+#include "io/md5.h"
 #include "ucaps.h"
 #include "color.h"
 #define MAX_DIGITS 6
@@ -2264,6 +2265,15 @@ uint64_t String::hash64() const {
 
 }
 
+String String::md5_text() const {
+
+	CharString cs=utf8();
+	MD5_CTX ctx;
+	MD5Init(&ctx);
+	MD5Update(&ctx,(unsigned char*)cs.ptr(),cs.length());
+	MD5Final(&ctx);
+	return String::md5(ctx.digest);
+}
 
 String String::insert(int p_at_pos,String p_string) const {
 

+ 2 - 1
core/ustring.h

@@ -181,7 +181,8 @@ public:
 	static uint32_t hash(const char* p_cstr,int p_len); /* hash the string */
 	static uint32_t hash(const char* p_cstr); /* hash the string */
 	uint32_t hash() const; /* hash the string */
-	uint64_t hash64() const; /* hash the string */
+	uint64_t hash64() const; /* hash the string */	
+	String md5_text() const;
 	
 	inline bool empty() const { return length() == 0; }	
 

+ 1 - 1
drivers/mpc/audio_stream_mpc.cpp

@@ -140,7 +140,7 @@ mpc_bool_t AudioStreamMPC::_mpc_canseek(mpc_reader *p_reader) {
 
 bool AudioStreamMPC::_can_mix() const {
 
-	return active && !paused;
+	return /*active &&*/ !paused;
 }
 
 

+ 111 - 0
drivers/openssl/stream_peer_ssl.cpp

@@ -0,0 +1,111 @@
+#include "stream_peer_ssl.h"
+
+
+int StreamPeerSSL::bio_create( BIO *b ) {
+	b->init = 1;
+	b->num = 0;
+	b->ptr = NULL;
+	b->flags = 0;
+	return 1;
+}
+
+int StreamPeerSSL::bio_destroy( BIO *b ) {
+
+	if ( b == NULL ) return 0;
+	b->ptr = NULL;		/* sb_tls_remove() will free it */
+	b->init = 0;
+	b->flags = 0;
+	return 1;
+}
+
+int StreamPeerSSL::bio_read( BIO *b, char *buf, int len ) {
+
+	if ( buf == NULL || len <= 0 ) return 0;
+
+	StreamPeerSSL * sp = (StreamPeerSSL*)b->ptr;
+
+	if (sp->base.is_null())
+		return 0;
+
+
+
+	BIO_clear_retry_flags( b );
+
+	Error err;
+	int ret=0;
+	if (sp->block) {
+		err = sp->base->get_data((const uint8_t*)buf,len);
+		if (err==OK)
+			ret=len;
+	} else {
+
+		err = sp->base->get_partial_data((const uint8_t*)buf,len,ret);
+		if (err==OK && ret!=len) {
+			BIO_set_retry_write( b );
+		}
+
+	}
+
+	return ret;
+}
+
+int StreamPeerSSL::bio_write( BIO *b, const char *buf, int len ) {
+
+	if ( buf == NULL || len <= 0 ) return 0;
+
+	StreamPeerSSL * sp = (StreamPeerSSL*)b->ptr;
+
+	if (sp->base.is_null())
+		return 0;
+
+	BIO_clear_retry_flags( b );
+
+	Error err;
+	int wrote=0;
+	if (sp->block) {
+		err = sp->base->put_data((const uint8_t*)buf,len);
+		if (err==OK)
+			wrote=len;
+	} else {
+
+		err = sp->base->put_partial_data((const uint8_t*)buf,len,wrote);
+		if (err==OK && wrote!=len) {
+			BIO_set_retry_write( b );
+		}
+
+	}
+
+	return wrote;
+}
+
+long StreamPeerSSL::bio_ctrl( BIO *b, int cmd, long num, void *ptr ) {
+	if ( cmd == BIO_CTRL_FLUSH ) {
+		/* The OpenSSL library needs this */
+		return 1;
+	}
+	return 0;
+}
+
+int StreamPeerSSL::bio_gets( BIO *b, char *buf, int len ) {
+	return -1;
+}
+
+int StreamPeerSSL::bio_puts( BIO *b, const char *str ) {
+	return StreamPeerSSL::bio_write( b, str, strlen( str ) );
+}
+
+BIO_METHOD StreamPeerSSL::bio_methods =
+{
+	( 100 | 0x400 ),		/* it's a source/sink BIO */
+	"sockbuf glue",
+	StreamPeerSSL::bio_write,
+	StreamPeerSSL::bio_read,
+	StreamPeerSSL::bio_puts,
+	StreamPeerSSL::bio_gets,
+	StreamPeerSSL::bio_ctrl,
+	StreamPeerSSL::bio_create,
+	StreamPeerSSL::bio_destroy
+};
+
+StreamPeerSSL::StreamPeerSSL() {
+}

+ 26 - 0
drivers/openssl/stream_peer_ssl.h

@@ -0,0 +1,26 @@
+#ifndef STREAM_PEER_SSL_H
+#define STREAM_PEER_SSL_H
+
+#include "io/stream_peer.h"
+
+class StreamPeerSSL : public StreamPeer {
+
+	OBJ_TYPE(StreamPeerSSL,StreamPeer);
+
+	Ref<StreamPeer> base;
+	bool block;
+	static BIO_METHOD bio_methods;
+
+	static int bio_create( BIO *b );
+	static int bio_destroy( BIO *b );
+	static int bio_read( BIO *b, char *buf, int len );
+	static int bio_write( BIO *b, const char *buf, int len );
+	static long bio_ctrl( BIO *b, int cmd, long num, void *ptr );
+	static int bio_gets( BIO *b, char *buf, int len );
+	static int bio_puts( BIO *b, const char *str );
+
+public:
+	StreamPeerSSL();
+};
+
+#endif // STREAM_PEER_SSL_H

+ 8 - 2
drivers/vorbis/audio_stream_ogg_vorbis.cpp

@@ -97,7 +97,7 @@ long AudioStreamOGGVorbis::_ov_tell_func(void *_f) {
 
 bool AudioStreamOGGVorbis::_can_mix() const {
 
-	return playing && !paused;
+	return /*playing &&*/ !paused;
 }
 
 
@@ -125,6 +125,8 @@ void AudioStreamOGGVorbis::update() {
 		if (ret<0) {
 
 			playing = false;
+			setting_up=false;
+
 			ERR_EXPLAIN("Error reading OGG Vorbis File: "+file);
 			ERR_BREAK(ret<0);
 		} else if (ret==0) { // end of song, reload?
@@ -135,7 +137,8 @@ void AudioStreamOGGVorbis::update() {
 
 			if (!has_loop()) {
 
-				playing=false;	
+				playing=false;
+				setting_up=false;
 				repeats=1;
 				return;
 			}
@@ -145,6 +148,7 @@ void AudioStreamOGGVorbis::update() {
 			int errv = ov_open_callbacks(f,&vf,NULL,0,_ov_callbacks);
 			if (errv!=0) {
 				playing=false;
+				setting_up=false;
 				return; // :(
 			}
 
@@ -179,6 +183,8 @@ void AudioStreamOGGVorbis::play() {
 	playing=false;
 	setting_up=true;
 	update();
+	if (!setting_up)
+		return;
 	setting_up=false;
 	playing=true;
 }

+ 40 - 1
modules/gdscript/gd_functions.cpp

@@ -31,6 +31,7 @@
 #include "object_type_db.h"
 #include "reference.h"
 #include "gd_script.h"
+#include "func_ref.h"
 #include "os/os.h"
 
 const char *GDFunctions::get_func_name(Function p_func) {
@@ -80,6 +81,7 @@ const char *GDFunctions::get_func_name(Function p_func) {
 		"clamp",
 		"nearest_po2",
 		"weakref",
+		"funcref",
 		"convert",
 		"typeof",
 		"str",
@@ -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: {
 

+ 1 - 0
modules/gdscript/gd_functions.h

@@ -77,6 +77,7 @@ public:
 		LOGIC_CLAMP,
 		LOGIC_NEAREST_PO2,
 		OBJ_WEAKREF,
+		FUNC_FUNCREF,
 		TYPE_CONVERT,
 		TYPE_OF,
 		TEXT_STR,

+ 0 - 7
modules/multiscript/SCsub

@@ -1,7 +0,0 @@
-Import('env')
-
-env.add_source_files(env.modules_sources,"*.cpp")
-
-Export('env')
-
-

+ 0 - 11
modules/multiscript/config.py

@@ -1,11 +0,0 @@
-
-
-def can_build(platform):
-  return True
-  
-  
-def configure(env):
-	pass
-  
-  
-  

+ 0 - 498
modules/multiscript/multi_script.cpp

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

+ 0 - 158
modules/multiscript/multi_script.h

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

+ 0 - 32
modules/multiscript/register_types.cpp

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

+ 0 - 30
modules/multiscript/register_types.h

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

+ 1 - 1
platform/android/AndroidManifest.xml.template

@@ -35,6 +35,6 @@ $$ADD_APPLICATION_CHUNKS$$
 	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
 	
-    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/>
+    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="15"/>
          
 </manifest> 

+ 3 - 1
platform/android/SCsub

@@ -8,7 +8,7 @@ android_files = [
 	'godot_android.cpp',
 	'file_access_android.cpp',
 	'dir_access_android.cpp',
-	'audio_driver_android.cpp',
+	'audio_driver_opensl.cpp',
 	'file_access_jandroid.cpp',
 	'dir_access_jandroid.cpp',
 	'thread_jandroid.cpp',
@@ -37,7 +37,9 @@ abspath=env.Dir(".").abspath
 pp_basein = open(abspath+"/project.properties.template","rb")
 pp_baseout = open(abspath+"/java/project.properties","wb")
 pp_baseout.write( pp_basein.read() )
+
 refcount=1
+
 for x in env.android_source_modules:
 	pp_baseout.write("android.library.reference."+str(refcount)+"="+x+"\n")
 	refcount+=1

+ 45 - 28
platform/android/audio_driver_android.cpp → platform/android/audio_driver_opensl.cpp

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  audio_driver_android.cpp                                             */
+/*  audio_driver_opensl.cpp                                             */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -26,9 +26,8 @@
 /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
-#include "audio_driver_android.h"
+#include "audio_driver_opensl.h"
 #include <string.h>
-#ifdef ANDROID_NATIVE_ACTIVITY
 
 
 
@@ -40,21 +39,32 @@
 /* Structure for passing information to callback function */
 
 
-void AudioDriverAndroid::_buffer_callback(
+void AudioDriverOpenSL::_buffer_callback(
     SLAndroidSimpleBufferQueueItf queueItf
  /*   SLuint32 eventFlags,
     const void * pBuffer,
     SLuint32 bufferSize,
     SLuint32 dataUsed*/) {
 
+	bool mix=true;
 
+	if (pause) {
+		mix=false;
+	} else if (mutex) {
+		mix = mutex->try_lock()==OK;
+	}
 
-	if (mutex)
-		mutex->lock();
+	if (mix) {
+		audio_server_process(buffer_size,mixdown_buffer);
+	} else {
 
-	audio_server_process(buffer_size,mixdown_buffer);
+		int32_t* src_buff=mixdown_buffer;
+		for(int i=0;i<buffer_size*2;i++) {
+			src_buff[i]=0;
+		}
+	}
 
-	if (mutex)
+	if (mutex && mix)
 		mutex->unlock();
 
 
@@ -87,7 +97,7 @@ void AudioDriverAndroid::_buffer_callback(
 #endif
 }
 
-void AudioDriverAndroid::_buffer_callbacks(
+void AudioDriverOpenSL::_buffer_callbacks(
     SLAndroidSimpleBufferQueueItf queueItf,
     /*SLuint32 eventFlags,
     const void * pBuffer,
@@ -96,7 +106,7 @@ void AudioDriverAndroid::_buffer_callbacks(
     void *pContext) {
 
 
-	AudioDriverAndroid *ad = (AudioDriverAndroid*)pContext;
+	AudioDriverOpenSL *ad = (AudioDriverOpenSL*)pContext;
 
 //	ad->_buffer_callback(queueItf,eventFlags,pBuffer,bufferSize,dataUsed);
 	ad->_buffer_callback(queueItf);
@@ -104,17 +114,17 @@ void AudioDriverAndroid::_buffer_callbacks(
 }
 
 
-AudioDriverAndroid* AudioDriverAndroid::s_ad=NULL;
+AudioDriverOpenSL* AudioDriverOpenSL::s_ad=NULL;
 
-const char* AudioDriverAndroid::get_name() const {
+const char* AudioDriverOpenSL::get_name() const {
 
 	return "Android";
 }
 
 #if 0
-int AudioDriverAndroid::thread_func(SceSize args, void *argp) {
+int AudioDriverOpenSL::thread_func(SceSize args, void *argp) {
 
-	AudioDriverAndroid* ad = s_ad;
+	AudioDriverOpenSL* ad = s_ad;
 	sceAudioOutput2Reserve(AUDIO_OUTPUT_SAMPLE);
 
 	int half=0;
@@ -170,7 +180,7 @@ int AudioDriverAndroid::thread_func(SceSize args, void *argp) {
 }
 
 #endif
-Error AudioDriverAndroid::init(){
+Error AudioDriverOpenSL::init(){
 
 	SLresult
 	res;
@@ -197,7 +207,7 @@ Error AudioDriverAndroid::init(){
 	return OK;
 
 }
-void AudioDriverAndroid::start(){
+void AudioDriverOpenSL::start(){
 
 
 	mutex = Mutex::create();
@@ -357,37 +367,44 @@ void AudioDriverAndroid::start(){
 
 	active=true;
 }
-int AudioDriverAndroid::get_mix_rate() const {
+int AudioDriverOpenSL::get_mix_rate() const {
 
 	return 44100;
 }
-AudioDriverSW::OutputFormat AudioDriverAndroid::get_output_format() const{
+AudioDriverSW::OutputFormat AudioDriverOpenSL::get_output_format() const{
 
 	return OUTPUT_STEREO;
 }
-void AudioDriverAndroid::lock(){
+void AudioDriverOpenSL::lock(){
 
-	//if (active && mutex)
-	//	mutex->lock();
+	if (active && mutex)
+		mutex->lock();
 
 }
-void AudioDriverAndroid::unlock() {
+void AudioDriverOpenSL::unlock() {
 
-	//if (active && mutex)
-	//	mutex->unlock();
+	if (active && mutex)
+		mutex->unlock();
 
 }
-void AudioDriverAndroid::finish(){
+void AudioDriverOpenSL::finish(){
 
 	(*sl)->Destroy(sl);
 
 }
 
+void AudioDriverOpenSL::set_pause(bool p_pause) {
 
-AudioDriverAndroid::AudioDriverAndroid()
+	pause=p_pause;
+}
+
+
+AudioDriverOpenSL::AudioDriverOpenSL()
 {
 	s_ad=this;
-	mutex=NULL;
+	mutex=Mutex::create();//NULL;
+	pause=false;
 }
 
-#endif
+
+

+ 12 - 9
platform/android/audio_driver_android.h → platform/android/audio_driver_opensl.h

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  audio_driver_android.h                                               */
+/*  audio_driver_opensl.h                                                */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -26,16 +26,18 @@
 /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
-#ifndef AUDIO_DRIVER_ANDROID_H
-#define AUDIO_DRIVER_ANDROID_H
+#ifndef AUDIO_DRIVER_OPENSL_H
+#define AUDIO_DRIVER_OPENSL_H
+
 
-#ifdef ANDROID_NATIVE_ACTIVITY
 
 #include "servers/audio/audio_server_sw.h"
 #include "os/mutex.h"
 #include <SLES/OpenSLES.h>
 #include "SLES/OpenSLES_Android.h"
-class AudioDriverAndroid : public AudioDriverSW {
+
+
+class AudioDriverOpenSL : public AudioDriverSW {
 
 	bool active;
 	Mutex *mutex;
@@ -45,7 +47,7 @@ class AudioDriverAndroid : public AudioDriverSW {
 		BUFFER_COUNT=2
 	};
 
-
+	bool pause;
 
 
 	uint32_t buffer_size;
@@ -67,7 +69,7 @@ class AudioDriverAndroid : public AudioDriverSW {
 	SLDataLocator_OutputMix locator_outputmix;
 	SLBufferQueueState state;
 
-	static AudioDriverAndroid* s_ad;
+	static AudioDriverOpenSL* s_ad;
 
 	void _buffer_callback(
 	    SLAndroidSimpleBufferQueueItf queueItf
@@ -97,9 +99,10 @@ public:
 	virtual void unlock();
 	virtual void finish();
 
+	virtual void set_pause(bool p_pause);
 
-	AudioDriverAndroid();
+	AudioDriverOpenSL();
 };
 
 #endif // AUDIO_DRIVER_ANDROID_H
-#endif
+

+ 5 - 6
platform/android/detect.py

@@ -14,6 +14,7 @@ def can_build():
         import os
         if (not os.environ.has_key("ANDROID_NDK_ROOT")):
         	return False
+
 	return True
 
 def get_opts():
@@ -23,7 +24,7 @@ def get_opts():
              ('NDK_TOOLCHAIN', 'toolchain to use for the NDK',"arm-eabi-4.4.0"), 	                      
              #android 2.3       
 		 ('ndk_platform', 'compile for platform: (2.2,2.3)',"2.2"),
-		 ('NDK_TARGET', 'toolchain to use for the NDK',"arm-linux-androideabi-4.7"),
+		 ('NDK_TARGET', 'toolchain to use for the NDK',"arm-linux-androideabi-4.8"),
 	     ('android_stl','enable STL support in android port (for modules)','no'),
 	     ('armv6','compile for older phones running arm v6 (instead of v7+neon+smp)','no')
 
@@ -55,13 +56,10 @@ def configure(env):
 		env.Tool('gcc')
 		env['SPAWN'] = methods.win32_spawn
 
+	env.android_source_modules.append("../libs/apk_expansion")	
 	ndk_platform=""
 
-	if (env["ndk_platform"]=="2.2"):
-		ndk_platform="android-8"
-	else:
-		ndk_platform="android-9"
-		env.Append(CPPFLAGS=["-DANDROID_NATIVE_ACTIVITY"])
+	ndk_platform="android-15"
 
 	print("Godot Android!!!!!")
 
@@ -111,6 +109,7 @@ def configure(env):
 		env['CCFLAGS'] = string.split('-DNO_STATVFS -MMD -MP -MF -fpic -ffunction-sections -funwind-tables -fstack-protector -D__ARM_ARCH_7__ -D__GLIBC__  -Wno-psabi -march=armv6 -mfpu=neon -mfloat-abi=softfp -ftree-vectorize -funsafe-math-optimizations -fno-strict-aliasing -DANDROID -Wa,--noexecstack -DGLES2_ENABLED -DGLES1_ENABLED')
 
 	env.Append(LDPATH=[ld_path])
+	env.Append(LIBS=['OpenSLES'])
 #	env.Append(LIBS=['c','m','stdc++','log','EGL','GLESv1_CM','GLESv2','OpenSLES','supc++','android'])
 	if (env["ndk_platform"]!="2.2"):
 		env.Append(LIBS=['EGL','OpenSLES','android'])

+ 4 - 4
platform/android/java/ant.properties

@@ -15,8 +15,8 @@
 #  'key.alias' for the name of the key to use.
 # The password will be asked during the build when you use the 'release' target.
 
-key.store=my-release-key.keystore
-key.alias=mykey
+key.store=/home/luis/Downloads/carnavalguachin.keystore
+key.alias=momoselacome
 
-key.store.password=123456
-key.alias.password=123456
+key.store.password=12345678
+key.alias.password=12345678

+ 36 - 8
platform/android/java/src/com/android/godot/Godot.java

@@ -49,15 +49,19 @@ import android.media.*;
 import android.hardware.*;
 import android.content.*;
 
+import android.net.Uri;
+import android.media.MediaPlayer;
+
 import java.lang.reflect.Method;
 import java.util.List;
 import java.util.ArrayList;
+import com.android.godot.payments.PaymentsManager;
+import java.io.IOException;
 import android.provider.Settings.Secure;
 
 
 public class Godot extends Activity implements SensorEventListener
-{
-
+{	
 	static public class SingletonBase {
 
 		protected void registerClass(String p_name, String[] p_methods) {
@@ -131,8 +135,12 @@ public class Godot extends Activity implements SensorEventListener
 	};
 	public ResultCallback result_callback;
 
+	private PaymentsManager mPaymentsManager;
+
 	@Override protected void onActivityResult (int requestCode, int resultCode, Intent data) {
-		if (result_callback != null) {
+		if(requestCode == PaymentsManager.REQUEST_CODE_FOR_PURCHASE){
+			mPaymentsManager.processPurchaseResponse(resultCode, data);
+		}else if (result_callback != null) {
 			result_callback.callback(requestCode, resultCode, data);
 			result_callback = null;
 		};
@@ -152,13 +160,17 @@ public class Godot extends Activity implements SensorEventListener
 
 	}
 
+	private static Godot _self;
+	
+	public static Godot getInstance(){
+		return Godot._self;
+	}
+	
 	@Override protected void onCreate(Bundle icicle) {
 
 
 		super.onCreate(icicle);
-
-
-
+		_self = this;
 		Window window = getWindow();
 		window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
 			| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -172,12 +184,20 @@ public class Godot extends Activity implements SensorEventListener
 		mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
 
 		result_callback = null;
-
+		
+		mPaymentsManager = PaymentsManager.createManager(this).initService();
+		
 	//	instanceSingleton( new GodotFacebook(this) );
 
 
 	}
 
+	@Override protected void onDestroy(){
+		
+		if(mPaymentsManager != null ) mPaymentsManager.destroy();
+		super.onDestroy();
+	}
+	
 	@Override protected void onPause() {
 		super.onPause();
 		mView.onPause();
@@ -291,7 +311,15 @@ public class Godot extends Activity implements SensorEventListener
 	@Override public boolean onKeyDown(int keyCode, KeyEvent event) {
 		GodotLib.key(event.getUnicodeChar(0), true);
 		return super.onKeyDown(keyCode, event);
-	};
+	}
+
+	public PaymentsManager getPaymentsManager() {
+		return mPaymentsManager;
+	}
+
+//	public void setPaymentsManager(PaymentsManager mPaymentsManager) {
+//		this.mPaymentsManager = mPaymentsManager;
+//	};
 
 
 	// Audio

+ 41 - 1
platform/android/java/src/com/android/godot/GodotIO.java

@@ -57,6 +57,9 @@ public class GodotIO {
 	AssetManager am;
 	Activity activity;
 
+	Context applicationContext;
+	MediaPlayer mediaPlayer;
+
 	final int SCREEN_LANDSCAPE=0;
 	final int SCREEN_PORTRAIT=1;
 	final int SCREEN_REVERSE_LANDSCAPE=2;
@@ -326,7 +329,7 @@ public class GodotIO {
 		activity=p_activity;
 		streams=new HashMap<Integer,AssetData>();
 		dirs=new HashMap<Integer,AssetDir>();
-
+		applicationContext = activity.getApplicationContext();
 
 	}
 
@@ -502,6 +505,43 @@ public class GodotIO {
 		}
 	};
 
+	public void playVideo(String p_path)
+	{
+		Uri filePath = Uri.parse(p_path);
+		mediaPlayer = new MediaPlayer();
+
+		try {
+			mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+			mediaPlayer.setDataSource(applicationContext, filePath);
+			mediaPlayer.prepare();
+			mediaPlayer.start();
+		}
+		catch(IOException e)
+        {
+            System.out.println("IOError while playing video");
+        }
+	}
+
+	public boolean isVideoPlaying() {
+		if (mediaPlayer != null) {
+			return mediaPlayer.isPlaying();
+		}
+		return false;
+	}
+
+	public void pauseVideo() {
+		if (mediaPlayer != null) {
+			mediaPlayer.pause();
+		}
+	}
+
+	public void stopVideo() {
+		if (mediaPlayer != null) {
+			mediaPlayer.release();
+			mediaPlayer = null;
+		}
+	}
+
 	protected static final String PREFS_FILE = "device_id.xml";
 	protected static final String PREFS_DEVICE_ID = "device_id";
 

+ 3 - 3
platform/android/java/src/com/android/godot/GodotLib.java

@@ -51,14 +51,14 @@ public class GodotLib {
      public static native void step();
      public static native void touch(int what,int pointer,int howmany, int[] arr);
      public static native void accelerometer(float x, float y, float z);
-	 public static native void key(int p_unicode_char, boolean p_pressed);
+	public static native void key(int p_unicode_char, boolean p_pressed);
      public static native void focusin();
      public static native void focusout();
      public static native void audio();
      public static native void singleton(String p_name,Object p_object);
      public static native void method(String p_sname,String p_name,String p_ret,String[] p_params);
      public static native String getGlobal(String p_key);
-	 public static native void callobject(int p_ID, String p_method, Object[] p_params);
-	 public static native void calldeferred(int p_ID, String p_method, Object[] p_params);
+	public static native void callobject(int p_ID, String p_method, Object[] p_params);
+	public static native void calldeferred(int p_ID, String p_method, Object[] p_params);
 
 }

+ 42 - 1
platform/android/java_glue.cpp

@@ -568,6 +568,11 @@ static jmethodID _hideKeyboard=0;
 static jmethodID _setScreenOrientation=0;
 static jmethodID _getUniqueID=0;
 
+static jmethodID _playVideo=0;
+static jmethodID _isVideoPlaying=0;
+static jmethodID _pauseVideo=0;
+static jmethodID _stopVideo=0;
+
 
 static void _gfx_init_func(void* ud, bool gl2) {
 
@@ -628,6 +633,31 @@ static void _hide_vk() {
 	env->CallVoidMethod(godot_io, _hideKeyboard);
 };
 
+// virtual Error native_video_play(String p_path);
+// virtual bool native_video_is_playing();
+// virtual void native_video_pause();
+// virtual void native_video_stop();
+
+static void _play_video(const String& p_path) {
+
+}
+
+static bool _is_video_playing() {
+	JNIEnv* env = ThreadAndroid::get_env();
+	return env->CallBooleanMethod(godot_io, _isVideoPlaying);
+	//return false;
+}
+
+static void _pause_video() {
+	JNIEnv* env = ThreadAndroid::get_env();
+	env->CallVoidMethod(godot_io, _pauseVideo);
+}
+
+static void _stop_video() {
+	JNIEnv* env = ThreadAndroid::get_env();
+	env->CallVoidMethod(godot_io, _stopVideo);
+}
+
 JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env, jobject obj, jobject activity,jboolean p_need_reload_hook) {
 
 	__android_log_print(ANDROID_LOG_INFO,"godot","**INIT EVENT! - %p\n",env);
@@ -675,6 +705,11 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env,
 			_showKeyboard = env->GetMethodID(c,"showKeyboard","(Ljava/lang/String;)V");
 			_hideKeyboard = env->GetMethodID(c,"hideKeyboard","()V");
 			_setScreenOrientation = env->GetMethodID(c,"setScreenOrientation","(I)V");
+
+			_playVideo = env->GetMethodID(c,"playVideo","(Ljava/lang/String;)V");
+			_isVideoPlaying = env->GetMethodID(c,"isVideoPlaying","()Z");
+			_pauseVideo = env->GetMethodID(c,"pauseVideo","()V");
+			_stopVideo = env->GetMethodID(c,"stopVideo","()V");
 		}
 
 		ThreadAndroid::make_default(jvm);
@@ -685,7 +720,7 @@ JNIEXPORT void JNICALL Java_com_android_godot_GodotLib_initialize(JNIEnv * env,
 
 
 
-    os_android = new OS_Android(_gfx_init_func,env,_open_uri,_get_data_dir,_get_locale, _get_model,_show_vk, _hide_vk,_set_screen_orient,_get_unique_id);
+    os_android = new OS_Android(_gfx_init_func,env,_open_uri,_get_data_dir,_get_locale, _get_model,_show_vk, _hide_vk,_set_screen_orient,_get_unique_id, _play_video, _is_video_playing, _pause_video, _stop_video);
     os_android->set_need_reload_hooks(p_need_reload_hook);
 
 	char wd[500];
@@ -803,6 +838,12 @@ static void _initialize_java_modules() {
 			__android_log_print(ANDROID_LOG_INFO,"godot","****^*^*?^*^*class data %x",singletonClass);
 			jmethodID initialize = env->GetStaticMethodID(singletonClass, "initialize", "(Landroid/app/Activity;)Lcom/android/godot/Godot$SingletonBase;");
 
+			if (!initialize) {
+
+				ERR_EXPLAIN("Couldn't find proper initialize function 'public static Godot.SingletonBase Class::initialize(Activity p_activity)' initializer for singleton class: "+m);
+				ERR_CONTINUE(!initialize);
+
+			}
 			jobject obj = env->CallStaticObjectMethod(singletonClass,initialize,_godot_instance);
 			__android_log_print(ANDROID_LOG_INFO,"godot","****^*^*?^*^*class instance %x",obj);
 			jobject gob = env->NewGlobalRef(obj);

+ 9 - 0
platform/android/libs/apk_expansion/AndroidManifest.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.vending.expansion.downloader"
+    android:versionCode="2"
+    android:versionName="1.1" >
+
+    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15"/>
+    
+</manifest>

+ 92 - 0
platform/android/libs/apk_expansion/build.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="apk_expansion" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- if sdk.dir was not set from one of the property file, then
+         get it from the ANDROID_HOME env var.
+         This must be done before we load project.properties since
+         the proguard config can use sdk.dir -->
+    <property environment="env" />
+    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+        <isset property="env.ANDROID_HOME" />
+    </condition>
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+            unless="sdk.dir"
+    />
+
+    <!--
+        Import per project custom build rules if present at the root of the project.
+        This is the place to put custom intermediary targets such as:
+            -pre-build
+            -pre-compile
+            -post-compile (This is typically used for code obfuscation.
+                           Compiled code location: ${out.classes.absolute.dir}
+                           If this is not done in place, override ${out.dex.input.absolute.dir})
+            -post-package
+            -post-build
+            -pre-clean
+    -->
+    <import file="custom_rules.xml" optional="true" />
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>

+ 20 - 0
platform/android/libs/apk_expansion/proguard-project.txt

@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 13 - 0
platform/android/libs/apk_expansion/project.properties

@@ -0,0 +1,13 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-15
+android.library=true
+android.library.reference.1=../play_licensing

BIN
platform/android/libs/apk_expansion/res/drawable-hdpi/notify_panel_notification_icon_bg.png


BIN
platform/android/libs/apk_expansion/res/drawable-mdpi/notify_panel_notification_icon_bg.png


+ 104 - 0
platform/android/libs/apk_expansion/res/layout/status_bar_ongoing_event_progress_bar.xml

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<LinearLayout android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:baselineAligned="false"
+    android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <RelativeLayout
+        android:layout_width="35dp"
+        android:layout_height="fill_parent"
+        android:paddingTop="10dp"
+        android:paddingBottom="8dp" >
+
+        <ImageView
+            android:id="@+id/appIcon"
+            android:layout_width="fill_parent"
+            android:layout_height="25dp"
+            android:scaleType="centerInside"            
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"            
+            android:src="@android:drawable/stat_sys_download" />
+
+        <TextView
+            android:id="@+id/progress_text"
+            style="@style/NotificationText"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentBottom="true"
+            android:layout_gravity="center_horizontal"
+            android:singleLine="true"
+            android:gravity="center" />
+    </RelativeLayout>
+
+    <RelativeLayout
+        android:layout_width="0dip"
+        android:layout_height="match_parent"
+        android:layout_weight="1.0"
+        android:clickable="true"
+        android:focusable="true"
+        android:paddingTop="10dp"
+        android:paddingRight="8dp"
+        android:paddingBottom="8dp" >
+
+        <TextView
+            android:id="@+id/title"
+            style="@style/NotificationTitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:singleLine="true"/>
+
+        <TextView
+            android:id="@+id/time_remaining"
+            style="@style/NotificationText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:singleLine="true"/>
+        <!-- Only one of progress_bar and paused_text will be visible. -->
+
+        <FrameLayout
+            android:id="@+id/progress_bar_frame"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true" >
+
+            <ProgressBar
+                android:id="@+id/progress_bar"
+                style="?android:attr/progressBarStyleHorizontal"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:paddingRight="25dp" />
+
+            <TextView
+                android:id="@+id/description"
+                style="@style/NotificationTextShadow"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:paddingRight="25dp"
+                android:singleLine="true" />
+        </FrameLayout>
+
+    </RelativeLayout>
+
+</LinearLayout>

+ 6 - 0
platform/android/libs/apk_expansion/res/values-v11/styles.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="NotificationTextSecondary" parent="NotificationText">
+        <item name="android:textSize">12sp</item>
+    </style>
+</resources>

+ 5 - 0
platform/android/libs/apk_expansion/res/values-v9/styles.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" />
+    <style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" />
+</resources>

+ 41 - 0
platform/android/libs/apk_expansion/res/values/strings.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <!-- When a download completes, a notification is displayed, and this
+        string is used to indicate that the download successfully completed.
+        Note that such a download could have been initiated by a variety of
+        applications, including (but not limited to) the browser, an email
+        application, a content marketplace. -->
+    <string name="notification_download_complete">Download complete</string>
+
+    <!-- When a download completes, a notification is displayed, and this
+        string is used to indicate that the download failed.
+        Note that such a download could have been initiated by a variety of
+        applications, including (but not limited to) the browser, an email
+        application, a content marketplace. -->
+    <string name="notification_download_failed">Download unsuccessful</string>
+
+
+    <string name="state_unknown">Starting..."</string>
+    <string name="state_idle">Waiting for download to start</string>
+    <string name="state_fetching_url">Looking for resources to download</string>
+    <string name="state_connecting">Connecting to the download server</string>
+    <string name="state_downloading">Downloading resources</string>
+    <string name="state_completed">Download finished</string>
+    <string name="state_paused_network_unavailable">Download paused because no network is available</string>
+    <string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string>
+    <string name="state_paused_by_request">Download paused</string>
+    <string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string>
+    <string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string>
+    <string name="state_paused_roaming">Download paused because you are roaming</string>
+    <string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string>
+    <string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string>
+    <string name="state_failed_fetching_url">Download failed because the resources could not be found</string>
+    <string name="state_failed_sdcard_full">Download failed because the external storage is full</string>
+    <string name="state_failed_cancelled">Download cancelled</string>
+    <string name="state_failed">Download failed</string>
+
+    <string name="kilobytes_per_second">%1$s KB/s</string>
+    <string name="time_remaining">Time remaining: %1$s</string>
+    <string name="time_remaining_notification">%1$s left</string>
+</resources>

+ 25 - 0
platform/android/libs/apk_expansion/res/values/styles.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="NotificationText">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="NotificationTextShadow" parent="NotificationText">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:shadowColor">@android:color/background_dark</item>
+        <item name="android:shadowDx">1.0</item>
+        <item name="android:shadowDy">1.0</item>
+        <item name="android:shadowRadius">1</item>
+    </style>
+
+    <style name="NotificationTitle">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
+    <style name="ButtonBackground">
+        <item name="android:background">@android:color/background_dark</item>
+    </style>
+
+</resources>

+ 236 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Constants.java

@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import java.io.File;
+
+
+/**
+ * Contains the internal constants that are used in the download manager.
+ * As a general rule, modifying these constants should be done with care.
+ */
+public class Constants {    
+    /** Tag used for debugging/logging */
+    public static final String TAG = "LVLDL";
+
+    /**
+     * Expansion path where we store obb files
+     */
+    public static final String EXP_PATH = File.separator + "Android"
+            + File.separator + "obb" + File.separator;
+    
+    /** The intent that gets sent when the service must wake up for a retry */
+    public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
+
+    /** the intent that gets sent when clicking a successful download */
+    public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
+
+    /** the intent that gets sent when clicking an incomplete/failed download  */
+    public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
+
+    /** the intent that gets sent when deleting the notification of a completed download */
+    public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
+
+    /**
+     * When a number has to be appended to the filename, this string is used to separate the
+     * base filename from the sequence number
+     */
+    public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
+
+    /** The default user agent used for downloads */
+    public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
+
+    /** The buffer size used to stream the data */
+    public static final int BUFFER_SIZE = 4096;
+
+    /** The minimum amount of progress that has to be done before the progress bar gets updated */
+    public static final int MIN_PROGRESS_STEP = 4096;
+
+    /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
+    public static final long MIN_PROGRESS_TIME = 1000;
+
+    /** The maximum number of rows in the database (FIFO) */
+    public static final int MAX_DOWNLOADS = 1000;
+
+    /**
+     * The number of times that the download manager will retry its network
+     * operations when no progress is happening before it gives up.
+     */
+    public static final int MAX_RETRIES = 5;
+
+    /**
+     * The minimum amount of time that the download manager accepts for
+     * a Retry-After response header with a parameter in delta-seconds.
+     */
+    public static final int MIN_RETRY_AFTER = 30; // 30s
+
+    /**
+     * The maximum amount of time that the download manager accepts for
+     * a Retry-After response header with a parameter in delta-seconds.
+     */
+    public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
+
+    /**
+     * The maximum number of redirects.
+     */
+    public static final int MAX_REDIRECTS = 5; // can't be more than 7.
+
+    /**
+     * The time between a failure and the first retry after an IOException.
+     * Each subsequent retry grows exponentially, doubling each time.
+     * The time is in seconds.
+     */
+    public static final int RETRY_FIRST_DELAY = 30;
+
+    /** Enable separate connectivity logging */
+    public static final boolean LOGX = true;
+
+    /** Enable verbose logging */
+    public static final boolean LOGV = false;
+    
+    /** Enable super-verbose logging */
+    private static final boolean LOCAL_LOGVV = false;
+    public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
+    
+    /**
+     * This download has successfully completed.
+     * Warning: there might be other status values that indicate success
+     * in the future.
+     * Use isSucccess() to capture the entire category.
+     */
+    public static final int STATUS_SUCCESS = 200;
+
+    /**
+     * This request couldn't be parsed. This is also used when processing
+     * requests with unknown/unsupported URI schemes.
+     */
+    public static final int STATUS_BAD_REQUEST = 400;
+
+    /**
+     * This download can't be performed because the content type cannot be
+     * handled.
+     */
+    public static final int STATUS_NOT_ACCEPTABLE = 406;
+
+    /**
+     * This download cannot be performed because the length cannot be
+     * determined accurately. This is the code for the HTTP error "Length
+     * Required", which is typically used when making requests that require
+     * a content length but don't have one, and it is also used in the
+     * client when a response is received whose length cannot be determined
+     * accurately (therefore making it impossible to know when a download
+     * completes).
+     */
+    public static final int STATUS_LENGTH_REQUIRED = 411;
+
+    /**
+     * This download was interrupted and cannot be resumed.
+     * This is the code for the HTTP error "Precondition Failed", and it is
+     * also used in situations where the client doesn't have an ETag at all.
+     */
+    public static final int STATUS_PRECONDITION_FAILED = 412;
+
+    /**
+     * The lowest-valued error status that is not an actual HTTP status code.
+     */
+    public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
+
+    /**
+     * The requested destination file already exists.
+     */
+    public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
+
+    /**
+     * Some possibly transient error occurred, but we can't resume the download.
+     */
+    public static final int STATUS_CANNOT_RESUME = 489;
+
+    /**
+     * This download was canceled
+     */
+    public static final int STATUS_CANCELED = 490;
+
+    /**
+     * This download has completed with an error.
+     * Warning: there will be other status values that indicate errors in
+     * the future. Use isStatusError() to capture the entire category.
+     */
+    public static final int STATUS_UNKNOWN_ERROR = 491;
+
+    /**
+     * This download couldn't be completed because of a storage issue.
+     * Typically, that's because the filesystem is missing or full.
+     * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
+     * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
+     */
+    public static final int STATUS_FILE_ERROR = 492;
+
+    /**
+     * This download couldn't be completed because of an HTTP
+     * redirect response that the download manager couldn't
+     * handle.
+     */
+    public static final int STATUS_UNHANDLED_REDIRECT = 493;
+
+    /**
+     * This download couldn't be completed because of an
+     * unspecified unhandled HTTP code.
+     */
+    public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
+
+    /**
+     * This download couldn't be completed because of an
+     * error receiving or processing data at the HTTP level.
+     */
+    public static final int STATUS_HTTP_DATA_ERROR = 495;
+
+    /**
+     * This download couldn't be completed because of an
+     * HttpException while setting up the request.
+     */
+    public static final int STATUS_HTTP_EXCEPTION = 496;
+
+    /**
+     * This download couldn't be completed because there were
+     * too many redirects.
+     */
+    public static final int STATUS_TOO_MANY_REDIRECTS = 497;
+
+    /**
+     * This download couldn't be completed due to insufficient storage
+     * space.  Typically, this is because the SD card is full.
+     */
+    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
+
+    /**
+     * This download couldn't be completed because no external storage
+     * device was found.  Typically, this is because the SD card is not
+     * mounted.
+     */
+    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
+
+    /**
+     * The wake duration to check to see if a download is possible.
+     */
+    public static final long WATCHDOG_WAKE_TIMER = 60*1000;    
+
+    /**
+     * The wake duration to check to see if the process was killed.
+     */
+    public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;    
+
+}

+ 80 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * This class contains progress information about the active download(s).
+ *
+ * When you build the Activity that initiates a download and tracks the
+ * progress by implementing the {@link IDownloaderClient} interface, you'll
+ * receive a DownloadProgressInfo object in each call to the {@link
+ * IDownloaderClient#onDownloadProgress} method. This allows you to update
+ * your activity's UI with information about the download progress, such
+ * as the progress so far, time remaining and current speed.
+ */
+public class DownloadProgressInfo implements Parcelable {
+    public long mOverallTotal;
+    public long mOverallProgress;
+    public long mTimeRemaining; // time remaining
+    public float mCurrentSpeed; // speed in KB/S
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel p, int i) {
+        p.writeLong(mOverallTotal);
+        p.writeLong(mOverallProgress);
+        p.writeLong(mTimeRemaining);
+        p.writeFloat(mCurrentSpeed);
+    }
+
+    public DownloadProgressInfo(Parcel p) {
+        mOverallTotal = p.readLong();
+        mOverallProgress = p.readLong();
+        mTimeRemaining = p.readLong();
+        mCurrentSpeed = p.readFloat();
+    }
+
+    public DownloadProgressInfo(long overallTotal, long overallProgress,
+            long timeRemaining,
+            float currentSpeed) {
+        this.mOverallTotal = overallTotal;
+        this.mOverallProgress = overallProgress;
+        this.mTimeRemaining = timeRemaining;
+        this.mCurrentSpeed = currentSpeed;
+    }
+
+    public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
+        @Override
+        public DownloadProgressInfo createFromParcel(Parcel parcel) {
+            return new DownloadProgressInfo(parcel);
+        }
+
+        @Override
+        public DownloadProgressInfo[] newArray(int i) {
+            return new DownloadProgressInfo[i];
+        }
+    };
+
+}

+ 277 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java

@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import com.google.android.vending.expansion.downloader.impl.DownloaderService;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+
+
+
+/**
+ * This class binds the service API to your application client.  It contains the IDownloaderClient proxy,
+ * which is used to call functions in your client as well as the Stub, which is used to call functions
+ * in the client implementation of IDownloaderClient.
+ * 
+ * <p>The IPC is implemented using an Android Messenger and a service Binder.  The connect method
+ * should be called whenever the client wants to bind to the service.  It opens up a service connection
+ * that ends up calling the onServiceConnected client API that passes the service messenger
+ * in.  If the client wants to be notified by the service, it is responsible for then passing its
+ * messenger to the service in a separate call.
+ *
+ * <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}.
+ *
+ * <p>When your application first starts, you should first check whether your app's expansion files are
+ * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which
+ * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method
+ * returns a value indicating whether download is required or not.
+ *
+ * <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through
+ * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link
+ * IStub} object that you need in order to receive calls through your {@link IDownloaderClient}
+ * interface.
+ */
+public class DownloaderClientMarshaller {
+    public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
+    public static final int MSG_ONDOWNLOADPROGRESS = 11;
+    public static final int MSG_ONSERVICECONNECTED = 12;
+
+    public static final String PARAM_NEW_STATE = "newState";
+    public static final String PARAM_PROGRESS = "progress";
+    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
+
+    public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
+    public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
+    public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
+
+    private static class Proxy implements IDownloaderClient {
+        private Messenger mServiceMessenger;
+
+        @Override
+        public void onDownloadStateChanged(int newState) {
+            Bundle params = new Bundle(1);
+            params.putInt(PARAM_NEW_STATE, newState);
+            send(MSG_ONDOWNLOADSTATE_CHANGED, params);
+        }
+
+        @Override
+        public void onDownloadProgress(DownloadProgressInfo progress) {
+            Bundle params = new Bundle(1);
+            params.putParcelable(PARAM_PROGRESS, progress);
+            send(MSG_ONDOWNLOADPROGRESS, params);
+        }
+
+        private void send(int method, Bundle params) {
+            Message m = Message.obtain(null, method);
+            m.setData(params);
+            try {
+                mServiceMessenger.send(m);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+        
+        public Proxy(Messenger msg) {
+            mServiceMessenger = msg;
+        }
+
+        @Override
+        public void onServiceConnected(Messenger m) {
+            /**
+             * This is never called through the proxy.
+             */
+        }
+    }
+
+    private static class Stub implements IStub {
+        private IDownloaderClient mItf = null;
+        private Class<?> mDownloaderServiceClass;
+        private boolean mBound;
+        private Messenger mServiceMessenger;
+        private Context mContext;
+        /**
+         * Target we publish for clients to send messages to IncomingHandler.
+         */
+        final Messenger mMessenger = new Messenger(new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_ONDOWNLOADPROGRESS:                        
+                        Bundle bun = msg.getData();
+                        if ( null != mContext ) {
+                            bun.setClassLoader(mContext.getClassLoader());
+                            DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
+                                    .getParcelable(PARAM_PROGRESS);
+                            mItf.onDownloadProgress(dpi);
+                        }
+                        break;
+                    case MSG_ONDOWNLOADSTATE_CHANGED:
+                        mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
+                        break;
+                    case MSG_ONSERVICECONNECTED:
+                        mItf.onServiceConnected(
+                                (Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
+                        break;
+                }
+            }
+        });
+
+        public Stub(IDownloaderClient itf, Class<?> downloaderService) {
+            mItf = itf;
+            mDownloaderServiceClass = downloaderService;
+        }
+
+        /**
+         * Class for interacting with the main interface of the service.
+         */
+        private ServiceConnection mConnection = new ServiceConnection() {
+            public void onServiceConnected(ComponentName className, IBinder service) {
+                // This is called when the connection with the service has been
+                // established, giving us the object we can use to
+                // interact with the service. We are communicating with the
+                // service using a Messenger, so here we get a client-side
+                // representation of that from the raw IBinder object.
+                mServiceMessenger = new Messenger(service);
+                mItf.onServiceConnected(
+                        mServiceMessenger);
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                // This is called when the connection with the service has been
+                // unexpectedly disconnected -- that is, its process crashed.
+                mServiceMessenger = null;
+            }
+        };
+
+        @Override
+        public void connect(Context c) {
+            mContext = c;
+            Intent bindIntent = new Intent(c, mDownloaderServiceClass);
+            bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
+            if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
+                if ( Constants.LOGVV ) {
+                    Log.d(Constants.TAG, "Service Unbound");
+                }
+            } else {
+                mBound = true;
+            }
+                
+        }
+
+        @Override
+        public void disconnect(Context c) {
+            if (mBound) {
+                c.unbindService(mConnection);
+                mBound = false;
+            }
+            mContext = null;
+        }
+
+        @Override
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+    }
+
+    /**
+     * Returns a proxy that will marshal calls to IDownloaderClient methods
+     * 
+     * @param msg
+     * @return
+     */
+    public static IDownloaderClient CreateProxy(Messenger msg) {
+        return new Proxy(msg);
+    }
+
+    /**
+     * Returns a stub object that, when connected, will listen for marshaled
+     * {@link IDownloaderClient} methods and translate them into calls to the supplied
+     * interface.
+     * 
+     * @param itf An implementation of IDownloaderClient that will be called
+     *            when remote method calls are unmarshaled.
+     * @param downloaderService The class for your implementation of {@link
+     * impl.DownloaderService}.
+     * @return The {@link IStub} that allows you to connect to the service such that
+     * your {@link IDownloaderClient} receives status updates.
+     */
+    public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
+        return new Stub(itf, downloaderService);
+    }
+    
+    /**
+     * Starts the download if necessary. This function starts a flow that does `
+     * many things. 1) Checks to see if the APK version has been checked and
+     * the metadata database updated 2) If the APK version does not match,
+     * checks the new LVL status to see if a new download is required 3) If the
+     * APK version does match, then checks to see if the download(s) have been
+     * completed 4) If the downloads have been completed, returns
+     * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the
+     * startup of an application to quickly ascertain if the application needs
+     * to wait to hear about any updated APK expansion files. Note that this does
+     * mean that the application MUST be run for the first time with a network
+     * connection, even if Market delivers all of the files.
+     * 
+     * @param context Your application Context.
+     * @param notificationClient A PendingIntent to start the Activity in your application
+     * that shows the download progress and which will also start the application when download
+     * completes.
+     * @param serviceClass the class of your {@link imp.DownloaderService} implementation
+     * @return whether the service was started and the reason for starting the service.
+     * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link
+     * #DOWNLOAD_REQUIRED}.
+     * @throws NameNotFoundException
+     */
+    public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, 
+            Class<?> serviceClass)
+            throws NameNotFoundException {
+        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
+                serviceClass);
+    }
+    
+    /**
+     * This version assumes that the intent contains the pending intent as a parameter. This
+     * is used for responding to alarms.
+     * <p>The pending intent must be in an extra with the key {@link 
+     * impl.DownloaderService#EXTRA_PENDING_INTENT}.
+     * 
+     * @param context
+     * @param notificationClient
+     * @param serviceClass the class of the service to start
+     * @return
+     * @throws NameNotFoundException
+     */
+    public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, 
+            Class<?> serviceClass)
+            throws NameNotFoundException {
+        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
+                serviceClass);
+    }    
+
+}

+ 181 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java

@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import com.google.android.vending.expansion.downloader.impl.DownloaderService;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+
+
+
+/**
+ * This class is used by the client activity to proxy requests to the Downloader
+ * Service.
+ *
+ * Most importantly, you must call {@link #CreateProxy} during the {@link
+ * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate
+ * an {@link IDownloaderService} object that you can then use to issue commands to the {@link
+ * DownloaderService} (such as to pause and resume downloads).
+ */
+public class DownloaderServiceMarshaller {
+
+    public static final int MSG_REQUEST_ABORT_DOWNLOAD =
+            1;
+    public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
+            2;
+    public static final int MSG_SET_DOWNLOAD_FLAGS =
+            3;
+    public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
+            4;
+    public static final int MSG_REQUEST_DOWNLOAD_STATE =
+            5;
+    public static final int MSG_REQUEST_CLIENT_UPDATE =
+            6;
+
+    public static final String PARAMS_FLAGS = "flags";
+    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
+
+    private static class Proxy implements IDownloaderService {
+        private Messenger mMsg;
+
+        private void send(int method, Bundle params) {
+            Message m = Message.obtain(null, method);
+            m.setData(params);
+            try {
+                mMsg.send(m);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+
+        public Proxy(Messenger msg) {
+            mMsg = msg;
+        }
+
+        @Override
+        public void requestAbortDownload() {
+            send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void requestPauseDownload() {
+            send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void setDownloadFlags(int flags) {
+            Bundle params = new Bundle();
+            params.putInt(PARAMS_FLAGS, flags);
+            send(MSG_SET_DOWNLOAD_FLAGS, params);
+        }
+
+        @Override
+        public void requestContinueDownload() {
+            send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void requestDownloadStatus() {
+            send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
+        }
+
+        @Override
+        public void onClientUpdated(Messenger clientMessenger) {
+            Bundle bundle = new Bundle(1);
+            bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
+            send(MSG_REQUEST_CLIENT_UPDATE, bundle);
+        }
+    }
+
+    private static class Stub implements IStub {
+        private IDownloaderService mItf = null;
+        final Messenger mMessenger = new Messenger(new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_REQUEST_ABORT_DOWNLOAD:
+                        mItf.requestAbortDownload();
+                        break;
+                    case MSG_REQUEST_CONTINUE_DOWNLOAD:
+                        mItf.requestContinueDownload();
+                        break;
+                    case MSG_REQUEST_PAUSE_DOWNLOAD:
+                        mItf.requestPauseDownload();
+                        break;
+                    case MSG_SET_DOWNLOAD_FLAGS:
+                        mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
+                        break;
+                    case MSG_REQUEST_DOWNLOAD_STATE:
+                        mItf.requestDownloadStatus();
+                        break;
+                    case MSG_REQUEST_CLIENT_UPDATE:
+                        mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
+                                PARAM_MESSENGER));
+                        break;
+                }
+            }
+        });
+
+        public Stub(IDownloaderService itf) {
+            mItf = itf;
+        }
+
+        @Override
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+
+        @Override
+        public void connect(Context c) {
+
+        }
+
+        @Override
+        public void disconnect(Context c) {
+
+        }
+    }
+
+    /**
+     * Returns a proxy that will marshall calls to IDownloaderService methods
+     * 
+     * @param ctx
+     * @return
+     */
+    public static IDownloaderService CreateProxy(Messenger msg) {
+        return new Proxy(msg);
+    }
+
+    /**
+     * Returns a stub object that, when connected, will listen for marshalled
+     * IDownloaderService methods and translate them into calls to the supplied
+     * interface.
+     * 
+     * @param itf An implementation of IDownloaderService that will be called
+     *            when remote method calls are unmarshalled.
+     * @return
+     */
+    public static IStub CreateStub(IDownloaderService itf) {
+        return new Stub(itf);
+    }
+
+}

+ 306 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java

@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import com.android.vending.expansion.downloader.R;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.StatFs;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Some helper functions for the download manager
+ */
+public class Helpers {
+
+    public static Random sRandom = new Random(SystemClock.uptimeMillis());
+
+    /** Regex used to parse content-disposition headers */
+    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
+            .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+
+    private Helpers() {
+    }
+
+    /*
+     * Parse the Content-Disposition HTTP Header. The format of the header is
+     * defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This
+     * header provides a filename for content that is going to be downloaded to
+     * the file system. We only support the attachment type.
+     */
+    static String parseContentDisposition(String contentDisposition) {
+        try {
+            Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+            if (m.find()) {
+                return m.group(1);
+            }
+        } catch (IllegalStateException ex) {
+            // This function is defined as returning null when it can't parse
+            // the header
+        }
+        return null;
+    }
+
+    /**
+     * @return the root of the filesystem containing the given path
+     */
+    public static File getFilesystemRoot(String path) {
+        File cache = Environment.getDownloadCacheDirectory();
+        if (path.startsWith(cache.getPath())) {
+            return cache;
+        }
+        File external = Environment.getExternalStorageDirectory();
+        if (path.startsWith(external.getPath())) {
+            return external;
+        }
+        throw new IllegalArgumentException(
+                "Cannot determine filesystem root for " + path);
+    }
+
+    public static boolean isExternalMediaMounted() {
+        if (!Environment.getExternalStorageState().equals(
+                Environment.MEDIA_MOUNTED)) {
+            // No SD card found.
+            if ( Constants.LOGVV ) {
+                Log.d(Constants.TAG, "no external storage");
+            }
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @return the number of bytes available on the filesystem rooted at the
+     *         given File
+     */
+    public static long getAvailableBytes(File root) {
+        StatFs stat = new StatFs(root.getPath());
+        // put a bit of margin (in case creating the file grows the system by a
+        // few blocks)
+        long availableBlocks = (long) stat.getAvailableBlocks() - 4;
+        return stat.getBlockSize() * availableBlocks;
+    }
+
+    /**
+     * Checks whether the filename looks legitimate
+     */
+    public static boolean isFilenameValid(String filename) {
+        filename = filename.replaceFirst("/+", "/"); // normalize leading
+                                                     // slashes
+        return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
+                || filename.startsWith(Environment.getExternalStorageDirectory().toString());
+    }
+
+    /*
+     * Delete the given file from device
+     */
+    /* package */static void deleteFile(String path) {
+        try {
+            File file = new File(path);
+            file.delete();
+        } catch (Exception e) {
+            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
+        }
+    }
+
+    /**
+     * Showing progress in MB here. It would be nice to choose the unit (KB, MB,
+     * GB) based on total file size, but given what we know about the expected
+     * ranges of file sizes for APK expansion files, it's probably not necessary.
+     * 
+     * @param overallProgress
+     * @param overallTotal
+     * @return
+     */
+
+    static public String getDownloadProgressString(long overallProgress, long overallTotal) {
+        if (overallTotal == 0) {
+            if ( Constants.LOGVV ) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        return String.format("%.2f",
+                (float) overallProgress / (1024.0f * 1024.0f))
+                + "MB /" +
+                String.format("%.2f", (float) overallTotal /
+                        (1024.0f * 1024.0f)) + "MB";
+    }
+
+    /**
+     * Adds a percentile to getDownloadProgressString.
+     * 
+     * @param overallProgress
+     * @param overallTotal
+     * @return
+     */
+    static public String getDownloadProgressStringNotification(long overallProgress,
+            long overallTotal) {
+        if (overallTotal == 0) {
+            if ( Constants.LOGVV ) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        return getDownloadProgressString(overallProgress, overallTotal) + " (" +
+                getDownloadProgressPercent(overallProgress, overallTotal) + ")";
+    }
+
+    public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
+        if (overallTotal == 0) {
+            if ( Constants.LOGVV ) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        return Long.toString(overallProgress * 100 / overallTotal) + "%";
+    }
+
+    public static String getSpeedString(float bytesPerMillisecond) {
+        return String.format("%.2f", bytesPerMillisecond * 1000 / 1024);
+    }
+
+    public static String getTimeRemaining(long durationInMilliseconds) {
+        SimpleDateFormat sdf;
+        if (durationInMilliseconds > 1000 * 60 * 60) {
+            sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
+        } else {
+            sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
+        }
+        return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
+    }
+
+    /**
+     * Returns the file name (without full path) for an Expansion APK file from
+     * the given context.
+     * 
+     * @param c the context
+     * @param mainFile true for main file, false for patch file
+     * @param versionCode the version of the file
+     * @return String the file name of the expansion file
+     */
+    public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
+        return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
+    }
+
+    /**
+     * Returns the filename (where the file should be saved) from info about a
+     * download
+     */
+    static public String generateSaveFileName(Context c, String fileName) {
+        String path = getSaveFilePath(c)
+                + File.separator + fileName;
+        return path;
+    }
+
+    static public String getSaveFilePath(Context c) {
+        File root = Environment.getExternalStorageDirectory();
+        String path = root.toString() + Constants.EXP_PATH + c.getPackageName();
+        return path;
+    }
+
+    /**
+     * Helper function to ascertain the existence of a file and return
+     * true/false appropriately
+     * 
+     * @param c the app/activity/service context
+     * @param fileName the name (sans path) of the file to query
+     * @param fileSize the size that the file must match
+     * @param deleteFileOnMismatch if the file sizes do not match, delete the
+     *            file
+     * @return true if it does exist, false otherwise
+     */
+    static public boolean doesFileExist(Context c, String fileName, long fileSize,
+            boolean deleteFileOnMismatch) {
+        // the file may have been delivered by Market --- let's make sure
+        // it's the size we expect
+        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
+        if (fileForNewFile.exists()) {
+            if (fileForNewFile.length() == fileSize) {
+                return true;
+            }
+            if (deleteFileOnMismatch) {
+                // delete the file --- we won't be able to resume
+                // because we cannot confirm the integrity of the file
+                fileForNewFile.delete();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Converts download states that are returned by the {@link 
+     * IDownloaderClient#onDownloadStateChanged} callback into usable strings.
+     * This is useful if using the state strings built into the library to display user messages.
+     * @param state One of the STATE_* constants from {@link IDownloaderClient}.
+     * @return string resource ID for the corresponding string.
+     */
+    static public int getDownloaderStringResourceIDFromState(int state) {
+        switch (state) {
+            case IDownloaderClient.STATE_IDLE:
+                return R.string.state_idle;
+            case IDownloaderClient.STATE_FETCHING_URL:
+                return R.string.state_fetching_url;
+            case IDownloaderClient.STATE_CONNECTING:
+                return R.string.state_connecting;
+            case IDownloaderClient.STATE_DOWNLOADING:
+                return R.string.state_downloading;
+            case IDownloaderClient.STATE_COMPLETED:
+                return R.string.state_completed;
+            case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
+                return R.string.state_paused_network_unavailable;
+            case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
+                return R.string.state_paused_by_request;
+            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
+                return R.string.state_paused_wifi_disabled;
+            case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
+                return R.string.state_paused_wifi_unavailable;
+            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
+                return R.string.state_paused_wifi_disabled;
+            case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
+                return R.string.state_paused_wifi_unavailable;
+            case IDownloaderClient.STATE_PAUSED_ROAMING:
+                return R.string.state_paused_roaming;
+            case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
+                return R.string.state_paused_network_setup_failure;
+            case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
+                return R.string.state_paused_sdcard_unavailable;
+            case IDownloaderClient.STATE_FAILED_UNLICENSED:
+                return R.string.state_failed_unlicensed;
+            case IDownloaderClient.STATE_FAILED_FETCHING_URL:
+                return R.string.state_failed_fetching_url;
+            case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
+                return R.string.state_failed_sdcard_full;
+            case IDownloaderClient.STATE_FAILED_CANCELED:
+                return R.string.state_failed_cancelled;
+            default:
+                return R.string.state_unknown;
+        }
+    }
+
+}

+ 126 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import android.os.Messenger;
+
+/**
+ * This interface should be implemented by the client activity for the
+ * downloader. It is used to pass status from the service to the client.
+ */
+public interface IDownloaderClient {
+    static final int STATE_IDLE = 1;
+    static final int STATE_FETCHING_URL = 2;
+    static final int STATE_CONNECTING = 3;
+    static final int STATE_DOWNLOADING = 4;
+    static final int STATE_COMPLETED = 5;
+
+    static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
+    static final int STATE_PAUSED_BY_REQUEST = 7;
+
+    /**
+     * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
+     * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
+     * cellular permission will restart the service. Wi-Fi disabled means that
+     * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the
+     * other case Wi-Fi is enabled but not available.
+     */
+    static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
+    static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
+
+    /**
+     * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
+     * Wi-Fi is unavailable and cellular permission will NOT restart the
+     * service. Wi-Fi disabled means that the Wi-Fi manager is returning that
+     * Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not
+     * available.
+     * <p>
+     * The service does not return these values. We recommend that app
+     * developers with very large payloads do not allow these payloads to be
+     * downloaded over cellular connections.
+     */
+    static final int STATE_PAUSED_WIFI_DISABLED = 10;
+    static final int STATE_PAUSED_NEED_WIFI = 11;
+
+    static final int STATE_PAUSED_ROAMING = 12;
+
+    /**
+     * Scary case. We were on a network that redirected us to another website
+     * that delivered us the wrong file.
+     */
+    static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13;
+
+    static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14;
+
+    static final int STATE_FAILED_UNLICENSED = 15;
+    static final int STATE_FAILED_FETCHING_URL = 16;
+    static final int STATE_FAILED_SDCARD_FULL = 17;
+    static final int STATE_FAILED_CANCELED = 18;
+
+    static final int STATE_FAILED = 19;
+
+    /**
+     * Called internally by the stub when the service is bound to the client.
+     * <p>
+     * Critical implementation detail. In onServiceConnected we create the
+     * remote service and marshaler. This is how we pass the client information
+     * back to the service so the client can be properly notified of changes. We
+     * must do this every time we reconnect to the service.
+     * <p>
+     * That is, when you receive this callback, you should call
+     * {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member
+     * instance of {@link IDownloaderService}, then call
+     * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved
+     * from your {@link IStub} proxy object.
+     * 
+     * @param m the service Messenger. This Messenger is used to call the
+     *            service API from the client.
+     */
+    void onServiceConnected(Messenger m);
+
+    /**
+     * Called when the download state changes. Depending on the state, there may
+     * be user requests. The service is free to change the download state in the
+     * middle of a user request, so the client should be able to handle this.
+     * <p>
+     * The Downloader Library includes a collection of string resources that
+     * correspond to each of the states, which you can use to provide users a
+     * useful message based on the state provided in this callback. To fetch the
+     * appropriate string for a state, call
+     * {@link Helpers#getDownloaderStringResourceIDFromState}.
+     * <p>
+     * What this means to the developer: The application has gotten a message
+     * that the download has paused due to lack of WiFi. The developer should
+     * then show UI asking the user if they want to enable downloading over
+     * cellular connections with appropriate warnings. If the application
+     * suddenly starts downloading, the application should revert to showing the
+     * progress again, rather than leaving up the download over cellular UI up.
+     * 
+     * @param newState one of the STATE_* values defined in IDownloaderClient
+     */
+    void onDownloadStateChanged(int newState);
+
+    /**
+     * Shows the download progress. This is intended to be used to fill out a
+     * client UI. This progress should only be shown in a few states such as
+     * STATE_DOWNLOADING.
+     * 
+     * @param progress the DownloadProgressInfo object containing the current
+     *            progress of all downloads.
+     */
+    void onDownloadProgress(DownloadProgressInfo progress);
+}

+ 83 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import com.google.android.vending.expansion.downloader.impl.DownloaderService;
+import android.os.Messenger;
+
+/**
+ * This interface is implemented by the DownloaderService and by the
+ * DownloaderServiceMarshaller. It contains functions to control the service.
+ * When a client binds to the service, it must call the onClientUpdated
+ * function.
+ * <p>
+ * You can acquire a proxy that implements this interface for your service by
+ * calling {@link DownloaderServiceMarshaller#CreateProxy} during the
+ * {@link IDownloaderClient#onServiceConnected} callback. At which point, you
+ * should immediately call {@link #onClientUpdated}.
+ */
+public interface IDownloaderService {
+    /**
+     * Set this flag in response to the
+     * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
+     * call RequestContinueDownload to resume a download
+     */
+    public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1;
+
+    /**
+     * Request that the service abort the current download. The service should
+     * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}.
+     */
+    void requestAbortDownload();
+
+    /**
+     * Request that the service pause the current download. The service should
+     * respond by changing the state to
+     * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
+     */
+    void requestPauseDownload();
+
+    /**
+     * Request that the service continue a paused download, when in any paused
+     * or failed state, including
+     * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
+     */
+    void requestContinueDownload();
+
+    /**
+     * Set the flags for this download (e.g.
+     * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
+     * 
+     * @param flags
+     */
+    void setDownloadFlags(int flags);
+
+    /**
+     * Requests that the download status be sent to the client.
+     */
+    void requestDownloadStatus();
+
+    /**
+     * Call this when you get {@link
+     * IDownloaderClient.onServiceConnected(Messenger m)} from the
+     * DownloaderClient to register the client with the service. It will
+     * automatically send the current status to the client.
+     * 
+     * @param clientMessenger
+     */
+    void onClientUpdated(Messenger clientMessenger);
+}

+ 41 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import android.content.Context;
+import android.os.Messenger;
+
+/**
+ * This is the interface that is used to connect/disconnect from the downloader
+ * service.
+ * <p>
+ * You should get a proxy object that implements this interface by calling
+ * {@link DownloaderClientMarshaller#CreateStub} in your activity when the
+ * downloader service starts. Then, call {@link #connect} during your activity's
+ * onResume() and call {@link #disconnect} during onStop().
+ * <p>
+ * Then during the {@link IDownloaderClient#onServiceConnected} callback, you
+ * should call {@link #getMessenger} to pass the stub's Messenger object to
+ * {@link IDownloaderService#onClientUpdated}.
+ */
+public interface IStub {
+    Messenger getMessenger();
+
+    void connect(Context c);
+
+    void disconnect(Context c);
+}

+ 123 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+/**
+ * Contains useful helper functions, typically tied to the application context.
+ */
+class SystemFacade {
+    private Context mContext;
+    private NotificationManager mNotificationManager;
+
+    public SystemFacade(Context context) {
+        mContext = context;
+        mNotificationManager = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    public long currentTimeMillis() {
+        return System.currentTimeMillis();
+    }
+
+    public Integer getActiveNetworkType() {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            Log.w(Constants.TAG, "couldn't get connectivity manager");
+            return null;
+        }
+
+        NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
+        if (activeInfo == null) {
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG, "network is not available");
+            }
+            return null;
+        }
+        return activeInfo.getType();
+    }
+
+    public boolean isNetworkRoaming() {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            Log.w(Constants.TAG, "couldn't get connectivity manager");
+            return false;
+        }
+
+        NetworkInfo info = connectivity.getActiveNetworkInfo();
+        boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
+        TelephonyManager tm = (TelephonyManager) mContext
+                .getSystemService(Context.TELEPHONY_SERVICE);
+        if (null == tm) {
+            Log.w(Constants.TAG, "couldn't get telephony manager");
+            return false;
+        }
+        boolean isRoaming = isMobile && tm.isNetworkRoaming();
+        if (Constants.LOGVV && isRoaming) {
+            Log.v(Constants.TAG, "network is roaming");
+        }
+        return isRoaming;
+    }
+
+    public Long getMaxBytesOverMobile() {
+        return (long) Integer.MAX_VALUE;
+    }
+
+    public Long getRecommendedMaxBytesOverMobile() {
+        return 2097152L;
+    }
+
+    public void sendBroadcast(Intent intent) {
+        mContext.sendBroadcast(intent);
+    }
+
+    public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
+        return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
+    }
+
+    public void postNotification(long id, Notification notification) {
+        /**
+         * TODO: The system notification manager takes ints, not longs, as IDs,
+         * but the download manager uses IDs take straight from the database,
+         * which are longs. This will have to be dealt with at some point.
+         */
+        mNotificationManager.notify((int) id, notification);
+    }
+
+    public void cancelNotification(long id) {
+        mNotificationManager.cancel((int) id);
+    }
+
+    public void cancelAllNotifications() {
+        mNotificationManager.cancelAll();
+    }
+
+    public void startThread(Thread thread) {
+        thread.start();
+    }
+}

+ 536 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java

@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of
+ * the SSLSessionCache added Froyo devices using reflection.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.client.protocol.ClientContext;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.scheme.SocketFactory;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.RequestWrapper;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.BasicHttpProcessor;
+import org.apache.http.protocol.HttpContext;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.SSLCertificateSocketFactory;
+import android.os.Looper;
+import android.util.Log;
+
+/**
+ * Subclass of the Apache {@link DefaultHttpClient} that is configured with
+ * reasonable default settings and registered schemes for Android, and
+ * also lets the user add {@link HttpRequestInterceptor} classes.
+ * Don't create this directly, use the {@link #newInstance} factory method.
+ *
+ * <p>This client processes cookies but does not retain them by default.
+ * To retain cookies, simply add a cookie store to the HttpContext:</p>
+ *
+ * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
+ */
+public final class AndroidHttpClient implements HttpClient {
+
+	static Class<?> sSslSessionCacheClass;
+	static {
+		// if we are on Froyo+ devices, we can take advantage of the SSLSessionCache
+		try {
+			sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache");
+		} catch (Exception e) {
+			
+		}
+	}
+	
+    // Gzip of data shorter than this probably won't be worthwhile
+    public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
+
+    // Default connection and socket timeout of 60 seconds.  Tweak to taste.
+    private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000;
+
+    private static final String TAG = "AndroidHttpClient";
+
+
+    /** Interceptor throws an exception if the executing thread is blocked */
+    private static final HttpRequestInterceptor sThreadCheckInterceptor =
+            new HttpRequestInterceptor() {
+        public void process(HttpRequest request, HttpContext context) {
+            // Prevent the HttpRequest from being sent on the main thread
+            if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) {
+                throw new RuntimeException("This thread forbids HTTP requests");
+            }
+        }
+    };
+
+    /**
+     * Create a new HttpClient with reasonable defaults (which you can update).
+     *
+     * @param userAgent to report in your HTTP requests
+     * @param context to use for caching SSL sessions (may be null for no caching)
+     * @return AndroidHttpClient for you to use for all your requests.
+     */
+    public static AndroidHttpClient newInstance(String userAgent, Context context) {
+        HttpParams params = new BasicHttpParams();
+
+        // Turn off stale checking.  Our connections break all the time anyway,
+        // and it's not worth it to pay the penalty of checking every time.
+        HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+        HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT);
+        HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT);
+        HttpConnectionParams.setSocketBufferSize(params, 8192);
+
+        // Don't handle redirects -- return them to the caller.  Our code
+        // often wants to re-POST after a redirect, which we must do ourselves.
+        HttpClientParams.setRedirecting(params, false);
+
+        Object sessionCache = null;
+        // Use a session cache for SSL sockets -- Froyo only
+        if ( null != context && null != sSslSessionCacheClass ) {
+             Constructor<?> ct;
+			try {
+				ct = sSslSessionCacheClass.getConstructor(Context.class);
+				sessionCache = ct.newInstance(context);             
+			} catch (SecurityException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (NoSuchMethodException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (IllegalArgumentException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (InstantiationException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (IllegalAccessException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (InvocationTargetException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			}
+        }
+
+        // Set the specified user agent and register standard protocols.
+        HttpProtocolParams.setUserAgent(params, userAgent);
+        SchemeRegistry schemeRegistry = new SchemeRegistry();
+        schemeRegistry.register(new Scheme("http",
+                PlainSocketFactory.getSocketFactory(), 80));
+        SocketFactory sslCertificateSocketFactory = null;
+        if ( null != sessionCache ) {
+        	Method getHttpSocketFactoryMethod;
+			try {
+				getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass);
+	        	sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache);
+			} catch (SecurityException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (NoSuchMethodException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (IllegalArgumentException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (IllegalAccessException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			} catch (InvocationTargetException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			}
+        }
+        if ( null == sslCertificateSocketFactory ) {
+        	sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory();
+        }
+        schemeRegistry.register(new Scheme("https",
+                sslCertificateSocketFactory, 443));
+
+        ClientConnectionManager manager =
+                new ThreadSafeClientConnManager(params, schemeRegistry);
+
+        // We use a factory method to modify superclass initialization
+        // parameters without the funny call-a-static-method dance.
+        return new AndroidHttpClient(manager, params);
+    }
+
+    /**
+     * Create a new HttpClient with reasonable defaults (which you can update).
+     * @param userAgent to report in your HTTP requests.
+     * @return AndroidHttpClient for you to use for all your requests.
+     */
+    public static AndroidHttpClient newInstance(String userAgent) {
+        return newInstance(userAgent, null /* session cache */);
+    }
+
+    private final HttpClient delegate;
+
+    private RuntimeException mLeakedException = new IllegalStateException(
+            "AndroidHttpClient created and never closed");
+
+    private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
+        this.delegate = new DefaultHttpClient(ccm, params) {
+            @Override
+            protected BasicHttpProcessor createHttpProcessor() {
+                // Add interceptor to prevent making requests from main thread.
+                BasicHttpProcessor processor = super.createHttpProcessor();
+                processor.addRequestInterceptor(sThreadCheckInterceptor);
+                processor.addRequestInterceptor(new CurlLogger());
+
+                return processor;
+            }
+
+            @Override
+            protected HttpContext createHttpContext() {
+                // Same as DefaultHttpClient.createHttpContext() minus the
+                // cookie store.
+                HttpContext context = new BasicHttpContext();
+                context.setAttribute(
+                        ClientContext.AUTHSCHEME_REGISTRY,
+                        getAuthSchemes());
+                context.setAttribute(
+                        ClientContext.COOKIESPEC_REGISTRY,
+                        getCookieSpecs());
+                context.setAttribute(
+                        ClientContext.CREDS_PROVIDER,
+                        getCredentialsProvider());
+                return context;
+            }
+        };
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        if (mLeakedException != null) {
+            Log.e(TAG, "Leak found", mLeakedException);
+            mLeakedException = null;
+        }
+    }
+
+    /**
+     * Modifies a request to indicate to the server that we would like a
+     * gzipped response.  (Uses the "Accept-Encoding" HTTP header.)
+     * @param request the request to modify
+     * @see #getUngzippedContent
+     */
+    public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
+        request.addHeader("Accept-Encoding", "gzip");
+    }
+
+    /**
+     * Gets the input stream from a response entity.  If the entity is gzipped
+     * then this will get a stream over the uncompressed data.
+     *
+     * @param entity the entity whose content should be read
+     * @return the input stream to read from
+     * @throws IOException
+     */
+    public static InputStream getUngzippedContent(HttpEntity entity)
+            throws IOException {
+        InputStream responseStream = entity.getContent();
+        if (responseStream == null) return responseStream;
+        Header header = entity.getContentEncoding();
+        if (header == null) return responseStream;
+        String contentEncoding = header.getValue();
+        if (contentEncoding == null) return responseStream;
+        if (contentEncoding.contains("gzip")) responseStream
+                = new GZIPInputStream(responseStream);
+        return responseStream;
+    }
+
+    /**
+     * Release resources associated with this client.  You must call this,
+     * or significant resources (sockets and memory) may be leaked.
+     */
+    public void close() {
+        if (mLeakedException != null) {
+            getConnectionManager().shutdown();
+            mLeakedException = null;
+        }
+    }
+
+    public HttpParams getParams() {
+        return delegate.getParams();
+    }
+
+    public ClientConnectionManager getConnectionManager() {
+        return delegate.getConnectionManager();
+    }
+
+    public HttpResponse execute(HttpUriRequest request) throws IOException {
+        return delegate.execute(request);
+    }
+
+    public HttpResponse execute(HttpUriRequest request, HttpContext context)
+            throws IOException {
+        return delegate.execute(request, context);
+    }
+
+    public HttpResponse execute(HttpHost target, HttpRequest request)
+            throws IOException {
+        return delegate.execute(target, request);
+    }
+
+    public HttpResponse execute(HttpHost target, HttpRequest request,
+            HttpContext context) throws IOException {
+        return delegate.execute(target, request, context);
+    }
+
+    public <T> T execute(HttpUriRequest request,
+            ResponseHandler<? extends T> responseHandler)
+            throws IOException, ClientProtocolException {
+        return delegate.execute(request, responseHandler);
+    }
+
+    public <T> T execute(HttpUriRequest request,
+            ResponseHandler<? extends T> responseHandler, HttpContext context)
+            throws IOException, ClientProtocolException {
+        return delegate.execute(request, responseHandler, context);
+    }
+
+    public <T> T execute(HttpHost target, HttpRequest request,
+            ResponseHandler<? extends T> responseHandler) throws IOException,
+            ClientProtocolException {
+        return delegate.execute(target, request, responseHandler);
+    }
+
+    public <T> T execute(HttpHost target, HttpRequest request,
+            ResponseHandler<? extends T> responseHandler, HttpContext context)
+            throws IOException, ClientProtocolException {
+        return delegate.execute(target, request, responseHandler, context);
+    }
+
+    /**
+     * Compress data to send to server.
+     * Creates a Http Entity holding the gzipped data.
+     * The data will not be compressed if it is too short.
+     * @param data The bytes to compress
+     * @return Entity holding the data
+     */
+    public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
+            throws IOException {
+        AbstractHttpEntity entity;
+        if (data.length < getMinGzipSize(resolver)) {
+            entity = new ByteArrayEntity(data);
+        } else {
+            ByteArrayOutputStream arr = new ByteArrayOutputStream();
+            OutputStream zipper = new GZIPOutputStream(arr);
+            zipper.write(data);
+            zipper.close();
+            entity = new ByteArrayEntity(arr.toByteArray());
+            entity.setContentEncoding("gzip");
+        }
+        return entity;
+    }
+
+    /**
+     * Retrieves the minimum size for compressing data.
+     * Shorter data will not be compressed.
+     */
+    public static long getMinGzipSize(ContentResolver resolver) {
+        return DEFAULT_SYNC_MIN_GZIP_BYTES;  // For now, this is just a constant.
+    }
+
+    /* cURL logging support. */
+
+    /**
+     * Logging tag and level.
+     */
+    private static class LoggingConfiguration {
+
+        private final String tag;
+        private final int level;
+
+        private LoggingConfiguration(String tag, int level) {
+            this.tag = tag;
+            this.level = level;
+        }
+
+        /**
+         * Returns true if logging is turned on for this configuration.
+         */
+        private boolean isLoggable() {
+            return Log.isLoggable(tag, level);
+        }
+
+        /**
+         * Prints a message using this configuration.
+         */
+        private void println(String message) {
+            Log.println(level, tag, message);
+        }
+    }
+
+    /** cURL logging configuration. */
+    private volatile LoggingConfiguration curlConfiguration;
+
+    /**
+     * Enables cURL request logging for this client.
+     *
+     * @param name to log messages with
+     * @param level at which to log messages (see {@link android.util.Log})
+     */
+    public void enableCurlLogging(String name, int level) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+        if (level < Log.VERBOSE || level > Log.ASSERT) {
+            throw new IllegalArgumentException("Level is out of range ["
+                + Log.VERBOSE + ".." + Log.ASSERT + "]");
+        }
+
+        curlConfiguration = new LoggingConfiguration(name, level);
+    }
+
+    /**
+     * Disables cURL logging for this client.
+     */
+    public void disableCurlLogging() {
+        curlConfiguration = null;
+    }
+
+    /**
+     * Logs cURL commands equivalent to requests.
+     */
+    private class CurlLogger implements HttpRequestInterceptor {
+        public void process(HttpRequest request, HttpContext context)
+                throws HttpException, IOException {
+            LoggingConfiguration configuration = curlConfiguration;
+            if (configuration != null
+                    && configuration.isLoggable()
+                    && request instanceof HttpUriRequest) {
+                // Never print auth token -- we used to check ro.secure=0 to
+                // enable that, but can't do that in unbundled code.
+                configuration.println(toCurl((HttpUriRequest) request, false));
+            }
+        }
+    }
+
+    /**
+     * Generates a cURL command equivalent to the given request.
+     */
+    private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
+        StringBuilder builder = new StringBuilder();
+
+        builder.append("curl ");
+
+        for (Header header: request.getAllHeaders()) {
+            if (!logAuthToken
+                    && (header.getName().equals("Authorization") ||
+                        header.getName().equals("Cookie"))) {
+                continue;
+            }
+            builder.append("--header \"");
+            builder.append(header.toString().trim());
+            builder.append("\" ");
+        }
+
+        URI uri = request.getURI();
+
+        // If this is a wrapped request, use the URI from the original
+        // request instead. getURI() on the wrapper seems to return a
+        // relative URI. We want an absolute URI.
+        if (request instanceof RequestWrapper) {
+            HttpRequest original = ((RequestWrapper) request).getOriginal();
+            if (original instanceof HttpUriRequest) {
+                uri = ((HttpUriRequest) original).getURI();
+            }
+        }
+
+        builder.append("\"");
+        builder.append(uri);
+        builder.append("\"");
+
+        if (request instanceof HttpEntityEnclosingRequest) {
+            HttpEntityEnclosingRequest entityRequest =
+                    (HttpEntityEnclosingRequest) request;
+            HttpEntity entity = entityRequest.getEntity();
+            if (entity != null && entity.isRepeatable()) {
+                if (entity.getContentLength() < 1024) {
+                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
+                    entity.writeTo(stream);
+                    String entityString = stream.toString();
+
+                    // TODO: Check the content type, too.
+                    builder.append(" --data-ascii \"")
+                            .append(entityString)
+                            .append("\"");
+                } else {
+                    builder.append(" [TOO MUCH DATA TO INCLUDE]");
+                }
+            }
+        }
+
+        return builder.toString();
+    }
+
+    /**
+     * Returns the date of the given HTTP date string. This method can identify
+     * and parse the date formats emitted by common HTTP servers, such as
+     * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>,
+     * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>,
+     * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>,
+     * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and
+     * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI
+     * C's asctime()</a>.
+     *
+     * @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
+     * @throws IllegalArgumentException if {@code dateString} is not a date or
+     *     of an unsupported format.
+     */
+    public static long parseDate(String dateString) {
+        return HttpDateTime.parse(dateString);
+    }
+}

+ 112 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * This service differs from IntentService in a few minor ways/ It will not
+ * auto-stop itself after the intent is handled unless the target returns "true"
+ * in should stop. Since the goal of this service is to handle a single kind of
+ * intent, it does not queue up batches of intents of the same type.
+ */
+public abstract class CustomIntentService extends Service {
+    private String mName;
+    private boolean mRedelivery;
+    private volatile ServiceHandler mServiceHandler;
+    private volatile Looper mServiceLooper;
+    private static final String LOG_TAG = "CancellableIntentService";
+    private static final int WHAT_MESSAGE = -10;
+
+    public CustomIntentService(String paramString) {
+        this.mName = paramString;
+    }
+
+    @Override
+    public IBinder onBind(Intent paramIntent) {
+        return null;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        HandlerThread localHandlerThread = new HandlerThread("IntentService["
+                + this.mName + "]");
+        localHandlerThread.start();
+        this.mServiceLooper = localHandlerThread.getLooper();
+        this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
+    }
+
+    @Override
+    public void onDestroy() {
+        Thread localThread = this.mServiceLooper.getThread();
+        if ((localThread != null) && (localThread.isAlive())) {
+            localThread.interrupt();
+        }
+        this.mServiceLooper.quit();
+        Log.d(LOG_TAG, "onDestroy");
+    }
+
+    protected abstract void onHandleIntent(Intent paramIntent);
+
+    protected abstract boolean shouldStop();
+
+    @Override
+    public void onStart(Intent paramIntent, int startId) {
+        if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
+            Message localMessage = this.mServiceHandler.obtainMessage();
+            localMessage.arg1 = startId;
+            localMessage.obj = paramIntent;
+            localMessage.what = WHAT_MESSAGE;
+            this.mServiceHandler.sendMessage(localMessage);
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent paramIntent, int flags, int startId) {
+        onStart(paramIntent, startId);
+        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
+    }
+
+    public void setIntentRedelivery(boolean enabled) {
+        this.mRedelivery = enabled;
+    }
+
+    private final class ServiceHandler extends Handler {
+        public ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message paramMessage) {
+            CustomIntentService.this
+                    .onHandleIntent((Intent) paramMessage.obj);
+            if (shouldStop()) {
+                Log.d(LOG_TAG, "stopSelf");
+                CustomIntentService.this.stopSelf(paramMessage.arg1);
+                Log.d(LOG_TAG, "afterStopSelf");
+            }
+        }
+    }
+}

+ 30 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+/**
+ * Uses the class-loader model to utilize the updated notification builders in
+ * Honeycomb while maintaining a compatible version for older devices.
+ */
+public class CustomNotificationFactory {
+    static public DownloadNotification.ICustomNotification createCustomNotification() {
+        if (android.os.Build.VERSION.SDK_INT > 13)
+            return new V14CustomNotification();
+        else
+            return new V3CustomNotification();
+    }
+}

+ 92 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.google.android.vending.expansion.downloader.Constants;
+import com.google.android.vending.expansion.downloader.Helpers;
+
+import android.util.Log;
+
+/**
+ * Representation of information about an individual download from the database.
+ */
+public class DownloadInfo {
+    public String mUri;
+    public final int mIndex;
+    public final String mFileName;
+    public String mETag;
+    public long mTotalBytes;
+    public long mCurrentBytes;
+    public long mLastMod;
+    public int mStatus;
+    public int mControl;
+    public int mNumFailed;
+    public int mRetryAfter;
+    public int mRedirectCount;
+
+    boolean mInitialized;
+
+    public int mFuzz;
+
+    public DownloadInfo(int index, String fileName, String pkg) {
+        mFuzz = Helpers.sRandom.nextInt(1001);
+        mFileName = fileName;
+        mIndex = index;
+    }
+
+    public void resetDownload() {
+        mCurrentBytes = 0;
+        mETag = "";
+        mLastMod = 0;
+        mStatus = 0;
+        mControl = 0;
+        mNumFailed = 0;
+        mRetryAfter = 0;
+        mRedirectCount = 0;
+    }
+
+    /**
+     * Returns the time when a download should be restarted.
+     */
+    public long restartTime(long now) {
+        if (mNumFailed == 0) {
+            return now;
+        }
+        if (mRetryAfter > 0) {
+            return mLastMod + mRetryAfter;
+        }
+        return mLastMod +
+                Constants.RETRY_FIRST_DELAY *
+                (1000 + mFuzz) * (1 << (mNumFailed - 1));
+    }
+
+    public void logVerboseInfo() {
+        Log.v(Constants.TAG, "Service adding new entry");
+        Log.v(Constants.TAG, "FILENAME: " + mFileName);
+        Log.v(Constants.TAG, "URI     : " + mUri);
+        Log.v(Constants.TAG, "FILENAME: " + mFileName);
+        Log.v(Constants.TAG, "CONTROL : " + mControl);
+        Log.v(Constants.TAG, "STATUS  : " + mStatus);
+        Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
+        Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
+        Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
+        Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
+        Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes);
+        Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
+        Log.v(Constants.TAG, "ETAG    : " + mETag);
+    }
+}

+ 231 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java

@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.android.vending.expansion.downloader.R;
+import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
+import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
+import com.google.android.vending.expansion.downloader.Helpers;
+import com.google.android.vending.expansion.downloader.IDownloaderClient;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Messenger;
+
+/**
+ * This class handles displaying the notification associated with the download
+ * queue going on in the download manager. It handles multiple status types;
+ * Some require user interaction and some do not. Some of the user interactions
+ * may be transient. (for example: the user is queried to continue the download
+ * on 3G when it started on WiFi, but then the phone locks onto WiFi again so
+ * the prompt automatically goes away)
+ * <p/>
+ * The application interface for the downloader also needs to understand and
+ * handle these transient states.
+ */
+public class DownloadNotification implements IDownloaderClient {
+
+    private int mState;
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private String mCurrentTitle;
+
+    private IDownloaderClient mClientProxy;
+    final ICustomNotification mCustomNotification;
+    private Notification mNotification;
+    private Notification mCurrentNotification;
+    private CharSequence mLabel;
+    private String mCurrentText;
+    private PendingIntent mContentIntent;
+    private DownloadProgressInfo mProgressInfo;
+
+    static final String LOGTAG = "DownloadNotification";
+    static final int NOTIFICATION_ID = LOGTAG.hashCode();
+
+    public PendingIntent getClientIntent() {
+        return mContentIntent;
+    }
+
+    public void setClientIntent(PendingIntent mClientIntent) {
+        this.mContentIntent = mClientIntent;
+    }
+
+    public void resendState() {
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadStateChanged(mState);
+        }
+    }
+
+    @Override
+    public void onDownloadStateChanged(int newState) {
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadStateChanged(newState);
+        }
+        if (newState != mState) {
+            mState = newState;
+            if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
+                return;
+            }
+            int stringDownloadID;
+            int iconResource;
+            boolean ongoingEvent;
+
+            // get the new title string and paused text
+            switch (newState) {
+                case 0:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = R.string.state_unknown;
+                    ongoingEvent = false;
+                    break;
+
+                case IDownloaderClient.STATE_DOWNLOADING:
+                    iconResource = android.R.drawable.stat_sys_download;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+
+                case IDownloaderClient.STATE_FETCHING_URL:
+                case IDownloaderClient.STATE_CONNECTING:
+                    iconResource = android.R.drawable.stat_sys_download_done;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+
+                case IDownloaderClient.STATE_COMPLETED:
+                case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
+                    iconResource = android.R.drawable.stat_sys_download_done;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = false;
+                    break;
+
+                case IDownloaderClient.STATE_FAILED:
+                case IDownloaderClient.STATE_FAILED_CANCELED:
+                case IDownloaderClient.STATE_FAILED_FETCHING_URL:
+                case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
+                case IDownloaderClient.STATE_FAILED_UNLICENSED:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = false;
+                    break;
+
+                default:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+            }
+            mCurrentText = mContext.getString(stringDownloadID);
+            mCurrentTitle = mLabel.toString();
+            mCurrentNotification.tickerText = mLabel + ": " + mCurrentText;
+            mCurrentNotification.icon = iconResource;
+            mCurrentNotification.setLatestEventInfo(mContext, mCurrentTitle, mCurrentText,
+                    mContentIntent);
+            if (ongoingEvent) {
+                mCurrentNotification.flags |= Notification.FLAG_ONGOING_EVENT;
+            } else {
+                mCurrentNotification.flags &= ~Notification.FLAG_ONGOING_EVENT;
+                mCurrentNotification.flags |= Notification.FLAG_AUTO_CANCEL;
+            }
+            mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification);
+        }
+    }
+
+    @Override
+    public void onDownloadProgress(DownloadProgressInfo progress) {
+        mProgressInfo = progress;
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadProgress(progress);
+        }
+        if (progress.mOverallTotal <= 0) {
+            // we just show the text
+            mNotification.tickerText = mCurrentTitle;
+            mNotification.icon = android.R.drawable.stat_sys_download;
+            mNotification.setLatestEventInfo(mContext, mLabel, mCurrentText, mContentIntent);
+            mCurrentNotification = mNotification;
+        } else {
+            mCustomNotification.setCurrentBytes(progress.mOverallProgress);
+            mCustomNotification.setTotalBytes(progress.mOverallTotal);
+            mCustomNotification.setIcon(android.R.drawable.stat_sys_download);
+            mCustomNotification.setPendingIntent(mContentIntent);
+            mCustomNotification.setTicker(mLabel + ": " + mCurrentText);
+            mCustomNotification.setTitle(mLabel);
+            mCustomNotification.setTimeRemaining(progress.mTimeRemaining);
+            mCurrentNotification = mCustomNotification.updateNotification(mContext);
+        }
+        mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotification);
+    }
+
+    public interface ICustomNotification {
+        void setTitle(CharSequence title);
+
+        void setTicker(CharSequence ticker);
+
+        void setPendingIntent(PendingIntent mContentIntent);
+
+        void setTotalBytes(long totalBytes);
+
+        void setCurrentBytes(long currentBytes);
+
+        void setIcon(int iconResource);
+
+        void setTimeRemaining(long timeRemaining);
+
+        Notification updateNotification(Context c);
+    }
+
+    /**
+     * Called in response to onClientUpdated. Creates a new proxy and notifies
+     * it of the current state.
+     * 
+     * @param msg the client Messenger to notify
+     */
+    public void setMessenger(Messenger msg) {
+        mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
+        if (null != mProgressInfo) {
+            mClientProxy.onDownloadProgress(mProgressInfo);
+        }
+        if (mState != -1) {
+            mClientProxy.onDownloadStateChanged(mState);
+        }
+    }
+
+    /**
+     * Constructor
+     * 
+     * @param ctx The context to use to obtain access to the Notification
+     *            Service
+     */
+    DownloadNotification(Context ctx, CharSequence applicationLabel) {
+        mState = -1;
+        mContext = ctx;
+        mLabel = applicationLabel;
+        mNotificationManager = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mCustomNotification = CustomNotificationFactory
+                .createCustomNotification();
+        mNotification = new Notification();
+        mCurrentNotification = mNotification;
+
+    }
+
+    @Override
+    public void onServiceConnected(Messenger m) {
+    }
+
+}

+ 963 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java

@@ -0,0 +1,963 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.google.android.vending.expansion.downloader.Constants;
+import com.google.android.vending.expansion.downloader.Helpers;
+import com.google.android.vending.expansion.downloader.IDownloaderClient;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.params.ConnRouteParams;
+
+import android.content.Context;
+import android.net.Proxy;
+import android.os.PowerManager;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SyncFailedException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+/**
+ * Runs an actual download
+ */
+public class DownloadThread {
+
+    private Context mContext;
+    private DownloadInfo mInfo;
+    private DownloaderService mService;
+    private final DownloadsDB mDB;
+    private final DownloadNotification mNotification;
+    private String mUserAgent;
+
+    public DownloadThread(DownloadInfo info, DownloaderService service,
+            DownloadNotification notification) {
+        mContext = service;
+        mInfo = info;
+        mService = service;
+        mNotification = notification;
+        mDB = DownloadsDB.getDB(service);
+        mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";"
+                + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/"
+                + android.os.Build.ID + ")" +
+                service.getPackageName();
+    }
+
+    /**
+     * Returns the default user agent
+     */
+    private String userAgent() {
+        return mUserAgent;
+    }
+
+    /**
+     * State for the entire run() method.
+     */
+    private static class State {
+        public String mFilename;
+        public FileOutputStream mStream;
+        public boolean mCountRetry = false;
+        public int mRetryAfter = 0;
+        public int mRedirectCount = 0;
+        public String mNewUri;
+        public boolean mGotData = false;
+        public String mRequestUri;
+
+        public State(DownloadInfo info, DownloaderService service) {
+            mRedirectCount = info.mRedirectCount;
+            mRequestUri = info.mUri;
+            mFilename = service.generateTempSaveFileName(info.mFileName);
+        }
+    }
+
+    /**
+     * State within executeDownload()
+     */
+    private static class InnerState {
+        public int mBytesSoFar = 0;
+        public int mBytesThisSession = 0;
+        public String mHeaderETag;
+        public boolean mContinuingDownload = false;
+        public String mHeaderContentLength;
+        public String mHeaderContentDisposition;
+        public String mHeaderContentLocation;
+        public int mBytesNotified = 0;
+        public long mTimeLastNotification = 0;
+    }
+
+    /**
+     * Raised from methods called by run() to indicate that the current request
+     * should be stopped immediately. Note the message passed to this exception
+     * will be logged and therefore must be guaranteed not to contain any PII,
+     * meaning it generally can't include any information about the request URI,
+     * headers, or destination filename.
+     */
+    private class StopRequest extends Throwable {
+        /**
+		 * 
+		 */
+        private static final long serialVersionUID = 6338592678988347973L;
+        public int mFinalStatus;
+
+        public StopRequest(int finalStatus, String message) {
+            super(message);
+            mFinalStatus = finalStatus;
+        }
+
+        public StopRequest(int finalStatus, String message, Throwable throwable) {
+            super(message, throwable);
+            mFinalStatus = finalStatus;
+        }
+    }
+
+    /**
+     * Raised from methods called by executeDownload() to indicate that the
+     * download should be retried immediately.
+     */
+    private class RetryDownload extends Throwable {
+
+        /**
+		 * 
+		 */
+        private static final long serialVersionUID = 6196036036517540229L;
+    }
+
+    /**
+     * Returns the preferred proxy to be used by clients. This is a wrapper
+     * around {@link android.net.Proxy#getHost()}. Currently no proxy will be
+     * returned for localhost or if the active network is Wi-Fi.
+     * 
+     * @param context the context which will be passed to
+     *            {@link android.net.Proxy#getHost()}
+     * @param url the target URL for the request
+     * @note Calling this method requires permission
+     *       android.permission.ACCESS_NETWORK_STATE
+     * @return The preferred proxy to be used by clients, or null if there is no
+     *         proxy.
+     */
+    public HttpHost getPreferredHttpHost(Context context,
+            String url) {
+        if (!isLocalHost(url) && !mService.isWiFi()) {
+            final String proxyHost = Proxy.getHost(context);
+            if (proxyHost != null) {
+                return new HttpHost(proxyHost, Proxy.getPort(context), "http");
+            }
+        }
+
+        return null;
+    }
+
+    static final private boolean isLocalHost(String url) {
+        if (url == null) {
+            return false;
+        }
+
+        try {
+            final URI uri = URI.create(url);
+            final String host = uri.getHost();
+            if (host != null) {
+                // TODO: InetAddress.isLoopbackAddress should be used to check
+                // for localhost. However no public factory methods exist which
+                // can be used without triggering DNS lookup if host is not
+                // localhost.
+                if (host.equalsIgnoreCase("localhost") ||
+                        host.equals("127.0.0.1") ||
+                        host.equals("[::1]")) {
+                    return true;
+                }
+            }
+        } catch (IllegalArgumentException iex) {
+            // Ignore (URI.create)
+        }
+
+        return false;
+    }
+
+    /**
+     * Executes the download in a separate thread
+     */
+    public void run() {
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+        State state = new State(mInfo, mService);
+        AndroidHttpClient client = null;
+        PowerManager.WakeLock wakeLock = null;
+        int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+
+        try {
+            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
+            wakeLock.acquire();
+
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+                Log.v(Constants.TAG, "  at " + mInfo.mUri);
+            }
+
+            client = AndroidHttpClient.newInstance(userAgent(), mContext);
+
+            boolean finished = false;
+            while (!finished) {
+                if (Constants.LOGV) {
+                    Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+                    Log.v(Constants.TAG, "  at " + mInfo.mUri);
+                }
+                // Set or unset proxy, which may have changed since last GET
+                // request.
+                // setDefaultProxy() supports null as proxy parameter.
+                ConnRouteParams.setDefaultProxy(client.getParams(),
+                        getPreferredHttpHost(mContext, state.mRequestUri));
+                HttpGet request = new HttpGet(state.mRequestUri);
+                try {
+                    executeDownload(state, client, request);
+                    finished = true;
+                } catch (RetryDownload exc) {
+                    // fall through
+                } finally {
+                    request.abort();
+                    request = null;
+                }
+            }
+
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
+                Log.v(Constants.TAG, "  at " + mInfo.mUri);
+            }
+            finalizeDestinationFile(state);
+            finalStatus = DownloaderService.STATUS_SUCCESS;
+        } catch (StopRequest error) {
+            // remove the cause before printing, in case it contains PII
+            Log.w(Constants.TAG,
+                    "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
+            error.printStackTrace();
+            finalStatus = error.mFinalStatus;
+            // fall through to finally block
+        } catch (Throwable ex) { // sometimes the socket code throws unchecked
+                                 // exceptions
+            Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
+            finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+            // falls through to the code that reports an error
+        } finally {
+            if (wakeLock != null) {
+                wakeLock.release();
+                wakeLock = null;
+            }
+            if (client != null) {
+                client.close();
+                client = null;
+            }
+            cleanupDestination(state, finalStatus);
+            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
+                    state.mRedirectCount, state.mGotData, state.mFilename);
+        }
+    }
+
+    /**
+     * Fully execute a single download request - setup and send the request,
+     * handle the response, and transfer the data to the destination file.
+     */
+    private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
+            throws StopRequest, RetryDownload {
+        InnerState innerState = new InnerState();
+        byte data[] = new byte[Constants.BUFFER_SIZE];
+
+        checkPausedOrCanceled(state);
+
+        setupDestinationFile(state, innerState);
+        addRequestHeaders(innerState, request);
+
+        // check just before sending the request to avoid using an invalid
+        // connection at all
+        checkConnectivity(state);
+
+        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
+        HttpResponse response = sendRequest(state, client, request);
+        handleExceptionalStatus(state, innerState, response);
+
+        if (Constants.LOGV) {
+            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+        }
+
+        processResponseHeaders(state, innerState, response);
+        InputStream entityStream = openResponseEntity(state, response);
+        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
+        transferData(state, innerState, data, entityStream);
+    }
+
+    /**
+     * Check if current connectivity is valid for this request.
+     */
+    private void checkConnectivity(State state) throws StopRequest {
+        switch (mService.getNetworkAvailabilityState(mDB)) {
+            case DownloaderService.NETWORK_OK:
+                return;
+            case DownloaderService.NETWORK_NO_CONNECTION:
+                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+                        "waiting for network to return");
+            case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
+                throw new StopRequest(
+                        DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
+                        "waiting for wifi or for download over cellular to be authorized");
+            case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
+                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+                        "roaming is not allowed");
+            case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
+                throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
+        }
+    }
+
+    /**
+     * Transfer as much data as possible from the HTTP response to the
+     * destination file.
+     * 
+     * @param data buffer to use to read data
+     * @param entityStream stream for reading the HTTP response entity
+     */
+    private void transferData(State state, InnerState innerState, byte[] data,
+            InputStream entityStream) throws StopRequest {
+        for (;;) {
+            int bytesRead = readFromResponse(state, innerState, data, entityStream);
+            if (bytesRead == -1) { // success, end of stream already reached
+                handleEndOfStream(state, innerState);
+                return;
+            }
+
+            state.mGotData = true;
+            writeDataToDestination(state, data, bytesRead);
+            innerState.mBytesSoFar += bytesRead;
+            innerState.mBytesThisSession += bytesRead;
+            reportProgress(state, innerState);
+
+            checkPausedOrCanceled(state);
+        }
+    }
+
+    /**
+     * Called after a successful completion to take any necessary action on the
+     * downloaded file.
+     */
+    private void finalizeDestinationFile(State state) throws StopRequest {
+        syncDestination(state);
+        String tempFilename = state.mFilename;
+        String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
+        if (!state.mFilename.equals(finalFilename)) {
+            File startFile = new File(tempFilename);
+            File destFile = new File(finalFilename);
+            if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
+                if (!startFile.renameTo(destFile)) {
+                    throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                            "unable to finalize destination file");
+                }
+            } else {
+                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+                        "file delivered with incorrect size. probably due to network not browser configured");
+            }
+        }
+    }
+
+    /**
+     * Called just before the thread finishes, regardless of status, to take any
+     * necessary action on the downloaded file.
+     */
+    private void cleanupDestination(State state, int finalStatus) {
+        closeDestination(state);
+        if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
+            new File(state.mFilename).delete();
+            state.mFilename = null;
+        }
+    }
+
+    /**
+     * Sync the destination file to storage.
+     */
+    private void syncDestination(State state) {
+        FileOutputStream downloadedFileStream = null;
+        try {
+            downloadedFileStream = new FileOutputStream(state.mFilename, true);
+            downloadedFileStream.getFD().sync();
+        } catch (FileNotFoundException ex) {
+            Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
+        } catch (SyncFailedException ex) {
+            Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
+        } catch (IOException ex) {
+            Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
+        } catch (RuntimeException ex) {
+            Log.w(Constants.TAG, "exception while syncing file: ", ex);
+        } finally {
+            if (downloadedFileStream != null) {
+                try {
+                    downloadedFileStream.close();
+                } catch (IOException ex) {
+                    Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
+                } catch (RuntimeException ex) {
+                    Log.w(Constants.TAG, "exception while closing file: ", ex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Close the destination output stream.
+     */
+    private void closeDestination(State state) {
+        try {
+            // close the file
+            if (state.mStream != null) {
+                state.mStream.close();
+                state.mStream = null;
+            }
+        } catch (IOException ex) {
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+            }
+            // nothing can really be done if the file can't be closed
+        }
+    }
+
+    /**
+     * Check if the download has been paused or canceled, stopping the request
+     * appropriately if it has been.
+     */
+    private void checkPausedOrCanceled(State state) throws StopRequest {
+        if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
+            int status = mService.getStatus();
+            switch (status) {
+                case DownloaderService.STATUS_PAUSED_BY_APP:
+                    throw new StopRequest(mService.getStatus(),
+                            "download paused");
+            }
+        }
+    }
+
+    /**
+     * Report download progress through the database if necessary.
+     */
+    private void reportProgress(State state, InnerState innerState) {
+        long now = System.currentTimeMillis();
+        if (innerState.mBytesSoFar - innerState.mBytesNotified
+                > Constants.MIN_PROGRESS_STEP
+                && now - innerState.mTimeLastNotification
+                > Constants.MIN_PROGRESS_TIME) {
+            // we store progress updates to the database here
+            mInfo.mCurrentBytes = innerState.mBytesSoFar;
+            mDB.updateDownloadCurrentBytes(mInfo);
+
+            innerState.mBytesNotified = innerState.mBytesSoFar;
+            innerState.mTimeLastNotification = now;
+
+            long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
+
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of "
+                        + mInfo.mTotalBytes);
+                Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of "
+                        + mService.mTotalLength);
+            }
+
+            mService.notifyUpdateBytes(totalBytesSoFar);
+        }
+    }
+
+    /**
+     * Write a data buffer to the destination file.
+     * 
+     * @param data buffer containing the data to write
+     * @param bytesRead how many bytes to write from the buffer
+     */
+    private void writeDataToDestination(State state, byte[] data, int bytesRead)
+            throws StopRequest {
+        for (;;) {
+            try {
+                if (state.mStream == null) {
+                    state.mStream = new FileOutputStream(state.mFilename, true);
+                }
+                state.mStream.write(data, 0, bytesRead);
+                // we close after every write --- this may be too inefficient
+                closeDestination(state);
+                return;
+            } catch (IOException ex) {
+                if (!Helpers.isExternalMediaMounted()) {
+                    throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
+                            "external media not mounted while writing destination file");
+                }
+
+                long availableBytes =
+                        Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
+                if (availableBytes < bytesRead) {
+                    throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
+                            "insufficient space while writing destination file", ex);
+                }
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "while writing destination file: " + ex.toString(), ex);
+            }
+        }
+    }
+
+    /**
+     * Called when we've reached the end of the HTTP response stream, to update
+     * the database and check for consistency.
+     */
+    private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+        mInfo.mCurrentBytes = innerState.mBytesSoFar;
+        // this should always be set from the market
+        // if ( innerState.mHeaderContentLength == null ) {
+        // mInfo.mTotalBytes = innerState.mBytesSoFar;
+        // }
+        mDB.updateDownload(mInfo);
+
+        boolean lengthMismatched = (innerState.mHeaderContentLength != null)
+                && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
+        if (lengthMismatched) {
+            if (cannotResume(innerState)) {
+                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                        "mismatched content length");
+            } else {
+                throw new StopRequest(getFinalStatusForHttpError(state),
+                        "closed socket before end of file");
+            }
+        }
+    }
+
+    private boolean cannotResume(InnerState innerState) {
+        return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
+    }
+
+    /**
+     * Read some data from the HTTP response stream, handling I/O errors.
+     * 
+     * @param data buffer to use to read data
+     * @param entityStream stream for reading the HTTP response entity
+     * @return the number of bytes actually read or -1 if the end of the stream
+     *         has been reached
+     */
+    private int readFromResponse(State state, InnerState innerState, byte[] data,
+            InputStream entityStream) throws StopRequest {
+        try {
+            return entityStream.read(data);
+        } catch (IOException ex) {
+            logNetworkState();
+            mInfo.mCurrentBytes = innerState.mBytesSoFar;
+            mDB.updateDownload(mInfo);
+            if (cannotResume(innerState)) {
+                String message = "while reading response: " + ex.toString()
+                        + ", can't resume interrupted download with no ETag";
+                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                        message, ex);
+            } else {
+                throw new StopRequest(getFinalStatusForHttpError(state),
+                        "while reading response: " + ex.toString(), ex);
+            }
+        }
+    }
+
+    /**
+     * Open a stream for the HTTP response entity, handling I/O errors.
+     * 
+     * @return an InputStream to read the response entity
+     */
+    private InputStream openResponseEntity(State state, HttpResponse response)
+            throws StopRequest {
+        try {
+            return response.getEntity().getContent();
+        } catch (IOException ex) {
+            logNetworkState();
+            throw new StopRequest(getFinalStatusForHttpError(state),
+                    "while getting entity: " + ex.toString(), ex);
+        }
+    }
+
+    private void logNetworkState() {
+        if (Constants.LOGX) {
+            Log.i(Constants.TAG,
+                    "Net "
+                            + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up"
+                                    : "Down"));
+        }
+    }
+
+    /**
+     * Read HTTP response headers and take appropriate action, including setting
+     * up the destination file and updating the database.
+     */
+    private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
+            throws StopRequest {
+        if (innerState.mContinuingDownload) {
+            // ignore response headers on resume requests
+            return;
+        }
+
+        readResponseHeaders(state, innerState, response);
+
+        try {
+            state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
+        } catch (DownloaderService.GenerateSaveFileError exc) {
+            throw new StopRequest(exc.mStatus, exc.mMessage);
+        }
+        try {
+            state.mStream = new FileOutputStream(state.mFilename);
+        } catch (FileNotFoundException exc) {
+            // make sure the directory exists
+            File pathFile = new File(Helpers.getSaveFilePath(mService));
+            try {
+                if (pathFile.mkdirs()) {
+                    state.mStream = new FileOutputStream(state.mFilename);
+                }
+            } catch (Exception ex) {
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "while opening destination file: " + exc.toString(), exc);
+            }
+        }
+        if (Constants.LOGV) {
+            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
+        }
+
+        updateDatabaseFromHeaders(state, innerState);
+        // check connectivity again now that we know the total size
+        checkConnectivity(state);
+    }
+
+    /**
+     * Update necessary database fields based on values of HTTP response headers
+     * that have been read.
+     */
+    private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+        mInfo.mETag = innerState.mHeaderETag;
+        mDB.updateDownload(mInfo);
+    }
+
+    /**
+     * Read headers from the HTTP response and store them into local state.
+     */
+    private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
+            throws StopRequest {
+        Header header = response.getFirstHeader("Content-Disposition");
+        if (header != null) {
+            innerState.mHeaderContentDisposition = header.getValue();
+        }
+        header = response.getFirstHeader("Content-Location");
+        if (header != null) {
+            innerState.mHeaderContentLocation = header.getValue();
+        }
+        header = response.getFirstHeader("ETag");
+        if (header != null) {
+            innerState.mHeaderETag = header.getValue();
+        }
+        String headerTransferEncoding = null;
+        header = response.getFirstHeader("Transfer-Encoding");
+        if (header != null) {
+            headerTransferEncoding = header.getValue();
+        }
+        String headerContentType = null;
+        header = response.getFirstHeader("Content-Type");
+        if (header != null) {
+            headerContentType = header.getValue();
+            if (!headerContentType.equals("application/vnd.android.obb")) {
+                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+                        "file delivered with incorrect Mime type");
+            }
+        }
+
+        if (headerTransferEncoding == null) {
+            header = response.getFirstHeader("Content-Length");
+            if (header != null) {
+                innerState.mHeaderContentLength = header.getValue();
+                // this is always set from Market
+                long contentLength = Long.parseLong(innerState.mHeaderContentLength);
+                if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
+                    // we're most likely on a bad wifi connection -- we should
+                    // probably
+                    // also look at the mime type --- but the size mismatch is
+                    // enough
+                    // to tell us that something is wrong here
+                    Log.e(Constants.TAG, "Incorrect file size delivered.");
+                }
+            }
+        } else {
+            // Ignore content-length with transfer-encoding - 2616 4.4 3
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG,
+                        "ignoring content-length because of xfer-encoding");
+            }
+        }
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "Content-Disposition: " +
+                    innerState.mHeaderContentDisposition);
+            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
+            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
+            Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
+            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+        }
+
+        boolean noSizeInfo = innerState.mHeaderContentLength == null
+                && (headerTransferEncoding == null
+                || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+        if (noSizeInfo) {
+            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+                    "can't know size of download, giving up");
+        }
+    }
+
+    /**
+     * Check the HTTP response status and handle anything unusual (e.g. not
+     * 200/206).
+     */
+    private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
+            throws StopRequest, RetryDownload {
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
+            handleServiceUnavailable(state, response);
+        }
+        if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
+            handleRedirect(state, response, statusCode);
+        }
+
+        int expectedStatus = innerState.mContinuingDownload ? 206
+                : DownloaderService.STATUS_SUCCESS;
+        if (statusCode != expectedStatus) {
+            handleOtherStatus(state, innerState, statusCode);
+        } else {
+            // no longer redirected
+            state.mRedirectCount = 0;
+        }
+    }
+
+    /**
+     * Handle a status that we don't know how to deal with properly.
+     */
+    private void handleOtherStatus(State state, InnerState innerState, int statusCode)
+            throws StopRequest {
+        int finalStatus;
+        if (DownloaderService.isStatusError(statusCode)) {
+            finalStatus = statusCode;
+        } else if (statusCode >= 300 && statusCode < 400) {
+            finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
+        } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
+            finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
+        } else {
+            finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
+        }
+        throw new StopRequest(finalStatus, "http error " + statusCode);
+    }
+
+    /**
+     * Handle a 3xx redirect status.
+     */
+    private void handleRedirect(State state, HttpResponse response, int statusCode)
+            throws StopRequest, RetryDownload {
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
+        }
+        if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
+            throw new StopRequest(DownloaderService.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
+        }
+        Header header = response.getFirstHeader("Location");
+        if (header == null) {
+            return;
+        }
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "Location :" + header.getValue());
+        }
+
+        String newUri;
+        try {
+            newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
+        } catch (URISyntaxException ex) {
+            if (Constants.LOGV) {
+                Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
+                        + " for " + mInfo.mUri);
+            }
+            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+                    "Couldn't resolve redirect URI");
+        }
+        ++state.mRedirectCount;
+        state.mRequestUri = newUri;
+        if (statusCode == 301 || statusCode == 303) {
+            // use the new URI for all future requests (should a retry/resume be
+            // necessary)
+            state.mNewUri = newUri;
+        }
+        throw new RetryDownload();
+    }
+
+    /**
+     * Add headers for this download to the HTTP request to allow for resume.
+     */
+    private void addRequestHeaders(InnerState innerState, HttpGet request) {
+        if (innerState.mContinuingDownload) {
+            if (innerState.mHeaderETag != null) {
+                request.addHeader("If-Match", innerState.mHeaderETag);
+            }
+            request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
+        }
+    }
+
+    /**
+     * Handle a 503 Service Unavailable status by processing the Retry-After
+     * header.
+     */
+    private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "got HTTP response code 503");
+        }
+        state.mCountRetry = true;
+        Header header = response.getFirstHeader("Retry-After");
+        if (header != null) {
+            try {
+                if (Constants.LOGVV) {
+                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
+                }
+                state.mRetryAfter = Integer.parseInt(header.getValue());
+                if (state.mRetryAfter < 0) {
+                    state.mRetryAfter = 0;
+                } else {
+                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+                    }
+                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+                    state.mRetryAfter *= 1000;
+                }
+            } catch (NumberFormatException ex) {
+                // ignored - retryAfter stays 0 in this case.
+            }
+        }
+        throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
+                "got 503 Service Unavailable, will retry later");
+    }
+
+    /**
+     * Send the request to the server, handling any I/O exceptions.
+     */
+    private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
+            throws StopRequest {
+        try {
+            return client.execute(request);
+        } catch (IllegalArgumentException ex) {
+            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+                    "while trying to execute request: " + ex.toString(), ex);
+        } catch (IOException ex) {
+            logNetworkState();
+            throw new StopRequest(getFinalStatusForHttpError(state),
+                    "while trying to execute request: " + ex.toString(), ex);
+        }
+    }
+
+    private int getFinalStatusForHttpError(State state) {
+        if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
+            return DownloaderService.STATUS_WAITING_FOR_NETWORK;
+        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+            state.mCountRetry = true;
+            return DownloaderService.STATUS_WAITING_TO_RETRY;
+        } else {
+            Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
+            return DownloaderService.STATUS_HTTP_DATA_ERROR;
+        }
+    }
+
+    /**
+     * Prepare the destination file to receive data. If the file already exists,
+     * we'll set up appropriately for resumption.
+     */
+    private void setupDestinationFile(State state, InnerState innerState)
+            throws StopRequest {
+        if (state.mFilename != null) { // only true if we've already run a
+                                       // thread for this download
+            if (!Helpers.isFilenameValid(state.mFilename)) {
+                // this should never happen
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "found invalid internal destination filename");
+            }
+            // We're resuming a download that got interrupted
+            File f = new File(state.mFilename);
+            if (f.exists()) {
+                long fileLength = f.length();
+                if (fileLength == 0) {
+                    // The download hadn't actually started, we can restart from
+                    // scratch
+                    f.delete();
+                    state.mFilename = null;
+                } else if (mInfo.mETag == null) {
+                    // This should've been caught upon failure
+                    f.delete();
+                    throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                            "Trying to resume a download that can't be resumed");
+                } else {
+                    // All right, we'll be able to resume this download
+                    try {
+                        state.mStream = new FileOutputStream(state.mFilename, true);
+                    } catch (FileNotFoundException exc) {
+                        throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                                "while opening destination for resuming: " + exc.toString(), exc);
+                    }
+                    innerState.mBytesSoFar = (int) fileLength;
+                    if (mInfo.mTotalBytes != -1) {
+                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
+                    }
+                    innerState.mHeaderETag = mInfo.mETag;
+                    innerState.mContinuingDownload = true;
+                }
+            }
+        }
+
+        if (state.mStream != null) {
+            closeDestination(state);
+        }
+    }
+
+    /**
+     * Stores information about the completed download, and notifies the
+     * initiating application.
+     */
+    private void notifyDownloadCompleted(
+            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+            String filename) {
+        updateDownloadDatabase(
+                status, countRetry, retryAfter, redirectCount, gotData, filename);
+        if (DownloaderService.isStatusCompleted(status)) {
+            // TBD: send status update?
+        }
+    }
+
+    private void updateDownloadDatabase(
+            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+            String filename) {
+        mInfo.mStatus = status;
+        mInfo.mRetryAfter = retryAfter;
+        mInfo.mRedirectCount = redirectCount;
+        mInfo.mLastMod = System.currentTimeMillis();
+        if (!countRetry) {
+            mInfo.mNumFailed = 0;
+        } else if (gotData) {
+            mInfo.mNumFailed = 1;
+        } else {
+            mInfo.mNumFailed++;
+        }
+        mDB.updateDownload(mInfo);
+    }
+
+}

+ 1341 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java

@@ -0,0 +1,1341 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.google.android.vending.expansion.downloader.Constants;
+import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
+import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
+import com.google.android.vending.expansion.downloader.Helpers;
+import com.google.android.vending.expansion.downloader.IDownloaderClient;
+import com.google.android.vending.expansion.downloader.IDownloaderService;
+import com.google.android.vending.expansion.downloader.IStub;
+import com.google.android.vending.licensing.AESObfuscator;
+import com.google.android.vending.licensing.APKExpansionPolicy;
+import com.google.android.vending.licensing.LicenseChecker;
+import com.google.android.vending.licensing.LicenseCheckerCallback;
+import com.google.android.vending.licensing.Policy;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Messenger;
+import android.os.SystemClock;
+import android.provider.Settings.Secure;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Performs the background downloads requested by applications that use the
+ * Downloads provider. This service does not run as a foreground task, so
+ * Android may kill it off at will, but it will try to restart itself if it can.
+ * Note that Android by default will kill off any process that has an open file
+ * handle on the shared (SD Card) partition if the partition is unmounted.
+ */
+public abstract class DownloaderService extends CustomIntentService implements IDownloaderService {
+
+    public DownloaderService() {
+        super("LVLDownloadService");
+    }
+
+    private static final String LOG_TAG = "LVLDL";
+
+    // the following NETWORK_* constants are used to indicates specific reasons
+    // for disallowing a
+    // download from using a network, since specific causes can require special
+    // handling
+
+    /**
+     * The network is usable for the given download.
+     */
+    public static final int NETWORK_OK = 1;
+
+    /**
+     * There is no network connectivity.
+     */
+    public static final int NETWORK_NO_CONNECTION = 2;
+
+    /**
+     * The download exceeds the maximum size for this network.
+     */
+    public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
+
+    /**
+     * The download exceeds the recommended maximum size for this network, the
+     * user must confirm for this download to proceed without WiFi.
+     */
+    public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
+
+    /**
+     * The current connection is roaming, and the download can't proceed over a
+     * roaming connection.
+     */
+    public static final int NETWORK_CANNOT_USE_ROAMING = 5;
+
+    /**
+     * The app requesting the download specific that it can't use the current
+     * network connection.
+     */
+    public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
+
+    /**
+     * For intents used to notify the user that a download exceeds a size
+     * threshold, if this extra is true, WiFi is required for this download
+     * size; otherwise, it is only recommended.
+     */
+    public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
+    public static final String EXTRA_FILE_NAME = "downloadId";
+
+    /**
+     * Used with DOWNLOAD_STATUS
+     */
+    public static final String EXTRA_STATUS_STATE = "ESS";
+    public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS";
+    public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS";
+    public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP";
+    public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP";
+
+    public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged";
+
+    /**
+     * Broadcast intent action sent by the download manager when a download
+     * completes.
+     */
+    public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE";
+
+    /**
+     * Broadcast intent action sent by the download manager when download status
+     * changes.
+     */
+    public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS";
+
+    /*
+     * Lists the states that the download manager can set on a download to
+     * notify applications of the download progress. The codes follow the HTTP
+     * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not
+     * used by the download manager)<br> 4xx: client errors<br> 5xx: server
+     * errors
+     */
+
+    /**
+     * Returns whether the status is informational (i.e. 1xx).
+     */
+    public static boolean isStatusInformational(int status) {
+        return (status >= 100 && status < 200);
+    }
+
+    /**
+     * Returns whether the status is a success (i.e. 2xx).
+     */
+    public static boolean isStatusSuccess(int status) {
+        return (status >= 200 && status < 300);
+    }
+
+    /**
+     * Returns whether the status is an error (i.e. 4xx or 5xx).
+     */
+    public static boolean isStatusError(int status) {
+        return (status >= 400 && status < 600);
+    }
+
+    /**
+     * Returns whether the status is a client error (i.e. 4xx).
+     */
+    public static boolean isStatusClientError(int status) {
+        return (status >= 400 && status < 500);
+    }
+
+    /**
+     * Returns whether the status is a server error (i.e. 5xx).
+     */
+    public static boolean isStatusServerError(int status) {
+        return (status >= 500 && status < 600);
+    }
+
+    /**
+     * Returns whether the download has completed (either with success or
+     * error).
+     */
+    public static boolean isStatusCompleted(int status) {
+        return (status >= 200 && status < 300)
+                || (status >= 400 && status < 600);
+    }
+
+    /**
+     * This download hasn't stated yet
+     */
+    public static final int STATUS_PENDING = 190;
+
+    /**
+     * This download has started
+     */
+    public static final int STATUS_RUNNING = 192;
+
+    /**
+     * This download has been paused by the owning app.
+     */
+    public static final int STATUS_PAUSED_BY_APP = 193;
+
+    /**
+     * This download encountered some network error and is waiting before
+     * retrying the request.
+     */
+    public static final int STATUS_WAITING_TO_RETRY = 194;
+
+    /**
+     * This download is waiting for network connectivity to proceed.
+     */
+    public static final int STATUS_WAITING_FOR_NETWORK = 195;
+
+    /**
+     * This download is waiting for a Wi-Fi connection to proceed or for
+     * permission to download over cellular.
+     */
+    public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196;
+
+    /**
+     * This download is waiting for a Wi-Fi connection to proceed.
+     */
+    public static final int STATUS_QUEUED_FOR_WIFI = 197;
+
+    /**
+     * This download has successfully completed. Warning: there might be other
+     * status values that indicate success in the future. Use isSucccess() to
+     * capture the entire category.
+     * 
+     * @hide
+     */
+    public static final int STATUS_SUCCESS = 200;
+
+    /**
+     * The requested URL is no longer available
+     */
+    public static final int STATUS_FORBIDDEN = 403;
+
+    /**
+     * The file was delivered incorrectly
+     */
+    public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487;
+
+    /**
+     * The requested destination file already exists.
+     */
+    public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
+
+    /**
+     * Some possibly transient error occurred, but we can't resume the download.
+     */
+    public static final int STATUS_CANNOT_RESUME = 489;
+
+    /**
+     * This download was canceled
+     * 
+     * @hide
+     */
+    public static final int STATUS_CANCELED = 490;
+
+    /**
+     * This download has completed with an error. Warning: there will be other
+     * status values that indicate errors in the future. Use isStatusError() to
+     * capture the entire category.
+     */
+    public static final int STATUS_UNKNOWN_ERROR = 491;
+
+    /**
+     * This download couldn't be completed because of a storage issue.
+     * Typically, that's because the filesystem is missing or full. Use the more
+     * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and
+     * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
+     * 
+     * @hide
+     */
+    public static final int STATUS_FILE_ERROR = 492;
+
+    /**
+     * This download couldn't be completed because of an HTTP redirect response
+     * that the download manager couldn't handle.
+     * 
+     * @hide
+     */
+    public static final int STATUS_UNHANDLED_REDIRECT = 493;
+
+    /**
+     * This download couldn't be completed because of an unspecified unhandled
+     * HTTP code.
+     * 
+     * @hide
+     */
+    public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
+
+    /**
+     * This download couldn't be completed because of an error receiving or
+     * processing data at the HTTP level.
+     * 
+     * @hide
+     */
+    public static final int STATUS_HTTP_DATA_ERROR = 495;
+
+    /**
+     * This download couldn't be completed because of an HttpException while
+     * setting up the request.
+     * 
+     * @hide
+     */
+    public static final int STATUS_HTTP_EXCEPTION = 496;
+
+    /**
+     * This download couldn't be completed because there were too many
+     * redirects.
+     * 
+     * @hide
+     */
+    public static final int STATUS_TOO_MANY_REDIRECTS = 497;
+
+    /**
+     * This download couldn't be completed due to insufficient storage space.
+     * Typically, this is because the SD card is full.
+     * 
+     * @hide
+     */
+    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
+
+    /**
+     * This download couldn't be completed because no external storage device
+     * was found. Typically, this is because the SD card is not mounted.
+     * 
+     * @hide
+     */
+    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
+
+    /**
+     * This download is allowed to run.
+     * 
+     * @hide
+     */
+    public static final int CONTROL_RUN = 0;
+
+    /**
+     * This download must pause at the first opportunity.
+     * 
+     * @hide
+     */
+    public static final int CONTROL_PAUSED = 1;
+
+    /**
+     * This download is visible but only shows in the notifications while it's
+     * in progress.
+     * 
+     * @hide
+     */
+    public static final int VISIBILITY_VISIBLE = 0;
+
+    /**
+     * This download is visible and shows in the notifications while in progress
+     * and after completion.
+     * 
+     * @hide
+     */
+    public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
+
+    /**
+     * This download doesn't show in the UI or in the notifications.
+     * 
+     * @hide
+     */
+    public static final int VISIBILITY_HIDDEN = 2;
+
+    /**
+     * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+     * {@link ConnectivityManager#TYPE_MOBILE}.
+     */
+    public static final int NETWORK_MOBILE = 1 << 0;
+
+    /**
+     * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+     * {@link ConnectivityManager#TYPE_WIFI}.
+     */
+    public static final int NETWORK_WIFI = 1 << 1;
+
+    private final static String TEMP_EXT = ".tmp";
+
+    /**
+     * Service thread status
+     */
+    private static boolean sIsRunning;
+
+    @Override
+    public IBinder onBind(Intent paramIntent) {
+        Log.d(Constants.TAG, "Service Bound");
+        return this.mServiceMessenger.getBinder();
+    }
+
+    /**
+     * Network state.
+     */
+    private boolean mIsConnected;
+    private boolean mIsFailover;
+    private boolean mIsCellularConnection;
+    private boolean mIsRoaming;
+    private boolean mIsAtLeast3G;
+    private boolean mIsAtLeast4G;
+    private boolean mStateChanged;
+
+    /**
+     * Download state
+     */
+    private int mControl;
+    private int mStatus;
+
+    public boolean isWiFi() {
+        return mIsConnected && !mIsCellularConnection;
+    }
+
+    /**
+     * Bindings to important services
+     */
+    private ConnectivityManager mConnectivityManager;
+    private WifiManager mWifiManager;
+
+    /**
+     * Package we are downloading for (defaults to package of application)
+     */
+    private PackageInfo mPackageInfo;
+
+    /**
+     * Byte counts
+     */
+    long mBytesSoFar;
+    long mTotalLength;
+    int mFileCount;
+
+    /**
+     * Used for calculating time remaining and speed
+     */
+    long mBytesAtSample;
+    long mMillisecondsAtSample;
+    float mAverageDownloadSpeed;
+
+    /**
+     * Our binding to the network state broadcasts
+     */
+    private BroadcastReceiver mConnReceiver;
+    final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this);
+    final private Messenger mServiceMessenger = mServiceStub.getMessenger();
+    private Messenger mClientMessenger;
+    private DownloadNotification mNotification;
+    private PendingIntent mPendingIntent;
+    private PendingIntent mAlarmIntent;
+
+    /**
+     * Updates the network type based upon the type and subtype returned from
+     * the connectivity manager. Subtype is only used for cellular signals.
+     * 
+     * @param type
+     * @param subType
+     */
+    private void updateNetworkType(int type, int subType) {
+        switch (type) {
+            case ConnectivityManager.TYPE_WIFI:
+            case ConnectivityManager.TYPE_ETHERNET:
+            case ConnectivityManager.TYPE_BLUETOOTH:
+                mIsCellularConnection = false;
+                mIsAtLeast3G = false;
+                mIsAtLeast4G = false;
+                break;
+            case ConnectivityManager.TYPE_WIMAX:
+                mIsCellularConnection = true;
+                mIsAtLeast3G = true;
+                mIsAtLeast4G = true;
+                break;
+            case ConnectivityManager.TYPE_MOBILE:
+                mIsCellularConnection = true;
+                switch (subType) {
+                    case TelephonyManager.NETWORK_TYPE_1xRTT:
+                    case TelephonyManager.NETWORK_TYPE_CDMA:
+                    case TelephonyManager.NETWORK_TYPE_EDGE:
+                    case TelephonyManager.NETWORK_TYPE_GPRS:
+                    case TelephonyManager.NETWORK_TYPE_IDEN:
+                        mIsAtLeast3G = false;
+                        mIsAtLeast4G = false;
+                        break;
+                    case TelephonyManager.NETWORK_TYPE_HSDPA:
+                    case TelephonyManager.NETWORK_TYPE_HSUPA:
+                    case TelephonyManager.NETWORK_TYPE_HSPA:
+                    case TelephonyManager.NETWORK_TYPE_EVDO_0:
+                    case TelephonyManager.NETWORK_TYPE_EVDO_A:
+                    case TelephonyManager.NETWORK_TYPE_UMTS:
+                        mIsAtLeast3G = true;
+                        mIsAtLeast4G = false;
+                        break;
+                    case TelephonyManager.NETWORK_TYPE_LTE: // 4G
+                    case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop
+                                                              // with 4G
+                    case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but
+                                                              // marketed as
+                                                              // 4G
+                        mIsAtLeast3G = true;
+                        mIsAtLeast4G = true;
+                        break;
+                    default:
+                        mIsCellularConnection = false;
+                        mIsAtLeast3G = false;
+                        mIsAtLeast4G = false;
+                }
+        }
+    }
+
+    private void updateNetworkState(NetworkInfo info) {
+        boolean isConnected = mIsConnected;
+        boolean isFailover = mIsFailover;
+        boolean isCellularConnection = mIsCellularConnection;
+        boolean isRoaming = mIsRoaming;
+        boolean isAtLeast3G = mIsAtLeast3G;
+        if (null != info) {
+            mIsRoaming = info.isRoaming();
+            mIsFailover = info.isFailover();
+            mIsConnected = info.isConnected();
+            updateNetworkType(info.getType(), info.getSubtype());
+        } else {
+            mIsRoaming = false;
+            mIsFailover = false;
+            mIsConnected = false;
+            updateNetworkType(-1, -1);
+        }
+        mStateChanged = (mStateChanged || isConnected != mIsConnected
+                || isFailover != mIsFailover
+                || isCellularConnection != mIsCellularConnection
+                || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G);
+        if (Constants.LOGVV) {
+            if (mStateChanged) {
+                Log.v(LOG_TAG, "Network state changed: ");
+                Log.v(LOG_TAG, "Starting State: " +
+                        (isConnected ? "Connected " : "Not Connected ") +
+                        (isCellularConnection ? "Cellular " : "WiFi ") +
+                        (isRoaming ? "Roaming " : "Local ") +
+                        (isAtLeast3G ? "3G+ " : "<3G "));
+                Log.v(LOG_TAG, "Ending State: " +
+                        (mIsConnected ? "Connected " : "Not Connected ") +
+                        (mIsCellularConnection ? "Cellular " : "WiFi ") +
+                        (mIsRoaming ? "Roaming " : "Local ") +
+                        (mIsAtLeast3G ? "3G+ " : "<3G "));
+
+                if (isServiceRunning()) {
+                    if (mIsRoaming) {
+                        mStatus = STATUS_WAITING_FOR_NETWORK;
+                        mControl = CONTROL_PAUSED;
+                    } else if (mIsCellularConnection) {
+                        DownloadsDB db = DownloadsDB.getDB(this);
+                        int flags = db.getFlags();
+                        if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
+                            mStatus = STATUS_QUEUED_FOR_WIFI;
+                            mControl = CONTROL_PAUSED;
+                        }
+                    }
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Polls the network state, setting the flags appropriately.
+     */
+    void pollNetworkState() {
+        if (null == mConnectivityManager) {
+            mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+        }
+        if (null == mWifiManager) {
+            mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+        }
+        if (mConnectivityManager == null) {
+            Log.w(Constants.TAG,
+                    "couldn't get connectivity manager to poll network state");
+        } else {
+            NetworkInfo activeInfo = mConnectivityManager
+                    .getActiveNetworkInfo();
+            updateNetworkState(activeInfo);
+        }
+    }
+
+    public static final int NO_DOWNLOAD_REQUIRED = 0;
+    public static final int LVL_CHECK_REQUIRED = 1;
+    public static final int DOWNLOAD_REQUIRED = 2;
+
+    public static final String EXTRA_PACKAGE_NAME = "EPN";
+    public static final String EXTRA_PENDING_INTENT = "EPI";
+    public static final String EXTRA_MESSAGE_HANDLER = "EMH";
+
+    /**
+     * Returns true if the LVL check is required
+     * 
+     * @param db a downloads DB synchronized with the latest state
+     * @param pi the package info for the project
+     * @return returns true if the filenames need to be returned
+     */
+    private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) {
+        // we need to update the LVL check and get a successful status to
+        // proceed
+        if (db.mVersionCode != pi.versionCode) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Careful! Only use this internally.
+     * 
+     * @return whether we think the service is running
+     */
+    private static synchronized boolean isServiceRunning() {
+        return sIsRunning;
+    }
+
+    private static synchronized void setServiceRunning(boolean isRunning) {
+        sIsRunning = isRunning;
+    }
+
+    public static int startDownloadServiceIfRequired(Context context,
+            Intent intent, Class<?> serviceClass) throws NameNotFoundException {
+        final PendingIntent pendingIntent = (PendingIntent) intent
+                .getParcelableExtra(EXTRA_PENDING_INTENT);
+        return startDownloadServiceIfRequired(context, pendingIntent,
+                serviceClass);
+    }
+
+    public static int startDownloadServiceIfRequired(Context context,
+            PendingIntent pendingIntent, Class<?> serviceClass)
+            throws NameNotFoundException
+    {
+        String packageName = context.getPackageName();
+        String className = serviceClass.getName();
+
+        return startDownloadServiceIfRequired(context, pendingIntent,
+                packageName, className);
+    }
+
+    /**
+     * Starts the download if necessary. This function starts a flow that does `
+     * many things. 1) Checks to see if the APK version has been checked and the
+     * metadata database updated 2) If the APK version does not match, checks
+     * the new LVL status to see if a new download is required 3) If the APK
+     * version does match, then checks to see if the download(s) have been
+     * completed 4) If the downloads have been completed, returns
+     * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the
+     * startup of an application to quickly ascertain if the application needs
+     * to wait to hear about any updated APK expansion files. Note that this
+     * does mean that the application MUST be run for the first time with a
+     * network connection, even if Market delivers all of the files.
+     * 
+     * @param context
+     * @param thisIntent
+     * @return true if the app should wait for more guidance from the
+     *         downloader, false if the app can continue
+     * @throws NameNotFoundException
+     */
+    public static int startDownloadServiceIfRequired(Context context,
+            PendingIntent pendingIntent, String classPackage, String className)
+            throws NameNotFoundException {
+        // first: do we need to do an LVL update?
+        // we begin by getting our APK version from the package manager
+        final PackageInfo pi = context.getPackageManager().getPackageInfo(
+                context.getPackageName(), 0);
+
+        int status = NO_DOWNLOAD_REQUIRED;
+
+        // the database automatically reads the metadata for version code
+        // and download status when the instance is created
+        DownloadsDB db = DownloadsDB.getDB(context);
+
+        // we need to update the LVL check and get a successful status to
+        // proceed
+        if (isLVLCheckRequired(db, pi)) {
+            status = LVL_CHECK_REQUIRED;
+        }
+        // we don't have to update LVL. do we still have a download to start?
+        if (db.mStatus == 0) {
+            DownloadInfo[] infos = db.getDownloads();
+            if (null != infos) {
+                for (DownloadInfo info : infos) {
+                    if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) {
+                        status = DOWNLOAD_REQUIRED;
+                        db.updateStatus(-1);
+                        break;
+                    }
+                }
+            }
+        } else {
+            status = DOWNLOAD_REQUIRED;
+        }
+        switch (status) {
+            case DOWNLOAD_REQUIRED:
+            case LVL_CHECK_REQUIRED:
+                Intent fileIntent = new Intent();
+                fileIntent.setClassName(classPackage, className);
+                fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
+                context.startService(fileIntent);
+                break;
+        }
+        return status;
+    }
+
+    @Override
+    public void requestAbortDownload() {
+        mControl = CONTROL_PAUSED;
+        mStatus = STATUS_CANCELED;
+    }
+
+    @Override
+    public void requestPauseDownload() {
+        mControl = CONTROL_PAUSED;
+        mStatus = STATUS_PAUSED_BY_APP;
+    }
+
+    @Override
+    public void setDownloadFlags(int flags) {
+        DownloadsDB.getDB(this).updateFlags(flags);
+    }
+
+    @Override
+    public void requestContinueDownload() {
+        if (mControl == CONTROL_PAUSED) {
+            mControl = CONTROL_RUN;
+        }
+        Intent fileIntent = new Intent(this, this.getClass());
+        fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+        this.startService(fileIntent);
+    }
+
+    public abstract String getPublicKey();
+
+    public abstract byte[] getSALT();
+
+    public abstract String getAlarmReceiverClassName();
+
+    private class LVLRunnable implements Runnable {
+        LVLRunnable(Context context, PendingIntent intent) {
+            mContext = context;
+            mPendingIntent = intent;
+        }
+
+        final Context mContext;
+
+        @Override
+        public void run() {
+            setServiceRunning(true);
+            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL);
+            String deviceId = Secure.getString(mContext.getContentResolver(),
+                    Secure.ANDROID_ID);
+
+            final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
+                    new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId));
+
+            // reset our policy back to the start of the world to force a
+            // re-check
+            aep.resetPolicy();
+
+            // let's try and get the OBB file from LVL first
+            // Construct the LicenseChecker with a Policy.
+            final LicenseChecker checker = new LicenseChecker(mContext, aep,
+                    getPublicKey() // Your public licensing key.
+            );
+            checker.checkAccess(new LicenseCheckerCallback() {
+
+                @Override
+                public void allow(int reason) {
+                    try {
+                        int count = aep.getExpansionURLCount();
+                        DownloadsDB db = DownloadsDB.getDB(mContext);
+                        int status = 0;
+                        if (count != 0) {
+                            for (int i = 0; i < count; i++) {
+                                String currentFileName = aep
+                                        .getExpansionFileName(i);
+                                if (null != currentFileName) {
+                                    DownloadInfo di = new DownloadInfo(i,
+                                            currentFileName, mContext.getPackageName());
+
+                                    long fileSize = aep.getExpansionFileSize(i);
+                                    if (handleFileUpdated(db, i, currentFileName,
+                                            fileSize)) {
+                                        status |= -1;
+                                        di.resetDownload();
+                                        di.mUri = aep.getExpansionURL(i);
+                                        di.mTotalBytes = fileSize;
+                                        di.mStatus = status;
+                                        db.updateDownload(di);
+                                    } else {
+                                        // we need to read the download
+                                        // information
+                                        // from
+                                        // the database
+                                        DownloadInfo dbdi = db
+                                                .getDownloadInfoByFileName(di.mFileName);
+                                        if (null == dbdi) {
+                                            // the file exists already and is
+                                            // the
+                                            // correct size
+                                            // was delivered by Market or
+                                            // through
+                                            // another mechanism
+                                            Log.d(LOG_TAG, "file " + di.mFileName
+                                                    + " found. Not downloading.");
+                                            di.mStatus = STATUS_SUCCESS;
+                                            di.mTotalBytes = fileSize;
+                                            di.mCurrentBytes = fileSize;
+                                            di.mUri = aep.getExpansionURL(i);
+                                            db.updateDownload(di);
+                                        } else if (dbdi.mStatus != STATUS_SUCCESS) {
+                                            // we just update the URL
+                                            dbdi.mUri = aep.getExpansionURL(i);
+                                            db.updateDownload(dbdi);
+                                            status |= -1;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        // first: do we need to do an LVL update?
+                        // we begin by getting our APK version from the package
+                        // manager
+                        PackageInfo pi;
+                        try {
+                            pi = mContext.getPackageManager().getPackageInfo(
+                                    mContext.getPackageName(), 0);
+                            db.updateMetadata(pi.versionCode, status);
+                            Class<?> serviceClass = DownloaderService.this.getClass();
+                            switch (startDownloadServiceIfRequired(mContext, mPendingIntent,
+                                    serviceClass)) {
+                                case NO_DOWNLOAD_REQUIRED:
+                                    mNotification
+                                            .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
+                                    break;
+                                case LVL_CHECK_REQUIRED:
+                                    // DANGER WILL ROBINSON!
+                                    Log.e(LOG_TAG, "In LVL checking loop!");
+                                    mNotification
+                                            .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
+                                    throw new RuntimeException(
+                                            "Error with LVL checking and database integrity");
+                                case DOWNLOAD_REQUIRED:
+                                    // do nothing. the download will notify the
+                                    // application
+                                    // when things are done
+                                    break;
+                            }
+                        } catch (NameNotFoundException e1) {
+                            e1.printStackTrace();
+                            throw new RuntimeException(
+                                    "Error with getting information from package name");
+                        }
+                    } finally {
+                        setServiceRunning(false);
+                    }
+                }
+
+                @Override
+                public void dontAllow(int reason) {
+                    try
+                    {
+                        switch (reason) {
+                            case Policy.NOT_LICENSED:
+                                mNotification
+                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
+                                break;
+                            case Policy.RETRY:
+                                mNotification
+                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
+                                break;
+                        }
+                    } finally {
+                        setServiceRunning(false);
+                    }
+
+                }
+
+                @Override
+                public void applicationError(int errorCode) {
+                    try {
+                        mNotification
+                                .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
+                    } finally {
+                        setServiceRunning(false);
+                    }
+                }
+
+            });
+
+        }
+
+    };
+
+    /**
+     * Updates the LVL information from the server.
+     * 
+     * @param context
+     */
+    public void updateLVL(final Context context) {
+        Context c = context.getApplicationContext();
+        Handler h = new Handler(c.getMainLooper());
+        h.post(new LVLRunnable(c, mPendingIntent));
+    }
+
+    /**
+     * The APK has been updated and a filename has been sent down from the
+     * Market call. If the file has the same name as the previous file, we do
+     * nothing as the file is guaranteed to be the same. If the file does not
+     * have the same name, we download it if it hasn't already been delivered by
+     * Market.
+     * 
+     * @param index the index of the file from market (0 = main, 1 = patch)
+     * @param filename the name of the new file
+     * @param fileSize the size of the new file
+     * @return
+     */
+    public boolean handleFileUpdated(DownloadsDB db, int index,
+            String filename, long fileSize) {
+        DownloadInfo di = db.getDownloadInfoByFileName(filename);
+        if (null != di) {
+            String oldFile = di.mFileName;
+            // cleanup
+            if (null != oldFile) {
+                if (filename.equals(oldFile)) {
+                    return false;
+                }
+
+                // remove partially downloaded file if it is there
+                String deleteFile = Helpers.generateSaveFileName(this, oldFile);
+                File f = new File(deleteFile);
+                if (f.exists())
+                    f.delete();
+            }
+        }
+        return !Helpers.doesFileExist(this, filename, fileSize, true);
+    }
+
+    private void scheduleAlarm(long wakeUp) {
+        AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+        if (alarms == null) {
+            Log.e(Constants.TAG, "couldn't get alarm manager");
+            return;
+        }
+
+        if (Constants.LOGV) {
+            Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+        }
+
+        String className = getAlarmReceiverClassName();
+        Intent intent = new Intent(Constants.ACTION_RETRY);
+        intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+        intent.setClassName(this.getPackageName(),
+                className);
+        mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent,
+                PendingIntent.FLAG_ONE_SHOT);
+        alarms.set(
+                AlarmManager.RTC_WAKEUP,
+                System.currentTimeMillis() + wakeUp, mAlarmIntent
+                );
+    }
+
+    private void cancelAlarms() {
+        if (null != mAlarmIntent) {
+            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+            if (alarms == null) {
+                Log.e(Constants.TAG, "couldn't get alarm manager");
+                return;
+            }
+            alarms.cancel(mAlarmIntent);
+            mAlarmIntent = null;
+        }
+    }
+
+    /**
+     * We use this to track network state, such as when WiFi, Cellular, etc. is
+     * enabled when downloads are paused or in progress.
+     */
+    private class InnerBroadcastReceiver extends BroadcastReceiver {
+        final Service mService;
+
+        InnerBroadcastReceiver(Service service) {
+            mService = service;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            pollNetworkState();
+            if (mStateChanged
+                    && !isServiceRunning()) {
+                Log.d(Constants.TAG, "InnerBroadcastReceiver Called");
+                Intent fileIntent = new Intent(context, mService.getClass());
+                fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+                // send a new intent to the service
+                context.startService(fileIntent);
+            }
+        }
+    };
+
+    /**
+     * This is the main thread for the Downloader. This thread is responsible
+     * for queuing up downloads and other goodness.
+     */
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        setServiceRunning(true);
+        try {
+            // the database automatically reads the metadata for version code
+            // and download status when the instance is created
+            DownloadsDB db = DownloadsDB.getDB(this);
+            final PendingIntent pendingIntent = (PendingIntent) intent
+                    .getParcelableExtra(EXTRA_PENDING_INTENT);
+
+            if (null != pendingIntent)
+            {
+                mNotification.setClientIntent(pendingIntent);
+                mPendingIntent = pendingIntent;
+            } else if (null != mPendingIntent) {
+                mNotification.setClientIntent(mPendingIntent);
+            } else {
+                Log.e(LOG_TAG, "Downloader started in bad state without notification intent.");
+                return;
+            }
+
+            // when the LVL check completes, a successful response will update
+            // the service
+            if (isLVLCheckRequired(db, mPackageInfo)) {
+                updateLVL(this);
+                return;
+            }
+
+            // get each download
+            DownloadInfo[] infos = db.getDownloads();
+            mBytesSoFar = 0;
+            mTotalLength = 0;
+            mFileCount = infos.length;
+            for (DownloadInfo info : infos) {
+                // We do an (simple) integrity check on each file, just to make
+                // sure
+                if (info.mStatus == STATUS_SUCCESS) {
+                    // verify that the file matches the state
+                    if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) {
+                        info.mStatus = 0;
+                        info.mCurrentBytes = 0;
+                    }
+                }
+                // get aggregate data
+                mTotalLength += info.mTotalBytes;
+                mBytesSoFar += info.mCurrentBytes;
+            }
+
+            // loop through all downloads and fetch them
+            pollNetworkState();
+            if (null == mConnReceiver) {
+
+                /**
+                 * We use this to track network state, such as when WiFi,
+                 * Cellular, etc. is enabled when downloads are paused or in
+                 * progress.
+                 */
+                mConnReceiver = new InnerBroadcastReceiver(this);
+                IntentFilter intentFilter = new IntentFilter(
+                        ConnectivityManager.CONNECTIVITY_ACTION);
+                intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+                registerReceiver(mConnReceiver, intentFilter);
+            }
+
+            for (DownloadInfo info : infos) {
+                long startingCount = info.mCurrentBytes;
+
+                if (info.mStatus != STATUS_SUCCESS) {
+                    DownloadThread dt = new DownloadThread(info, this, mNotification);
+                    cancelAlarms();
+                    scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG);
+                    dt.run();
+                    cancelAlarms();
+                }
+                db.updateFromDb(info);
+                boolean setWakeWatchdog = false;
+                int notifyStatus;
+                switch (info.mStatus) {
+                    case STATUS_FORBIDDEN:
+                        // the URL is out of date
+                        updateLVL(this);
+                        return;
+                    case STATUS_SUCCESS:
+                        mBytesSoFar += info.mCurrentBytes - startingCount;
+                        db.updateMetadata(mPackageInfo.versionCode, 0);
+                        continue;
+                    case STATUS_FILE_DELIVERED_INCORRECTLY:
+                        // we may be on a network that is returning us a web
+                        // page on redirect
+                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE;
+                        info.mCurrentBytes = 0;
+                        db.updateDownload(info);
+                        setWakeWatchdog = true;
+                        break;
+                    case STATUS_PAUSED_BY_APP:
+                        notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST;
+                        break;
+                    case STATUS_WAITING_FOR_NETWORK:
+                    case STATUS_WAITING_TO_RETRY:
+                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE;
+                        setWakeWatchdog = true;
+                        break;
+                    case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION:
+                    case STATUS_QUEUED_FOR_WIFI:
+                        // look for more detail here
+                        if (null != mWifiManager) {
+                            if (!mWifiManager.isWifiEnabled()) {
+                                notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION;
+                                setWakeWatchdog = true;
+                                break;
+                            }
+                        }
+                        notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION;
+                        setWakeWatchdog = true;
+                        break;
+                    case STATUS_CANCELED:
+                        notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED;
+                        setWakeWatchdog = true;
+                        break;
+
+                    case STATUS_INSUFFICIENT_SPACE_ERROR:
+                        notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL;
+                        setWakeWatchdog = true;
+                        break;
+
+                    case STATUS_DEVICE_NOT_FOUND_ERROR:
+                        notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE;
+                        setWakeWatchdog = true;
+                        break;
+
+                    default:
+                        notifyStatus = IDownloaderClient.STATE_FAILED;
+                        break;
+                }
+                if (setWakeWatchdog) {
+                    scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER);
+                } else {
+                    cancelAlarms();
+                }
+                // failure or pause state
+                mNotification.onDownloadStateChanged(notifyStatus);
+                return;
+            }
+
+            // all downloads complete
+            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
+        } finally {
+            setServiceRunning(false);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        if (null != mConnReceiver) {
+            unregisterReceiver(mConnReceiver);
+            mConnReceiver = null;
+        }
+        mServiceStub.disconnect(this);
+        super.onDestroy();
+    }
+
+    public int getNetworkAvailabilityState(DownloadsDB db) {
+        if (mIsConnected) {
+            if (!mIsCellularConnection)
+                return NETWORK_OK;
+            int flags = db.mFlags;
+            if (mIsRoaming)
+                return NETWORK_CANNOT_USE_ROAMING;
+            if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
+                return NETWORK_OK;
+            } else {
+                return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+            }
+        }
+        return NETWORK_NO_CONNECTION;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        try {
+            mPackageInfo = getPackageManager().getPackageInfo(
+                    getPackageName(), 0);
+            ApplicationInfo ai = getApplicationInfo();
+            CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai);
+            mNotification = new DownloadNotification(this, applicationLabel);
+
+        } catch (NameNotFoundException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Exception thrown from methods called by generateSaveFile() for any fatal
+     * error.
+     */
+    public static class GenerateSaveFileError extends Exception {
+        private static final long serialVersionUID = 3465966015408936540L;
+        int mStatus;
+        String mMessage;
+
+        public GenerateSaveFileError(int status, String message) {
+            mStatus = status;
+            mMessage = message;
+        }
+    }
+
+    /**
+     * Returns the filename (where the file should be saved) from info about a
+     * download
+     */
+    public String generateTempSaveFileName(String fileName) {
+        String path = Helpers.getSaveFilePath(this)
+                + File.separator + fileName + TEMP_EXT;
+        return path;
+    }
+
+    /**
+     * Creates a filename (where the file should be saved) from info about a
+     * download.
+     */
+    public String generateSaveFile(String filename, long filesize)
+            throws GenerateSaveFileError {
+        String path = generateTempSaveFileName(filename);
+        File expPath = new File(path);
+        if (!Helpers.isExternalMediaMounted()) {
+            Log.d(Constants.TAG, "External media not mounted: " + path);
+            throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR,
+                    "external media is not yet mounted");
+
+        }
+        if (expPath.exists()) {
+            Log.d(Constants.TAG, "File already exists: " + path);
+            throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR,
+                    "requested destination file already exists");
+        }
+        if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) {
+            throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR,
+                    "insufficient space on external storage");
+        }
+        return path;
+    }
+
+    /**
+     * @return a non-localized string appropriate for logging corresponding to
+     *         one of the NETWORK_* constants.
+     */
+    public String getLogMessageForNetworkError(int networkError) {
+        switch (networkError) {
+            case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
+                return "download size exceeds recommended limit for mobile network";
+
+            case NETWORK_UNUSABLE_DUE_TO_SIZE:
+                return "download size exceeds limit for mobile network";
+
+            case NETWORK_NO_CONNECTION:
+                return "no network connection available";
+
+            case NETWORK_CANNOT_USE_ROAMING:
+                return "download cannot use the current network connection because it is roaming";
+
+            case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
+                return "download was requested to not use the current network type";
+
+            default:
+                return "unknown error with network connectivity";
+        }
+    }
+
+    public int getControl() {
+        return mControl;
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Calculating a moving average for the speed so we don't get jumpy
+     * calculations for time etc.
+     */
+    static private final float SMOOTHING_FACTOR = 0.005f;
+
+    public void notifyUpdateBytes(long totalBytesSoFar) {
+        long timeRemaining;
+        long currentTime = SystemClock.uptimeMillis();
+        if (0 != mMillisecondsAtSample) {
+            // we have a sample.
+            long timePassed = currentTime - mMillisecondsAtSample;
+            long bytesInSample = totalBytesSoFar - mBytesAtSample;
+            float currentSpeedSample = (float) bytesInSample / (float) timePassed;
+            if (0 != mAverageDownloadSpeed) {
+                mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample
+                        + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed;
+            } else {
+                mAverageDownloadSpeed = currentSpeedSample;
+            }
+            timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed);
+        } else {
+            timeRemaining = -1;
+        }
+        mMillisecondsAtSample = currentTime;
+        mBytesAtSample = totalBytesSoFar;
+        mNotification.onDownloadProgress(
+                new DownloadProgressInfo(mTotalLength,
+                        totalBytesSoFar,
+                        timeRemaining,
+                        mAverageDownloadSpeed)
+                );
+
+    }
+
+    @Override
+    protected boolean shouldStop() {
+        // the database automatically reads the metadata for version code
+        // and download status when the instance is created
+        DownloadsDB db = DownloadsDB.getDB(this);
+        if (db.mStatus == 0) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void requestDownloadStatus() {
+        mNotification.resendState();
+    }
+
+    @Override
+    public void onClientUpdated(Messenger clientMessenger) {
+        this.mClientMessenger = clientMessenger;
+        mNotification.setMessenger(mClientMessenger);
+    }
+
+}

+ 510 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java

@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.util.Log;
+
+public class DownloadsDB {
+    private static final String DATABASE_NAME = "DownloadsDB";
+    private static final int DATABASE_VERSION = 7;
+    public static final String LOG_TAG = DownloadsDB.class.getName();
+    final SQLiteOpenHelper mHelper;
+    SQLiteStatement mGetDownloadByIndex;
+    SQLiteStatement mUpdateCurrentBytes;
+    private static DownloadsDB mDownloadsDB;
+    long mMetadataRowID = -1;
+    int mVersionCode = -1;
+    int mStatus = -1;
+    int mFlags;
+
+    static public synchronized DownloadsDB getDB(Context paramContext) {
+        if (null == mDownloadsDB) {
+            return new DownloadsDB(paramContext);
+        }
+        return mDownloadsDB;
+    }
+
+    private SQLiteStatement getDownloadByIndexStatement() {
+        if (null == mGetDownloadByIndex) {
+            mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
+                    "SELECT " + BaseColumns._ID + " FROM "
+                            + DownloadColumns.TABLE_NAME + " WHERE "
+                            + DownloadColumns.INDEX + " = ?");
+        }
+        return mGetDownloadByIndex;
+    }
+
+    private SQLiteStatement getUpdateCurrentBytesStatement() {
+        if (null == mUpdateCurrentBytes) {
+            mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
+                    "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES
+                            + " = ?" +
+                            " WHERE " + DownloadColumns.INDEX + " = ?");
+        }
+        return mUpdateCurrentBytes;
+    }
+
+    private DownloadsDB(Context paramContext) {
+        this.mHelper = new DownloadsContentDBHelper(paramContext);
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        // Query for the version code, the row ID of the metadata (for future
+        // updating) the status and the flags
+        Cursor cur = sqldb.rawQuery("SELECT " +
+                MetadataColumns.APKVERSION + "," +
+                BaseColumns._ID + "," +
+                MetadataColumns.DOWNLOAD_STATUS + "," +
+                MetadataColumns.FLAGS +
+                " FROM "
+                + MetadataColumns.TABLE_NAME + " LIMIT 1", null);
+        if (null != cur && cur.moveToFirst()) {
+            mVersionCode = cur.getInt(0);
+            mMetadataRowID = cur.getLong(1);
+            mStatus = cur.getInt(2);
+            mFlags = cur.getInt(3);
+            cur.close();
+        }
+        mDownloadsDB = this;
+    }
+
+    protected DownloadInfo getDownloadInfoByFileName(String fileName) {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor itemcur = null;
+        try {
+            itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
+                    DownloadColumns.FILENAME + " = ?",
+                    new String[] {
+                        fileName
+                    }, null, null, null);
+            if (null != itemcur && itemcur.moveToFirst()) {
+                return getDownloadInfoFromCursor(itemcur);
+            }
+        } finally {
+            if (null != itemcur)
+                itemcur.close();
+        }
+        return null;
+    }
+
+    public long getIDForDownloadInfo(final DownloadInfo di) {
+        return getIDByIndex(di.mIndex);
+    }
+
+    public long getIDByIndex(int index) {
+        SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
+        downloadByIndex.clearBindings();
+        downloadByIndex.bindLong(1, index);
+        try {
+            return downloadByIndex.simpleQueryForLong();
+        } catch (SQLiteDoneException e) {
+            return -1;
+        }
+    }
+
+    public void updateDownloadCurrentBytes(final DownloadInfo di) {
+        SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
+        downloadCurrentBytes.clearBindings();
+        downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
+        downloadCurrentBytes.bindLong(2, di.mIndex);
+        downloadCurrentBytes.execute();
+    }
+
+    public void close() {
+        this.mHelper.close();
+    }
+
+    protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
+        DownloadsContentDBHelper(Context paramContext) {
+            super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        private String createTableQueryFromArray(String paramString,
+                String[][] paramArrayOfString) {
+            StringBuilder localStringBuilder = new StringBuilder();
+            localStringBuilder.append("CREATE TABLE ");
+            localStringBuilder.append(paramString);
+            localStringBuilder.append(" (");
+            int i = paramArrayOfString.length;
+            for (int j = 0;; j++) {
+                if (j >= i) {
+                    localStringBuilder
+                            .setLength(localStringBuilder.length() - 1);
+                    localStringBuilder.append(");");
+                    return localStringBuilder.toString();
+                }
+                String[] arrayOfString = paramArrayOfString[j];
+                localStringBuilder.append(' ');
+                localStringBuilder.append(arrayOfString[0]);
+                localStringBuilder.append(' ');
+                localStringBuilder.append(arrayOfString[1]);
+                localStringBuilder.append(',');
+            }
+        }
+
+        /**
+         * These two arrays must match and have the same order. For every Schema
+         * there must be a corresponding table name.
+         */
+        static final private String[][][] sSchemas = {
+                DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
+        };
+
+        static final private String[] sTables = {
+                DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
+        };
+
+        /**
+         * Goes through all of the tables in sTables and drops each table if it
+         * exists. Altered to no longer make use of reflection.
+         */
+        private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
+            for (String table : sTables) {
+                try {
+                    paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
+                } catch (Exception localException) {
+                    localException.printStackTrace();
+                }
+            }
+        }
+
+        /**
+         * Goes through all of the tables in sTables and creates a database with
+         * the corresponding schema described in sSchemas. Altered to no longer
+         * make use of reflection.
+         */
+        public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
+            int numSchemas = sSchemas.length;
+            for (int i = 0; i < numSchemas; i++) {
+                try {
+                    String[][] schema = (String[][]) sSchemas[i];
+                    paramSQLiteDatabase.execSQL(createTableQueryFromArray(
+                            sTables[i], schema));
+                } catch (Exception localException) {
+                    while (true)
+                        localException.printStackTrace();
+                }
+            }
+        }
+
+        public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
+                int paramInt1, int paramInt2) {
+            Log.w(DownloadsContentDBHelper.class.getName(),
+                    "Upgrading database from version " + paramInt1 + " to "
+                            + paramInt2 + ", which will destroy all old data");
+            dropTables(paramSQLiteDatabase);
+            onCreate(paramSQLiteDatabase);
+        }
+    }
+
+    public static class MetadataColumns implements BaseColumns {
+        public static final String APKVERSION = "APKVERSION";
+        public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
+        public static final String FLAGS = "DOWNLOADFLAGS";
+
+        public static final String[][] SCHEMA = {
+                {
+                        BaseColumns._ID, "INTEGER PRIMARY KEY"
+                },
+                {
+                        APKVERSION, "INTEGER"
+                }, {
+                        DOWNLOAD_STATUS, "INTEGER"
+                },
+                {
+                        FLAGS, "INTEGER"
+                }
+        };
+        public static final String TABLE_NAME = "MetadataColumns";
+        public static final String _ID = "MetadataColumns._id";
+    }
+
+    public static class DownloadColumns implements BaseColumns {
+        public static final String INDEX = "FILEIDX";
+        public static final String URI = "URI";
+        public static final String FILENAME = "FN";
+        public static final String ETAG = "ETAG";
+
+        public static final String TOTALBYTES = "TOTALBYTES";
+        public static final String CURRENTBYTES = "CURRENTBYTES";
+        public static final String LASTMOD = "LASTMOD";
+
+        public static final String STATUS = "STATUS";
+        public static final String CONTROL = "CONTROL";
+        public static final String NUM_FAILED = "FAILCOUNT";
+        public static final String RETRY_AFTER = "RETRYAFTER";
+        public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
+
+        public static final String[][] SCHEMA = {
+                {
+                        BaseColumns._ID, "INTEGER PRIMARY KEY"
+                },
+                {
+                        INDEX, "INTEGER UNIQUE"
+                }, {
+                        URI, "TEXT"
+                },
+                {
+                        FILENAME, "TEXT UNIQUE"
+                }, {
+                        ETAG, "TEXT"
+                },
+                {
+                        TOTALBYTES, "INTEGER"
+                }, {
+                        CURRENTBYTES, "INTEGER"
+                },
+                {
+                        LASTMOD, "INTEGER"
+                }, {
+                        STATUS, "INTEGER"
+                },
+                {
+                        CONTROL, "INTEGER"
+                }, {
+                        NUM_FAILED, "INTEGER"
+                },
+                {
+                        RETRY_AFTER, "INTEGER"
+                }, {
+                        REDIRECT_COUNT, "INTEGER"
+                }
+        };
+        public static final String TABLE_NAME = "DownloadColumns";
+        public static final String _ID = "DownloadColumns._id";
+    }
+
+    private static final String[] DC_PROJECTION = {
+            DownloadColumns.FILENAME,
+            DownloadColumns.URI, DownloadColumns.ETAG,
+            DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
+            DownloadColumns.LASTMOD, DownloadColumns.STATUS,
+            DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
+            DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
+            DownloadColumns.INDEX
+    };
+
+    private static final int FILENAME_IDX = 0;
+    private static final int URI_IDX = 1;
+    private static final int ETAG_IDX = 2;
+    private static final int TOTALBYTES_IDX = 3;
+    private static final int CURRENTBYTES_IDX = 4;
+    private static final int LASTMOD_IDX = 5;
+    private static final int STATUS_IDX = 6;
+    private static final int CONTROL_IDX = 7;
+    private static final int NUM_FAILED_IDX = 8;
+    private static final int RETRY_AFTER_IDX = 9;
+    private static final int REDIRECT_COUNT_IDX = 10;
+    private static final int INDEX_IDX = 11;
+
+    /**
+     * This function will add a new file to the database if it does not exist.
+     * 
+     * @param di DownloadInfo that we wish to store
+     * @return the row id of the record to be updated/inserted, or -1
+     */
+    public boolean updateDownload(DownloadInfo di) {
+        ContentValues cv = new ContentValues();
+        cv.put(DownloadColumns.INDEX, di.mIndex);
+        cv.put(DownloadColumns.FILENAME, di.mFileName);
+        cv.put(DownloadColumns.URI, di.mUri);
+        cv.put(DownloadColumns.ETAG, di.mETag);
+        cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
+        cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
+        cv.put(DownloadColumns.LASTMOD, di.mLastMod);
+        cv.put(DownloadColumns.STATUS, di.mStatus);
+        cv.put(DownloadColumns.CONTROL, di.mControl);
+        cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
+        cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
+        cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
+        return updateDownload(di, cv);
+    }
+
+    public boolean updateDownload(DownloadInfo di, ContentValues cv) {
+        long id = di == null ? -1 : getIDForDownloadInfo(di);
+        try {
+            final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
+            if (id != -1) {
+                if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
+                        cv, DownloadColumns._ID + " = " + id, null)) {
+                    return false;
+                }
+            } else {
+                return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
+                        DownloadColumns.URI, cv);
+            }
+        } catch (android.database.sqlite.SQLiteException ex) {
+            ex.printStackTrace();
+        }
+        return false;
+    }
+
+    public int getLastCheckedVersionCode() {
+        return mVersionCode;
+    }
+
+    public boolean isDownloadRequired() {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM "
+                + DownloadColumns.TABLE_NAME + " WHERE "
+                + DownloadColumns.STATUS + " <> 0", null);
+        try {
+            if (null != cur && cur.moveToFirst()) {
+                return 0 == cur.getInt(0);
+            }
+        } finally {
+            if (null != cur)
+                cur.close();
+        }
+        return true;
+    }
+
+    public int getFlags() {
+        return mFlags;
+    }
+
+    public boolean updateFlags(int flags) {
+        if (mFlags != flags) {
+            ContentValues cv = new ContentValues();
+            cv.put(MetadataColumns.FLAGS, flags);
+            if (updateMetadata(cv)) {
+                mFlags = flags;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return true;
+        }
+    };
+
+    public boolean updateStatus(int status) {
+        if (mStatus != status) {
+            ContentValues cv = new ContentValues();
+            cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
+            if (updateMetadata(cv)) {
+                mStatus = status;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return true;
+        }
+    };
+
+    public boolean updateMetadata(ContentValues cv) {
+        final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
+        if (-1 == this.mMetadataRowID) {
+            long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
+                    MetadataColumns.APKVERSION, cv);
+            if (-1 == newID)
+                return false;
+            mMetadataRowID = newID;
+        } else {
+            if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
+                    BaseColumns._ID + " = " + mMetadataRowID, null))
+                return false;
+        }
+        return true;
+    }
+
+    public boolean updateMetadata(int apkVersion, int downloadStatus) {
+        ContentValues cv = new ContentValues();
+        cv.put(MetadataColumns.APKVERSION, apkVersion);
+        cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
+        if (updateMetadata(cv)) {
+            mVersionCode = apkVersion;
+            mStatus = downloadStatus;
+            return true;
+        } else {
+            return false;
+        }
+    };
+
+    public boolean updateFromDb(DownloadInfo di) {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = null;
+        try {
+            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
+                    DownloadColumns.FILENAME + "= ?",
+                    new String[] {
+                        di.mFileName
+                    }, null, null, null);
+            if (null != cur && cur.moveToFirst()) {
+                setDownloadInfoFromCursor(di, cur);
+                return true;
+            }
+            return false;
+        } finally {
+            if (null != cur) {
+                cur.close();
+            }
+        }
+    }
+
+    public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
+        di.mUri = cur.getString(URI_IDX);
+        di.mETag = cur.getString(ETAG_IDX);
+        di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
+        di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
+        di.mLastMod = cur.getLong(LASTMOD_IDX);
+        di.mStatus = cur.getInt(STATUS_IDX);
+        di.mControl = cur.getInt(CONTROL_IDX);
+        di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
+        di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
+        di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
+    }
+
+    public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
+        DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
+                cur.getString(FILENAME_IDX), this.getClass().getPackage()
+                        .getName());
+        setDownloadInfoFromCursor(di, cur);
+        return di;
+    }
+
+    public DownloadInfo[] getDownloads() {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = null;
+        try {
+            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
+                    null, null, null, null);
+            if (null != cur && cur.moveToFirst()) {
+                DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
+                int idx = 0;
+                do {
+                    DownloadInfo di = getDownloadInfoFromCursor(cur);
+                    retInfos[idx++] = di;
+                } while (cur.moveToNext());
+                return retInfos;
+            }
+            return null;
+        } finally {
+            if (null != cur) {
+                cur.close();
+            }
+        }
+    }
+
+}

+ 200 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import android.text.format.Time;
+
+import java.util.Calendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper for parsing an HTTP date.
+ */
+public final class HttpDateTime {
+
+    /*
+     * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
+     * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850,
+     * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format
+     * with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon
+     * YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS
+     * GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon
+     * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first
+     * digit is zero. Mon can be the full name of the month.
+     */
+    private static final String HTTP_DATE_RFC_REGEXP =
+            "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
+                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
+
+    private static final String HTTP_DATE_ANSIC_REGEXP =
+            "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
+                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
+
+    /**
+     * The compiled version of the HTTP-date regular expressions.
+     */
+    private static final Pattern HTTP_DATE_RFC_PATTERN =
+            Pattern.compile(HTTP_DATE_RFC_REGEXP);
+    private static final Pattern HTTP_DATE_ANSIC_PATTERN =
+            Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
+
+    private static class TimeOfDay {
+        TimeOfDay(int h, int m, int s) {
+            this.hour = h;
+            this.minute = m;
+            this.second = s;
+        }
+
+        int hour;
+        int minute;
+        int second;
+    }
+
+    public static long parse(String timeString)
+            throws IllegalArgumentException {
+
+        int date = 1;
+        int month = Calendar.JANUARY;
+        int year = 1970;
+        TimeOfDay timeOfDay;
+
+        Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
+        if (rfcMatcher.find()) {
+            date = getDate(rfcMatcher.group(1));
+            month = getMonth(rfcMatcher.group(2));
+            year = getYear(rfcMatcher.group(3));
+            timeOfDay = getTime(rfcMatcher.group(4));
+        } else {
+            Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
+            if (ansicMatcher.find()) {
+                month = getMonth(ansicMatcher.group(1));
+                date = getDate(ansicMatcher.group(2));
+                timeOfDay = getTime(ansicMatcher.group(3));
+                year = getYear(ansicMatcher.group(4));
+            } else {
+                throw new IllegalArgumentException();
+            }
+        }
+
+        // FIXME: Y2038 BUG!
+        if (year >= 2038) {
+            year = 2038;
+            month = Calendar.JANUARY;
+            date = 1;
+        }
+
+        Time time = new Time(Time.TIMEZONE_UTC);
+        time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
+                month, year);
+        return time.toMillis(false /* use isDst */);
+    }
+
+    private static int getDate(String dateString) {
+        if (dateString.length() == 2) {
+            return (dateString.charAt(0) - '0') * 10
+                    + (dateString.charAt(1) - '0');
+        } else {
+            return (dateString.charAt(0) - '0');
+        }
+    }
+
+    /*
+     * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0
+     * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20
+     * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19
+     * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
+     */
+    private static int getMonth(String monthString) {
+        int hash = Character.toLowerCase(monthString.charAt(0)) +
+                Character.toLowerCase(monthString.charAt(1)) +
+                Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
+        switch (hash) {
+            case 22:
+                return Calendar.JANUARY;
+            case 10:
+                return Calendar.FEBRUARY;
+            case 29:
+                return Calendar.MARCH;
+            case 32:
+                return Calendar.APRIL;
+            case 36:
+                return Calendar.MAY;
+            case 42:
+                return Calendar.JUNE;
+            case 40:
+                return Calendar.JULY;
+            case 26:
+                return Calendar.AUGUST;
+            case 37:
+                return Calendar.SEPTEMBER;
+            case 35:
+                return Calendar.OCTOBER;
+            case 48:
+                return Calendar.NOVEMBER;
+            case 9:
+                return Calendar.DECEMBER;
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private static int getYear(String yearString) {
+        if (yearString.length() == 2) {
+            int year = (yearString.charAt(0) - '0') * 10
+                    + (yearString.charAt(1) - '0');
+            if (year >= 70) {
+                return year + 1900;
+            } else {
+                return year + 2000;
+            }
+        } else if (yearString.length() == 3) {
+            // According to RFC 2822, three digit years should be added to 1900.
+            int year = (yearString.charAt(0) - '0') * 100
+                    + (yearString.charAt(1) - '0') * 10
+                    + (yearString.charAt(2) - '0');
+            return year + 1900;
+        } else if (yearString.length() == 4) {
+            return (yearString.charAt(0) - '0') * 1000
+                    + (yearString.charAt(1) - '0') * 100
+                    + (yearString.charAt(2) - '0') * 10
+                    + (yearString.charAt(3) - '0');
+        } else {
+            return 1970;
+        }
+    }
+
+    private static TimeOfDay getTime(String timeString) {
+        // HH might be H
+        int i = 0;
+        int hour = timeString.charAt(i++) - '0';
+        if (timeString.charAt(i) != ':')
+            hour = hour * 10 + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+
+        int minute = (timeString.charAt(i++) - '0') * 10
+                + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+
+        int second = (timeString.charAt(i++) - '0') * 10
+                + (timeString.charAt(i++) - '0');
+
+        return new TimeOfDay(hour, minute, second);
+    }
+}

+ 101 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.android.vending.expansion.downloader.R;
+import com.google.android.vending.expansion.downloader.Helpers;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+
+public class V14CustomNotification implements DownloadNotification.ICustomNotification {
+
+    CharSequence mTitle;
+    CharSequence mTicker;
+    int mIcon;
+    long mTotalKB = -1;
+    long mCurrentKB = -1;
+    long mTimeRemaining;
+    PendingIntent mPendingIntent;
+
+    @Override
+    public void setIcon(int icon) {
+        mIcon = icon;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+    }
+
+    @Override
+    public void setTotalBytes(long totalBytes) {
+        mTotalKB = totalBytes;
+    }
+
+    @Override
+    public void setCurrentBytes(long currentBytes) {
+        mCurrentKB = currentBytes;
+    }
+
+    void setProgress(Notification.Builder builder) {
+
+    }
+
+    @Override
+    public Notification updateNotification(Context c) {
+        Notification.Builder builder = new Notification.Builder(c);
+        builder.setContentTitle(mTitle);
+        if (mTotalKB > 0 && -1 != mCurrentKB) {
+            builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false);
+        } else {
+            builder.setProgress(0, 0, true);
+        }
+        builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB));
+        builder.setContentInfo(c.getString(R.string.time_remaining_notification,
+                Helpers.getTimeRemaining(mTimeRemaining)));
+        if (mIcon != 0) {
+            builder.setSmallIcon(mIcon);
+        } else {
+            int iconResource = android.R.drawable.stat_sys_download;
+            builder.setSmallIcon(iconResource);
+        }
+        builder.setOngoing(true);
+        builder.setTicker(mTicker);
+        builder.setContentIntent(mPendingIntent);
+        builder.setOnlyAlertOnce(true);
+
+        return builder.getNotification();
+    }
+
+    @Override
+    public void setPendingIntent(PendingIntent contentIntent) {
+        mPendingIntent = contentIntent;
+    }
+
+    @Override
+    public void setTicker(CharSequence ticker) {
+        mTicker = ticker;
+    }
+
+    @Override
+    public void setTimeRemaining(long timeRemaining) {
+        mTimeRemaining = timeRemaining;
+    }
+
+}

+ 116 - 0
platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/V3CustomNotification.java

@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.expansion.downloader.impl;
+
+import com.android.vending.expansion.downloader.R;
+import com.google.android.vending.expansion.downloader.Helpers;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.view.View;
+import android.widget.RemoteViews;
+
+public class V3CustomNotification implements DownloadNotification.ICustomNotification {
+
+    CharSequence mTitle;
+    CharSequence mTicker;
+    int mIcon;
+    long mTotalBytes = -1;
+    long mCurrentBytes = -1;
+    long mTimeRemaining;
+    PendingIntent mPendingIntent;
+    Notification mNotification = new Notification();
+
+    @Override
+    public void setIcon(int icon) {
+        mIcon = icon;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+    }
+
+    @Override
+    public void setTotalBytes(long totalBytes) {
+        mTotalBytes = totalBytes;
+    }
+
+    @Override
+    public void setCurrentBytes(long currentBytes) {
+        mCurrentBytes = currentBytes;
+    }
+
+    @Override
+    public Notification updateNotification(Context c) {
+        Notification n = mNotification;
+
+        n.icon = mIcon;
+
+        n.flags |= Notification.FLAG_ONGOING_EVENT;
+
+        if (android.os.Build.VERSION.SDK_INT > 10) {
+            n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; // only matters for
+                                                          // Honeycomb
+        }
+
+        // Build the RemoteView object
+        RemoteViews expandedView = new RemoteViews(
+                c.getPackageName(),
+                R.layout.status_bar_ongoing_event_progress_bar);
+
+        expandedView.setTextViewText(R.id.title, mTitle);
+        // look at strings
+        expandedView.setViewVisibility(R.id.description, View.VISIBLE);
+        expandedView.setTextViewText(R.id.description,
+                Helpers.getDownloadProgressString(mCurrentBytes, mTotalBytes));
+        expandedView.setViewVisibility(R.id.progress_bar_frame, View.VISIBLE);
+        expandedView.setProgressBar(R.id.progress_bar,
+                (int) (mTotalBytes >> 8),
+                (int) (mCurrentBytes >> 8),
+                mTotalBytes <= 0);
+        expandedView.setViewVisibility(R.id.time_remaining, View.VISIBLE);
+        expandedView.setTextViewText(
+                R.id.time_remaining,
+                c.getString(R.string.time_remaining_notification,
+                        Helpers.getTimeRemaining(mTimeRemaining)));
+        expandedView.setTextViewText(R.id.progress_text,
+                Helpers.getDownloadProgressPercent(mCurrentBytes, mTotalBytes));
+        expandedView.setImageViewResource(R.id.appIcon, mIcon);
+        n.contentView = expandedView;
+        n.contentIntent = mPendingIntent;
+        return n;
+    }
+
+    @Override
+    public void setPendingIntent(PendingIntent contentIntent) {
+        mPendingIntent = contentIntent;
+    }
+
+    @Override
+    public void setTicker(CharSequence ticker) {
+        mTicker = ticker;
+    }
+
+    @Override
+    public void setTimeRemaining(long timeRemaining) {
+        mTimeRemaining = timeRemaining;
+    }
+
+}

+ 24 - 0
platform/android/libs/play_licensing/AndroidManifest.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.vending.licensing"
+    android:versionCode="2"
+    android:versionName="1.5">
+    <!-- Devices >= 3 have version of Android Market that supports licensing. -->
+    <uses-sdk android:minSdkVersion="3" android:targetSdkVersion="15" />
+    <!-- Required permission to check licensing. -->
+    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />
+</manifest>

+ 23 - 0
platform/android/libs/play_licensing/aidl/ILicenseResultListener.aidl

@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.licensing;
+
+// Android library projects do not yet support AIDL, so this has been
+// precompiled into the src directory.
+oneway interface ILicenseResultListener {
+  void verifyLicense(int responseCode, String signedData, String signature);
+}

+ 25 - 0
platform/android/libs/play_licensing/aidl/ILicensingService.aidl

@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.licensing;
+
+import com.android.vending.licensing.ILicenseResultListener;
+
+// Android library projects do not yet support AIDL, so this has been
+// precompiled into the src directory.
+oneway interface ILicensingService {
+  void checkLicense(long nonce, String packageName, in ILicenseResultListener listener);
+}

+ 92 - 0
platform/android/libs/play_licensing/build.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="play_licensing" default="help">
+
+    <!-- The local.properties file is created and updated by the 'android' tool.
+         It contains the path to the SDK. It should *NOT* be checked into
+         Version Control Systems. -->
+    <property file="local.properties" />
+
+    <!-- The ant.properties file can be created by you. It is only edited by the
+         'android' tool to add properties to it.
+         This is the place to change some Ant specific build properties.
+         Here are some properties you may want to change/update:
+
+         source.dir
+             The name of the source directory. Default is 'src'.
+         out.dir
+             The name of the output directory. Default is 'bin'.
+
+         For other overridable properties, look at the beginning of the rules
+         files in the SDK, at tools/ant/build.xml
+
+         Properties related to the SDK location or the project target should
+         be updated using the 'android' tool with the 'update' action.
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems.
+
+         -->
+    <property file="ant.properties" />
+
+    <!-- if sdk.dir was not set from one of the property file, then
+         get it from the ANDROID_HOME env var.
+         This must be done before we load project.properties since
+         the proguard config can use sdk.dir -->
+    <property environment="env" />
+    <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+        <isset property="env.ANDROID_HOME" />
+    </condition>
+
+    <!-- The project.properties file is created and updated by the 'android'
+         tool, as well as ADT.
+
+         This contains project specific properties such as project target, and library
+         dependencies. Lower level build properties are stored in ant.properties
+         (or in .classpath for Eclipse projects).
+
+         This file is an integral part of the build system for your
+         application and should be checked into Version Control Systems. -->
+    <loadproperties srcFile="project.properties" />
+
+    <!-- quick check on sdk.dir -->
+    <fail
+            message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+            unless="sdk.dir"
+    />
+
+    <!--
+        Import per project custom build rules if present at the root of the project.
+        This is the place to put custom intermediary targets such as:
+            -pre-build
+            -pre-compile
+            -post-compile (This is typically used for code obfuscation.
+                           Compiled code location: ${out.classes.absolute.dir}
+                           If this is not done in place, override ${out.dex.input.absolute.dir})
+            -post-package
+            -post-build
+            -pre-clean
+    -->
+    <import file="custom_rules.xml" optional="true" />
+
+    <!-- Import the actual build file.
+
+         To customize existing targets, there are two options:
+         - Customize only one target:
+             - copy/paste the target into this file, *before* the
+               <import> task.
+             - customize it to your needs.
+         - Customize the whole content of build.xml
+             - copy/paste the content of the rules files (minus the top node)
+               into this file, replacing the <import> task.
+             - customize to your needs.
+
+         ***********************
+         ****** IMPORTANT ******
+         ***********************
+         In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+         in order to avoid having your file be overridden by tools such as "android update project"
+    -->
+    <!-- version-tag: 1 -->
+    <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>

+ 20 - 0
platform/android/libs/play_licensing/proguard-project.txt

@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 12 - 0
platform/android/libs/play_licensing/project.properties

@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+android.library=true
+# Project target.
+target=android-15

+ 110 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/AESObfuscator.java

@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.spec.KeySpec;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An Obfuscator that uses AES to encrypt data.
+ */
+public class AESObfuscator implements Obfuscator {
+    private static final String UTF8 = "UTF-8";
+    private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
+    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final byte[] IV =
+        { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
+    private static final String header = "com.android.vending.licensing.AESObfuscator-1|";
+
+    private Cipher mEncryptor;
+    private Cipher mDecryptor;
+
+    /**
+     * @param salt an array of random bytes to use for each (un)obfuscation
+     * @param applicationId application identifier, e.g. the package name
+     * @param deviceId device identifier. Use as many sources as possible to
+     *    create this unique identifier.
+     */
+    public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
+        try {
+            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
+            KeySpec keySpec =   
+                new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
+            SecretKey tmp = factory.generateSecret(keySpec);
+            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+            mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
+            mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
+        } catch (GeneralSecurityException e) {
+            // This can't happen on a compatible Android device.
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+
+    public String obfuscate(String original, String key) {
+        if (original == null) {
+            return null;
+        }
+        try {
+            // Header is appended as an integrity check
+            return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+
+    public String unobfuscate(String obfuscated, String key) throws ValidationException {
+        if (obfuscated == null) {
+            return null;
+        }
+        try {
+            String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
+            // Check for presence of header. This serves as a final integrity check, for cases
+            // where the block size is correct during decryption.
+            int headerIndex = result.indexOf(header+key);
+            if (headerIndex != 0) {
+                throw new ValidationException("Header not found (invalid data or key)" + ":" +
+                        obfuscated);
+            }
+            return result.substring(header.length()+key.length(), result.length());
+        } catch (Base64DecoderException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (IllegalBlockSizeException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (BadPaddingException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+}

+ 397 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/APKExpansionPolicy.java

@@ -0,0 +1,397 @@
+
+package com.google.android.vending.licensing;
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * Default policy. All policy decisions are based off of response data received
+ * from the licensing service. Specifically, the licensing server sends the
+ * following information: response validity period, error retry period, and
+ * error retry count.
+ * <p>
+ * These values will vary based on the the way the application is configured in
+ * the Android Market publishing console, such as whether the application is
+ * marked as free or is within its refund period, as well as how often an
+ * application is checking with the licensing service.
+ * <p>
+ * Developers who need more fine grained control over their application's
+ * licensing policy should implement a custom Policy.
+ */
+public class APKExpansionPolicy implements Policy {
+
+    private static final String TAG = "APKExpansionPolicy";
+    private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy";
+    private static final String PREF_LAST_RESPONSE = "lastResponse";
+    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+    private static final String PREF_RETRY_UNTIL = "retryUntil";
+    private static final String PREF_MAX_RETRIES = "maxRetries";
+    private static final String PREF_RETRY_COUNT = "retryCount";
+    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+    private static final String DEFAULT_RETRY_UNTIL = "0";
+    private static final String DEFAULT_MAX_RETRIES = "0";
+    private static final String DEFAULT_RETRY_COUNT = "0";
+
+    private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+    private long mValidityTimestamp;
+    private long mRetryUntil;
+    private long mMaxRetries;
+    private long mRetryCount;
+    private long mLastResponseTime = 0;
+    private int mLastResponse;
+    private PreferenceObfuscator mPreferences;
+    private Vector<String> mExpansionURLs = new Vector<String>();
+    private Vector<String> mExpansionFileNames = new Vector<String>();
+    private Vector<Long> mExpansionFileSizes = new Vector<Long>();
+
+    /**
+     * The design of the protocol supports n files. Currently the market can
+     * only deliver two files. To accommodate this, we have these two constants,
+     * but the order is the only relevant thing here.
+     */
+    public static final int MAIN_FILE_URL_INDEX = 0;
+    public static final int PATCH_FILE_URL_INDEX = 1;
+
+    /**
+     * @param context The context for the current application
+     * @param obfuscator An obfuscator to be used with preferences.
+     */
+    public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
+        // Import old values
+        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+        mPreferences = new PreferenceObfuscator(sp, obfuscator);
+        mLastResponse = Integer.parseInt(
+                mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+                DEFAULT_VALIDITY_TIMESTAMP));
+        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+    }
+
+    /**
+     * We call this to guarantee that we fetch a fresh policy from the server.
+     * This is to be used if the URL is invalid.
+     */
+    public void resetPolicy() {
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
+        setRetryUntil(DEFAULT_RETRY_UNTIL);
+        setMaxRetries(DEFAULT_MAX_RETRIES);
+        setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
+        setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+        mPreferences.commit();
+    }
+
+    /**
+     * Process a new response from the license server.
+     * <p>
+     * This data will be used for computing future policy decisions. The
+     * following parameters are processed:
+     * <ul>
+     * <li>VT: the timestamp that the client should consider the response valid
+     * until
+     * <li>GT: the timestamp that the client should ignore retry errors until
+     * <li>GR: the number of retry errors that the client should ignore
+     * </ul>
+     * 
+     * @param response the result from validating the server response
+     * @param rawData the raw server response data
+     */
+    public void processServerResponse(int response,
+            com.google.android.vending.licensing.ResponseData rawData) {
+
+        // Update retry counter
+        if (response != Policy.RETRY) {
+            setRetryCount(0);
+        } else {
+            setRetryCount(mRetryCount + 1);
+        }
+
+        if (response == Policy.LICENSED) {
+            // Update server policy data
+            Map<String, String> extras = decodeExtras(rawData.extra);
+            mLastResponse = response;
+            setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
+            Set<String> keys = extras.keySet();
+            for (String key : keys) {
+                if (key.equals("VT")) {
+                    setValidityTimestamp(extras.get(key));
+                } else if (key.equals("GT")) {
+                    setRetryUntil(extras.get(key));
+                } else if (key.equals("GR")) {
+                    setMaxRetries(extras.get(key));
+                } else if (key.startsWith("FILE_URL")) {
+                    int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
+                    setExpansionURL(index, extras.get(key));
+                } else if (key.startsWith("FILE_NAME")) {
+                    int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
+                    setExpansionFileName(index, extras.get(key));
+                } else if (key.startsWith("FILE_SIZE")) {
+                    int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
+                    setExpansionFileSize(index, Long.parseLong(extras.get(key)));
+                }
+            }
+        } else if (response == Policy.NOT_LICENSED) {
+            // Clear out stale policy data
+            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+            setRetryUntil(DEFAULT_RETRY_UNTIL);
+            setMaxRetries(DEFAULT_MAX_RETRIES);
+        }
+
+        setLastResponse(response);
+        mPreferences.commit();
+    }
+
+    /**
+     * Set the last license response received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param l the response
+     */
+    private void setLastResponse(int l) {
+        mLastResponseTime = System.currentTimeMillis();
+        mLastResponse = l;
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
+    }
+
+    /**
+     * Set the current retry count and add to preferences. You must manually
+     * call PreferenceObfuscator.commit() to commit these changes to disk.
+     * 
+     * @param c the new retry count
+     */
+    private void setRetryCount(long c) {
+        mRetryCount = c;
+        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
+    }
+
+    public long getRetryCount() {
+        return mRetryCount;
+    }
+
+    /**
+     * Set the last validity timestamp (VT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param validityTimestamp the VT string received
+     */
+    private void setValidityTimestamp(String validityTimestamp) {
+        Long lValidityTimestamp;
+        try {
+            lValidityTimestamp = Long.parseLong(validityTimestamp);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire in one minute.
+            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
+            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
+            validityTimestamp = Long.toString(lValidityTimestamp);
+        }
+
+        mValidityTimestamp = lValidityTimestamp;
+        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
+    }
+
+    public long getValidityTimestamp() {
+        return mValidityTimestamp;
+    }
+
+    /**
+     * Set the retry until timestamp (GT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param retryUntil the GT string received
+     */
+    private void setRetryUntil(String retryUntil) {
+        Long lRetryUntil;
+        try {
+            lRetryUntil = Long.parseLong(retryUntil);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
+            retryUntil = "0";
+            lRetryUntil = 0l;
+        }
+
+        mRetryUntil = lRetryUntil;
+        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
+    }
+
+    public long getRetryUntil() {
+        return mRetryUntil;
+    }
+
+    /**
+     * Set the max retries value (GR) as received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param maxRetries the GR string received
+     */
+    private void setMaxRetries(String maxRetries) {
+        Long lMaxRetries;
+        try {
+            lMaxRetries = Long.parseLong(maxRetries);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
+            maxRetries = "0";
+            lMaxRetries = 0l;
+        }
+
+        mMaxRetries = lMaxRetries;
+        mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
+    }
+
+    public long getMaxRetries() {
+        return mMaxRetries;
+    }
+
+    /**
+     * Gets the count of expansion URLs. Since expansionURLs are not committed
+     * to preferences, this will return zero if there has been no LVL fetch
+     * in the current session.
+     * 
+     * @return the number of expansion URLs. (0,1,2)
+     */
+    public int getExpansionURLCount() {
+        return mExpansionURLs.size();
+    }
+
+    /**
+     * Gets the expansion URL. Since these URLs are not committed to
+     * preferences, this will always return null if there has not been an LVL
+     * fetch in the current session.
+     * 
+     * @param index the index of the URL to fetch. This value will be either
+     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
+     * @param URL the URL to set
+     */
+    public String getExpansionURL(int index) {
+        if (index < mExpansionURLs.size()) {
+            return mExpansionURLs.elementAt(index);
+        }
+        return null;
+    }
+
+    /**
+     * Sets the expansion URL. Expansion URL's are not committed to preferences,
+     * but are instead intended to be stored when the license response is
+     * processed by the front-end.
+     * 
+     * @param index the index of the expansion URL. This value will be either
+     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
+     * @param URL the URL to set
+     */
+    public void setExpansionURL(int index, String URL) {
+        if (index >= mExpansionURLs.size()) {
+            mExpansionURLs.setSize(index + 1);
+        }
+        mExpansionURLs.set(index, URL);
+    }
+
+    public String getExpansionFileName(int index) {
+        if (index < mExpansionFileNames.size()) {
+            return mExpansionFileNames.elementAt(index);
+        }
+        return null;
+    }
+
+    public void setExpansionFileName(int index, String name) {
+        if (index >= mExpansionFileNames.size()) {
+            mExpansionFileNames.setSize(index + 1);
+        }
+        mExpansionFileNames.set(index, name);
+    }
+
+    public long getExpansionFileSize(int index) {
+        if (index < mExpansionFileSizes.size()) {
+            return mExpansionFileSizes.elementAt(index);
+        }
+        return -1;
+    }
+
+    public void setExpansionFileSize(int index, long size) {
+        if (index >= mExpansionFileSizes.size()) {
+            mExpansionFileSizes.setSize(index + 1);
+        }
+        mExpansionFileSizes.set(index, size);
+    }
+
+    /**
+     * {@inheritDoc} This implementation allows access if either:<br>
+     * <ol>
+     * <li>a LICENSED response was received within the validity period
+     * <li>a RETRY response was received in the last minute, and we are under
+     * the RETRY count or in the RETRY period.
+     * </ol>
+     */
+    public boolean allowAccess() {
+        long ts = System.currentTimeMillis();
+        if (mLastResponse == Policy.LICENSED) {
+            // Check if the LICENSED response occurred within the validity
+            // timeout.
+            if (ts <= mValidityTimestamp) {
+                // Cached LICENSED response is still valid.
+                return true;
+            }
+        } else if (mLastResponse == Policy.RETRY &&
+                ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+            // Only allow access if we are within the retry period or we haven't
+            // used up our
+            // max retries.
+            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+        }
+        return false;
+    }
+
+    private Map<String, String> decodeExtras(String extras) {
+        Map<String, String> results = new HashMap<String, String>();
+        try {
+            URI rawExtras = new URI("?" + extras);
+            List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8");
+            for (NameValuePair item : extraList) {
+                String name = item.getName();
+                int i = 0;
+                while (results.containsKey(name)) {
+                    name = item.getName() + ++i;
+                }
+                results.put(name, item.getValue());
+            }
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
+
+}

+ 47 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/DeviceLimiter.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Allows the developer to limit the number of devices using a single license.
+ * <p>
+ * The LICENSED response from the server contains a user identifier unique to
+ * the &lt;application, user&gt; pair. The developer can send this identifier
+ * to their own server along with some device identifier (a random number
+ * generated and stored once per application installation,
+ * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
+ * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
+ * The more sources used to identify the device, the harder it will be for an
+ * attacker to spoof.
+ * <p>
+ * The server can look at the &lt;application, user, device id&gt; tuple and
+ * restrict a user's application license to run on at most 10 different devices
+ * in a week (for example). We recommend not being too restrictive because a
+ * user might legitimately have multiple devices or be in the process of
+ * changing phones. This will catch egregious violations of multiple people
+ * sharing one license.
+ */
+public interface DeviceLimiter {
+
+    /**
+     * Checks if this device is allowed to use the given user's license.
+     *
+     * @param userId the user whose license the server responded with
+     * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
+     */
+    int isDeviceAllowed(String userId);
+}

+ 99 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java

@@ -0,0 +1,99 @@
+/*
+ * This file is auto-generated.  DO NOT MODIFY.
+ * Original file: aidl/ILicenseResultListener.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicenseResultListener extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicenseResultListener interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
+return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
+}
+return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_verifyLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+int _arg0;
+_arg0 = data.readInt();
+java.lang.String _arg1;
+_arg1 = data.readString();
+java.lang.String _arg2;
+_arg2 = data.readString();
+this.verifyLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeInt(responseCode);
+_data.writeString(signedData);
+_data.writeString(signature);
+mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
+}

+ 99 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java

@@ -0,0 +1,99 @@
+/*
+ * This file is auto-generated.  DO NOT MODIFY.
+ * Original file: aidl/ILicensingService.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicensingService extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicensingService interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) {
+return ((com.google.android.vending.licensing.ILicensingService)iin);
+}
+return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_checkLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+long _arg0;
+_arg0 = data.readLong();
+java.lang.String _arg1;
+_arg1 = data.readString();
+com.google.android.vending.licensing.ILicenseResultListener _arg2;
+_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
+this.checkLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicensingService
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeLong(nonce);
+_data.writeString(packageName);
+_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
+mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
+}

+ 351 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java

@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Settings.Secure;
+import android.util.Log;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Client library for Android Market license verifications.
+ * <p>
+ * The LicenseChecker is configured via a {@link Policy} which contains the
+ * logic to determine whether a user should have access to the application. For
+ * example, the Policy can define a threshold for allowable number of server or
+ * client failures before the library reports the user as not having access.
+ * <p>
+ * Must also provide the Base64-encoded RSA public key associated with your
+ * developer account. The public key is obtainable from the publisher site.
+ */
+public class LicenseChecker implements ServiceConnection {
+    private static final String TAG = "LicenseChecker";
+
+    private static final String KEY_FACTORY_ALGORITHM = "RSA";
+
+    // Timeout value (in milliseconds) for calls to service.
+    private static final int TIMEOUT_MS = 10 * 1000;
+
+    private static final SecureRandom RANDOM = new SecureRandom();
+    private static final boolean DEBUG_LICENSE_ERROR = false;
+
+    private ILicensingService mService;
+
+    private PublicKey mPublicKey;
+    private final Context mContext;
+    private final Policy mPolicy;
+    /**
+     * A handler for running tasks on a background thread. We don't want license
+     * processing to block the UI thread.
+     */
+    private Handler mHandler;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
+    private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
+
+    /**
+     * @param context a Context
+     * @param policy implementation of Policy
+     * @param encodedPublicKey Base64-encoded RSA public key
+     * @throws IllegalArgumentException if encodedPublicKey is invalid
+     */
+    public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
+        mContext = context;
+        mPolicy = policy;
+        mPublicKey = generatePublicKey(encodedPublicKey);
+        mPackageName = mContext.getPackageName();
+        mVersionCode = getVersionCode(context, mPackageName);
+        HandlerThread handlerThread = new HandlerThread("background thread");
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+    }
+
+    /**
+     * Generates a PublicKey instance from a string containing the
+     * Base64-encoded public key.
+     * 
+     * @param encodedPublicKey Base64-encoded public key
+     * @throws IllegalArgumentException if encodedPublicKey is invalid
+     */
+    private static PublicKey generatePublicKey(String encodedPublicKey) {
+        try {
+            byte[] decodedKey = Base64.decode(encodedPublicKey);
+            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+
+            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+        } catch (NoSuchAlgorithmException e) {
+            // This won't happen in an Android-compatible environment.
+            throw new RuntimeException(e);
+        } catch (Base64DecoderException e) {
+            Log.e(TAG, "Could not decode from Base64.");
+            throw new IllegalArgumentException(e);
+        } catch (InvalidKeySpecException e) {
+            Log.e(TAG, "Invalid key specification.");
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Checks if the user should have access to the app.  Binds the service if necessary.
+     * <p>
+     * NOTE: This call uses a trivially obfuscated string (base64-encoded).  For best security,
+     * we recommend obfuscating the string that is passed into bindService using another method
+     * of your own devising.
+     * <p>
+     * source string: "com.android.vending.licensing.ILicensingService"
+     * <p>
+     * @param callback
+     */
+    public synchronized void checkAccess(LicenseCheckerCallback callback) {
+        // If we have a valid recent LICENSED response, we can skip asking
+        // Market.
+        if (mPolicy.allowAccess()) {
+            Log.i(TAG, "Using cached license response");
+            callback.allow(Policy.LICENSED);
+        } else {
+            LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
+                    callback, generateNonce(), mPackageName, mVersionCode);
+
+            if (mService == null) {
+                Log.i(TAG, "Binding to licensing service.");
+                try {
+                    boolean bindResult = mContext
+                            .bindService(
+                                    new Intent(
+                                            new String(
+                                                    Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
+                                    this, // ServiceConnection.
+                                    Context.BIND_AUTO_CREATE);
+
+                    if (bindResult) {
+                        mPendingChecks.offer(validator);
+                    } else {
+                        Log.e(TAG, "Could not bind to service.");
+                        handleServiceConnectionError(validator);
+                    }
+                } catch (SecurityException e) {
+                    callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
+                } catch (Base64DecoderException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                mPendingChecks.offer(validator);
+                runChecks();
+            }
+        }
+    }
+
+    private void runChecks() {
+        LicenseValidator validator;
+        while ((validator = mPendingChecks.poll()) != null) {
+            try {
+                Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
+                mService.checkLicense(
+                        validator.getNonce(), validator.getPackageName(),
+                        new ResultListener(validator));
+                mChecksInProgress.add(validator);
+            } catch (RemoteException e) {
+                Log.w(TAG, "RemoteException in checkLicense call.", e);
+                handleServiceConnectionError(validator);
+            }
+        }
+    }
+
+    private synchronized void finishCheck(LicenseValidator validator) {
+        mChecksInProgress.remove(validator);
+        if (mChecksInProgress.isEmpty()) {
+            cleanupService();
+        }
+    }
+
+    private class ResultListener extends ILicenseResultListener.Stub {
+        private final LicenseValidator mValidator;
+        private Runnable mOnTimeout;
+
+        public ResultListener(LicenseValidator validator) {
+            mValidator = validator;
+            mOnTimeout = new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Check timed out.");
+                    handleServiceConnectionError(mValidator);
+                    finishCheck(mValidator);
+                }
+            };
+            startTimeout();
+        }
+
+        private static final int ERROR_CONTACTING_SERVER = 0x101;
+        private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+        private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+        // Runs in IPC thread pool. Post it to the Handler, so we can guarantee
+        // either this or the timeout runs.
+        public void verifyLicense(final int responseCode, final String signedData,
+                final String signature) {
+            mHandler.post(new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Received response.");
+                    // Make sure it hasn't already timed out.
+                    if (mChecksInProgress.contains(mValidator)) {
+                        clearTimeout();
+                        mValidator.verify(mPublicKey, responseCode, signedData, signature);
+                        finishCheck(mValidator);
+                    }
+                    if (DEBUG_LICENSE_ERROR) {
+                        boolean logResponse;
+                        String stringError = null;
+                        switch (responseCode) {
+                            case ERROR_CONTACTING_SERVER:
+                                logResponse = true;
+                                stringError = "ERROR_CONTACTING_SERVER";
+                                break;
+                            case ERROR_INVALID_PACKAGE_NAME:
+                                logResponse = true;
+                                stringError = "ERROR_INVALID_PACKAGE_NAME";
+                                break;
+                            case ERROR_NON_MATCHING_UID:
+                                logResponse = true;
+                                stringError = "ERROR_NON_MATCHING_UID";
+                                break;
+                            default:
+                                logResponse = false;
+                        }
+
+                        if (logResponse) {
+                            String android_id = Secure.getString(mContext.getContentResolver(),
+                                    Secure.ANDROID_ID);
+                            Date date = new Date();
+                            Log.d(TAG, "Server Failure: " + stringError);
+                            Log.d(TAG, "Android ID: " + android_id);
+                            Log.d(TAG, "Time: " + date.toGMTString());
+                        }
+                    }
+
+                }
+            });
+        }
+
+        private void startTimeout() {
+            Log.i(TAG, "Start monitoring timeout.");
+            mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
+        }
+
+        private void clearTimeout() {
+            Log.i(TAG, "Clearing timeout.");
+            mHandler.removeCallbacks(mOnTimeout);
+        }
+    }
+
+    public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+        mService = ILicensingService.Stub.asInterface(service);
+        runChecks();
+    }
+
+    public synchronized void onServiceDisconnected(ComponentName name) {
+        // Called when the connection with the service has been
+        // unexpectedly disconnected. That is, Market crashed.
+        // If there are any checks in progress, the timeouts will handle them.
+        Log.w(TAG, "Service unexpectedly disconnected.");
+        mService = null;
+    }
+
+    /**
+     * Generates policy response for service connection errors, as a result of
+     * disconnections or timeouts.
+     */
+    private synchronized void handleServiceConnectionError(LicenseValidator validator) {
+        mPolicy.processServerResponse(Policy.RETRY, null);
+
+        if (mPolicy.allowAccess()) {
+            validator.getCallback().allow(Policy.RETRY);
+        } else {
+            validator.getCallback().dontAllow(Policy.RETRY);
+        }
+    }
+
+    /** Unbinds service if necessary and removes reference to it. */
+    private void cleanupService() {
+        if (mService != null) {
+            try {
+                mContext.unbindService(this);
+            } catch (IllegalArgumentException e) {
+                // Somehow we've already been unbound. This is a non-fatal
+                // error.
+                Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
+            }
+            mService = null;
+        }
+    }
+
+    /**
+     * Inform the library that the context is about to be destroyed, so that any
+     * open connections can be cleaned up.
+     * <p>
+     * Failure to call this method can result in a crash under certain
+     * circumstances, such as during screen rotation if an Activity requests the
+     * license check or when the user exits the application.
+     */
+    public synchronized void onDestroy() {
+        cleanupService();
+        mHandler.getLooper().quit();
+    }
+
+    /** Generates a nonce (number used once). */
+    private int generateNonce() {
+        return RANDOM.nextInt();
+    }
+
+    /**
+     * Get version code for the application package name.
+     * 
+     * @param context
+     * @param packageName application package name
+     * @return the version code or empty string if package not found
+     */
+    private static String getVersionCode(Context context, String packageName) {
+        try {
+            return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0).
+                    versionCode);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Package not found. could not get version code.");
+            return "";
+        }
+    }
+}

+ 67 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Callback for the license checker library.
+ * <p>
+ * Upon checking with the Market server and conferring with the {@link Policy},
+ * the library calls the appropriate callback method to communicate the result.
+ * <p>
+ * <b>The callback does not occur in the original checking thread.</b> Your
+ * application should post to the appropriate handling thread or lock
+ * accordingly.
+ * <p>
+ * The reason that is passed back with allow/dontAllow is the base status handed
+ * to the policy for allowed/disallowing the license. Policy.RETRY will call
+ * allow or dontAllow depending on other statistics associated with the policy,
+ * while in most cases Policy.NOT_LICENSED will call dontAllow and
+ * Policy.LICENSED will Allow.
+ */
+public interface LicenseCheckerCallback {
+
+    /**
+     * Allow use. App should proceed as normal.
+     * 
+     * @param reason Policy.LICENSED or Policy.RETRY typically. (although in
+     *            theory the policy can return Policy.NOT_LICENSED here as well)
+     */
+    public void allow(int reason);
+
+    /**
+     * Don't allow use. App should inform user and take appropriate action.
+     * 
+     * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
+     *            the policy can return Policy.LICENSED here as well ---
+     *            perhaps the call to the LVL took too long, for example)
+     */
+    public void dontAllow(int reason);
+
+    /** Application error codes. */
+    public static final int ERROR_INVALID_PACKAGE_NAME = 1;
+    public static final int ERROR_NON_MATCHING_UID = 2;
+    public static final int ERROR_NOT_MARKET_MANAGED = 3;
+    public static final int ERROR_CHECK_IN_PROGRESS = 4;
+    public static final int ERROR_INVALID_PUBLIC_KEY = 5;
+    public static final int ERROR_MISSING_PERMISSION = 6;
+
+    /**
+     * Error in application code. Caller did not call or set up license checker
+     * correctly. Should be considered fatal.
+     */
+    public void applicationError(int errorCode);
+}

+ 224 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java

@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * Contains data related to a licensing request and methods to verify
+ * and process the response.
+ */
+class LicenseValidator {
+    private static final String TAG = "LicenseValidator";
+
+    // Server response codes.
+    private static final int LICENSED = 0x0;
+    private static final int NOT_LICENSED = 0x1;
+    private static final int LICENSED_OLD_KEY = 0x2;
+    private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
+    private static final int ERROR_SERVER_FAILURE = 0x4;
+    private static final int ERROR_OVER_QUOTA = 0x5;
+
+    private static final int ERROR_CONTACTING_SERVER = 0x101;
+    private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+    private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+    private final Policy mPolicy;
+    private final LicenseCheckerCallback mCallback;
+    private final int mNonce;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final DeviceLimiter mDeviceLimiter;
+
+    LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
+             int nonce, String packageName, String versionCode) {
+        mPolicy = policy;
+        mDeviceLimiter = deviceLimiter;
+        mCallback = callback;
+        mNonce = nonce;
+        mPackageName = packageName;
+        mVersionCode = versionCode;
+    }
+
+    public LicenseCheckerCallback getCallback() {
+        return mCallback;
+    }
+
+    public int getNonce() {
+        return mNonce;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+    /**
+     * Verifies the response from server and calls appropriate callback method.
+     *
+     * @param publicKey public key associated with the developer account
+     * @param responseCode server response code
+     * @param signedData signed data from server
+     * @param signature server signature
+     */
+    public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
+        String userId = null;
+        // Skip signature check for unsuccessful requests
+        ResponseData data = null;
+        if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
+                responseCode == LICENSED_OLD_KEY) {
+            // Verify signature.
+            try {
+                Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+                sig.initVerify(publicKey);
+                sig.update(signedData.getBytes());
+
+                if (!sig.verify(Base64.decode(signature))) {
+                    Log.e(TAG, "Signature verification failed.");
+                    handleInvalidResponse();
+                    return;
+                }
+            } catch (NoSuchAlgorithmException e) {
+                // This can't happen on an Android compatible device.
+                throw new RuntimeException(e);
+            } catch (InvalidKeyException e) {
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
+                return;
+            } catch (SignatureException e) {
+                throw new RuntimeException(e);
+            } catch (Base64DecoderException e) {
+                Log.e(TAG, "Could not Base64-decode signature.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Parse and validate response.
+            try {
+                data = ResponseData.parse(signedData);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Could not parse response.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.responseCode != responseCode) {
+                Log.e(TAG, "Response codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.nonce != mNonce) {
+                Log.e(TAG, "Nonce doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.packageName.equals(mPackageName)) {
+                Log.e(TAG, "Package name doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.versionCode.equals(mVersionCode)) {
+                Log.e(TAG, "Version codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Application-specific user identifier.
+            userId = data.userId;
+            if (TextUtils.isEmpty(userId)) {
+                Log.e(TAG, "User identifier is empty.");
+                handleInvalidResponse();
+                return;
+            }
+        }
+
+        switch (responseCode) {
+            case LICENSED:
+            case LICENSED_OLD_KEY:
+                int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
+                handleResponse(limiterResponse, data);
+                break;
+            case NOT_LICENSED:
+                handleResponse(Policy.NOT_LICENSED, data);
+                break;
+            case ERROR_CONTACTING_SERVER:
+                Log.w(TAG, "Error contacting licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_SERVER_FAILURE:
+                Log.w(TAG, "An error has occurred on the licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_OVER_QUOTA:
+                Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_INVALID_PACKAGE_NAME:
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
+                break;
+            case ERROR_NON_MATCHING_UID:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
+                break;
+            case ERROR_NOT_MARKET_MANAGED:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
+                break;
+            default:
+                Log.e(TAG, "Unknown response code for license check.");
+                handleInvalidResponse();
+        }
+    }
+
+    /**
+     * Confers with policy and calls appropriate callback method.
+     *
+     * @param response
+     * @param rawData
+     */
+    private void handleResponse(int response, ResponseData rawData) {
+        // Update policy data and increment retry counter (if needed)
+        mPolicy.processServerResponse(response, rawData);
+
+        // Given everything we know, including cached data, ask the policy if we should grant
+        // access.
+        if (mPolicy.allowAccess()) {
+            mCallback.allow(response);
+        } else {
+            mCallback.dontAllow(response);
+        }
+    }
+
+    private void handleApplicationError(int code) {
+        mCallback.applicationError(code);
+    }
+
+    private void handleInvalidResponse() {
+        mCallback.dontAllow(Policy.NOT_LICENSED);
+    }
+}

+ 32 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * A DeviceLimiter that doesn't limit the number of devices that can use a
+ * given user's license.
+ * <p>
+ * Unless you have reason to believe that your application is being pirated
+ * by multiple users using the same license (signing in to Market as the same
+ * user), we recommend you use this implementation.
+ */
+public class NullDeviceLimiter implements DeviceLimiter {
+
+    public int isDeviceAllowed(String userId) {
+        return Policy.LICENSED;
+    }
+}

+ 48 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Interface used as part of a {@link Policy} to allow application authors to obfuscate
+ * licensing data that will be stored into a SharedPreferences file.
+ * <p>
+ * Any transformation scheme must be reversable. Implementing classes may optionally implement an
+ * integrity check to further prevent modification to preference data. Implementing classes
+ * should use device-specific information as a key in the obfuscation algorithm to prevent
+ * obfuscated preferences from being shared among devices.
+ */
+public interface Obfuscator {
+
+    /**
+     * Obfuscate a string that is being stored into shared preferences.
+     *
+     * @param original The data that is to be obfuscated.
+     * @param key The key for the data that is to be obfuscated.
+     * @return A transformed version of the original data.
+     */
+    String obfuscate(String original, String key);
+
+    /**
+     * Undo the transformation applied to data by the obfuscate() method.
+     *
+     * @param original The data that is to be obfuscated.
+     * @param key The key for the data that is to be obfuscated.
+     * @return A transformed version of the original data.
+     * @throws ValidationException Optionally thrown if a data integrity check fails.
+     */
+    String unobfuscate(String obfuscated, String key) throws ValidationException;
+}

+ 59 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Policy used by {@link LicenseChecker} to determine whether a user should have
+ * access to the application.
+ */
+public interface Policy {
+
+    /**
+     * Change these values to make it more difficult for tools to automatically
+     * strip LVL protection from your APK.
+     */
+
+    /**
+     * LICENSED means that the server returned back a valid license response
+     */
+    public static final int LICENSED = 0x0100;
+    /**
+     * NOT_LICENSED means that the server returned back a valid license response
+     * that indicated that the user definitively is not licensed
+     */
+    public static final int NOT_LICENSED = 0x0231;
+    /**
+     * RETRY means that the license response was unable to be determined ---
+     * perhaps as a result of faulty networking
+     */
+    public static final int RETRY = 0x0123;
+
+    /**
+     * Provide results from contact with the license server. Retry counts are
+     * incremented if the current value of response is RETRY. Results will be
+     * used for any future policy decisions.
+     * 
+     * @param response the result from validating the server response
+     * @param rawData the raw server response data, can be null for RETRY
+     */
+    void processServerResponse(int response, ResponseData rawData);
+
+    /**
+     * Check if the user should be allowed access to the application.
+     */
+    boolean allowAccess();
+}

+ 77 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * An wrapper for SharedPreferences that transparently performs data obfuscation.
+ */
+public class PreferenceObfuscator {
+
+    private static final String TAG = "PreferenceObfuscator";
+
+    private final SharedPreferences mPreferences;
+    private final Obfuscator mObfuscator;
+    private SharedPreferences.Editor mEditor;
+
+    /**
+     * Constructor.
+     *
+     * @param sp A SharedPreferences instance provided by the system.
+     * @param o The Obfuscator to use when reading or writing data.
+     */
+    public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
+        mPreferences = sp;
+        mObfuscator = o;
+        mEditor = null;
+    }
+
+    public void putString(String key, String value) {
+        if (mEditor == null) {
+            mEditor = mPreferences.edit();
+        }
+        String obfuscatedValue = mObfuscator.obfuscate(value, key);
+        mEditor.putString(key, obfuscatedValue);
+    }
+
+    public String getString(String key, String defValue) {
+        String result;
+        String value = mPreferences.getString(key, null);
+        if (value != null) {
+            try {
+                result = mObfuscator.unobfuscate(value, key);
+            } catch (ValidationException e) {
+                // Unable to unobfuscate, data corrupt or tampered
+                Log.w(TAG, "Validation error while reading preference: " + key);
+                result = defValue;
+            }
+        } else {
+            // Preference not found
+            result = defValue;
+        }
+        return result;
+    }
+
+    public void commit() {
+        if (mEditor != null) {
+            mEditor.commit();
+            mEditor = null;
+        }
+    }
+}

+ 79 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import java.util.regex.Pattern;
+
+import android.text.TextUtils;
+
+/**
+ * ResponseData from licensing server.
+ */
+public class ResponseData {
+
+    public int responseCode;
+    public int nonce;
+    public String packageName;
+    public String versionCode;
+    public String userId;
+    public long timestamp;
+    /** Response-specific data. */
+    public String extra;
+
+    /**
+     * Parses response string into ResponseData.
+     *
+     * @param responseData response data string
+     * @throws IllegalArgumentException upon parsing error
+     * @return ResponseData object
+     */
+    public static ResponseData parse(String responseData) {
+        // Must parse out main response data and response-specific data.
+    	int index = responseData.indexOf(':');
+    	String mainData, extraData;
+    	if ( -1 == index ) {
+    		mainData = responseData;
+    		extraData = "";
+    	} else {
+    		mainData = responseData.substring(0, index);
+    		extraData = index >= responseData.length() ? "" : responseData.substring(index+1);
+    	}
+
+        String [] fields = TextUtils.split(mainData, Pattern.quote("|"));
+        if (fields.length < 6) {
+            throw new IllegalArgumentException("Wrong number of fields.");
+        }
+
+        ResponseData data = new ResponseData();
+        data.extra = extraData;
+        data.responseCode = Integer.parseInt(fields[0]);
+        data.nonce = Integer.parseInt(fields[1]);
+        data.packageName = fields[2];
+        data.versionCode = fields[3];
+        // Application-specific user identifier.
+        data.userId = fields[4];
+        data.timestamp = Long.parseLong(fields[5]);
+
+        return data;
+    }
+
+    @Override
+    public String toString() {
+        return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode,
+            userId, timestamp });
+    }
+}

+ 276 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java

@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * Default policy. All policy decisions are based off of response data received
+ * from the licensing service. Specifically, the licensing server sends the
+ * following information: response validity period, error retry period, and
+ * error retry count.
+ * <p>
+ * These values will vary based on the the way the application is configured in
+ * the Android Market publishing console, such as whether the application is
+ * marked as free or is within its refund period, as well as how often an
+ * application is checking with the licensing service.
+ * <p>
+ * Developers who need more fine grained control over their application's
+ * licensing policy should implement a custom Policy.
+ */
+public class ServerManagedPolicy implements Policy {
+
+    private static final String TAG = "ServerManagedPolicy";
+    private static final String PREFS_FILE = "com.android.vending.licensing.ServerManagedPolicy";
+    private static final String PREF_LAST_RESPONSE = "lastResponse";
+    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+    private static final String PREF_RETRY_UNTIL = "retryUntil";
+    private static final String PREF_MAX_RETRIES = "maxRetries";
+    private static final String PREF_RETRY_COUNT = "retryCount";
+    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+    private static final String DEFAULT_RETRY_UNTIL = "0";
+    private static final String DEFAULT_MAX_RETRIES = "0";
+    private static final String DEFAULT_RETRY_COUNT = "0";
+
+    private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+    private long mValidityTimestamp;
+    private long mRetryUntil;
+    private long mMaxRetries;
+    private long mRetryCount;
+    private long mLastResponseTime = 0;
+    private int mLastResponse;
+    private PreferenceObfuscator mPreferences;
+
+    /**
+     * @param context The context for the current application
+     * @param obfuscator An obfuscator to be used with preferences.
+     */
+    public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
+        // Import old values
+        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+        mPreferences = new PreferenceObfuscator(sp, obfuscator);
+        mLastResponse = Integer.parseInt(
+            mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+                DEFAULT_VALIDITY_TIMESTAMP));
+        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+    }
+
+    /**
+     * Process a new response from the license server.
+     * <p>
+     * This data will be used for computing future policy decisions. The
+     * following parameters are processed:
+     * <ul>
+     * <li>VT: the timestamp that the client should consider the response
+     *   valid until
+     * <li>GT: the timestamp that the client should ignore retry errors until
+     * <li>GR: the number of retry errors that the client should ignore
+     * </ul>
+     *
+     * @param response the result from validating the server response
+     * @param rawData the raw server response data
+     */
+    public void processServerResponse(int response, ResponseData rawData) {
+
+        // Update retry counter
+        if (response != Policy.RETRY) {
+            setRetryCount(0);
+        } else {
+            setRetryCount(mRetryCount + 1);
+        }
+
+        if (response == Policy.LICENSED) {
+            // Update server policy data
+            Map<String, String> extras = decodeExtras(rawData.extra);
+            mLastResponse = response;
+            setValidityTimestamp(extras.get("VT"));
+            setRetryUntil(extras.get("GT"));
+            setMaxRetries(extras.get("GR"));
+        } else if (response == Policy.NOT_LICENSED) {
+            // Clear out stale policy data
+            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+            setRetryUntil(DEFAULT_RETRY_UNTIL);
+            setMaxRetries(DEFAULT_MAX_RETRIES);
+        }
+
+        setLastResponse(response);
+        mPreferences.commit();
+    }
+
+    /**
+     * Set the last license response received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     *
+     * @param l the response
+     */
+    private void setLastResponse(int l) {
+        mLastResponseTime = System.currentTimeMillis();
+        mLastResponse = l;
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
+    }
+
+    /**
+     * Set the current retry count and add to preferences. You must manually
+     * call PreferenceObfuscator.commit() to commit these changes to disk.
+     *
+     * @param c the new retry count
+     */
+    private void setRetryCount(long c) {
+        mRetryCount = c;
+        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
+    }
+
+    public long getRetryCount() {
+        return mRetryCount;
+    }
+
+    /**
+     * Set the last validity timestamp (VT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     *
+     * @param validityTimestamp the VT string received
+     */
+    private void setValidityTimestamp(String validityTimestamp) {
+        Long lValidityTimestamp;
+        try {
+            lValidityTimestamp = Long.parseLong(validityTimestamp);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire in one minute.
+            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
+            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
+            validityTimestamp = Long.toString(lValidityTimestamp);
+        }
+
+        mValidityTimestamp = lValidityTimestamp;
+        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
+    }
+
+    public long getValidityTimestamp() {
+        return mValidityTimestamp;
+    }
+
+    /**
+     * Set the retry until timestamp (GT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     *
+     * @param retryUntil the GT string received
+     */
+    private void setRetryUntil(String retryUntil) {
+        Long lRetryUntil;
+        try {
+            lRetryUntil = Long.parseLong(retryUntil);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire immediately
+            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
+            retryUntil = "0";
+            lRetryUntil = 0l;
+        }
+
+        mRetryUntil = lRetryUntil;
+        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
+    }
+
+    public long getRetryUntil() {
+      return mRetryUntil;
+    }
+
+    /**
+     * Set the max retries value (GR) as received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     *
+     * @param maxRetries the GR string received
+     */
+    private void setMaxRetries(String maxRetries) {
+        Long lMaxRetries;
+        try {
+            lMaxRetries = Long.parseLong(maxRetries);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire immediately
+            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
+            maxRetries = "0";
+            lMaxRetries = 0l;
+        }
+
+        mMaxRetries = lMaxRetries;
+        mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
+    }
+
+    public long getMaxRetries() {
+        return mMaxRetries;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * This implementation allows access if either:<br>
+     * <ol>
+     * <li>a LICENSED response was received within the validity period
+     * <li>a RETRY response was received in the last minute, and we are under
+     * the RETRY count or in the RETRY period.
+     * </ol>
+     */
+    public boolean allowAccess() {
+        long ts = System.currentTimeMillis();
+        if (mLastResponse == Policy.LICENSED) {
+            // Check if the LICENSED response occurred within the validity timeout.
+            if (ts <= mValidityTimestamp) {
+                // Cached LICENSED response is still valid.
+                return true;
+            }
+        } else if (mLastResponse == Policy.RETRY &&
+                   ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+            // Only allow access if we are within the retry period or we haven't used up our
+            // max retries.
+            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+        }
+        return false;
+    }
+
+    private Map<String, String> decodeExtras(String extras) {
+        Map<String, String> results = new HashMap<String, String>();
+        try {
+            URI rawExtras = new URI("?" + extras);
+            List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8");
+            for (NameValuePair item : extraList) {
+                results.put(item.getName(), item.getValue());
+            }
+        } catch (URISyntaxException e) {
+          Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
+
+}

+ 63 - 0
platform/android/libs/play_licensing/src/com/google/android/vending/licensing/StrictPolicy.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Non-caching policy. All requests will be sent to the licensing service,
+ * and no local caching is performed.
+ * <p>
+ * Using a non-caching policy ensures that there is no local preference data
+ * for malicious users to tamper with. As a side effect, applications
+ * will not be permitted to run while offline. Developers should carefully
+ * weigh the risks of using this Policy over one which implements caching,
+ * such as ServerManagedPolicy.
+ * <p>
+ * Access to the application is only allowed if a LICESNED response is.
+ * received. All other responses (including RETRY) will deny access.
+ */
+public class StrictPolicy implements Policy {
+
+    private int mLastResponse;
+
+    public StrictPolicy() {
+        // Set default policy. This will force the application to check the policy on launch.
+        mLastResponse = Policy.RETRY;
+    }
+
+    /**
+     * Process a new response from the license server. Since we aren't
+     * performing any caching, this equates to reading the LicenseResponse.
+     * Any ResponseData provided is ignored.
+     *
+     * @param response the result from validating the server response
+     * @param rawData the raw server response data
+     */
+    public void processServerResponse(int response, ResponseData rawData) {
+        mLastResponse = response;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * This implementation allows access if and only if a LICENSED response
+     * was received the last time the server was contacted.
+     */
+    public boolean allowAccess() {
+        return (mLastResponse == Policy.LICENSED);
+    }
+
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно