Stark Wong 的個人開發網站
 


 此頁面:更新於 2019 年 5 月 2 日 12 時 04 分 09 秒,頁面處理需時 0.0012 秒
 網站內容版權所有(C)Stark Wong。頁面(不包括檔案)可自由連結。網站系統版本 1.90-AngularJSBase (2015/9/27)
 
網站地圖

去網路化 - okhttp 4.3

某程式的新版本改用了 okhttp 4.3 或以上的版本,沿用的去網路化方式不再適用。

在舊版本的 okhttp 中,Dns$1.lookup() 裡可以直接跳到 :cond_0 以拋出 UnknownHostException,然而在新版本的 okhttp,Dns$1 被 Dns$Companion$DnsSystem 所取代,而 lookup() 方法裡並沒有檢查參數是否空值 (即原來 if-eqz p1, :cond_0),改為依賴 InetAddress.getAllByName() 拋出 NullPointerException,然後再改拋 UnknownHostException。

catch_0 就是改拋 UnknownHostException 的位置,但由於它會將原來捕捉到的例外存到 v0 並在後面插入到例外原因,所以我們不能直接用 goto 跳到那個位置。理論上我們可以把 move-exception v0 和 .line 3 後面 initCause 那句刪掉就應該可以用 goto,但有沒有更簡單的方法呢?

答案是有的!既然我們知道這個 lookup() 方法會捕捉 NullPointerException,那麼我們讓 InetAddress.getAllByName() 拋出 NullPointerException 就行。至於方法當然就是直接讓傳入參數變成 null!我們只需要在呼叫 InetAddress.getAllByName() 前 (在 .line 1 與 :try_start_0 之間) 加 “const/4 p1, 0x0” 就可以讓 lookup() 的第一個參數變成 null (Dalvik 中物件是以地址方式存放在變數中,所以將地址改成 0 即為 null),然後在呼叫 InetAddress.getAllByName() 的時候就會拋出 NullPointerException,至此對 okhttp 4.3 的去網路化完成。


撰寫於:2022/5/25 18:00:09 / 回應已關閉
正在讀取回響內容...
請小心為你的 Android Layout 改名,否則可能會出現奇怪的問題

在我們所開發的程式中,其中一個是以 aar 方式封裝播放器的介面,再加上第三方的 SDK (同樣以 aar 方式) 亦有播放介面,然後一併交由另一個團隊進行整合,一直都沒有問題。然而就在一個比較大的播放器介面更新時,我們改動過一些 Layout XML 並將重用的部份分離出一些新的 Layout XML,結果出現一個非常奇怪的問題:

  • 我們有一個自己寫的程式可以測試介面 aar 以及第三方播放 SDK 的功能,用那個程式測試時沒有問題
  • 對方團隊那邊執行我們的測試程式時也沒有發現問題
  • 對方團隊在將所有 aar 整合後,發現第三方播放器 SDK 開啟時發生 NullPointerException

初步分析

當時我們覺得很奇怪,在 Android Studio 的反編譯功能協助下,我們發現 NullPointerException 的原因是前面的 findViewById() 傳回 Null 值,但既然該播放器 SDK 單獨使用並無問題,而 findViewById() 指向的 ID 又確實存在 (否則應該無法編譯),所以肯定可以排除是第三方播放器 SDK 的問題。那樣的話我們只能在該次介面更新的範圍進行測試,最後發現把重用的 Layout XML 刪除就沒有問題,然後就一直到現在都再沒問題。

進階測試

我一直對這個解決方法都有疑問,直至最近想起,Android 上無論是 Java 還是 Kotlin 都有 package 的概念 (也就是只要大家的 package 不同,同樣名稱的源碼檔案其實是不同的類,只要匯入時使用正確的 package 名就能使用正確的類),至於 XML 呢?於是我就寫了一個小程式來測試這個問題。

測試程式可以在 https://github.com/starkwong/AndroidLayoutCollison 裡找到。

測試程式的原理很簡單,首先有一個程式庫 library1 內含 MainActivity,而它會使用 R.layout.activity_main (內含 ID 為 R.id.textview1 的 TextView),然後主程式也有 MainActivity 和 R.layout.activity_main (內含 ID 為 R.id.textview2 的 TextView)。library1 的 MainActivity 裡另加了修改 R.id.textview2 文字的語句,正常來說是完全沒問題的。然而當執行這個程式時,嘗試開啟 library1.MainActivity 的就會發生 NullPointerException:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.mergetest/com.example.library1.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference

這正正就是當時我們所遇到的問題!很明顯地當 Android SDK 進行資源合併時,可同時存在的 XML 內容可以合併 (例如是 strings.xml,colors.xml 等,而且由於裡面的 ID 會轉化成數字 ID 並對應到有區分 package 的 R 類,所以並不會有同名問題,除非是同一個 package 裡有兩個資源目錄結構有相同的 xml 時則會在編譯時報錯),但同名的 Layout XML 由於不能合併,Android SDK 現在的處理似乎只會使用其中一個,而我們遇到的問題估計是 build.gradle 中兩個 aar 的匯入次序不同所致。

衍生行為

再確認這個行為後,我突然又想到…既然我可以取代別人程式裡的 Layout XML,那麼我可以在不修改或 Extend 別人 Activity 的情況下修改顯示的內容或邏輯嗎?

