2023-09-09

【Kotlin】GeckoView 動態顯示本機網頁

參考資料 ----
Match patterns

動機:
因為 GeckoView 的顯示效果比 Android 系統內建 WebView 元件強上許多,因為探索是否能以 GeckoView 取代 WebView,達到 WebView.loadDataWithBaseURL() 的目的。

先說結論:截至目前為止(2023.09.09), GeckoView 版本為 117.0.20230824132758 之際,
無法實作動態顯示本機網頁,只能顯示靜態網頁,原因在:

GeckoView 目前尚 不支援 url 為 resource://... 的 scheme


所以當您想顯示動態內容網頁,而網頁又引入了 css、javascript... 等檔案,則這些 css、javascript...檔案必須置放在 internet 的網站上,然後呼叫 loadString(html string)。





下面的筆記就先不予理會了...



有時候我們並不是要去下載某個網站的網頁,而是想利用 GeckoView 顯示 HTML+CSS 豐富的版面樣式,要讓 app 和 GeckoView 能互傳訊息,這就要利用 WebExtension(擴充套件) 的功能。

開發工具:Android Studio Chipmunk 2021.2.1 Patch 2

/assets 內建立要置放 javascript 、 CSS 和 WebExtension 的目錄,並將需要的檔案分別置放於內;再建立一個 html 首頁檔,該網頁的 <head> 區塊要引入前述的 javascriptcss

WebExtensionGeckoViewapp 之間扮演重要的角色,是兩者的溝通橋樑。

每建立一個 WebExtensionWebExtension 要有自己的目錄,在本例,我們建立 /assets/messaging/ ,然後每個 WebExtension 要有自己的 manifest.json

manifest.json
 
{
  "manifest_version": 2,
  "name": "messaging",
  "version": "1.0",
  "description": "Example messaging web extension.",
  "browser_specific_settings": {
    "gecko": {
      "id": "messaging@example.com"
    }
  },
  "content_scripts": [
    {
      // "matches": ["*://*.twitter.com/*"],
      "matches": ["*://*/*.htm"],
      "js": ["messaging.js"]
    }
  ],
  "permissions": [
    // "activeTab",
    "nativeMessaging",
    "nativeMessagingFromContent",
    "geckoViewAddons"
  ]
}
 


再建立 app 要與 GeckoView 通訊的 javascript 程式,在本例為 background.js
 
background.js
 
// Establish connection with app
let port = browser.runtime.connectNative("browser");
port.onMessage.addListener(response => {
    // Let's just echo the message back
    port.postMessage(`Received: ${JSON.stringify(response)}`);
});
port.postMessage("Hello from WebExtension!");
 


WWW


index.htm (檔名可自定喔)
 
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <script type="text/javascript" src="./js/jquery-3.5.1.min.js"></script>
    <script type="text/javascript" src="./js/jquery-ui-1.8.16.custom.min.js"></script>
    <link rel="stylesheet" href="./css/jquery-ui-1.8.16.custom.css">
	<style>
        img{    max-width:50%;  }
        .matrix-block		{ font-size: 150%; text-align: center }
	    mtable.thin-column-padding > mtr > mtd,
		fmath span.thin-column-padding > table > tbody > tr > td
		{ padding: 0 0.11em !important }
    </style>
</head>
<body>
hello
</body>
</html>
 


MainActivity.kt
 
...
...

import org.mozilla.geckoview.GeckoRuntime;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoView;

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var view:GeckoView = findViewById(R.id.geckoview)
        val session = GeckoSession()
        val runtime = GeckoRuntime.create(this)
        
        session.contentDelegate = object : ContentDelegate {}
        if(runtime==null) {
            runtime = GeckoRuntime.create(this)
        }
        
        session.open(runtime)
        view.setSession(session)
        session.loadUri(""resource://android/assets/index.htm"")    // 先載入首頁,讓 GeckoView 記住相關的設定
        
        
        // 再載入自定的網頁內容
        
    }
}
 



相關筆記 ----


2023-07-16

【Oracle】查詢指定月份的天數

重點是這個語法不論平年、閏年都可查詢得知(而閏年的 2 月有 29 天)。

 
 
SELECT EXTRACT(day FROM LAST_DAY(date'2016-02-01')) AS day_count FROM dual;
 

