apktools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,586 @@
1
+ # Copyright (C) 2012 Dave Smith
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ # software and associated documentation files (the "Software"), to deal in the Software
5
+ # without restriction, including without limitation the rights to use, copy, modify,
6
+ # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
8
+ #
9
+ # The above copyright notice and this permission notice shall be included in all copies
10
+ # or substantial portions of the Software.
11
+ #
12
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15
+ # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17
+ # DEALINGS IN THE SOFTWARE.
18
+
19
+ require 'zip/zip'
20
+
21
+ ##
22
+ # Class to parse an APK's resources.arsc data and retrieve resource
23
+ # data associated with a given R.id value
24
+ class ApkResources
25
+
26
+ DEBUG = false # :nodoc:
27
+
28
+ ##
29
+ # Structure defining the type and size of each resource chunk
30
+ #
31
+ # ChunkHeader = Struct.new(:type, :size, :chunk_size)
32
+ ChunkHeader = Struct.new(:type, :size, :chunk_size)
33
+
34
+ ##
35
+ # Structure that houses a group of strings
36
+ #
37
+ # StringPool = Struct.new(:header, :string_count, :style_count, :values)
38
+ #
39
+ # * +header+ = ChunkHeader
40
+ # * +string_count+ = Number of normal strings in the pool
41
+ # * +style_count+ = Number of styled strings in the pool
42
+ # * +values+ = Array of the string values
43
+ StringPool = Struct.new(:header, :string_count, :style_count, :values)
44
+
45
+ ##
46
+ # Structure defining the data inside of the package chunk
47
+ #
48
+ # PackageHeader = Struct.new(:header, :id, :name, :type_strings, :key_strings)
49
+ #
50
+ # * +header+ = ChunkHeader
51
+ # * +id+ = Package id; usually 0x7F for application resources
52
+ # * +name+ = Package name (e.g. "com.example.application")
53
+ # * +type_strings+ = Array of the type string values present (e.g. "drawable")
54
+ # * +key_strings+ = Array of the key string values present (e.g. "ic_launcher")
55
+ PackageHeader = Struct.new(:header, :id, :name, :type_strings, :key_strings)
56
+
57
+ ##
58
+ # Structure defining the flags for a block of common resources
59
+ #
60
+ # ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
61
+ #
62
+ # * +header+ = ChunkHeader
63
+ # * +id+ = String value of the referenced type (e.g. "drawable")
64
+ # * +entry_count+ = Number of type entries in this chunk
65
+ # * +entries+ = Array of config flags for each type entry
66
+ # * +types+ = The ResType associated with this spec
67
+ ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
68
+
69
+ ##
70
+ # Structure that houses all the resources for a given type
71
+ #
72
+ # ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
73
+ #
74
+ # * +header+ = ChunkHeader
75
+ # * +id+ = String value of the referenced type (e.g. "drawable")
76
+ # * +config+ = ResTypeConfig defining the configuration for this type
77
+ # * +entry_count+ = Number of entries in this chunk
78
+ # * +entries+ = Array of Hashes of [ResTypeConfig, ResTypeEntry] in this chunk
79
+ ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
80
+
81
+ ##
82
+ # Structure that houses the configuration flags for a given resource.
83
+ #
84
+ # ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
85
+ #
86
+ # * +imsi+ = Flags marking country code and network code
87
+ # * +locale+ = Flags marking locale requirements (language)
88
+ # * +screen_type+ = Flags/values for screen density
89
+ # * +input+ = Flags marking input types and visibility status
90
+ # * +screen_size+ = Flags marking screen size and length
91
+ # * +version+ = Minimum API version
92
+ # * +screen_config+ = Flags marking screen configuration (like orientation)
93
+ # * +screen_size_dp+ = Flags marking smallest width constraints
94
+ #
95
+ # A default configuration is defined as ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
96
+ ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
97
+
98
+ ##
99
+ # Structure that houses the data for a given resource entry
100
+ #
101
+ # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
102
+ #
103
+ # * +flags+ = Flags marking if the resource is complex or public
104
+ # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
105
+ # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
106
+ # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
107
+ #
108
+ # A single resource key can have multiple entries depending on configuration, so these structs
109
+ # are often returned in groups, keyed by a ResTypeConfig
110
+ ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
111
+
112
+ # PackageHeader containing information about all the type and key strings in the package
113
+ attr_reader :package_header
114
+ # StringPool containing all value strings in the package
115
+ attr_reader :stringpool_main
116
+ # StringPool containing all type strings in the package
117
+ attr_reader :stringpool_typestrings
118
+ # StringPool containing all key strings in the package
119
+ attr_reader :stringpool_keystrings
120
+ # Array of the ResTypeSpec chunks in the package
121
+ attr_reader :type_data
122
+
123
+ ##
124
+ # Create a new ApkResources instance from the specified +apk_file+
125
+ #
126
+ # This opens and parses the contents of the APK's resources.arsc file.
127
+
128
+ def initialize(apk_file)
129
+ data = nil
130
+ # Get resources.arsc from the APK file
131
+ Zip::ZipFile.foreach(apk_file) do |f|
132
+ if f.name.match(/resources.arsc/)
133
+ data = f.get_input_stream.read
134
+ end
135
+ end
136
+
137
+ # Parse the Table Chunk
138
+ ## Header
139
+ header_type = read_short(data, HEADER_START)
140
+ header_size = read_short(data, HEADER_START+2)
141
+ header_chunk_size = read_word(data, HEADER_START+4)
142
+ header_package_count = read_word(data, HEADER_START+8)
143
+ puts "Resource Package Count = #{header_package_count}" if DEBUG
144
+ if header_package_count > 1
145
+ puts "ApkResources only supports single package resources."
146
+ exit(1)
147
+ end
148
+
149
+ # Parse the StringPool Chunk
150
+ ## Header
151
+ startoffset_pool = HEADER_START + header_size
152
+ puts "Parse Main StringPool Chunk" if DEBUG
153
+ @stringpool_main = parse_stringpool(data, startoffset_pool)
154
+ puts "#{@stringpool_main.values.length} strings found" if DEBUG
155
+
156
+ # Parse the Package Chunk
157
+ ## Header
158
+ startoffset_package = startoffset_pool + @stringpool_main.header.chunk_size
159
+ header = ChunkHeader.new( read_short(data, startoffset_package),
160
+ read_short(data, startoffset_package+2),
161
+ read_word(data, startoffset_package+4) )
162
+
163
+ package_id = read_word(data, startoffset_package+8)
164
+ package_name = read_string(data, startoffset_package+12, 256, "UTF-8")
165
+ package_type_strings = read_word(data, startoffset_package+268)
166
+ package_last_type = read_word(data, startoffset_package+272)
167
+ package_key_strings = read_word(data, startoffset_package+276)
168
+ package_last_key = read_word(data, startoffset_package+280)
169
+
170
+ @package_header = PackageHeader.new(header, package_id, package_name, package_type_strings, package_key_strings)
171
+
172
+ ## typeStrings StringPool
173
+ startoffset_typestrings = startoffset_package + package_type_strings
174
+ puts "Parse typeStrings StringPool Chunk" if DEBUG
175
+ @stringpool_typestrings = parse_stringpool(data, startoffset_typestrings)
176
+
177
+ ## keyStrings StringPool
178
+ startoffset_keystrings = startoffset_package + package_key_strings
179
+ puts "Parse keyStrings StringPool Chunk" if DEBUG
180
+ @stringpool_keystrings = parse_stringpool(data, startoffset_keystrings)
181
+
182
+ ## typeSpec/type Chunks
183
+ @type_data = Array.new()
184
+ current_spec = nil
185
+
186
+ current = startoffset_keystrings + @stringpool_keystrings.header.chunk_size
187
+ puts "Parse Type/TypeSpec Chunks" if DEBUG
188
+ while current < data.length
189
+ ## Parse Header
190
+ header = ChunkHeader.new( read_short(data, current),
191
+ read_short(data, current+2),
192
+ read_word(data, current+4) )
193
+ ## Check Type
194
+ if header.type == CHUNKTYPE_TYPESPEC
195
+ typespec_id = read_byte(data, current+8)
196
+ typespec_entrycount = read_word(data, current+12)
197
+
198
+ ## Parse the config flags for each entry
199
+ typespec_entries = Array.new()
200
+ i=0
201
+ while i < typespec_entrycount
202
+ offset = i * 4 + (current+16)
203
+ typespec_entries << read_word(data, offset)
204
+
205
+ i += 1
206
+ end
207
+
208
+ typespec_name = @stringpool_typestrings.values[typespec_id - 1]
209
+ current_spec = ResTypeSpec.new(header, typespec_name, typespec_entrycount, typespec_entries, nil)
210
+
211
+ @type_data << current_spec
212
+ current += header.chunk_size
213
+ elsif header.type == CHUNKTYPE_TYPE
214
+ type_id = read_byte(data, current+8)
215
+ type_entrycount = read_word(data, current+12)
216
+ type_entryoffset = read_word(data, current+16)
217
+
218
+ ## The config flags set for this type chunk
219
+ ## TODO: Vary the size of the config structure based on size to accomodate for new flags
220
+ config_size = read_word(data, current+20) # Number of bytes in structure
221
+ type_config = ResTypeConfig.new( read_word(data, current+24),
222
+ read_word(data, current+28),
223
+ read_word(data, current+32),
224
+ read_word(data, current+36 ),
225
+ read_word(data, current+40),
226
+ read_word(data, current+44),
227
+ read_word(data, current+48),
228
+ read_word(data, current+52) )
229
+
230
+ ## The remainder of the chunk is a list of the entry values for that type/configuration
231
+ type_name = @stringpool_typestrings.values[type_id - 1]
232
+ if current_spec.types == nil
233
+ current_spec.types = ResType.new(header, type_name, type_config, type_entrycount, Array.new())
234
+ end
235
+
236
+ i=0
237
+ while i < type_entrycount
238
+ ## Ensure a hash exists for each type
239
+ if current_spec.types.entries[i] == nil
240
+ current_spec.types.entries[i] = Hash.new()
241
+ end
242
+ current_entry = current_spec.types.entries[i]
243
+
244
+ ## Get the start of the type from the offsets table
245
+ index_offset = i * 4 + (current+56)
246
+ start_offset = read_word(data, index_offset)
247
+ if start_offset != OFFSET_NO_ENTRY
248
+ ## Set the index_offset to the start of the current entry
249
+ index_offset = current + type_entryoffset + start_offset
250
+
251
+ entry_flags = read_short(data, index_offset+2)
252
+ entry_key = read_word(data, index_offset+4)
253
+ entry_data_type = read_byte(data, index_offset+11)
254
+ entry_data = read_word(data, index_offset+12)
255
+
256
+ # Find the key in our strings index
257
+ key_name = @stringpool_keystrings.values[entry_key]
258
+ # Parse the value into a string
259
+ data_value = nil
260
+ case type_name
261
+ when TYPE_STRING, TYPE_DRAWABLE
262
+ data_value = get_resource_string(entry_data_type, entry_data)
263
+ when TYPE_COLOR
264
+ data_value = get_resource_color(entry_data_type, entry_data)
265
+ when TYPE_DIMENSION
266
+ data_value = get_resource_dimension(entry_data_type, entry_data)
267
+ when TYPE_INTEGER
268
+ data_value = get_resource_integer(entry_data_type, entry_data)
269
+ when TYPE_BOOLEAN
270
+ data_value = get_resource_bool(entry_data_type, entry_data)
271
+ else
272
+ puts "Complex Resources not yet supported." if DEBUG
273
+ data_value = entry_data.to_s
274
+ end
275
+ current_entry[type_config] = ResTypeEntry.new(entry_flags, key_name, entry_data_type, data_value)
276
+ end
277
+ i += 1
278
+ end
279
+
280
+ current += header.chunk_size
281
+ else
282
+ puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
283
+ ## End Immediately
284
+ current = data.length
285
+ end
286
+ end
287
+
288
+ end #initalize
289
+
290
+ ##
291
+ # Return array of all string values in the file
292
+
293
+ def get_all_strings
294
+ return @stringpool_main.values
295
+ end
296
+
297
+ ##
298
+ # Return array of all the type values in the file
299
+
300
+ def get_all_types
301
+ return @stringpool_typestrings.values
302
+ end
303
+
304
+ ##
305
+ # Return array of all the key values in the file
306
+
307
+ def get_all_keys
308
+ return @stringpool_keystrings.values
309
+ end
310
+
311
+ ##
312
+ # Obtain the key value for a given resource id
313
+ #
314
+ # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
315
+ # xml_format: Optionally format return string for XML files.
316
+ #
317
+ # If xml_format is true, return value will be @<type>/<key>
318
+ # If xml_format is false or missing, return value will be R.<type>.<key>
319
+
320
+ def get_resource_key(res_id, xml_format=false)
321
+ if res_id.is_a? String
322
+ res_id = res_id.hex
323
+ end
324
+
325
+ # R.id integers are a concatenation of package_id, type_id, and entry index
326
+ res_package = (res_id >> 24) & 0xFF
327
+ res_type = (res_id >> 16) & 0xFF
328
+ res_index = res_id & 0xFFFF
329
+
330
+ if res_package != @package_header.id
331
+ # This is not a resource we can parse
332
+ return res_id_to_s(res_id)
333
+ end
334
+
335
+ res_spec = @type_data[res_type-1]
336
+ entry = res_spec.types.entries[res_index]
337
+
338
+ if entry == nil
339
+ # There is no entry in our table for this resource
340
+ return res_id_to_s(res_id)
341
+ end
342
+
343
+ if xml_format
344
+ return "@#{res_spec.id}/#{entry.values[0].key}"
345
+ else
346
+ return "R.#{res_spec.id}.#{entry.values[0].key}"
347
+ end
348
+ end
349
+
350
+ ##
351
+ # Obtain the default value for a given resource id
352
+ #
353
+ # res_id: ID values of a resources as a FixNum or String representation (i.e. 0x7F060001)
354
+ #
355
+ # Returns: The default ResTypeEntry to the given id, or nil if no default exists
356
+
357
+ def get_default_resource_value(res_id)
358
+ entries = get_resource_value(res_id)
359
+ default = ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
360
+
361
+ return entries[default]
362
+ end
363
+
364
+ ##
365
+ # Obtain the value(s) for a given resource id.
366
+ # A default resource is one defined in an unqualified directory.
367
+ #
368
+ # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
369
+ #
370
+ # Returns: Hash of all entries matching this id, keyed by their matching ResTypeConfig
371
+
372
+ def get_resource_value(res_id)
373
+ if res_id.is_a? String
374
+ res_id = res_id.hex
375
+ end
376
+
377
+ # R.id integers are a concatenation of package_id, type_id, and entry index
378
+ res_package = (res_id >> 24) & 0xFF
379
+ res_type = (res_id >> 16) & 0xFF
380
+ res_index = res_id & 0xFFFF
381
+
382
+ if res_package != @package_header.id
383
+ # This is not a resource we can parse
384
+ return res_id_to_s(res_id)
385
+ end
386
+
387
+ res_spec = @type_data[res_type-1]
388
+
389
+ entries = res_spec.types.entries[res_index]
390
+ if entries == nil
391
+ puts "Could not find #{type_name} ResType chunk" if DEBUG
392
+ return res_id_to_s(res_id)
393
+ end
394
+
395
+ return entries
396
+ end
397
+
398
+ private # Private Helper Methods
399
+
400
+ # Type Constants
401
+ TYPE_ARRAY = "array" # :nodoc:
402
+ TYPE_ATTRIBUTE = "attr" # :nodoc:
403
+ TYPE_BOOLEAN = "bool" # :nodoc:
404
+ TYPE_COLOR = "color" # :nodoc:
405
+ TYPE_DIMENSION = "dimen" # :nodoc:
406
+ TYPE_DRAWABLE = "drawable" # :nodoc:
407
+ TYPE_FRACTION = "fraction" # :nodoc:
408
+ TYPE_INTEGER = "integer" # :nodoc:
409
+ TYPE_LAYOUT = "layout" # :nodoc:
410
+ TYPE_PLURALS = "plurals" # :nodoc:
411
+ TYPE_STRING = "string" # :nodoc:
412
+ TYPE_STYLE = "style" # :nodoc:
413
+
414
+ # Data Type Constants
415
+ TYPE_INT_DEC = 0x10 # :nodoc:
416
+ TYPE_INT_HEX = 0x11 # :nodoc:
417
+ TYPE_BOOL = 0x12 # :nodoc:
418
+ TYPE_INT_COLOR_RGB4 = 0x1F # :nodoc:
419
+ TYPE_INT_COLOR_ARGB4 = 0x1E # :nodoc:
420
+ TYPE_INT_COLOR_RGB8 = 0x1D # :nodoc:
421
+ TYPE_INT_COLOR_ARGB8 = 0x1C # :nodoc:
422
+ COMPLEX_UNIT_PX = 0x0 # :nodoc:
423
+ COMPLEX_UNIT_DIP = 0x1 # :nodoc:
424
+ COMPLEX_UNIT_SP = 0x2 # :nodoc:
425
+ COMPLEX_UNIT_PT = 0x3 # :nodoc:
426
+ COMPLEX_UNIT_IN = 0x4 # :nodoc:
427
+ COMPLEX_UNIT_MM = 0x5 # :nodoc:
428
+
429
+ # Data Constants
430
+ TYPE_BOOL_TRUE = 0xFFFFFFFF # :nodoc:
431
+ TYPE_BOOL_FALSE = 0x00000000 # :nodoc:
432
+
433
+ # Header Constants
434
+ CHUNKTYPE_TYPESPEC = 0x202 # :nodoc:
435
+ CHUNKTYPE_TYPE = 0x201 # :nodoc:
436
+
437
+ #Flag Constants
438
+ FLAG_UTF8 = 0x100 # :nodoc:
439
+
440
+ OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
441
+ HEADER_START = 0 # :nodoc:
442
+
443
+ # Read a 32-bit word from a specific location in the data
444
+ def read_word(data, offset)
445
+ out = data[offset,4].unpack('V').first rescue 0
446
+ return out
447
+ end
448
+
449
+ # Read a 16-bit short from a specific location in the data
450
+ def read_short(data, offset)
451
+ out = data[offset,2].unpack('v').first rescue 0
452
+ return out
453
+ end
454
+
455
+ # Read a 8-bit byte from a specific location in the data
456
+ def read_byte(data, offset)
457
+ out = data[offset,1].unpack('C').first rescue 0
458
+ return out
459
+ end
460
+
461
+ # Read in length bytes in as a String
462
+ def read_string(data, offset, length, encoding)
463
+ if "UTF-16".casecmp(encoding) == 0
464
+ out = data[offset, length].unpack('v*').pack('U*')
465
+ else
466
+ out = data[offset, length].unpack('C*').pack('U*')
467
+ end
468
+ return out
469
+ end
470
+
471
+ # Return id as a hex string
472
+ def res_id_to_s(res_id)
473
+ return "0x#{res_id.to_s(16)}"
474
+ end
475
+
476
+ # Parse out a StringPool chunk
477
+ def parse_stringpool(data, offset)
478
+ pool_header = ChunkHeader.new( read_short(data, offset),
479
+ read_short(data, offset+2),
480
+ read_word(data, offset+4) )
481
+
482
+ pool_string_count = read_word(data, offset+8)
483
+ pool_style_count = read_word(data, offset+12)
484
+ pool_flags = read_word(data, offset+16)
485
+ format_utf8 = (pool_flags & FLAG_UTF8) != 0
486
+ puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
487
+
488
+ pool_string_offset = read_word(data, offset+20)
489
+ pool_style_offset = read_word(data, offset+24)
490
+
491
+ values = Array.new()
492
+ i = 0
493
+ while i < pool_string_count
494
+ # Read the string value
495
+ index = i * 4 + (offset+28)
496
+ offset_addr = pool_string_offset + offset + read_word(data, index)
497
+ if format_utf8
498
+ length = read_byte(data, offset_addr)
499
+ if (length & 0x80) != 0
500
+ length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
501
+ end
502
+
503
+ values << read_string(data, offset_addr + 2, length, "UTF-8")
504
+ else
505
+ length = read_short(data, offset_addr)
506
+ if (length & 0x8000) != 0
507
+ #There is one more length value before the data
508
+ length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
509
+ values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
510
+ else
511
+ # Read the data
512
+ values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
513
+ end
514
+ end
515
+
516
+ i += 1
517
+ end
518
+
519
+ return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
520
+ end
521
+
522
+ # Obtain string value for resource id
523
+ def get_resource_string(entry_datatype, entry_data)
524
+ result = @stringpool_main.values[entry_data]
525
+ return result
526
+ end
527
+
528
+ # Obtain boolean value for resource id
529
+ def get_resource_bool(entry_datatype, entry_data)
530
+ if entry_data == TYPE_BOOL_TRUE
531
+ return "true"
532
+ elsif entry_data == TYPE_BOOL_FALSE
533
+ return "false"
534
+ else
535
+ return "undefined"
536
+ end
537
+ end
538
+
539
+ # Obtain integer value for resource id
540
+ def get_resource_integer(entry_datatype, entry_data)
541
+ if entry_datatype == TYPE_INT_HEX
542
+ return "0x#{entry_data.to_s(16)}"
543
+ else
544
+ return entry_data.to_s
545
+ end
546
+ end
547
+
548
+ # Obtain color value for resource id
549
+ def get_resource_color(entry_datatype, entry_data)
550
+ case entry_datatype
551
+ when TYPE_INT_COLOR_RGB4
552
+ return "#" + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
553
+ when TYPE_INT_COLOR_ARGB4
554
+ return "#" + ((entry_data >> 24) & 0xF).to_s(16) + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
555
+ when TYPE_INT_COLOR_RGB8
556
+ return "#" + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
557
+ when TYPE_INT_COLOR_ARGB8
558
+ return "#" + ((entry_data >> 24) & 0xFF).to_s(16) + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
559
+ else
560
+ return "0x#{entry_data.to_s(16)}"
561
+ end
562
+ end
563
+
564
+ # Obtain dimension value for resource id
565
+ def get_resource_dimension(entry_datatype, entry_data)
566
+ unit_type = (entry_data & 0xFF)
567
+ case unit_type
568
+ when COMPLEX_UNIT_PX
569
+ unit_name = "px"
570
+ when COMPLEX_UNIT_DIP
571
+ unit_name = "dp"
572
+ when COMPLEX_UNIT_SP
573
+ unit_name = "sp"
574
+ when COMPLEX_UNIT_PT
575
+ unit_name = "pt"
576
+ when COMPLEX_UNIT_IN
577
+ unit_name = "in"
578
+ when COMPLEX_UNIT_MM
579
+ unit_name = "mm"
580
+ else
581
+ unit_name = ""
582
+ end
583
+
584
+ return ((entry_data >> 8) & 0xFFFFFF).to_s + unit_name
585
+ end
586
+ end
@@ -0,0 +1,424 @@
1
+ # Copyright (C) 2012 Dave Smith
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ # software and associated documentation files (the "Software"), to deal in the Software
5
+ # without restriction, including without limitation the rights to use, copy, modify,
6
+ # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
8
+ #
9
+ # The above copyright notice and this permission notice shall be included in all copies
10
+ # or substantial portions of the Software.
11
+ #
12
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15
+ # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17
+ # DEALINGS IN THE SOFTWARE.
18
+
19
+ require 'zip/zip'
20
+ require 'apktools/apkresources'
21
+
22
+ ##
23
+ # Class to parse an APK's binary XML format back into textual XML
24
+ class ApkXml
25
+
26
+ DEBUG = false # :nodoc:
27
+
28
+ ##
29
+ # Structure defining the type and size of each resource chunk
30
+ #
31
+ # ChunkHeader = Struct.new(:type, :size, :chunk_size)
32
+ ChunkHeader = Struct.new(:type, :size, :chunk_size)
33
+
34
+ ##
35
+ # Structure that houses a group of strings
36
+ #
37
+ # StringPool = Struct.new(:header, :string_count, :style_count, :values)
38
+ #
39
+ # * +header+ = ChunkHeader
40
+ # * +string_count+ = Number of normal strings in the pool
41
+ # * +style_count+ = Number of styled strings in the pool
42
+ # * +values+ = Array of the string values
43
+ StringPool = Struct.new(:header, :string_count, :style_count, :values)
44
+
45
+ ##
46
+ # Structure to house mappings of resource ids to strings
47
+ #
48
+ # XmlResourceMap = Struct.new(:header, :ids, :strings)
49
+ #
50
+ # * +header+ = ChunkHeader
51
+ # * +ids+ = Array of resource ids
52
+ # * +strings+ = Matching Array of resource strings
53
+ XmlResourceMap = Struct.new(:header, :ids, :strings)
54
+
55
+ ##
56
+ # Structure defining header of an XML node
57
+ #
58
+ # XmlTreeHeader = Struct.new(:header, :line_num, :comment)
59
+ #
60
+ # * +header+ = ChunkHeader
61
+ # * +line_num+ = Line number in original file
62
+ # * +comment+ = Optional comment
63
+ XmlTreeHeader = Struct.new(:header, :line_num, :comment)
64
+
65
+ ##
66
+ # Structure defining an XML element
67
+ #
68
+ # XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
69
+ #
70
+ # * +header+ = XmlTreeHeader
71
+ # * +namespace+ = Namespace prefix of the element
72
+ # * +name+ = Name of the element
73
+ # * +id_idx+ = Index of the attribute that represents the "id" in this element, if any
74
+ # * +class_idx+ = Index of the attribute that represents the "class" in this element, if any
75
+ # * +style_idx+ = Index of the attribute that represents the "style" in this element, if any
76
+ # * +attributes+ = Array of XmlAttribute elements
77
+ # * +is_root+ = Marks if this is the root element
78
+ XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
79
+
80
+ ##
81
+ # Structure defining an XML element's attribute
82
+ #
83
+ # XmlAttribute = Struct.new(:namespace, :name, :raw, :value)
84
+ #
85
+ # * +namespace+ = Namespace prefix of the attribute
86
+ # * +name+ = Name of the attribute
87
+ # * +raw+ = Original raw string value of the attribute, if one exists
88
+ # * +value+ = ResTypeEntry for the typed value of the attribute, if one exists
89
+ XmlAttribute = Struct.new(:namespace, :name, :raw, :value)
90
+
91
+ ##
92
+ # Structure that houses the data for a given resource entry
93
+ #
94
+ # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
95
+ #
96
+ # * +flags+ = Flags marking if the resource is complex or public
97
+ # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
98
+ # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
99
+ # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
100
+ #
101
+ # A single resource key can have multiple entries depending on configuration, so these structs
102
+ # are often returned in groups, keyed by a ResTypeConfig
103
+ ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
104
+
105
+ # APK file where parser will search for XML
106
+ attr_reader :current_apk
107
+ # ApkResources instance used to resolve resources in this APK
108
+ attr_reader :apk_resources
109
+
110
+ ##
111
+ # Create a new ApkXml instance from the specified +apk_file+
112
+ #
113
+ # This opens and parses the contents of the APK's resources.arsc file.
114
+ def initialize(apk_file)
115
+ @current_apk = apk_file
116
+ @apk_resources = ApkResources.new(apk_file)
117
+ end #initialize
118
+
119
+ ##
120
+ # Read the requested XML file from inside the APK and parse out into
121
+ # readable textual XML. Returns a string of the parsed XML.
122
+ #
123
+ # xml_file: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
124
+ # pretty: Optionally format the XML output as human readable
125
+ # resolve_resources: Optionally, where possible, resolve resource references to their default value
126
+ #
127
+ # This opens and parses the contents of the APK's resources.arsc file.
128
+ def parse_xml(xml_file, pretty = false, resolve_resources = false)
129
+ xml_output = ""
130
+ indent = 0
131
+ data = nil
132
+
133
+ # Get the XML from the APK file
134
+ Zip::ZipFile.foreach(@current_apk) do |f|
135
+ if f.name.match(xml_file)
136
+ data = f.get_input_stream.read
137
+ end
138
+ end
139
+
140
+ # Parse the Header Chunk
141
+ header = ChunkHeader.new( read_short(data, HEADER_START),
142
+ read_short(data, HEADER_START+2),
143
+ read_word(data, HEADER_START+4) )
144
+
145
+ # Parse the StringPool Chunk
146
+ startoffset_pool = HEADER_START + header.size
147
+ puts "Parse Main StringPool Chunk" if DEBUG
148
+ stringpool_main = parse_stringpool(data, startoffset_pool)
149
+ puts "#{stringpool_main.values.length} strings found" if DEBUG
150
+
151
+ # Parse the remainder of the file chunks based on type
152
+ namespaces = Hash.new()
153
+ current = startoffset_pool + stringpool_main.header.chunk_size
154
+ puts "Parse Remaining Chunks" if DEBUG
155
+ while current < data.length
156
+ ## Parse Header
157
+ header = ChunkHeader.new( read_short(data, current),
158
+ read_short(data, current+2),
159
+ read_word(data, current+4) )
160
+ ## Check Type
161
+ if header.type == TYPE_XML_RESOURCEMAP
162
+ ## Maps resource ids to strings in the pool
163
+ map_ids = Array.new()
164
+ map_strings = Array.new()
165
+
166
+ index_offset = current + header.size
167
+ i = 0
168
+ while index_offset < (current + header.chunk_size)
169
+ map_ids << read_word(data, index_offset)
170
+ map_strings << stringpool_main.values[i]
171
+
172
+ i += 1
173
+ index_offset = i * 4 + (current + header.size)
174
+ end
175
+
176
+ current += header.chunk_size
177
+ elsif header.type == TYPE_XML_STARTNAMESPACE
178
+ tree_header = parse_tree_header(header, data, current)
179
+ body_start = current+header.size
180
+ prefix = stringpool_main.values[read_word(data, body_start)]
181
+ uri = stringpool_main.values[read_word(data, body_start+4)]
182
+ namespaces[uri] = prefix
183
+ puts "NAMESPACE_START: xmlns:#{prefix} = '#{uri}'" if DEBUG
184
+ current += header.chunk_size
185
+ elsif header.type == TYPE_XML_ENDNAMESPACE
186
+ tree_header = parse_tree_header(header, data, current)
187
+ body_start = current+header.size
188
+ prefix = stringpool_main.values[read_word(data, body_start)]
189
+ uri = stringpool_main.values[read_word(data, body_start+4)]
190
+ puts "NAMESPACE_END: xmlns:#{prefix} = '#{uri}'" if DEBUG
191
+ current += header.chunk_size
192
+ elsif header.type == TYPE_XML_STARTELEMENT
193
+ tree_header = parse_tree_header(header, data, current)
194
+ body_start = current+header.size
195
+ namespace = nil
196
+ if read_word(data, body_start) != OFFSET_NO_ENTRY
197
+ namespace = stringpool_main.values[read_word(data, body_start)]
198
+ end
199
+ name = stringpool_main.values[read_word(data, body_start+4)]
200
+
201
+ attribute_offset = read_short(data, body_start+8)
202
+ attribute_size = read_short(data, body_start+10)
203
+ attribute_count = read_short(data, body_start+12)
204
+ id_idx = read_short(data, body_start+14)
205
+ class_idx = read_short(data, body_start+16)
206
+ style_idx = read_short(data, body_start+18)
207
+
208
+ attributes = Array.new()
209
+ i=0
210
+ while i < attribute_count
211
+ index_offset = i * attribute_size + (body_start + attribute_offset)
212
+ attr_namespace = nil
213
+ if read_word(data, index_offset) != OFFSET_NO_ENTRY
214
+ attr_uri = stringpool_main.values[read_word(data, index_offset)]
215
+ attr_namespace = namespaces[attr_uri]
216
+ end
217
+ attr_name = stringpool_main.values[read_word(data, index_offset+4)]
218
+ attr_raw = nil
219
+ if read_word(data, index_offset+8) != OFFSET_NO_ENTRY
220
+ attr_raw = stringpool_main.values[read_word(data, index_offset+8)]
221
+ end
222
+ entry = ResTypeEntry.new(0, nil, read_byte(data, index_offset+15), read_word(data, index_offset+16))
223
+
224
+ attributes << XmlAttribute.new(attr_namespace, attr_name, attr_raw, entry)
225
+ i += 1
226
+ end
227
+
228
+ element = XmlElement.new(tree_header, namespace, name, id_idx, class_idx, style_idx, attributes, xml_output == "")
229
+
230
+ puts "ELEMENT_START: #{element.namespace} #{element.name}" if DEBUG
231
+ display_name = element.namespace == nil ? element.name : "#{element.namespace}:#{element.name}"
232
+
233
+ if pretty
234
+ xml_output += "\n" + (" " * indent)
235
+ indent += 1
236
+ end
237
+ xml_output += "<#{display_name} "
238
+ # Only print namespaces on the root element
239
+ if element.is_root
240
+ keys = namespaces.keys
241
+ keys.each do |key|
242
+ xml_output += "xmlns:#{namespaces[key]}=\"#{key}\" "
243
+ if pretty && key != keys.last
244
+ xml_output += "\n" + (" " * indent)
245
+ end
246
+ end
247
+ end
248
+
249
+ element.attributes.each do |attr|
250
+ puts "---ATTRIBUTE: #{attr.namespace} #{attr.name} #{attr.raw} => #{attr.value.data_type} #{attr.value.data.to_s(16)}" if DEBUG
251
+ display_name = attr.namespace == nil ? attr.name : "#{attr.namespace}:#{attr.name}"
252
+ display_value = nil
253
+ if attr.raw != nil # Use raw value
254
+ display_value = attr.raw
255
+ elsif attr.value.data_type == 1
256
+ # Find the resource
257
+ default_res = apk_resources.get_default_resource_value(attr.value.data)
258
+ if resolve_resources && default_res != nil
259
+ display_value = default_res.data
260
+ else
261
+ display_value = apk_resources.get_resource_key(attr.value.data, true)
262
+ end
263
+ else # Value is a constant
264
+ display_value = "0x#{attr.value.data.to_s(16)}"
265
+ end
266
+
267
+ if pretty
268
+ xml_output += "\n" + (" " * indent)
269
+ end
270
+ xml_output += "#{display_name}=\"#{display_value}\" "
271
+ end
272
+
273
+ xml_output += ">"
274
+
275
+ current += header.chunk_size
276
+ elsif header.type == TYPE_XML_ENDELEMENT
277
+ tree_header = parse_tree_header(header, data, current)
278
+ body_start = current+header.size
279
+ namespace = nil
280
+ if read_word(data, body_start) != OFFSET_NO_ENTRY
281
+ namespace = stringpool_main.values[read_word(data, body_start)]
282
+ end
283
+ name = stringpool_main.values[read_word(data, body_start+4)]
284
+
285
+ puts "ELEMENT END: #{namespace} #{name}" if DEBUG
286
+ display_name = namespace == nil ? name : "#{namespace}:#{name}"
287
+ if pretty
288
+ indent -= 1
289
+ if indent < 0
290
+ indent = 0
291
+ end
292
+ xml_output += "\n" + (" " * indent)
293
+ end
294
+ xml_output += "</#{display_name}>"
295
+
296
+
297
+ current += header.chunk_size
298
+ elsif header.type == TYPE_XML_CDATA
299
+ tree_header = parse_tree_header(header, data, current)
300
+ body_start = current+header.size
301
+
302
+ cdata = stringpool_main.values[read_word(data, body_start)]
303
+ cdata_type = read_word(data, body_start+7)
304
+ cdata_value = read_word(data, body_start+8)
305
+ puts "CDATA: #{cdata} #{cdata_type} #{cdata_value}" if DEBUG
306
+
307
+ cdata.split(/\r?\n/).each do |item|
308
+ if pretty
309
+ xml_output += "\n" + (" " * indent)
310
+ end
311
+ xml_output += "<![CDATA[#{item.strip}]]>"
312
+ end
313
+
314
+ current += header.chunk_size
315
+ else
316
+ puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
317
+ ## End Immediately
318
+ current = data.length
319
+ end
320
+ end
321
+
322
+ return xml_output
323
+ end #parse_xml
324
+
325
+ private # Private Helper Methods
326
+
327
+ #Flag Constants
328
+ FLAG_UTF8 = 0x100 # :nodoc:
329
+
330
+ OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
331
+ HEADER_START = 0 # :nodoc:
332
+
333
+ TYPE_XML_RESOURCEMAP = 0x180 # :nodoc:
334
+ TYPE_XML_STARTNAMESPACE = 0x100 # :nodoc:
335
+ TYPE_XML_ENDNAMESPACE = 0x101 # :nodoc:
336
+ TYPE_XML_STARTELEMENT = 0x102 # :nodoc:
337
+ TYPE_XML_ENDELEMENT = 0x103 # :nodoc:
338
+ TYPE_XML_CDATA = 0x104 # :nodoc:
339
+
340
+ # Read a 32-bit word from a specific location in the data
341
+ def read_word(data, offset)
342
+ out = data[offset,4].unpack('V').first rescue 0
343
+ return out
344
+ end
345
+
346
+ # Read a 16-bit short from a specific location in the data
347
+ def read_short(data, offset)
348
+ out = data[offset,2].unpack('v').first rescue 0
349
+ return out
350
+ end
351
+
352
+ # Read a 8-bit byte from a specific location in the data
353
+ def read_byte(data, offset)
354
+ out = data[offset,1].unpack('C').first rescue 0
355
+ return out
356
+ end
357
+
358
+ # Read in length bytes in as a String
359
+ def read_string(data, offset, length, encoding)
360
+ if "UTF-16".casecmp(encoding) == 0
361
+ out = data[offset, length].unpack('v*').pack('U*')
362
+ else
363
+ out = data[offset, length].unpack('C*').pack('U*')
364
+ end
365
+ return out
366
+ end
367
+
368
+ # Parse out an XmlTreeHeader
369
+ def parse_tree_header(chunk_header, data, offset)
370
+ line_num = read_word(data, offset+8)
371
+ comment = nil
372
+ if read_word(data, offset+12) != OFFSET_NO_ENTRY
373
+ comment = stringpool_main.values[read_word(data, offset+12)]
374
+ end
375
+ return XmlTreeHeader.new(chunk_header, line_num, comment)
376
+ end
377
+
378
+ # Parse out a StringPool chunk
379
+ def parse_stringpool(data, offset)
380
+ pool_header = ChunkHeader.new( read_short(data, offset),
381
+ read_short(data, offset+2),
382
+ read_word(data, offset+4) )
383
+
384
+ pool_string_count = read_word(data, offset+8)
385
+ pool_style_count = read_word(data, offset+12)
386
+ pool_flags = read_word(data, offset+16)
387
+ format_utf8 = (pool_flags & FLAG_UTF8) != 0
388
+ puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
389
+
390
+ pool_string_offset = read_word(data, offset+20)
391
+ pool_style_offset = read_word(data, offset+24)
392
+
393
+ values = Array.new()
394
+ i = 0
395
+ while i < pool_string_count
396
+ # Read the string value
397
+ index = i * 4 + (offset+28)
398
+ offset_addr = pool_string_offset + offset + read_word(data, index)
399
+ if format_utf8
400
+ length = read_byte(data, offset_addr)
401
+ if (length & 0x80) != 0
402
+ length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
403
+ end
404
+
405
+ values << read_string(data, offset_addr + 2, length, "UTF-8")
406
+ else
407
+ length = read_short(data, offset_addr)
408
+ if (length & 0x8000) != 0
409
+ #There is one more length value before the data
410
+ length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
411
+ values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
412
+ else
413
+ # Read the data
414
+ values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
415
+ end
416
+ end
417
+
418
+ i += 1
419
+ end
420
+
421
+ return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
422
+ end
423
+
424
+ end
@@ -0,0 +1,102 @@
1
+ # Copyright (C) 2012 Dave Smith
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ # software and associated documentation files (the "Software"), to deal in the Software
5
+ # without restriction, including without limitation the rights to use, copy, modify,
6
+ # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
8
+ #
9
+ # The above copyright notice and this permission notice shall be included in all copies
10
+ # or substantial portions of the Software.
11
+ #
12
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15
+ # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17
+ # DEALINGS IN THE SOFTWARE.
18
+
19
+ ##
20
+ # Constants for configuration flags
21
+ module ResConfiguration
22
+
23
+ ACONFIGURATION_ORIENTATION_ANY = 0x0000
24
+ ACONFIGURATION_ORIENTATION_PORT = 0x0001
25
+ ACONFIGURATION_ORIENTATION_LAND = 0x0002
26
+ ACONFIGURATION_ORIENTATION_SQUARE = 0x0003
27
+
28
+ ACONFIGURATION_TOUCHSCREEN_ANY = 0x0000
29
+ ACONFIGURATION_TOUCHSCREEN_NOTOUCH = 0x0001
30
+ ACONFIGURATION_TOUCHSCREEN_STYLUS = 0x0002
31
+ ACONFIGURATION_TOUCHSCREEN_FINGER = 0x0003
32
+
33
+ ACONFIGURATION_DENSITY_DEFAULT = 0
34
+ ACONFIGURATION_DENSITY_LOW = 120
35
+ ACONFIGURATION_DENSITY_MEDIUM = 160
36
+ ACONFIGURATION_DENSITY_TV = 213
37
+ ACONFIGURATION_DENSITY_HIGH = 240
38
+ ACONFIGURATION_DENSITY_XHIGH = 320
39
+ ACONFIGURATION_DENSITY_XXHIGH = 480
40
+ ACONFIGURATION_DENSITY_NONE = 0xffff
41
+
42
+ ACONFIGURATION_KEYBOARD_ANY = 0x0000
43
+ ACONFIGURATION_KEYBOARD_NOKEYS = 0x0001
44
+ ACONFIGURATION_KEYBOARD_QWERTY = 0x0002
45
+ ACONFIGURATION_KEYBOARD_12KEY = 0x0003
46
+
47
+ ACONFIGURATION_NAVIGATION_ANY = 0x0000
48
+ ACONFIGURATION_NAVIGATION_NONAV = 0x0001
49
+ ACONFIGURATION_NAVIGATION_DPAD = 0x0002
50
+ ACONFIGURATION_NAVIGATION_TRACKBALL = 0x0003
51
+ ACONFIGURATION_NAVIGATION_WHEEL = 0x0004
52
+
53
+ ACONFIGURATION_KEYSHIDDEN_ANY = 0x0000
54
+ ACONFIGURATION_KEYSHIDDEN_NO = 0x0001
55
+ ACONFIGURATION_KEYSHIDDEN_YES = 0x0002
56
+ ACONFIGURATION_KEYSHIDDEN_SOFT = 0x0003
57
+
58
+ ACONFIGURATION_NAVHIDDEN_ANY = 0x0000
59
+ ACONFIGURATION_NAVHIDDEN_NO = 0x0001
60
+ ACONFIGURATION_NAVHIDDEN_YES = 0x0002
61
+
62
+ ACONFIGURATION_SCREENSIZE_ANY = 0x00
63
+ ACONFIGURATION_SCREENSIZE_SMALL = 0x01
64
+ ACONFIGURATION_SCREENSIZE_NORMAL = 0x02
65
+ ACONFIGURATION_SCREENSIZE_LARGE = 0x03
66
+ ACONFIGURATION_SCREENSIZE_XLARGE = 0x04
67
+
68
+ ACONFIGURATION_SCREENLONG_ANY = 0x00
69
+ ACONFIGURATION_SCREENLONG_NO = 0x1
70
+ ACONFIGURATION_SCREENLONG_YES = 0x2
71
+
72
+ ACONFIGURATION_UI_MODE_TYPE_ANY = 0x00
73
+ ACONFIGURATION_UI_MODE_TYPE_NORMAL = 0x01
74
+ ACONFIGURATION_UI_MODE_TYPE_DESK = 0x02
75
+ ACONFIGURATION_UI_MODE_TYPE_CAR = 0x03
76
+ ACONFIGURATION_UI_MODE_TYPE_TELEVISION = 0x04
77
+ ACONFIGURATION_UI_MODE_TYPE_APPLIANCE = 0x05
78
+
79
+ ACONFIGURATION_UI_MODE_NIGHT_ANY = 0x00
80
+ ACONFIGURATION_UI_MODE_NIGHT_NO = 0x1
81
+ ACONFIGURATION_UI_MODE_NIGHT_YES = 0x2
82
+
83
+ ACONFIGURATION_SCREEN_WIDTH_DP_ANY = 0x0000
84
+ ACONFIGURATION_SCREEN_HEIGHT_DP_ANY = 0x0000
85
+ ACONFIGURATION_SMALLEST_SCREEN_WIDTH_DP_ANY = 0x0000
86
+
87
+ ACONFIGURATION_MCC = 0x0001
88
+ ACONFIGURATION_MNC = 0x0002
89
+ ACONFIGURATION_LOCALE = 0x0004
90
+ ACONFIGURATION_TOUCHSCREEN = 0x0008
91
+ ACONFIGURATION_KEYBOARD = 0x0010
92
+ ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020
93
+ ACONFIGURATION_NAVIGATION = 0x0040
94
+ ACONFIGURATION_ORIENTATION = 0x0080
95
+ ACONFIGURATION_DENSITY = 0x0100
96
+ ACONFIGURATION_SCREEN_SIZE = 0x0200
97
+ ACONFIGURATION_VERSION = 0x0400
98
+ ACONFIGURATION_SCREEN_LAYOUT = 0x0800
99
+ ACONFIGURATION_UI_MODE = 0x1000
100
+ ACONFIGURATION_SMALLEST_SCREEN_SIZE = 0x2000
101
+
102
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apktools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dave Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rubyzip
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Library to assist reading resource data out of Android APKs
31
+ email: dave@xcellentcreations.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/apktools/apkresources.rb
37
+ - lib/apktools/apkxml.rb
38
+ - lib/apktools/resconfiguration.rb
39
+ homepage: http://github.com/devunwired/apktools
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.22
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: APKTools
63
+ test_files: []