於是我在上面的測試程式上再加入了 library2,裡面的 MainActivity 會使用 R.layout.activity_library (內含 ID 為 R.id.textview3 的 TextView),然後在整個 Layout XML 複製到主程式,並將 Root View 的 ConstraintLayout 改成我們自己的繼承版本 HijackConstraintLayout,並在裡面修改 R.id.textview3 的文字。

當執行測試程式時,我發現我的想法是完全正確,可以經由取代 Layout XML 來修改其他程式庫中 Activity 的行為,就算是把裡面按鈕的 OnClickListener 換掉也很簡單。

結論

從以上兩個測試,可以歸納出以下的結論:

  1. 不同套件間的同名 Layout XML,Android SDK 只會使用其中一個
  2. 但沒有使用的 Layout XML 裡面的 ID 仍然會被加到 R 類,所以不會編譯失敗
  3. 當第三方套件使用被替換的 Layout XML 後,引用該 Layout XML 不存在的 View 就會導致程式錯誤
  4. 第三方套件為免出現 Layout XML 被意外替換的問題,應該使用非常用的名稱
  5. 不同套件間的同名 ID 具有相同的資源編號,所以可以對第三方 Activity 進行 Layout XML 替換與邏輯修改

撰寫於:2022/1/23 17:08:53 / 回應已關閉
正在讀取回響內容...
如何在 NGINX 進行多條件式流程控制

最近我們在加入一個新功能時需要與第三方合作,以識別第三方服務的使用者。然而過程並沒有想像中簡單,而且還涉及多重條件,雖然我們大可以使用 PHP 來處理這個問題,但由於該伺服器本身提供這個功能僅為兼任角色,可免的情況下也不想使用 FCGI,因為始終會消耗比較多系統資源。這裡簡單回顧一下整個過程。

  1. 伺服器回應特定標頭
    最初雙方協議好是第三方網絡在把回應傳給客戶端時會自動植入特定標頭,這樣客戶端程式只需要檢測出特定標頭的存在時就通過識別檢查。對伺服器端來說只需要提供一個特定 URL 就可以。這個在 NGINX 下的特定路徑設定就一句令伺服器提供空白回應:

    return 200 ‘’;

  2. 伺服器回傳特定標頭
    然後我們在 UAT 時發現客戶端並沒有收到特定的標頭,在對方查證後才發現原來他們植入特定標頭是植入到遠端請求而並非回應,這樣我們就需要設定伺服器端回傳特定的標頭 (這裡假設特定標頭為 X-Header1, X-Header2 及 X-Header3):

    add_header X-Header1 $http_x_header1;
    add_header X-Header2 $http_x_header2;
    add_header X-Header3 $http_x_header3;
    return 200 ‘’;

  3. 加入 IP 檢測
    在進行以上修改後再次測試發現,並且所有客戶端均能接收到其中一個標頭,經對方檢測後發現有技術問題導致出現問題,要解決就需要加入客戶端 IP 檢測,我們若檢測到符合 IP 範圍的時候就會取代 X-Header1 令客戶端可通過測試:

    (以下這段必須加到全域設定,也就是說無論是否相關的請求也會進行 IP 區域檢查)
    geo $client_ip $isp_geo {
    default 0;
    xxx.xxx.xxx.0/21 geo;
    yyy.yyy.yyy.0/21 geo;
    zzz.zzz.zzz.0/21 geo;
    255.255.255.255 -1;
    }

    (以下是特定路徑的設定)
    if ($isp_geo = "geo") {
    add_header X-Header1 ip;
    }
  4. if ($isp_geo != "geo") {
    add_header X-Header1 $http_x_header1;
    add_header X-Header2 $http_x_header2;
    add_header X-Header3 $http_x_header3;
    }
    return 200 ‘’;

  5. 需要先傳回特定標頭後才植入 ip
    雖然上列的修改基本上已經能解決偵測的問題,不過我方團隊想知道究竟有多少客戶能通過原來的標頭檢測。上列的修改雖然能解決檢測,但卻不能解決這個新要求,因為基本上會在伺服器端收到特定標頭的請求都必然通過 IP 測試,那樣就根本就不會看到有回傳真正的特定標頭的客戶。這樣的邏輯一般作法只需要用多個 if 條件及/或 if else 就能輕鬆解決。然而 NGINX 的 if 條件式是非常區限性:
    - if 裡面只能檢查單一條件
    - 不支援 else
    - 不支援巢狀式 if (即 if 區域裡面不可以再有 if)

    所以我們需要只用 if = 和 if != 才能解決問題,那就需要創造一個可讓 if 處理的值。既然我們知道 X-Header1, X-Header2 及 X-Header3 的值都可以是空值,那麼我們可以創造一個值來指出是否只有 IP 通過,例如:

    set $result "${http_x_header1}${http_x_header2}${http_x_header3}${isp_geo}";

    這樣,如果 $result 只有 “geo” 的話就代表所有特定標頭都不存在而只有 IP 檢測通過 (即等於 if ($http_x_header1 = ‘’ && $http_x_header2 = ‘’ && $http_x_header3 = ‘’ && $isp_geo = ‘geo’)),那時候我們就可以放心植入 ip 值到 X-Header1了。

    if ($result = "geo") {
    add_header X-Header1 ip;
    }
    if ($result != "geo") {
    add_header X-Header1 $http_x_header1;
    add_header X-Header2 $http_x_header2;
    add_header X-Neader3 $http_x_header3;
    }
    return 200 ‘’;

    這樣修改下,如果客戶有特定標頭,就會回傳特定標頭而不論是否通過 IP 檢測;而如果用戶沒有特定標頭但通過 IP 檢測,就只會回應 ip 作為 X-Header1 的值;而如果客戶兩個條件都不符合,則仍然會回傳特定標頭,但因為特定標頭不存在而不會進行回傳。