2023-07-08

【Linux】LinuxMint 21.1 安裝/設定中文輸入法

參考資料 ----

滑鼠點擊左下角的開始
→ 控制中心
→ 輸入法
→ 出現視窗

預設已經安裝 fcitx5 輸入法框架,並且已安裝 新酷音
 
頗讓人不爽的是它將 倉頡輸入法 歸到簡體中文內
滑鼠點擊左下角的開始
→ 附屬應用程式
→ 文字編輯器
→ 營幕右下角出現鍵盤圖示
→ 滑鼠在鍵盤圖示點右鍵 → 設定
→ 視窗右側的 "可用輸入法" 往下捲,就會看到 倉頡 被歸在 简体中文(中国)

 

先不關閉 "設定" 視窗,另開個終端機視窗,試著以命令方式安裝 倉頡,也沒看到列出的套件有 cangjie 字樣的...

更令人火大的是,回到 "設定" 視窗,雙擊 "倉頡"(倉頡就移到左邊列表) → 套用 後

回到 文字編輯器,切換輸入法為 倉頡,雖然輸入的是繁體中文拆法,但顯示的卻是簡體中文!! 例如:
輸入 【女火竹水卜】畫面出現的候選字居然是【】!!

注意:不要在 "輸入法" 視窗 按 那個 "安裝" 鈕,會出現一堆套件衝突的錯誤訊息

開一個 終端機視窗,將 fcitx5 移除,再安裝舊版的 fcitx
 
[user]$ sudo  apt  remove  fcitx5*

# 一併安裝 新酷音 & 倉3
[user]$ sudo  apt  install fcitx  fcitx-chewing  fcitx-table-cangjie3
 
→ 滑鼠點擊左下角的開始
→ 控制中心
→ 輸入法
→ 此時是無 輸入法框架 的狀態
→ 下拉並選取 fcitx
→ 重開機

接下來的設定步驟就參考上面的 LinuxMint 20.3 安裝/設定中文輸入法 連結吧






2023-06-01

【Kotlin】計算起、迄時刻所經歷的時間

app 層級 build.gradle

 
android {
    ...
    ...
    
    buildFeatures {
        viewBinding true
    }
}
 



activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/txtHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="timeDuration"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
 



MainActivity.kt
 
package 完整package名

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import com.example.timeduration.databinding.ActivityMainBinding
import java.util.Date


class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    fun timeDuration(vv: View) {
        // 狀況 1: 變數型態為 Date 類別
        val a1 = Date()
        SystemClock.sleep(123)
        val a2 = Date()

        // 狀況 2: 變數型態為字串, 則轉換為 Date 類別
        /*
        val pattern = "yyyy.MM.dd HH:mm:ss.SSS"
        val formatter = SimpleDateFormat(pattern, Locale.getDefault())
        val a1 = formatter.parse("2023.05.01 00:00:00.000")
        val a2 = formatter.parse("2023.05.01 01:10:11.123")
        */

        val durationMillis = a2.time - a1.time

        // 精確度達 0.001 秒
        val hours = durationMillis / (1000 * 60 * 60)
        val minutes = durationMillis % (1000 * 60 * 60) / (1000 * 60)
        val seconds = durationMillis % (1000 * 60) / 1000
        val millis = durationMillis % 1000

        // 輸出格式為  時:分:秒.毫秒
        val sDuration = String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis)
        binding.txtHello.text = sDuration
    }
}
 



【Kotlin】socket ping 判斷是否連上網際網路

2023.06.01
ping 的回應速度極快,還可指定等待 timeout 的時間限制,目前為止找到的最佳解


app 層級 build.gradle
 
android {
    ...
    ...
    
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    ...
    ...
    
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
}
 



AndroidManifest.xml
 
<uses-permission android:name="android.permission.INTERNET" />

<application
    ...
    ...
 



activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/txtHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="pingDuration"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
 



MainActivity.kt
 
package 完整的package名稱

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import com.example.chkinternet.databinding.ActivityMainBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetSocketAddress
import java.net.Socket
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    fun pingDuration(vv: View) {
        GlobalScope.launch(Dispatchers.IO) {
            var sDuration = ""
            try {
                val a1 = Date()
                val timeoutMs = 1000    // 設定 timeout 為 1 秒
                val socket = Socket()
                val socketAddress = InetSocketAddress("8.8.8.8", 53)
                socket.connect(socketAddress, timeoutMs)
                socket.close()
                val a2 = Date()

                val durationMillis = a2.time - a1.time    // 平均小於 0.07 秒
                val hours = durationMillis / (1000 * 60 * 60)
                val minutes = durationMillis % (1000 * 60 * 60) / (1000 * 60)
                val seconds = durationMillis % (1000 * 60) / 1000
                val millis = durationMillis % 1000
                sDuration = String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis)
            } catch (e: Exception) {
                sDuration = "斷網 -- "+e.message
            }
            withContext(Dispatchers.Main) {
                binding.txtHello.text = sDuration
            }
        }
    }
}
 



2023-05-20

【Linux】在 CentOS7 製作 LinuxMint21 USB 開機碟

參考資料 ----

以指令方式製作

開啟終端機
 
切換到存放要製作的 LinuxMint21 iso 檔的目錄, 在本例為 /vm
而我的隨身碟目錄為 /dev/sdf
[user]$  cd  /vm/
[user]$  su
[root]#  dd if=linuxmint-21.1-mate-64bit.iso of=/dev/sdf
 
指令模式不會顯示執行進度只好靜靜等待


以圖形界面方式製作

CentOS7 要製作 USB 開機碟,網上推薦的圖形界面工具軟體都是 unetbootin

CentOS7 並未內建安裝 unetbootin,所以要另外尋找套件庫 -- nux-dextop-release

 
[user]$  su
[root]#  yum  install  nux-dextop-release-0-5.el7.nux.noarch.rpm
[root]#  yum  install  unetbootin
 


執行 unetbootin(需要 root 權限)
 
[root]#  cd  /usr/bin/
[root]#  ./unetbootin
 

按下 "確定" 鈕,等待幾分鐘後就製作完成了




2023-05-16

【PHP】讀取 .csv 檔轉入資料庫,且檔案內容包含 html

參考資料 ----

fgetcsv


 
$file = fopen($fileName, 'r'); // 開啟 CSV 檔案
if($file) 
{
    fgetcsv($file);    // 如果第一列是標題列, 空跑一次不讀取, 略過標題列
    
    while (($data = fgetcsv($file)) !== false) 
    {
        // 逐行讀取 CSV 檔案的內容
        $num_fields = count($data);    // 取得欄位數
        $sField0 = $data[0];    // 數字
        $sField1 = $data[1];    // 數字
        $sField2 = $data[2];    // 字串
        $sField3 = $data[3];    // 字串
        $sField4 = $data[4];    // html 字串
        $sField5 = $data[5];    // 字串
        switch($num_fields)    // 因為我的 csv 檔有 2 種格式,欄位數 5 欄 & 6 欄 2 種
        {
            case 5: // 無第 6 欄
                $sField6 = 'null';
                break;
            case 6: // 有第 6 欄
                if(length($data[6])>0)
                    // 第 6 欄有值
                    $sField6 = "'".$data[6]."'";
                else
                    $sField6 = 'null';
                break;
        }
        
        // 因為要防止 SQL injection, 當 SQL 語法中含有 html 時, html 會被 "洗掉"!
        // 所以要以 bindParam() 參數的方式組成 SQL 語法
        $sql = "INSERT INTO qbankd
                (Field1, Field2, Field3, Field4, Field5, Field6)
                VALUES
                ($sField1, '$sField2', '$sField3', :sField4, '$sField5', $sField6) ";
        $pdoStat = $pdo->prepare($sql);
        $pdoStat->bindParam(':sField4', $sField4);
        $pdoStat->execute();
    }
    fclose($file);  // 關閉 CSV 檔案
    unlink($file);  // 刪除檔案
}
 



2023-05-07

【Python2】計算 起、迄 時刻經過的時間

 
#!/usr/bin/python
#-*- coding:utf-8 -*-

import datetime

tBeginTime = datetime.datetime.now()
sBegin = tBeginTime.strftime("%Y.%m.%d  %H:%M:%S")

...
...

tStopTime = datetime.datetime.now()
sStop = tStopTime.strftime("%Y.%m.%d  %H:%M:%S")

print('開始時刻: '+sBegin+'\n'+
      '結束時刻: '+sStop+'\n'+
      '歷時: '+str(tStopTime-tBeginTime))


# 顯示類似下述
# 開始時間: 2023.05.05  10:19:18
# 結束時間: 2023.05.06  22:46:04
# 歷時: 1 day, 12:26:46.230894
 



2023-05-01

【軟體筆記】將目錄下的檔名匯入至純文字檔內

開啟 命令提示字元(DOS 視窗)

 
# 切換至要匯出檔名的目錄
cd\dir1

# 以 dir 指令將檔名匯到指定的文字檔
dir /b  > c:\myfilelist.txt
 

2023-04-16

【Kotlin】AdMob 插頁式廣告(InterstitialAd AdMob20[含]↑)

參考資料 ----

插页式广告

InterstitialAd

不允許的插頁式廣告導入方式

 

 

app 層級的 build.gradle 

 
dependencies {
    ...
    ...
    
    // adMob
    implementation 'com.google.android.gms:play-services-ads:22.0.0'
}

apply plugin: 'com.google.gms.google-services'
 

 

 

AndroidManifest.xml 

 
<application
    ...
    ...>
    
    <meta-data
        android:name="com.google.android.gms.ads.APPLICATION_ID"
        android:value="@string/admob_app_id" />

    ...
    ...
 

 

 

string.xml 

 
<!-- adMob app id 測試 -->
<string name="admob_app_id">ca-app-pub-3940256099942544~3347511713</string>
<!-- 橫幅 測試 -->
<string name="banner_ad_unit_id">ca-app-pub-3940256099942544/6300978111</string>;
<!-- 插頁 測試 -->
<string name="interstitial_ad_unit_id">ca-app-pub-3940256099942544/8691691433</string>
<!-- 影片 測試 -->
<string name="video_ad_id">ca-app-pub-3940256099942544/5224354917</string>
 

 

 

MainActivity.java 

 
class MyActivity : Activity()
{
    var mInterstitialAd: InterstitialAd? = null
    
    ...
    ...
    
    override fun onResume()
    {
        var adRequest = AdRequest.Builder().build()
        InterstitialAd.load(this,getString(R.string.interstitial_ad_unit_id), adRequest, object : InterstitialAdLoadCallback() {
            override fun onAdFailedToLoad(adError: LoadAdError) {
                Log.d(TAG, adError.toString())
                mInterstitialAd = null
            }

            override fun onAdLoaded(interstitialAd: InterstitialAd) {
                Log.d(TAG, "Ad was loaded.")
                mInterstitialAd = interstitialAd
            }
        })
        super.onResume()
    }
    
    
    // 當 usser 按了按鈕時
    fun onImgClicked(vv: View)
    {
        toWatchIntersticialAd()
    }
    
    
    // 觀看插頁廣告
    private fun toWatchIntersticialAd()
    {
        val random = Random()
        if ((mInterstitialAd!=null) && (random.nextInt(3)==0))
        // if(mInterstitialAd!= null) // 測試用, 每次都播放
            {
                // 若廣告已載入, 且亂數是 3 的倍數, 則播放
                // (AdMob 規定: 插頁播放的比例須 < 50%)
                mInterstitialAd?.fullScreenContentCallback =
                    object : FullScreenContentCallback() {
                        override fun onAdDismissedFullScreenContent() {
                            Log.d(TAG, "Ad was dismissed.")
                            // user 關閉廣告, 回主畫面
                            mInterstitialAd = null
                            // 返回主畫面
                            NavUtils.navigateUpFromSameTask(this@MyActivity)
                        }

                        override fun onAdFailedToShowFullScreenContent(adError: AdError) {
                            Log.d(TAG, "Ad failed to show.")
                            mInterstitialAd = null
                            // 返回主畫面
                            NavUtils.navigateUpFromSameTask(this@MyActivity)
                        }

                        override fun onAdShowedFullScreenContent() {
                            Log.d(TAG, "Ad showed fullscreen content.")
                            // Called when ad is dismissed.
                        }
                    }
                mInterstitialAd!!.show(this)
            }
        else
            // 直接返回主畫面
            NavUtils.navigateUpFromSameTask(this)
    }
}