Stark Wong 的個人開發網站
 


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

有關 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 / 回應已關閉
正在讀取回響內容...
流動巴士版圖停止支援通告

由於現在基本上已沒有離線查詢巴士資料的需要,而且程式的各個版本已被相應下架 (Android 版本在不久前也因為定位權限問題被下架),所以流動巴士版圖程式將定於 2022 年 1 月 1 日前停止支援,視乎在此期間內是否有任何導致資料庫無法更新的事件發生。在程式停止支援後,資料庫將不會再更新。

本人在此感謝一直以來有使用過該程式的支持者,沒有你們的支持,流動巴士版圖絕對沒有辦法活得那麼久。

這裡以後會改為發佈一些工作或閒活時遇到的編程問題及解決技巧,歡迎各位繼續支持。


撰寫於:2021/7/24 22:33:54 / 回應已關閉
正在讀取回響內容...
掃書程式資料來源修正 + 流動巴士版圖狀況更新

最近去買書的時候使用自己之前開發的掃書程式,發現顯示的書名錯掉了,所以今天進行修復。今天作出的修改如下:

  1. 重新驗證並確保全部 3 個資料來源都能正常使用
  2. 由於 Google 政策改變,將查詢後台的 API 由 Google Apps Engine 遷回 AWS LightSail
  3. 將資料庫中的不正確項目刪除

遲些會設置監察器偵測資料來源的變更以適時向開發者作出提示。

至於流動巴士版圖,由於資料提供方再次更改 API 的保護,與其繼續貓捉老鼠,我決定不再提供離站時間提示服務。而資料庫更新則繼續提供至任何資料來源無法再使用為止,當發生任何來源無法使用時,所有資料將不會再更新,而程式會即時下架並在程式作出通知。


撰寫於:2020/12/13 16:03:26 / 回應已關閉
正在讀取回響內容...
流動巴士版圖離站時間查詢功能暫停

由於有人濫用流動巴士版圖的離站時間查詢服務,導致資料供應方被封 IP,只能暫停此功能直至加入反制措施為止。

致某裝成 "Dalvik/2.1.0 (Linux; U; Android 10; SM-N9750 Build/QP1A.190711.020)" 的人:大家都是九巴不開放資料下的受害者,但沒有需要攬炒吧?你這樣繁密讀取資料,我的伺服器不封,你也不可能沒料到上流的伺服器會封吧?這下你可開心吧?大家都不用玩下去了。


撰寫於:2020/7/2 21:48:40 / 回應已關閉
正在讀取回響內容...
其他較舊內容請移步至舊部落格版面