撰寫於:2021/12/19 14:21:02 / 回應已關閉
正在讀取回響內容...
有關 APK 使用 Apktool 重建後無法在 Android 11 或以上的裝置

我在之前修改 APK 用 Apktool 重建後曾經試過安裝在 Android 11 或以上裝置會失敗,但由於再重建後問題又消失了,讓我以為是由於我用的 Apktool 版本問題所導致,誰知道今天又再次遇到同一個問題。

症狀

APK 在重建時不會遇到任何問題,但當使用 adb install 安裝時則會顯示如下的錯誤:

Performing Streamed Install
adb: failed to install xxx.apk: Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]

其實這個問題在官方 issue tracker 裡也有報告 (https://github.com/iBotPeaches/Apktool/issues/2421),然而問題直到最新的 Apktool 2.6.0 仍然沒有解決。

原因及解決方法

從錯誤訊息中看到,目標為 Android 11 的 APK 中的 resources.arsc 檔案必須符合 2 個條件:

  1. 檔案必須沒有進行壓縮
  2. 檔案必須對齊到4位元組的邊界

將重建後的 APK 用壓縮程式開啟,可以看到 resources.arsc 的壓縮前後大小是相同的,也就是沒有進行過壓縮。至於要驗證4位元組邊界,我們可以使用 zipalign 工具,輸入 zipalign.exe -c -v 4 xxx.apk | find "resources.arsc" 的結果如下:

25786697 resources.arsc (BAD - 1)

果然 Apktool 並沒有正確地將 resources.arsc 對齊到 4 位元組的邊界。要解決這個問題,我們也可以使用 zipalign 工具 (zipalign.exe -p -f -v 4 input.apk output.apk) 將 APK 裡的檔案重新對齊一次,然後對輸出檔案再次驗證後的結果如下:

25787768 resources.arsc (OK)

確認過後,再將 APK 重新進行數位簽名後就再不會安裝失敗了 (注意數位簽名必須使用 Android Build Tools 裡的 apksigner,不能再使用 jarsigner)。

為什麼要對齊邊界?

大部份作業系統都支援將檔案直接對應成虛擬的記憶體區域 (*nix / Android 稱為 mmap,Windows 稱為 File Mapping),這樣的話就可以直接將檔案的區域指定到 C Struct、陣列等直接使用而不需要先分配記憶體,再將檔案中所需部份複製到記憶體後才能使用。然而系統本身的記憶體管理會有邊界要求,以進行優化及允許偵測問題,所以進行記憶體影射時也有同樣要求。

為什麼不能壓縮?

由於 resources.arsc 本身是一個二進制檔案,你可以把它看成是一個封裝檔或壓縮檔,檔案裡面除了有各被封裝檔案的內容外,也有列出封裝檔案的索引,而這索引有特定格式及位置,當使用 mmap 時就可以像記憶體般直接進行存取。如果將這個檔案壓縮過,索引就會成為被壓縮內容的一部份,也就是說除非先將索引解壓縮,否則無法直接進行存取。


撰寫於:2021/11/26 22:08:01 / 回應已關閉
正在讀取回響內容...
診斷 AVPlayer 錯誤 -11848 及 -12925 的原因

我們打算容許 iOS 裝置播放 4K 的錄製內容,本來已經在 QA 環境設定好 VOS 的編碼格式並在 iPhone + iOS14 下測試通過,不過最近 QA 發現 iPhone 在連接 HDMI 時播放 4K 頻道或錄製內容都會顯示播放失敗。

該錯誤大致如下:

Optional(Error Domain=AVFoundationErrorDomain Code=-11848 "Cannot Open" UserInfo={NSUnderlyingError=0x156d78f30 {Error Domain=NSOSStatusErrorDomain Code=-12925 "(null)"}, NSLocalizedFailureReason=The media cannot be used on this device., NSLocalizedDescription=Cannot Open})

雖然錯誤訊息指明裝置無法播放該媒體,卻沒有指出是哪一個原因導致無法播放。從網上的搜尋結果沒有得到任何有用的資料,然而我在調查系統記錄時發現一些決定性的資訊。

無論是在 iOS、MacOS 甚至是 Safari 播放 HLS 串流時,系統都會透過 mediaserverd 進行解碼,而 -12925 的錯誤也是由 mediaserverd 所拋出的:

mediaserverd <SEGPUMP> segPumpSetCurrentAlternate: 0x1050ec000 0x1050ec000: Attempting SwitchToAlternate bw 896000, at time nan, duration nan 1
mediaserverd <SEGPUMP> segPumpCreateHTTPRequest: Byte pump 0x1050ec020 created new request 0x10a627280 for session ref 0x10a640040
mediaserverd <SEGPUMP> segPumpSendIndexFileRequest: 0x1050ec000:1 session 0x10a640040 index req []
mediaserverd <SEGPUMP> segPumpSetCurrentAlternate: starting audio group on stream index 1, switching from stream index 0, isTrial=0
mediaserverd <<<< FigStreamPlayer >>>> fpfs_ReportCriticalThroughFigLog: [QE Critical]fpfs_ReportVariantSwitchStart: [0x10d50e370]: <0x10411d000>: (11): starting switch up from [<FigAlternate:[0x0]>] to [<FigAlternate:[0x108ea4a80] [Peak 896000] [640x360] [AudioGroup audio1] [hvc1.1.6.L90.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 25.000]>]
mediaserverd <<<< FigStreamPlayer >>>> fpfs_SetAlternateWithContext: <0x10411d000|I/VBQ.01> substream 1 will change gear
mediaserverd <<<< FigStreamPlayer >>>> fpfs_PostNotificationFromDispatch: posting AlternateStreamChanged on 0x10411d000 (0x10a644820)
mediaserverd <<<< FigStreamPlayer >>>> fpfs_StopPlayingItem: [0x10d50e370|P/IP] <0x10411d000|I/VBQ.01> Pausing, err=-12925
mediaserverd <<<< FigStreamPlayer >>>> fpfs_haltStream: item <0x10411d000|I/VBQ.01> pump 0x1050ec000
mediaserverd <<<< FigStreamPlayer >>>> fpfs_haltStream: item <0x10411d000|I/VBQ.01> pump 0x0
mediaserverd <<<< FigStreamPlayer >>>> fpfs_ResetAudioHardwareFormat: [0x10d50e370|P/IP] No more audio rendering, set preferred channel count back to stereo

不過這裡並未看出任何主要線索,只能知道是開啟第一個 alternate 時就已經出現錯誤而停止播放。但是在這段記錯前,原來有一段標示為 HLS-FASB 的記錄清楚說明了系統播放器會套用的篩選器、播放串流的 alternate 以及套用篩選器的順序及結果。以下是在 iPhone 中播放 4K 頻道 (正常播放) 時的 FASB 篩選器記錄:

mediaserverd    <<HLS-FASB>> fasb_log: [0x10a70ade0:I/VNB.01]
<FigAlternateSelectionBoss:0x10a70ade0 [filterCount 13] [alternateCount 4] [filteredAlternateCount 3] [mediaSelectionArrayCount 2]>
   Monitors:
   {
     [FigAlternateFilterMonitorForHDCP currentMaxProtectedHDCPLevel:-1 currentEPM:(null)]
     [FigAlternateFilterMonitorForNotification<0x10a7335d0> "DisplayVideoRangeChanged" state:5 currentFilter:[FigAlternateFilter <PreferredVideoFormat:0x108ec8380> priority:800]]
     [FigAlternateFilterMonitorForNotification<0x1095fda70> "DisplayVideoRangeChanged" state:1 currentFilter:[FigAlternateFilter <FrameRateBucketCap:0x109513200> priority:700]]
     [FigAlternateFilterMonitorForNotification<0x108e941c0> "DisplayVideoRangeChanged" state:5 currentFilter:[FigAlternateFilter <SupportedVideoRange:0x10a729640> priority:1000]]
     [FigAlternateFilterMonitorForNotification<0x109582400> "PowerStateChanged" state:0 currentFilter:(null)]
   }
   Filters:
   {
mediaserverd    <<HLS-FASB>> fasb_log: [0x10a70ade0:I/VNB.01]
       4 ->  4: [FigSimpleAlternateFilter <SuppressVP9:0x10a42b1f0> priority:1100]
       4 ->  4: [FigSimpleAlternateFilter <SupportedVideoRange:0x10a729640> priority:1000 mode:[Internal (HDR OK)]]
       4 ->  4: [FigSimpleAlternateFilter <SupportedAudioFormat:0x10a427cf0> priority:1000 ac3IsDecodable:YES atmosIsDecodable:YES ec3IsDecodable:YES, ac3CanPassthrough:NO]
       4 ->  4: [FigSimpleAlternateFilter <MediaValidation:0x108ff8b30> priority:1000 allowUnknownCodecs:NO]
       4 ->  4: [FigSimpleAlternateFilter <AllowedCPC:0x10a45bf40> priority:1000 systemCPC:0xffffffffffffffff]
       4 ->  4: [FigMediaSelectionAudibleAlternateFilter <MediaSelectionAudible: 0x10a74dbf0> priority: 950 persistantIDs: 0]
       4 ->  4: [FigSimpleAlternateFilter <ScanModePreference:0x10a413ed0> priority:820 contiguous]
       4 ->  4: [FigHDCPAlternateFilter <HDCP: 0x10a734b20> priority: 810 hdcp0:Unknown hdcp1:Unknown]
       4 ->  4: [FigSimpleAlternateFilter <PreferredVideoFormat:0x108ec8380> priority:800 preferredRange:PQ preferredFormat:Maximum]
       4 ->  4: [CombinedAudioPreferenceFilter <CombinedAudioPreference: 0x10a71bba0> priority:750 channels:16 sampleRateConstraints:none preferLossyEncodings=NO preferredAudioFormat:Unknown ac3CanPassthrough:NO permitSpatialization=YES spatialAudioResolutionCutoffSize:1280.000000X720.000000 contentType=moov spatialSources=mlti]
       4 ->  4: [FigSimpleAlternateFilter <PreferBestFormatForVideoRange:0x108eaf060> priority:700 videoRange:SDR bestFormat:HEVC]
       4 ->  3: [FigSimpleAlternateFilter <DisplaySize:0x108ee7fc0> priority:700 displaySize:[2048x1152]]
       3 ->  3: [FigSimpleAlternateFilter <FrameRateBucketCap:0x109513200> priority:700 framerateBucketCap:60fpsBucket]
   }
   Alternates:
   {
     <FigAlternate:[0x103736550] [Peak 896000] [640x360] [AudioGroup audio1] [hvc1.1.6.L90.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 25.000]>
     <FigAlternate:[0x109559bf0] [Peak 2096000] [1280x720] [AudioGroup audio1] [hvc1.1.6.L120.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
     <FigAlternate:[0x109562fe0] [Peak 5096000] [1920x1080] [AudioGroup audio1] [hvc1.1.6.L123.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
     <FigAlternate:[0x109588640] [Peak 15096000] [3840x2160] [AudioGroup audio1] [hvc1.1.6.L153.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
   }
   Filtered Alternates:
   {
     <FigAlternate:[0x103736550] [Peak 896000] [640x360] [AudioGroup audio1] [hvc1.1.6.L90.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 25.000]>
     <FigAlternate:[0x109559bf0] [Peak 2096000] [1280x720] [AudioGroup audio1] [hvc1.1.6.L120.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
     <FigAlternate:[0x109562fe0] [Peak 5096000] [1920x1080] [AudioGroup audio1] [hvc1.1.6.L123.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
   }
LOG COMPLETE

在記錄中可以看到,串流中共有 4 個 alternate,而篩選器 11 至 13 分別是指系統最高可接受 SDR 的 HEVC、不多於 2048x1152 的解析度及不高於 60fps 的幀率,所以篩選結果是 4K 解析度的那個 alternate 因為超出 2048x1152 的解析度而被篩選掉,但其餘三個 alternate 仍能播放,只是最高只能顯示 1080p 的解析度而已。

現在讓我們來看看連接 HDMI 後的 HLS-FASB 記錄:

mediaserverd    <<HLS-FASB>> fasb_log: [0x108e902d0:I/VBQ.01]
<FigAlternateSelectionBoss:0x108e902d0 [filterCount 14] [alternateCount 4] [filteredAlternateCount 0] [mediaSelectionArrayCount 2]>
   Monitors:
   {
     [FigAlternateFilterMonitorForHDCP currentMaxProtectedHDCPLevel:-1 currentEPM:(null)]
     [FigAlternateFilterMonitorForNotification<0x10a6ef960> "DisplayVideoRangeChanged" state:5 currentFilter:[FigAlternateFilter <PreferredVideoFormat:0x10a6dcd50> priority:800]]
     [FigAlternateFilterMonitorForNotification<0x108fca140> "DisplayVideoRangeChanged" state:1 currentFilter:[FigAlternateFilter <FrameRateBucketCap:0x10a67e600> priority:700]]
     [FigAlternateFilterMonitorForNotification<0x10a6efd60> "DisplayVideoRangeChanged" state:5 currentFilter:[FigAlternateFilter <SupportedVideoRange:0x10a63fbd0> priority:1000]]
     [FigAlternateFilterMonitorForNotification<0x10a67e8e0> "PowerStateChanged" state:0 currentFilter:(null)]
   }
   Filters:
   {
       4 ->  4: [FigSimpleAlternateFilter <SuppressVP9:0x1095a5660> priority:1100]
       4 ->  4: [FigSimpleAlternateFilter <SupportedVideoRange:0x10a63fbd0> priority:1000 mode:[Internal (HDR OK)]]
       4 ->  0: [FigSimpleAlternateFilter <NeroSupportedVideoFormat:0x10a650c80> priority:1000 preferredRange:SDR preferredFormat:AVC]
       0 ->  0: [FigSimpleAlternateFilter <SupportedAudioFormat:0x109521590> priority:1000 ac3IsDecodable:YES atmosIsDecodable:NO ec3IsDecodable:YES, ac3CanPassthrough:NO]
       0 ->  0: [FigSimpleAlternateFilter <MediaValidation:0x109505220> priority:1000 allowUnknownCodecs:NO]
       0 ->  0: [FigSimpleAlternateFilter <AllowedCPC:0x109586db0> priority:1000 systemCPC:0xffffffffffffffff]
       0 ->  0: [FigMediaSelectionAudibleAlternateFilter <MediaSelectionAudible: 0x10a67d420> priority: 950 persistantIDs: 0]
       0 ->  0: [FigSimpleAlternateFilter <ScanModePreference:0x10a628e90> priority:820 contiguous]
       0 ->  0: [FigHDCPAlternateFilter <HDCP: 0x10a6ef8d0> priority: 810 hdcp0:Unknown hdcp1:Unknown]
       0 ->  0: [FigSimpleAlternateFilter <PreferredVideoFormat:0x10a6dcd50> priority:800 preferredRange:PQ preferredFormat:Maximum]
       0 ->  0: [CombinedAudioPreferenceFilter <CombinedAudioPreference: 0x10a67e1b0> priority:750 channels:2 sampleRateConstraints:none preferLossyEncodings=NO preferredAudioFormat:Unknown ac3CanPassthrough:NO permitSpatialization=NO spatialAudioResolutionCutoffSize:1280.000000X720.000000 contentType=moov spatialSources=none]
       0 ->  0: [FigSimpleAlternateFilter <PreferBestFormatForVideoRange:0x10a624a80> priority:700 videoRange:SDR bestFormat:Unknown]
       0 ->  0: [FigSimpleAlternateFilter <DisplaySize:0x10a640100> priority:700 displaySize:[1920x1080]]
       0 ->  0: [FigSimpleAlternateFilter <FrameRateBucketCap:0x10a67e600> priority:700 framerateBucketCap:60fpsBucket]
   }
   Alternates:
   {
     <FigAlternate:[0x108ea4a80] [Peak 896000] [640x360] [AudioGroup audio1] [hvc1.1.6.L90.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 25.000]>
     <FigAlternate:[0x1095aafb0] [Peak 2096000] [1280x720] [AudioGroup audio1] [hvc1.1.6.L120.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
     <FigAlternate:[0x109559bf0] [Peak 5096000] [1920x1080] [AudioGroup audio1] [hvc1.1.6.L123.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
     <FigAlternate:[0x109588640] [Peak 15096000] [3840x2160] [AudioGroup audio1] [hvc1.1.6.L153.B0,mp4a.40.2] [VideoRange SDR] [FrameRate 50.000]>
   }
   Filtered Alternates:
   {
   }
LOG COMPLETE

從這次的篩選記錄可以看到,系統現在支援的格式只有 SDR 格式的 AVC,最高 1920x1080 解析度及最高 60fps 的幀率,而由於所有 alternate 均是 HEVC 編碼,所以所有 alternate 都在經過第 3 個篩選器時被篩選掉了,以致最後沒有可以播放的 alternate 而導致 -12925 的錯誤,也就是說 iOS 支援 HEVC 解碼只限使用內建輸出,當連接 HDMI 時是不支援 HEVC 解碼的。


撰寫於:2021/9/20 00:01:47 / 回應已關閉
正在讀取回響內容...
未預期的 AndroidX Fragment 更新

最近因為某個奇怪問題而嘗試把程式中的第三方程式庫版本更新,而當把 Dagger 和 AndroidX 更新的時候發現一個很奇怪的問題。

我們的程式由於可以在程式中變更語言,所以為方便不需要在界面初始化時自行設定正確語言的文字,有在一些地方進行特別設定 Locale (包括系統 Locale 及資源 Locale),但當把 Dagger 更新後,發現變更語言後有很多地方都無法套用新的語言設定。

據我們初步的測試所知,當把 Dagger 2.9 或 AndroidX AppCompat 1.1 更新後就會發生這個情況。在調查過兩者的 pom 檔案後發現,Dagger 2.37 和 AppCompat 1.3.0 都有一個共通點,就是 AndroidX Fragment 的版本被更新到 1.3.0 或以上的版本。

然後我們再看看 AndroidX Fragment 的更新記錄 (https://bit.ly/3tgLdUN),Bingo! 原來 Google 在 AndroidX 1.3.0alpha08 的時候引入了重寫的 State Manager 而且預設使用,而似乎這個新 State Manager 的行為與舊版有點分別而導致問題。

那麼我們該如何解決那個問題呢?幸好目前可以使用 FragmentManager.enableNewStateManager(false) 來強制使用舊的 State Manager,經我們測試過果然有效。然而,根據官方網誌 (https://bit.ly/38WBlGJ) 所述,這個方法並不視為官方 API 且會在 1.3.1 版考慮移除 (不過目前最新版本還沒有看到被移除)。那就是說長遠來看,我們還是得想辦法在新的 State Manager 尋找出路,除非 Google 注意到這個問題而進行修正。


撰寫於:2021/9/5 17:45:24 / 回應已關閉
正在讀取回響內容...
Android 程式斷網化 - Firebase 篇

當一個程式被切斷網路通訊 (見上一篇) 後,若程式有植入 Firebase 時,程式啟動後短時間就會自動關閉且顯示如下的錯誤記錄:

FATAL EXCEPTION: firebase-installations-executor-2
Process:                           , PID: 3006
java.lang.SecurityException: Permission denied (missing INTERNET permission?)
        at java.net.Inet6AddressImpl.lookupHostByName(…)
        at java.net.Inet6AddressImpl.lookupAllHostAddr(…)
        at java.net.InetAddress.getAllByName(InetAddress.java:1154)
        at com.android.okhttp.Dns$1.lookup(Dns.java:39)
        at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(…)
        at com.android.okhttp.internal.http.RouteSelector.nextProxy(…)
        at com.android.okhttp.internal.http.RouteSelector.next(…)
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(…)
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(…)
        at com.android.okhttp.internal.http.StreamAllocation.newStream(…)
        at com.android.okhttp.internal.http.HttpEngine.connect(…)
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(…)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(…)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(…)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(…)
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getOutputStream(…)
        at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getOutputStream(…)
        at com.google.firebase.installations.s.c.a(Unknown Source:0)
        at com.google.firebase.installations.s.c.a(Unknown Source:8)
        at com.google.firebase.installations.s.c.a(Unknown Source:53)
        at com.google.firebase.installations.h.d(Unknown Source:45)
        at com.google.firebase.installations.h.d(Unknown Source:34)
        at com.google.firebase.installations.h.b(Unknown Source:0)
        at com.google.firebase.installations.b.run(Unknown Source:4)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

雖然從 Stack Trace 中看到也是 okhttp.Dns$1.lookup 拋出的錯誤,但由於 com.android.okhttp 是存在於系統的套件,無法直接在程式源碼層面中修改。要避過 Firebase 的斷網問題,有至少兩個方法可以實行。

1. 停止 Firebase 的自動初始化功能

有進行過植入 Firebase 的網友都應該記得,我們並不需要寫任何編碼 Firebase 就能自動初始化。究竟 Firebase 是如何自動初始化的呢?根據官方解釋 (https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html),Firebase 是利用 Content Provider 會自動被系統建立的特性來故意濫用作自動初始化 Firebase 的,所以最簡單迴避 Firebase 初始化的方法就是把 AndroidManifest 裡如下列的定義刪掉:

<provider android:authorities="                          .firebaseinitprovider" android:directBootAware="true" android:exported="false" android:initOrder="100" android:name="com.google.firebase.provider.FirebaseInitProvider"/>

這個方法對只使用單一 Firebase App 的程式上沒有問題,不過對於會手動呼叫 FirebaseApp.initializeApp() 的程式則沒有作用,需要使用方法 2 解決。

2. 強制初始化失敗

對於會呼 FirebaseApp.initializeApp(),我們需要參照上列的 Stack Trace 研究可以防止初始化的方法。首先在com\google\firebase\installations\s\c.smali 中尋找呼叫 getOutputStream() 的位置,然後可以確定是在 private static void a(URLConnection p0, byte[] p1)。往下面看一點,:cond_0 是拋出 IOException 的地方,我們又可以利用這個地方進行拋出。在 getOutputStream() 那一句前面加 goto :cond_0 作無條件式跳到 :cond_0 就完成修改。

在防止 Firebase 初始化後,程式就可以完全斷網而不會因沒有定義 INTERNET 權限而發生錯誤。


撰寫於:2021/8/29 00:37:27 / 回應已關閉
正在讀取回響內容...
Android 程式斷網化 - 從第三方程式庫着手

要將一個本身可離線使用的程式徹底斷網,其實並不需要修改每一個發出連線要求的位置。其實無論是原生程式,還是 React Native 程式,絕大多數不是使用第三方程式庫就是有一個中央的地方處理網路要求。所以只需要修改這個中央的地方就可以將所有網路要求都處理掉。

當去除 INTERNET 權限後,React Native 程式在執行時出現這樣的致命錯誤:

--------- beginning of crash
FATAL EXCEPTION: OkHttp Dispatcher
Process:                           , PID: 2938
java.lang.SecurityException: Permission denied (missing INTERNET permission?)
        at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:151)
        at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:105)
        at java.net.InetAddress.getAllByName(InetAddress.java:1154)
        at okhttp3.Dns$1.lookup(Unknown Source:2)
        at okhttp3.internal.connection.RouteSelector.resetNextInetSocketAddress(Unknown Source:129)
        at okhttp3.internal.connection.RouteSelector.nextProxy(Unknown Source:20)
        at okhttp3.internal.connection.RouteSelector.next(Unknown Source:17)
        at okhttp3.internal.connection.StreamAllocation.findConnection(Unknown Source:109)
        at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(Unknown Source:0)
        at okhttp3.internal.connection.StreamAllocation.newStream(Unknown Source:22)
        at okhttp3.internal.connection.ConnectInterceptor.intercept(Unknown Source:25)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:158)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:6)
        at okhttp3.internal.cache.CacheInterceptor.intercept(Unknown Source:132)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:158)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:6)
        at okhttp3.internal.http.BridgeInterceptor.intercept(Unknown Source:161)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:158)
        at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(Unknown Source:48)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:158)
        at okhttp3.internal.http.RealInterceptorChain.proceed(Unknown Source:6)
        at okhttp3.RealCall.getResponseWithInterceptorChain(Unknown Source:115)
        at okhttp3.RealCall$AsyncCall.execute(Unknown Source:11)
        at okhttp3.internal.NamedRunnable.run(Unknown Source:17)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

由於 React Native 的網路部份是使用 OkHttp3,所以當進行網路要求的時候自然也是由 OkHttp3 拋出錯誤。直接拋出 SecurityException 的位置 java.net.Inet6AddressImpl 由於是系統套件,我們無法修改。但我們可以從最接近系統呼叫的地方 okhttp3.Dns$1 找尋修改的機會。

okhttp_dns_1

上圖標示的位置就是拋出 SecurityException 的呼叫。要注意的是 Stack Dump 中所標示的第二行,由於 smali 已被移除行號,所以無法直接對應正確的位置,這時候只能透過方法名稱 (即 lookup) 來尋找正確的方法。在這裡有好幾個解決方法,例如將該句 invoke-static 替換成拋出 UnknownHostException,或將參數 p1 替換成不會構成 SecurityException 的地址 (例如 127.0.0.1)。不過這裡可以用另外一個更簡單的方法。從 invoke-static 該句向上看,有一句 if-eqz p1, :cond_0,這個用 Pseudo code 的意思就是 if (p1 == null) goto cond_0。而 cond_0 又是什麼呢?

okhttp_dns_2

Bingo! 也就是說如果將 null 傳入 p1 的時候會拋出 UnknownHostException,這部份正是我們想要的結果,那麼我們只需要修改跳到 cond_0 的條件即可。if-eqz p1, :cond_0 這句我們只需要由 if-eqz (如果等於 0) 改成 if-nez (如果不等於 0) 就可以將所有有效參數都全部導向 cond_0 而拋出 UnknownHostException。

這樣就已經處理大部份的網路要求。然後還剩下什麼呢?當程式使用 Firebase 的時候,Firebase 初始化時也會拋出 SecurityException,這個會在下一篇再討論。


撰寫於:2021/8/21 23:35:19 / 回應已關閉
正在讀取回響內容...
將 Android 程式斷網的方法

要說最近最多人修改離線化的程式肯定是某個宣稱不會上傳資料的程式。相信有試過動手的人都會發現 Android 裡要令一個程式斷網,純粹把 AndroidManifest.xml 裡的 android.permission.INTERNET 權限請求刪除是不行的,因為系統會拋出 SecurityException,而且因為 SecurityException 並非繼承自 IOException,所以當程式執行時會導致 FC 而不是被當成網路異常被處理掉。

再令整件事複雜一點,該程式是用 ReactNative 開發的,也就是要修改的話要先拆解 index.android.bundle 然後再修改。不過事實真的需要這樣做嗎?

在之後的 2 篇文章中將講解如何以我認為最簡單的方法迴避 SecurityException,另外會再有幾篇相關的文章講解其他技巧。多數文章都需要對 Dalvik Opcodes 有一定認識。

* 注意:本系文章僅供學術交流使用,文章中並不會提供確實的修改方法,亦不會提供任何下載。

screenshot-2021-08-14_22.20.33.491


撰寫於:2021/8/14 23:39:52 / 回應已關閉
正在讀取回響內容...
iOS + Android 的 IMA SDK 與 Safe Area Insets

最近在為某個播放程式加入 VAST 支援,由於目前使用第三方播放器的 VAST 支援並不能完全支援 Google Ads Manager,所以決定使用官方的 IMA SDK。不過官方的 IMA SDK 有一個缺點,就是不讓開發者自行定義界面 (可以設定是否隱藏或顯示某些項目,但不能自行作出其他調整。雖然官方說明中有可搜尋但沒有列出的頁面說明如何修改,但官方有解釋並不支援:https://groups.google.com/g/ima-sdk/c/yVBKLyO9jfY/m/dzYerQH-AQAJ),所以我們會使用自帶的廣告界面。

然而,在 QA 測試時發現無論是 iOS 或是 Android,當裝置螢幕為圓角 (例如 iPhone X, Samsung A70) 的時候,在左下角顯示的倒數時間及右上角的 “瞭解更多” 字樣均超出了圓角的範圍而導致無法完全顯示。

我們的設計其實比較簡單,播放器的容器就只會放第三方播放器和 IMA SDK 的控制項:

  • 根容器 (佔全螢幕包括所有安全區域)
    • PlayerContainer (UIView / FrameLayout)
      • IMA SDK 控制項 (IMA SDK 會自動放在最頂端)
      • 第三方播放器
    • 播放器控制
    • 其他重疊顯示

iOS 的處理相對簡單,只需要在 PlayerContainer 與 IMA SDK 之間加一個左右邊符合 playerContainer.safeAreaLayoutGuide 的 UIView 就可以。

Android 就麻煩得多,Google 在 API level 20 (Android 4.4W) 的時候加入了 View.setOnApplyWindowInsetsListener(),理論上可以接收到系統要求特定 View 套用所要求的 WindowInsets,但經測試後發現在全螢幕 (作為播放器,當然會用 SYSTEM_UI_FLAG_FULLSCREEN 及 SYSTEM_UI_FLAG_IMMERSIVE) 時,就算設定了 WindowInsetsListener 系統根本不會呼叫,所以沒用。

然後改為試用 API level 23 (Android 6) 加入的 View.getRootWindowInsets(),但只有在 View.onAttachedToWindow() 的時候呼叫時,WindowInsets.displayCutout 才不是 null,也就是我不能在初始化 IMA SDK 的時候就已經設定好 Margins。

最後,我從 stackoverflow 找到這篇方法 (https://stackoverflow.com/questions/61241255/windowinsets-getdisplaycutout-is-null-everywhere-except-within-onattachedtowindo)。

透過 Activity.getWindowManager().getDefaultDisplay().getCutout() 的確可以正確地取得不包括圓角區域的顯示範圍,不過最大的缺點是這是 API Level 29 (Android 10) 的 API,然而只有這個方法能正確取得可用的數值,所以也只能這樣了。

不過使用時有一點要留意,估計由於計算預設 Cutout 的時候並不假定 Immersive Mode,所以機頂向左的橫屏時只有 getSafeInsetLeft() 會傳回有效數值而 getSafeInsetRight() 會傳回 0,而機頂向右的時候則剛好相反,所以我們在套用 Margin 的時候會在左右兩邊同時套用任何一邊的有效數值,由於目前應該沒有裝置會在橫屏時是左圓角右方角,這個假定應該暫時還是可接受的。


撰寫於:2021/8/1 13:53:46 / 回應已關閉
正在讀取回響內容...
其他較舊內容請移步至舊部落格版面