Stark Wong 的個人開發網站
 


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

未預期的 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 / 回應已關閉
正在讀取回響內容...
流動巴士版圖資料庫更新恢復正常

由於接連有突破性的發展,流動巴士版圖的新巴/城巴資料由即日起恢復正常,而離站時間查詢功能亦已恢復。

Android 版本的流動巴士版圖將重新上架,至於 iOS 版本則由於開發者資格已過期,暫時未知會否重新續期以讓程式重新上線。


撰寫於:2020/6/27 18:05:10 / 回應已關閉
正在讀取回響內容...
流動巴士版圖 - 九巴離站時間查詢功能恢復

由於九巴離站時間資料提供方取得突破性發展,以令流動巴士版圖的九巴離站時間查詢功能可暫時恢復,至於新巴/城巴的離站時間查詢功能及資料庫更新則尚未有恢復時間表。


撰寫於:2020/6/25 15:19:49 / 回應已關閉
正在讀取回響內容...
其他較舊內容請移步至舊部落格版面