Module: Pindo::AndroidResHelper

Extended by:
Executable
Defined in:
lib/pindo/module/android/android_res_helper.rb

Constant Summary collapse

ANDROID_ICON_DENSITIES =

Android icon 各密度尺寸定义

{
    'mdpi' => 48,
    'hdpi' => 72,
    'xhdpi' => 96,
    'xxhdpi' => 144,
    'xxxhdpi' => 192
}.freeze
ADAPTIVE_FOREGROUND_DENSITIES =

Android Adaptive Icon 前景层尺寸(108dp,按密度换算)参考: Android 官方 Adaptive Icon 资源尺寸建议

{
    'mdpi' => 108,
    'hdpi' => 162,
    'xhdpi' => 216,
    'xxhdpi' => 324,
    'xxxhdpi' => 432
}.freeze

Class Method Summary collapse

Methods included from Executable

capture_command, executable, execute_command, popen3, reader, which, which!

Class Method Details

.adaptive_icon_xml_resource_dir?(dir_path) ⇒ Boolean

mipmap-anydpi-v* 为 Adaptive Icon 的 layer XML 目录,清理时不可按 basename 删文件,否则会移除 ic_launcher.xml 等

Returns:

  • (Boolean)


147
148
149
# File 'lib/pindo/module/android/android_res_helper.rb', line 147

def self.adaptive_icon_xml_resource_dir?(dir_path)
    File.basename(dir_path.to_s).match?(/\Amipmap-anydpi-v\d+\z/i)
end

.clean_old_icons(res_dir: nil, old_icon_refs: []) ⇒ Boolean

清理所有旧的 icon 文件和自定义配置

Parameters:

  • res_dir (String) (defaults to: nil)

    res 目录路径

  • old_icon_refs (Array<Hash>) (defaults to: [])

    旧 icon 引用,格式: [{ type: “mipmap”, name: “xxx” }]

Returns:

  • (Boolean)

    是否成功清理



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/pindo/module/android/android_res_helper.rb', line 155

def self.clean_old_icons(res_dir:nil, old_icon_refs: [])
    return false unless res_dir && File.directory?(res_dir)

    begin
        cleaned_items = []

        # 1. 删除配置中引用的旧 icon 文件(根据资源类型与名称精确定位)
        configured_icon_count = 0
        old_icon_refs.each do |ref|
            next unless ref.is_a?(Hash)

            resource_type = ref[:type].to_s
            basename = ref[:name].to_s
            next if resource_type.empty? || basename.empty?

            candidate_dirs = Dir.glob(File.join(res_dir, "#{resource_type}-*"))
            candidate_dirs.each do |resource_dir|
                next unless File.directory?(resource_dir)
                next if adaptive_icon_xml_resource_dir?(resource_dir)

                configured_icon_count += remove_icon_files_with_same_basename(
                    mipmap_dir: resource_dir,
                    basename: basename
                )
            end
        end
        cleaned_items << "配置icon文件(#{configured_icon_count}个)" if configured_icon_count > 0

        # 2. 删除所有 mipmap 目录中的常见旧 icon 文件(兼容历史逻辑)
        old_png_count = 0
        mipmap_dirs = Dir.glob(File.join(res_dir, "mipmap-*"))

        mipmap_dirs.each do |mipmap_dir|
            next unless File.directory?(mipmap_dir)
            next if adaptive_icon_xml_resource_dir?(mipmap_dir)

            old_icon_basenames = [
                'app_icon',
                'app_icon_round',
                'ic_launcher',
                'ic_launcher_round'
            ]

            old_icon_basenames.each do |basename|
                old_png_count += remove_icon_files_with_same_basename(
                    mipmap_dir: mipmap_dir,
                    basename: basename
                )
            end
        end

        cleaned_items << "旧icon文件(#{old_png_count}个)" if old_png_count > 0

        if cleaned_items.any?
            puts "  ✓ 已清理: #{cleaned_items.join(', ')}"
        end

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("清理旧 icon 失败: #{e.message}")
        return false
    end
