apktools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []