Android 定位結果的 `Location.getTime()` 不一定是衛星時間

Photo by SpaceX on Unsplash 前言 最近的專案需要取定位的時間,並用該時間判斷使用者是否有在指定時間完成任務,而業主就提出希望以定位所取得的時間做為判斷依據。 然後,就發現事情沒有想像中的那麼單純。 原來目前在 Android 的定位結果中所取得的時間,只有在定位來源為純 GPS 的情況下才會是衛星所回傳的時間。 來看看究竟是怎麼一回事吧… 先認識 Location 型別 不論是使用 Android 官方推薦的 Fused Location Provider 或是 LocationManager ,定位結果所回傳的型別都是 Location。這個型別除了基本的經緯度與海拔等資訊,其中也包含定位的時間。 通常開發上指的定位時間,我們會使用 Location.getTime() 取得 Unix epoch time,再將這串數字轉成人類可閱讀的時間格式。 Unix epoch time:從 UTC 1970 年 1 月 1 日 0 時 0 分 0 秒起至現在的總秒數,不考慮閏秒。 – Wikipedia Location.getTime() 會跟定位來源有關 從 Android Doc: Location.getTime() 的說明會發現,不同的定位結果所取得的時間來源不見得相同。 ...

May 27, 2023 · 2 min · 257 words · Daniel Huang

比 Android 原生更方便的 Log 工具: Timber

本文同步發表在 HackMD & Medium Timber 是什麼 Timber 是一個以 Android Log 為基底所開發的 Logger Library,由 Jake Wharton 大神所開發。 Timber 為了解決什麼問題 1. 開發時可以留著,但發佈版本需要移除 Log 1 2 3 4 // 你可能很常看到類似這樣的寫法... if (BuildConfig.DEBUG) { Log.d(TAG, "Hello World!") } 一般來說在開發上,我們習慣使用 Android 的 Log class 來印出所需的資訊。但是當今天開發到一定的階段,程式必須發布上線時,為了資訊安全等需求,需要將這些 Log 給全部註解或移除,又或是加上 buildFlavor 或 buildType 判斷,這一切實在是太麻煩了… 2. 每次在新的類別中使用 Log 就要建一個該類別的 TAG String 1 2 3 4 5 val TAG: String = Hello::class.java.simple if (BuildConfig.DEBUG) { Log.d(TAG, "Hello World!") } 同步發表在 HackMD & Medium Timber 怎麼使用 1. Dependency 在 build.gradle 中加入以下的 Dependency。 ...

May 27, 2023 · 1 min · 170 words · Daniel Huang

Android 取得即時定位 LocationManager

📢 除非有特殊需求,官方建議使用 Google Fused Location Provider API。 權限請求 AndroidManifest.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <!-- 宣告定位權限 --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <application> <!-- 略 --> </application> </manifest> 請求動態權限 因為定位屬於危險權限需要動態向使用者請求。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import android.Manifest import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts class MainActivity : AppCompatActivity() { // -----------------------Request Location Permission------------------------------ private val resultLauncherPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) { initLocationManager() } else { Toast.makeText(this, "請允許權限以開啟定位", Toast.LENGTH_SHORT).show() finish() } } // -----------------------------LifeCycle Event------------------------------------- override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 啟動時請求權限 resultLauncherPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) } // -------------------------------------------------------------------------------- private fun initLocationManager() { Toast.makeText(this, "定位權限請求成功,準備初始化 LocationManager", Toast.LENGTH_SHORT).show() } } LocationManager 實例化 1 2 3 import android.location.LocationManager val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager 請求定位 Android S 以上支援 FUSED_PROVIDER,不過建議直接用 Android Fused Location Provider API,支援的系統版本比較廣。 ...

May 24, 2023 · 2 min · 271 words · Daniel Huang

網站憑證在 PC 端有效但 Android 上出現無效憑證問題

問題 網站更換憑證後在電腦瀏覽器上可正常瀏覽,但手機端卻出現憑證無效的錯誤。 1 javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. 原因:缺乏中繼憑證 👀 以下截自 【茶包射手日記】網站憑證無效案例分析 這篇問題的確單純就是沒裝中繼 CA 憑證,而這問題用PC瀏覽器是測不出來的,因為PC瀏覽器在缺少中繼憑證時,會從憑證的擴充欄位>授權資訊存取>憑證授權單位簽發者中的網址,自動下載中繼憑證,所以不會有問題。但PC瀏覽器以外的client如手機版瀏覽器、curl、寫程式連線等都沒有這個自動下載。 另外 SSLLab 其實會指出這個問題,在Additional Certificates (if supplied)區塊就會列出server提供了哪些憑證,如果有缺少中繼憑證問題也會顯示Chain issues: Incomplete 簡單來說就是: 伺服器端沒提供中繼憑證,導致憑證無效。 PC 上的瀏覽器因為會自動從憑證授權單位自動下載中繼憑證,所以不會有問題。 其他類型的 Client 沒有這個自動下載的機制,所以會有問題。 驗證問題 嘗試手動將中繼憑證加入手機中,確認可以正常瀏覽。 使用 What’s My Chain Cert 憑證設定檢查網站,比對兩個網址的設定,也確實異常的那個網站是有錯誤的。 解決方案 很簡單,伺服器端修正憑證設定即可。 補充案例 2023/05/30 App 下載離線圖資失敗,經查測後發現一樣是在瀏覽器上可正常連線,但是 App 無法連線。 使用 What’s My Chain Cert 憑證設定檢查網站 檢查,確認是 Misconfigured。 改用 SSLChecker 憑證設定檢查網站 ,更明確指出斷在哪裡。 伺服器端匯入中繼憑證後,使用 SSLShopper SSL Checker 檢查通過。 參考資料 StackOverflow: Trust Anchor not found for Android SSL Connection 【茶包射手日記】網站憑證無效案例分析 中繼憑證設定遺失問題 【第2代通用憑證管理中心新舊中繼CA憑證差異說明】 什麼是中繼憑證 憑證檢查網站 What’sMyChainCert 憑證設定檢查網站 SSLChecker 憑證設定檢查網站 SSLShopper SSL Checker

October 26, 2022 · 1 min · 89 words · Daniel Huang

Android CameraX Bitmap 繪製後照片方向錯誤

問題說明 在使用 CameraX API 時,如果有在相機初始時實作下列程式碼,原則上API 會根據目前手機的方向來自動轉正相片。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 控制照片轉正 val orientationEventListener = object : OrientationEventListener(this) { override fun onOrientationChanged(orientation: Int) { val rotation: Int = when (orientation) { in 45..134 -> Surface.ROTATION_270 // 頭朝右 in 135..224 -> Surface.ROTATION_180 // 頭朝下 in 225..314 -> Surface.ROTATION_90 // 頭朝左 else -> Surface.ROTATION_0 // 頭朝上 } this@MainActivity.rotation = rotation imageCapture!!.targetRotation = rotation } } orientationEventListener.enable() 但是,當我們需要在輸出的照片上繪製文字或圖案時,就會遇到讀入的Bitmap 都是預設的橫向,而當我們需要繪製的時候,就會有長寬座標錯置的問題。 解決方式 必須把照片轉兩次。 需要轉兩次的原因在於,第一次的轉正是為了讓我們能夠根據使用者的角度,去繪製我們需要繪製的圖案或文字,但當繪製完成後,在輸出之前必須再將照片轉回原始讀入時的橫向,讓電腦端能夠用EXIF來自動轉正相片。 轉正跟轉回,可以試著拿張名片或紙自己轉轉看就會懂了。 實作的程式碼如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 companion object { private const val ROTATION_HEAD_UP = 0 private const val ROTATION_HEAD_LEFT = 1 private const val ROTATION_HEAD_DOWN = 2 private const val ROTATION_HEAD_RIGHT = 3 } private fun processPictureFile(photoFile: File) { // 建立 Bitmap的設定 val bitmapOptions = BitmapFactory.Options() bitmapOptions.inPurgeable = true bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565 bitmapOptions.inDither = true bitmapOptions.inMutable = true // 避免Canvas物件建立時picBitmap為immutable // 1. 旋轉照片 val mat = Matrix() when (rotation) { ROTATION_HEAD_UP -> mat.postRotate(90f) // 手機上端朝右時,照片會上下顛倒,需特別處理轉正。 ROTATION_HEAD_RIGHT -> mat.postRotate(180f) ROTATION_HEAD_DOWN -> mat.postRotate(270f) ROTATION_HEAD_LEFT -> mat.postRotate(0f) } try { // 讀入原始相片 val inputStream = ByteArrayInputStream(photoFile.readBytes()) // 原始相片轉成 Bitmap val bitmap = BitmapFactory.decodeStream(inputStream, null, bitmapOptions) ?: return Log.d(TAG, "processPictureFile: bitmap.width ${bitmap.width}") Log.d(TAG, "processPictureFile: bitmap.height ${bitmap.height}") // 複製一份要轉方向的 Bitmap val picBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, mat, true) // 建立畫布 val canvas = Canvas(picBitmap) // 繪製在畫布上 canvas.drawBitmap(bitmap, mat, null) // TODO: 執行你需要做的繪製行為... // 設定轉回去的矩陣 val restoreMat = Matrix() when (rotation) { ROTATION_HEAD_UP -> restoreMat.postRotate(270f) ROTATION_HEAD_RIGHT -> restoreMat.postRotate(180f) ROTATION_HEAD_DOWN -> restoreMat.postRotate(90f) ROTATION_HEAD_LEFT -> restoreMat.postRotate(0f) } // 執行轉回去 val restoreBitmap = Bitmap.createBitmap(picBitmap, 0, 0, picBitmap.width, picBitmap.height, restoreMat, true) val restoreCanvas = Canvas(restoreBitmap) restoreCanvas.drawBitmap(restoreBitmap, mat, null) // TODO: 輸出... } catch (e: java.lang.Exception) { Log.e(TAG, "processPictureFile: ", e) } } 補充: 相片轉正的邏輯 對 Android 手機而言,頭朝左的水平方向(landscape),才是正常的方向,所以預設讀取的圖片都會是橫向。 因此直向拍照等方式都需要轉向。 ...

March 25, 2022 · 2 min · 361 words · Daniel Huang