end

.color_resource_exists?(res_dir:, resource_name:) ⇒ Boolean

检查 colors.xml 中是否声明了指定 color 资源(任意 values 变体目录)

Parameters:

  • res_dir (String)

    res 目录路径

  • resource_name (String)

    资源名

Returns:

  • (Boolean)


291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/pindo/module/android/android_res_helper.rb', line 291

def self.color_resource_exists?(res_dir:, resource_name:)
    values_dirs = Dir.glob(File.join(res_dir, "values*")).select { |d| File.directory?(d) }
    return false if values_dirs.empty?

    values_dirs.any? do |dir|
        colors_file = File.join(dir, "colors.xml")
        next false unless File.file?(colors_file)

        content = File.read(colors_file)
        content.match?(/<color\s+name=["']#{Regexp.escape(resource_name)}["']/)
    end
rescue
    false
end

.create_icon_for_density(icon_name: nil, new_icon_dir: nil, density: nil, size: nil) ⇒ Hash

创建单个密度的Android icon(同时生成方形和圆形命名)

Parameters:

  • icon_name (String) (defaults to: nil)

    原始icon路径

  • new_icon_dir (String) (defaults to: nil)

    输出目录

  • density (String) (defaults to: nil)

    密度名称(mdpi, hdpi等)

  • size (Integer) (defaults to: nil)

    图标尺寸

Returns:

  • (Hash)

    生成的图标路径,失败返回nil



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/pindo/module/android/android_res_helper.rb', line 37

def self.create_icon_for_density(icon_name:nil, new_icon_dir:nil, density:nil, size:nil)
    unless File.exist?(icon_name)
        Funlog.instance.fancyinfo_error("文件不存在: #{icon_name}")
        return nil
    end

    begin
        density_dir = File.join(new_icon_dir, "mipmap-#{density}")
        FileUtils.mkdir_p(density_dir)

        result = {}

        # 生成两个 legacy launcher 文件:
        # ic_launcher.png 和 ic_launcher_round.png
        # 注意:两个都是方形,不切圆角,只是文件名不同
        ['ic_launcher.png', 'ic_launcher_round.png'].each do |filename|
            output_path = File.join(density_dir, filename)

            command = [
                'sips',
                # 强制输出 PNG,避免源图为 JPEG/WebP 时仅缩放仍写出 JPEG 导致扩展名与内容不符(AAPT2 报错)
                '-s', 'format', 'png',
                '--matchTo', '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
                '-z', size.to_s, size.to_s,
                icon_name,
                '--out', output_path
            ]

            Executable.capture_command('sips', command, capture: :out)

            unless File.exist?(output_path)
                Funlog.instance.fancyinfo_error("生成icon失败: #{output_path}")
                return nil
            end

            result[filename] = output_path
        end

        # 生成 adaptive icon 前景层(用于 mipmap-anydpi-v26/ic_launcher*.xml 引用)
        adaptive_size = ADAPTIVE_FOREGROUND_DENSITIES[density]
        if adaptive_size
            foreground_filename = 'ic_launcher_foreground.png'
            foreground_output_path = File.join(density_dir, foreground_filename)
            foreground_command = [
                'sips',
                '-s', 'format', 'png',
                '--matchTo', '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
                '-z', adaptive_size.to_s, adaptive_size.to_s,
                icon_name,
                '--out', foreground_output_path
            ]

            Executable.capture_command('sips', foreground_command, capture: :out)

            unless File.exist?(foreground_output_path)
                Funlog.instance.fancyinfo_error("生成icon失败: #{foreground_output_path}")
                return nil
            end

            result[foreground_filename] = foreground_output_path
        end

        return result
    rescue => e
        Funlog.instance.fancyinfo_error("生成#{density}密度icon时出错: #{e.message}")
        return nil
    end
end

.create_icons(icon_name: nil, new_icon_dir: nil) ⇒ Hash

创建所有密度的Android icons

Parameters:

  • icon_name (String) (defaults to: nil)

    原始icon路径(建议512x512或1024x1024)

  • new_icon_dir (String) (defaults to: nil)

    输出目录

Returns:

  • (Hash)

    所有生成的图标路径,按密度分组,失败返回nil



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/pindo/module/android/android_res_helper.rb', line 110

def self.create_icons(icon_name:nil, new_icon_dir:nil)
    unless File.exist?(icon_name)
        Funlog.instance.fancyinfo_error("文件不存在: #{icon_name}")
        return nil
    end

    begin
        FileUtils.mkdir_p(new_icon_dir)
    rescue => e
        Funlog.instance.fancyinfo_error("创建输出目录失败: #{e.message}")
        return nil
    end

    all_icons = {}
    success_count = 0

    ANDROID_ICON_DENSITIES.each do |density, size|
        icons = create_icon_for_density(
            icon_name: icon_name,
            new_icon_dir: new_icon_dir,
            density: density,
            size: size
        )

        if icons
            all_icons[density] = icons
            success_count += 1
        else
            Funlog.instance.fancyinfo_error("#{density}密度icon生成失败,跳过")
        end
    end

    # 如果所有密度都失败了,返回 nil
    return success_count > 0 ? all_icons : nil
end

.drawable_resource_exists?(res_dir:, resource_name:) ⇒ Boolean

检查 drawable 资源是否存在(任意 drawable 变体目录、任意常见后缀)

Parameters:

  • res_dir (String)

    res 目录路径

  • resource_name (String)

    资源名

Returns:

  • (Boolean)


310
311
312
313
314
315
316
317
318
319
# File 'lib/pindo/module/android/android_res_helper.rb', line 310

def self.drawable_resource_exists?(res_dir:, resource_name:)
    drawable_dirs = Dir.glob(File.join(res_dir, "drawable*")).select { |d| File.directory?(d) }
    return false if drawable_dirs.empty?

    drawable_dirs.any? do |dir|
        Dir.glob(File.join(dir, "#{resource_name}.*")).any? { |f| File.file?(f) }
    end
rescue
    false
end

.ensure_standard_adaptive_icon_xml(res_dir: nil) ⇒ Boolean

确保存在标准 adaptive icon XML(Android 8+)若项目已存在 mipmap-anydpi-v26,则保持原文件不覆盖

Parameters:

  • res_dir (String) (defaults to: nil)

    res 目录路径

Returns:

  • (Boolean)

    是否成功处理



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/pindo/module/android/android_res_helper.rb', line 223

def self.ensure_standard_adaptive_icon_xml(res_dir:nil)
    return false unless res_dir && File.directory?(res_dir)

    begin
        adaptive_dir = File.join(res_dir, "mipmap-anydpi-v26")
        if Dir.exist?(adaptive_dir)
            puts "  ✓ 检测到现有 adaptive icon 配置,保持不覆盖: mipmap-anydpi-v26"
            return true
        end

        FileUtils.mkdir_p(adaptive_dir)

        # Android 官方建议:
        # background 使用 @color/ic_launcher_background
        # foreground 使用 @drawable/ic_launcher_foreground
        # 若项目缺失上述资源,则回退到可用引用,避免构建失败。
        background_drawable = color_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_background") ?
            "@color/ic_launcher_background" : "@android:color/white"
        # 优先级:
        # 1) drawable/ic_launcher_foreground(兼容已有项目)
        # 2) mipmap/ic_launcher_foreground(本工具自动生成)
        # 3) 回退到 legacy icon,避免构建失败
        foreground_drawable = if drawable_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
            "@drawable/ic_launcher_foreground"
        elsif mipmap_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
            "@mipmap/ic_launcher_foreground"
        else
            "@mipmap/ic_launcher"
        end
        round_foreground_drawable = if drawable_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
            "@drawable/ic_launcher_foreground"
        elsif mipmap_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
            "@mipmap/ic_launcher_foreground"
        else
            "@mipmap/ic_launcher_round"
        end

        launcher_xml = <<~XML
            <?xml version="1.0" encoding="utf-8"?>
            <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
                <background android:drawable="#{background_drawable}" />
                <foreground android:drawable="#{foreground_drawable}" />
            </adaptive-icon>
        XML

        launcher_round_xml = <<~XML
            <?xml version="1.0" encoding="utf-8"?>
            <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
                <background android:drawable="#{background_drawable}" />
                <foreground android:drawable="#{round_foreground_drawable}" />
            </adaptive-icon>
        XML

        File.write(File.join(adaptive_dir, "ic_launcher.xml"), launcher_xml)
        File.write(File.join(adaptive_dir, "ic_launcher_round.xml"), launcher_round_xml)

        puts "  ✓ 缺失时自动创建 adaptive icon XML: mipmap-anydpi-v26"
        true
    rescue => e
        Funlog.instance.fancyinfo_error("处理 adaptive icon XML 失败: #{e.message}")
        false
    end
end

.extract_icon_resource_refs_from_manifest(manifest_path: nil) ⇒ Array<Hash>

从 AndroidManifest.xml 中提取 application 的 icon 资源引用

Parameters:

  • manifest_path (String) (defaults to: nil)

    AndroidManifest.xml 文件路径

Returns:

  • (Array<Hash>)

    资源引用数组,格式: [{ type: “mipmap”, name: “ic_launcher”, kind: “icon” }]



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/pindo/module/android/android_res_helper.rb', line 339

def self.extract_icon_resource_refs_from_manifest(manifest_path:nil)
    return [] unless manifest_path && File.exist?(manifest_path)

    begin
        require 'nokogiri'

        content = File.read(manifest_path)
        doc = Nokogiri::XML(content)
        app_node = doc.at_xpath('//application')
        return [] unless app_node

        refs = []
        {
            'android:icon' => 'icon',
            'android:roundIcon' => 'roundIcon'
        }.each do |attr_name, kind|
            attr_value = app_node[attr_name]
            next unless attr_value.is_a?(String)

            if (matched = attr_value.match(/^@([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)$/))
                refs << { type: matched[1], name: matched[2], kind: kind }
            end
        end

        refs.uniq { |ref| [ref[:type], ref[:name], ref[:kind]] }
    rescue => e
        Funlog.instance.fancyinfo_error("解析 AndroidManifest icon 配置失败: #{e.message}")
        []
    end
end

.find_best_icon_source_file(source_dir:, round: false) ⇒ String?

在目录内查找最匹配的 icon 文件

Parameters:

  • source_dir (String)

    目录路径

  • round (Boolean) (defaults to: false)

    是否查找 round icon

Returns:

  • (String, nil)

    文件绝对路径



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/pindo/module/android/android_res_helper.rb', line 374

def self.find_best_icon_source_file(source_dir:, round: false)
    return nil unless source_dir && File.directory?(source_dir)

    exact_names = round ? %w[ic_launcher_round app_icon_round] : %w[ic_launcher app_icon]
    ext_order = %w[png webp]

    exact_names.each do |basename|
        ext_order.each do |ext|
            candidate = File.join(source_dir, "#{basename}.#{ext}")
            return candidate if File.file?(candidate)
        end
    end

    image_files = Dir.glob(File.join(source_dir, "*.{png,webp}")).select { |f| File.file?(f) }
    return nil if image_files.empty?

    round_match = /(?:^|[_-])(round|launcher_round|ic_round)(?:[_-]|$)/i
    non_round_files = image_files.reject { |f| File.basename(f, File.extname(f)).match?(round_match) }
    round_files = image_files.select { |f| File.basename(f, File.extname(f)).match?(round_match) }

    selected = round ? round_files.first : non_round_files.first
    selected || image_files.first
end

.install_icon(proj_dir: nil, new_icon_dir: nil) ⇒ Boolean

安装Android icon到项目中(完全标准化方案)

Parameters:

  • proj_dir (String) (defaults to: nil)

    Android项目目录

  • new_icon_dir (String) (defaults to: nil)

    icon文件目录(包含各密度文件夹)

Returns:

  • (Boolean)

    是否成功安装



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/pindo/module/android/android_res_helper.rb', line 466

def self.install_icon(proj_dir:nil, new_icon_dir:nil)
    unless proj_dir && File.directory?(proj_dir)
        Funlog.instance.fancyinfo_error("项目目录不存在: #{proj_dir}")
        return false
    end

    unless new_icon_dir && File.directory?(new_icon_dir)
        Funlog.instance.fancyinfo_error("Icon目录不存在: #{new_icon_dir}")
        return false
    end

    begin
        # 1. 查找 res 目录(支持 launcher、app 和 unityLibrary)
        possible_res_dirs = [
            File.join(proj_dir, "launcher/src/main/res"),
            File.join(proj_dir, "app/src/main/res"),
            File.join(proj_dir, "unityLibrary/src/main/res")
        ]

        res_dir = possible_res_dirs.find { |dir| File.directory?(dir) }

        # 如果没找到,创建默认目录
        unless res_dir
            res_dir = File.join(proj_dir, "launcher/src/main/res")
            FileUtils.mkdir_p(res_dir)
        end

        # 2. 查找 AndroidManifest.xml 并提取旧 icon 引用
        manifest_paths = [
            File.join(proj_dir, "launcher/src/main/AndroidManifest.xml"),
            File.join(proj_dir, "app/src/main/AndroidManifest.xml"),
            File.join(proj_dir, "src/main/AndroidManifest.xml")
        ]

        manifest_path = manifest_paths.find { |path| File.exist?(path) }
        old_icon_refs = extract_icon_resource_refs_from_manifest(manifest_path: manifest_path)

        # 3. 清理所有旧的 icon 资源
        clean_old_icons(res_dir: res_dir, old_icon_refs: old_icon_refs)

        # 4. 更新 AndroidManifest.xml 为标准 icon 配置

        if manifest_path
            update_manifest_to_standard(manifest_path: manifest_path)
        end

        # 4.5 更新所有 XML 文件中的图标引用
        update_all_xml_icon_references(proj_dir: proj_dir, old_icon_refs: old_icon_refs)

        # 5. 安装标准 icon 文件
        success_count = 0
        densities_installed = []

        ANDROID_ICON_DENSITIES.keys.each do |density|
            source_dir = File.join(new_icon_dir, "mipmap-#{density}")
            target_dir = File.join(res_dir, "mipmap-#{density}")

            next unless File.directory?(source_dir)

            # 创建目标目录
            FileUtils.mkdir_p(target_dir) unless File.directory?(target_dir)

            # 先清理同名不同后缀,避免 aapt2 Duplicate resources (png/webp 并存)
            remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher")
            remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher_round")
            remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher_foreground")

            icon_source = find_best_icon_source_file(source_dir: source_dir, round: false)
            round_source = find_best_icon_source_file(source_dir: source_dir, round: true)
            foreground_source = find_best_icon_source_file(source_dir: source_dir, round: false)
            explicit_foreground_png = File.join(source_dir, "ic_launcher_foreground.png")
            explicit_foreground_webp = File.join(source_dir, "ic_launcher_foreground.webp")
            foreground_source = explicit_foreground_png if File.file?(explicit_foreground_png)
            foreground_source = explicit_foreground_webp if File.file?(explicit_foreground_webp)
            files_to_copy = [
                { source_file: icon_source, target: 'ic_launcher.png' },
                { source_file: round_source, target: 'ic_launcher_round.png' },
                { source_file: foreground_source, target: 'ic_launcher_foreground.png' }
            ]

            density_success = 0
            files_to_copy.each do |file_map|
                source_file = file_map[:source_file]
                target_file = File.join(target_dir, file_map[:target])

                if source_file && File.exist?(source_file)
                    FileUtils.cp(source_file, target_file)
                    success_count += 1
                    density_success += 1
                else
                    Funlog.instance.fancyinfo_error("源文件不存在: #{file_map[:target]}")
                end
            end

            densities_installed << density if density_success > 0
        end

        if success_count > 0
            puts "  ✓ 已安装标准 icon: #{densities_installed.join(', ')} (共#{success_count}个文件)"
        end

        # 6. 确保 adaptive icon XML 存在(已有配置不覆盖,缺失时补齐)
        ensure_standard_adaptive_icon_xml(res_dir: res_dir)

        return success_count > 0

    rescue => e
        Funlog.instance.fancyinfo_error("安装icon时出错: #{e.message}")
        puts e.backtrace.join("\n") if ENV['PINDO_DEBUG']
        return false
    end
end

.mipmap_resource_exists?(res_dir:, resource_name:) ⇒ Boolean

检查 mipmap 资源是否存在(任意 mipmap 变体目录、任意常见后缀)

Parameters:

  • res_dir (String)

    res 目录路径

  • resource_name (String)

    资源名

Returns:

  • (Boolean)


325
326
327
328
329
330
331
332
333
334
# File 'lib/pindo/module/android/android_res_helper.rb', line 325

def self.mipmap_resource_exists?(res_dir:, resource_name:)
    mipmap_dirs = Dir.glob(File.join(res_dir, "mipmap*")).select { |d| File.directory?(d) }
    return false if mipmap_dirs.empty?

    mipmap_dirs.any? do |dir|
        Dir.glob(File.join(dir, "#{resource_name}.*")).any? { |f| File.file?(f) }
    end
rescue
    false
end

.remove_icon_files_with_same_basename(mipmap_dir:, basename:) ⇒ Integer

删除目录下同名不同后缀图标(例如 ic_launcher.png / ic_launcher.webp)

Parameters:

  • mipmap_dir (String)

    mipmap 目录路径

  • basename (String)

    图标基础名称(不带后缀)

Returns:

  • (Integer)

    删除的文件数量



402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/pindo/module/android/android_res_helper.rb', line 402

def self.remove_icon_files_with_same_basename(mipmap_dir:, basename:)
    return 0 unless File.directory?(mipmap_dir)
    return 0 if basename.nil? || basename.empty?

    removed = 0
    Dir.glob(File.join(mipmap_dir, "#{basename}.*")).each do |file_path|
        next unless File.file?(file_path)

        File.delete(file_path)
        removed += 1
    end
    removed
end

.update_all_xml_icon_references(proj_dir: nil, old_icon_refs: []) ⇒ Boolean

更新所有 XML 文件中的图标引用(从旧名称更新为标准名称)

Parameters:

  • proj_dir (String) (defaults to: nil)

    Android项目目录

  • old_icon_refs (Array<Hash>) (defaults to: [])

    旧 icon 引用

Returns:

  • (Boolean)

    是否成功更新



583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'lib/pindo/module/android/android_res_helper.rb', line 583

def self.update_all_xml_icon_references(proj_dir: nil, old_icon_refs: [])
    return false unless proj_dir && File.directory?(proj_dir)

    begin
        updated_files = []

        # 查找所有 XML 文件
        xml_files = Dir.glob(File.join(proj_dir, "**/*.xml"))
            .reject { |f| f.include?("/build/") || f.include?("/.gradle/") }

        xml_files.each do |xml_file|
            content = File.read(xml_file)
            original_content = content.dup

            # 替换 manifest 中已解析到的旧图标引用
            old_icon_refs.each do |ref|
                next unless ref.is_a?(Hash)

                old_name = ref[:name].to_s
                kind = ref[:kind].to_s
                next if old_name.empty?

                new_name = kind == "roundIcon" ? "ic_launcher_round" : "ic_launcher"
                content.gsub!(/@mipmap\/#{Regexp.escape(old_name)}(?=["'\s>])/, "@mipmap/#{new_name}")
                content.gsub!(/@drawable\/#{Regexp.escape(old_name)}(?=["'\s>])/, "@mipmap/#{new_name}")
            end

            # 替换常见旧图标引用为标准名称
            # app_icon -> ic_launcher
            # app_icon_round -> ic_launcher_round
            # 使用正则表达式精确匹配,避免替换 app_icon_topleft 等变体
            # (?=["'\s>]) 表示后面必须是引号、空格或尖括号(但不包含在替换结果中)
            content.gsub!(/@mipmap\/app_icon_round(?=["'\s>])/, '@mipmap/ic_launcher_round')
            content.gsub!(/@mipmap\/app_icon(?=["'\s>])/, '@mipmap/ic_launcher')
            content.gsub!(/@drawable\/app_icon_round(?=["'\s>])/, '@mipmap/ic_launcher_round')
            content.gsub!(/@drawable\/app_icon(?=["'\s>])/, '@mipmap/ic_launcher')

            # 如果内容有变化,保存文件
            if content != original_content
                File.write(xml_file, content)
                relative_path = xml_file.sub(proj_dir + '/', '')
                updated_files << relative_path
                puts "  ✓ 更新图标引用: #{relative_path}"
            end
        end

        if updated_files.empty?
            puts "  ✓ 所有 XML 文件的图标引用已是标准配置"
        else
            puts "  ✓ 已更新 #{updated_files.length} 个文件的图标引用"
        end

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("更新 XML 图标引用失败: #{e.message}")
        return false
    end
end

.update_manifest_icon_attributes_in_place(xml_content) ⇒ Object



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/pindo/module/android/android_res_helper.rb', line 438

def self.update_manifest_icon_attributes_in_place(xml_content)
    icon_value = "@mipmap/ic_launcher"
    round_value = "@mipmap/ic_launcher_round"

    # 仅修改 <application ...> 开始标签内的 android:icon/android:roundIcon,避免整体格式化。
    xml_content.sub(/<application\b[^>]*>/m) do |app_tag|
        updated_tag = app_tag.dup

        if updated_tag.match?(/\bandroid:icon\s*=\s*["'][^"']*["']/)
            updated_tag.gsub!(/\bandroid:icon\s*=\s*(["'])[^"']*\1/, %(android:icon="#{icon_value}"))
        else
            updated_tag.sub!(/<application\b/, %(<application android:icon="#{icon_value}"))
        end

        if updated_tag.match?(/\bandroid:roundIcon\s*=\s*["'][^"']*["']/)
            updated_tag.gsub!(/\bandroid:roundIcon\s*=\s*(["'])[^"']*\1/, %(android:roundIcon="#{round_value}"))
        else
            updated_tag.sub!(/<application\b/, %(<application android:roundIcon="#{round_value}"))
        end

        updated_tag
    end
end

.update_manifest_to_standard(manifest_path: nil) ⇒ Boolean

更新 AndroidManifest.xml 为标准配置

Parameters:

  • manifest_path (String) (defaults to: nil)

    AndroidManifest.xml 文件路径

Returns:

  • (Boolean)

    是否成功更新



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/pindo/module/android/android_res_helper.rb', line 419

def self.update_manifest_to_standard(manifest_path:nil)
    return false unless manifest_path && File.exist?(manifest_path)

    begin
        content = File.read(manifest_path)
        updated = update_manifest_icon_attributes_in_place(content)
        return false unless updated

        File.write(manifest_path, updated)

        puts "  ✓ AndroidManifest.xml 已更新为标准 icon 配置"

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("更新 AndroidManifest.xml 失败: #{e.message}")
        return false
    end
end