<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-TW, en-US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://robertnotes.com/atom.xml" rel="self" type="application/atom+xml" /><link href="https://robertnotes.com/" rel="alternate" type="text/html" hreflang="zh-TW, en-US" /><updated>2025-11-03T06:10:36+00:00</updated><id>https://robertnotes.com/atom.xml</id><title type="html">Robert Notes</title><subtitle>Robert Wang</subtitle><author><name>Robert Wang</name></author><entry><title type="html">rsync files to Android</title><link href="https://robertnotes.com/2025/11/03/rsync-to-android.html" rel="alternate" type="text/html" title="rsync files to Android" /><published>2025-11-03T00:00:00+00:00</published><updated>2025-11-03T00:00:00+00:00</updated><id>https://robertnotes.com/2025/11/03/rsync-to-android</id><content type="html" xml:base="https://robertnotes.com/2025/11/03/rsync-to-android.html"><![CDATA[<p>我一直在使用 Obsidian，雖然不算是大量高頻率使用者，一兩年來還是累積了近千個檔案。</p>

<p>除了寫一些心得，我還會把 PDF 放在 Vault 裡，一邊讀一邊寫筆記或重點。當然我發現還是手寫對記憶最有幫助，儘管它可能散落在各個筆記本，畢竟我習慣不是很好，Bullet Journal 也荒廢了兩年，因為寫過後沒有很常回顧，沒有回顧就沒有它所謂的 migration，基本上 journaling 的效果就廢了一半，只剩下類似專注冥想反思的功能。</p>

<p>也接近年底了，也許重拾 Bullet Journal 可以排進明年的新希望之一。</p>

<p>好的進入本文主題。</p>

<p>累積大量的文件，免不了就是得同步到不同電腦上。Obsidian 本身提供<a href="https://help.obsidian.md/sync/switch">付費方案</a>，身為免費仔的我，只把小小的 Vault 放在 iCloud 裡，任隨它進行同步。</p>

<p>我對同步這件事的看法是，這是廠商想要賣給你更多台裝置的陰謀（無論是手機平板筆電桌機）！當然這其中技術成分也不低，不能否認其中的價值，否則 DropBox 當初那麼紅而且還被幾家大公司邀請併購，直到 Apple, Google, Microsoft 都做出各自的同步版本。</p>

<p>你買越多台裝置，就檔案同步的問題就越大，讓你一個腦袋兩隻手在多裝置上忙不過來：廠商製造問題，接著廠商解決問題。</p>

<p>回到我自己的經驗來說，真正使用、真正需要生產力的電腦，並不會超過兩台，甚至說只有一台主要電腦，另一台只會是配角備用。以寫作的情境來說，不管是文章撰寫或是程式開發，舒服有生產力的環境通常需要合適的座位、鍵盤滑鼠，甚至是燈光，所以這通常只會有一套設備，一套專注沈浸在產出的設備，以及這套設備所搭配的環境，也就是說這是一套專用的輸出的配置（也可說我還不夠有錢）。</p>

<p>以 Obsidian 的使用情境來說，以檔案為主的同步機制早已普遍出現在各平台上。但我認為隨時頻繁同步的價值並不高，原因是產出裝置/環境只有一個，即使需要在其他裝置上閱讀，到時候再同步即可。</p>

<p>自從換掉 iPhone 回到 Android 後，我一直有同步 Obsidian Vault 到 Android 的需求，儘管頻率不高但一個禮拜或是一個月一次還是會發生的，偏手動的方式我想到了 rsync。</p>

<p>從 Mac 上把 Vault 搬到 Android 手機裡，這樣 Android 的 Obsidian App 也可以打開同一個 Vault。</p>

<h3 id="需要的工具有">需要的工具有：</h3>
<ul>
  <li>Android Phone</li>
  <li>Termux App</li>
  <li>rsync</li>
  <li>openssh</li>
</ul>

<h3 id="步驟">步驟：</h3>

<ol>
  <li>在 Termux 裡安裝這三項工具
    <blockquote>
      <p>pkg install rysnc openssh termux-services</p>
    </blockquote>
  </li>
  <li>執行 <code class="language-plaintext highlighter-rouge">termux-setup-storage</code> 給 termux 全域磁碟權限</li>
  <li>執行 <code class="language-plaintext highlighter-rouge">passwd</code> 設定密碼（讓 Mac 連線時輸入使用）</li>
  <li>執行 <code class="language-plaintext highlighter-rouge">sshd</code> 預設開在 port 8022</li>
  <li>確認 Mac 及 Android 在同一個網域裡（192.168.X.X）
    <blockquote>
      <p>開啟手機熱點讓 Mac 連上就可以在同一個網域裡了</p>
    </blockquote>
  </li>
  <li>確認 Android ip 位置在 <code class="language-plaintext highlighter-rouge">termux</code> 執行 <code class="language-plaintext highlighter-rouge">ipconfig</code>
    <blockquote>
      <p>ipconfig 
…..
inet 192.168.11.88</p>
    </blockquote>
  </li>
  <li>在 <code class="language-plaintext highlighter-rouge">termux</code> 執行 <code class="language-plaintext highlighter-rouge">whoami</code> 並記下來稍後輸入
    <blockquote>
      <p>u0_a345</p>
    </blockquote>
  </li>
</ol>

<h3 id="將以下-script-存檔並改為可執行">將以下 script 存檔並改為可執行：</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Rsync to Android (Termux) Script</span>
<span class="c"># This script helps you sync files from Mac to Android via SSH/rsync</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="c"># Colors for output</span>
<span class="nv">RED</span><span class="o">=</span><span class="s1">'\033[0;31m'</span>
<span class="nv">GREEN</span><span class="o">=</span><span class="s1">'\033[0;32m'</span>
<span class="nv">YELLOW</span><span class="o">=</span><span class="s1">'\033[1;33m'</span>
<span class="nv">NC</span><span class="o">=</span><span class="s1">'\033[0m'</span> <span class="c"># No Color</span>

<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">GREEN</span><span class="k">}</span><span class="s2">=== Rsync to Android (Termux) ===</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="se">\n</span><span class="s2">"</span>

<span class="c"># Check if rsync is installed</span>
<span class="k">if</span> <span class="o">!</span> <span class="nb">command</span> <span class="nt">-v</span> rsync &amp;&gt; /dev/null<span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Error: rsync is not installed</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"Install it with: brew install rsync"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Get Android username</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter Android username (e.g., u0_a123): "</span> ANDROID_USER
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$ANDROID_USER</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Username cannot be empty</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Get Android IP address</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter Android IP address (e.g., 192.168.1.100): "</span> ANDROID_IP
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$ANDROID_IP</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">IP address cannot be empty</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Get SSH port (default 8022)</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter SSH port [default: 8022]: "</span> SSH_PORT
<span class="nv">SSH_PORT</span><span class="o">=</span><span class="k">${</span><span class="nv">SSH_PORT</span><span class="k">:-</span><span class="nv">8022</span><span class="k">}</span>

<span class="c"># Get source directory</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter source directory/file on Mac: "</span> SOURCE_PATH
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$SOURCE_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Source path cannot be empty</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Expand tilde in source path</span>
<span class="nv">SOURCE_PATH</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">SOURCE_PATH</span><span class="p">/#\~/</span><span class="nv">$HOME</span><span class="k">}</span><span class="s2">"</span>

<span class="c"># Check if source exists</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$SOURCE_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Error: Source path does not exist: </span><span class="nv">$SOURCE_PATH</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Get destination directory</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Common Android destinations:</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"  ~/storage/downloads/  - Downloads folder"</span>
<span class="nb">echo</span> <span class="s2">"  ~/storage/shared/     - Internal storage"</span>
<span class="nb">echo</span> <span class="s2">"  ~/received/           - Home directory folder"</span>
<span class="nb">echo</span> <span class="s2">"  ~/                    - Termux home"</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter destination path on Android: "</span> DEST_PATH
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$DEST_PATH</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Destination path cannot be empty</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Authentication method</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Select authentication method:</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"  1) Password"</span>
<span class="nb">echo</span> <span class="s2">"  2) SSH key"</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter choice [1-2]: "</span> AUTH_METHOD

<span class="c"># Build rsync command</span>
<span class="nv">RSYNC_CMD</span><span class="o">=</span><span class="s2">"rsync -avzh --progress"</span>

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$AUTH_METHOD</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"2"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Enter path to SSH private key [default: ~/.ssh/id_rsa]: "</span> SSH_KEY
    <span class="nv">SSH_KEY</span><span class="o">=</span><span class="k">${</span><span class="nv">SSH_KEY</span><span class="k">:-</span><span class="p">~/.ssh/id_rsa</span><span class="k">}</span>
    <span class="nv">SSH_KEY</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">SSH_KEY</span><span class="p">/#\~/</span><span class="nv">$HOME</span><span class="k">}</span><span class="s2">"</span>
    
    <span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$SSH_KEY</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">Error: SSH key not found: </span><span class="nv">$SSH_KEY</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
        <span class="nb">exit </span>1
    <span class="k">fi
    
    </span><span class="nv">RSYNC_CMD</span><span class="o">=</span><span class="s2">"</span><span class="nv">$RSYNC_CMD</span><span class="s2"> -e </span><span class="se">\"</span><span class="s2">ssh -p </span><span class="nv">$SSH_PORT</span><span class="s2"> -i </span><span class="nv">$SSH_KEY</span><span class="se">\"</span><span class="s2">"</span>
<span class="k">else
    </span><span class="nv">RSYNC_CMD</span><span class="o">=</span><span class="s2">"</span><span class="nv">$RSYNC_CMD</span><span class="s2"> -e </span><span class="se">\"</span><span class="s2">ssh -p </span><span class="nv">$SSH_PORT</span><span class="se">\"</span><span class="s2">"</span>
<span class="k">fi</span>

<span class="c"># Add source and destination</span>
<span class="nv">RSYNC_CMD</span><span class="o">=</span><span class="s2">"</span><span class="nv">$RSYNC_CMD</span><span class="s2"> </span><span class="se">\"</span><span class="nv">$SOURCE_PATH</span><span class="se">\"</span><span class="s2"> </span><span class="k">${</span><span class="nv">ANDROID_USER</span><span class="k">}</span><span class="s2">@</span><span class="k">${</span><span class="nv">ANDROID_IP</span><span class="k">}</span><span class="s2">:</span><span class="se">\"</span><span class="k">${</span><span class="nv">DEST_PATH</span><span class="k">}</span><span class="se">\"</span><span class="s2">"</span>

<span class="c"># Confirm before executing</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">=== Summary ===</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"Source:      </span><span class="nv">$SOURCE_PATH</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"Destination: </span><span class="k">${</span><span class="nv">ANDROID_USER</span><span class="k">}</span><span class="s2">@</span><span class="k">${</span><span class="nv">ANDROID_IP</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">DEST_PATH</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"SSH Port:    </span><span class="nv">$SSH_PORT</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"Auth:        </span><span class="si">$(</span><span class="o">[</span> <span class="s2">"</span><span class="nv">$AUTH_METHOD</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"2"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"SSH Key (</span><span class="nv">$SSH_KEY</span><span class="s2">)"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Password"</span><span class="si">)</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Command to execute:</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$RSYNC_CMD</span><span class="s2">"</span>
<span class="nb">echo

read</span> <span class="nt">-p</span> <span class="s2">"Proceed with transfer? (y/n): "</span> CONFIRM
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$CONFIRM</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"y"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$CONFIRM</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"Y"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Transfer cancelled."</span>
    <span class="nb">exit </span>0
<span class="k">fi</span>

<span class="c"># Test SSH connection first</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Testing SSH connection...</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$AUTH_METHOD</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"2"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span>ssh <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$SSH_PORT</span><span class="s2">"</span> <span class="nt">-i</span> <span class="s2">"</span><span class="nv">$SSH_KEY</span><span class="s2">"</span> <span class="nt">-o</span> <span class="nv">ConnectTimeout</span><span class="o">=</span>5 <span class="s2">"</span><span class="k">${</span><span class="nv">ANDROID_USER</span><span class="k">}</span><span class="s2">@</span><span class="k">${</span><span class="nv">ANDROID_IP</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"echo 'Connection successful'"</span> 2&gt;/dev/null
<span class="k">else
    </span>ssh <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$SSH_PORT</span><span class="s2">"</span> <span class="nt">-o</span> <span class="nv">ConnectTimeout</span><span class="o">=</span>5 <span class="s2">"</span><span class="k">${</span><span class="nv">ANDROID_USER</span><span class="k">}</span><span class="s2">@</span><span class="k">${</span><span class="nv">ANDROID_IP</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"echo 'Connection successful'"</span> 2&gt;/dev/null
<span class="k">fi

if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">GREEN</span><span class="k">}</span><span class="s2">✓ SSH connection successful</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="se">\n</span><span class="s2">"</span>
<span class="k">else
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">✗ SSH connection failed. Please check:</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"  - Android device is on the same network"</span>
    <span class="nb">echo</span> <span class="s2">"  - SSH server is running on Android (sshd)"</span>
    <span class="nb">echo</span> <span class="s2">"  - IP address and username are correct"</span>
    <span class="nb">echo</span> <span class="s2">"  - Port </span><span class="nv">$SSH_PORT</span><span class="s2"> is accessible"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Execute rsync</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Starting transfer...</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="se">\n</span><span class="s2">"</span>
<span class="nb">eval</span> <span class="nv">$RSYNC_CMD</span>

<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">GREEN</span><span class="k">}</span><span class="s2">✓ Transfer completed successfully!</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="k">else
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">RED</span><span class="k">}</span><span class="s2">✗ Transfer failed</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Ask if user wants to save configuration</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\n</span><span class="k">${</span><span class="nv">YELLOW</span><span class="k">}</span><span class="s2">Save this configuration for future use?</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"(y/n): "</span> SAVE_CONFIG
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$SAVE_CONFIG</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"y"</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$SAVE_CONFIG</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"Y"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nv">CONFIG_FILE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.rsync_android_config"</span>
    <span class="nb">echo</span> <span class="s2">"ANDROID_USER=</span><span class="se">\"</span><span class="nv">$ANDROID_USER</span><span class="se">\"</span><span class="s2">"</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$CONFIG_FILE</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"ANDROID_IP=</span><span class="se">\"</span><span class="nv">$ANDROID_IP</span><span class="se">\"</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$CONFIG_FILE</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"SSH_PORT=</span><span class="se">\"</span><span class="nv">$SSH_PORT</span><span class="se">\"</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$CONFIG_FILE</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"AUTH_METHOD=</span><span class="se">\"</span><span class="nv">$AUTH_METHOD</span><span class="se">\"</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$CONFIG_FILE</span><span class="s2">"</span>
    <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$AUTH_METHOD</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"2"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"SSH_KEY=</span><span class="se">\"</span><span class="nv">$SSH_KEY</span><span class="se">\"</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$CONFIG_FILE</span><span class="s2">"</span>
    <span class="k">fi
    </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="k">${</span><span class="nv">GREEN</span><span class="k">}</span><span class="s2">✓ Configuration saved to </span><span class="nv">$CONFIG_FILE</span><span class="k">${</span><span class="nv">NC</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"You can edit this file to quickly load settings next time"</span>
<span class="k">fi</span>
</code></pre></div></div>

<ol>
  <li>存檔成 rsync_to_android.sh</li>
  <li>chmod +x ./rsync_to_android.sh</li>
  <li>./rsync_to_android.sh</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=== Rsync to Android (Termux) ===

Enter Android username (e.g., u0_a123): u0_a345
Enter Android IP address (e.g., 192.168.1.100): 192.168.11.88 
Enter SSH port [default: 8022]:
Enter source directory/file on Mac: /Users/robert.wang/Documents/ob_vault

Common Android destinations:
  ~/storage/downloads/  - Downloads folder
  ~/storage/shared/     - Internal storage
  ~/received/           - Home directory folder
  ~/                    - Termux home
Enter destination path on Android: /storage/emulated/0/Documents/

Select authentication method:
  1) Password
  2) SSH key
Enter choice [1-2]: 1

=== Summary ===
Source:      /Users/robert.wang/Documents/ob_vault
Destination: u0_a345@172.16.99.163:/storage/emulated/0/Documents/
SSH Port:    8022
Auth:        Password

Command to execute:
rsync -avzh --progress -e "ssh -p 8022" "/Users/robert.wang/Documents/ob_vault" u0_a345@172.16.99.163:"/storage/emulated/0/Documents/"

Proceed with transfer? (y/n): y

Testing SSH connection...
u0_a345@172.16.99.163's password:
Connection successful
✓ SSH connection successful

Starting transfer...

u0_a345@172.16.99.163's password:
Transfer starting: 6 files
WFH_LOG/
WFH_LOG/20250722_WFH.txt
             87 100%  240.78KB/s   00:00:00 (xfer#1, to-check=1/6)
WFH_LOG/20250826_WFH.txt
             23 100%   16.32KB/s   00:00:00 (xfer#2, to-check=2/6)
WFH_LOG/20250926_WFH.txt
             23 100%   30.19KB/s   00:00:00 (xfer#3, to-check=3/6)
WFH_LOG/20251031_WFH.txt
             16 100%   11.74KB/s   00:00:00 (xfer#4, to-check=4/6)

sent 123k bytes  received 136 bytes  369k bytes/sec
total size is 132k  speedup is 1.07

✓ Transfer completed successfully!

Save this configuration for future use?
(y/n): y
✓ Configuration saved to /Users/robert.wang/.rsync_android_config
You can edit this file to quickly load settings next time

</code></pre></div></div>

<p><a href="https://claude.ai/public/artifacts/e68c2a15-4b32-4003-b3bc-12e52be5b7c5">以上的 script 是由 Claude AI 產生</a>，實測過可以正常使用，並儲存前一次的設定方便下次使用</p>

<p>這樣就達到我久久一次的 Obsidian Vault 同步程序了！一點點研究，一點點麻煩，不過有 script 可以省掉很多心思了！</p>

<p>最後你還可以在 Mac 設定 alias，以便快速叫出這個指令：</p>

<blockquote>
  <p>vim ~/.zshrc
alias rsync-android=’~/rsync-to-android.sh’</p>
</blockquote>]]></content><author><name>Robert Wang</name></author><category term="rsync" /><category term="sshd" /><category term="obsidian" /><category term="Android" /><summary type="html"><![CDATA[我一直在使用 Obsidian，雖然不算是大量高頻率使用者，一兩年來還是累積了近千個檔案。]]></summary></entry><entry><title type="html">Things about calling toString on BigDecimal</title><link href="https://robertnotes.com/2024/11/06/big-decimal-to-string.html" rel="alternate" type="text/html" title="Things about calling toString on BigDecimal" /><published>2024-11-06T00:00:00+00:00</published><updated>2024-11-06T00:00:00+00:00</updated><id>https://robertnotes.com/2024/11/06/big-decimal-to-string</id><content type="html" xml:base="https://robertnotes.com/2024/11/06/big-decimal-to-string.html"><![CDATA[<h1 id="the-trade-offs-between-tostring-and-toplainstring-in-bigdecimal">The Trade-offs Between toString() and toPlainString() in BigDecimal</h1>

<h2 id="introduction">Introduction</h2>

<p>In financial applications, precise decimal representation is crucial. Recently, our team debated the use of <code class="language-plaintext highlighter-rouge">toString()</code> versus <code class="language-plaintext highlighter-rouge">toPlainString()</code> when working with BigDecimal values. This article explores the performance implications and practical considerations of both methods.</p>

<h2 id="performance-analysis">Performance Analysis</h2>

<p>To quantify the performance difference, we conducted a benchmark comparing both methods across various number types:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">numbers</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span>
    <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"12345.56789"</span><span class="p">),</span>             <span class="c1">// Regular number</span>
    <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"1234567890.0123456789"</span><span class="p">),</span>   <span class="c1">// Regular number with high precision</span>
    <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"1E20"</span><span class="p">),</span>                    <span class="c1">// Very large number</span>
    <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"1E-8"</span><span class="p">),</span>                    <span class="c1">// Very small number</span>
    <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"999999999999999999.999999999999999999"</span><span class="p">)</span> <span class="c1">// Large precision</span>
<span class="p">)</span>

<span class="kd">val</span> <span class="py">iterations</span> <span class="p">=</span> <span class="mi">1_000_000</span>

<span class="n">numbers</span><span class="p">.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">number</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"\nTesting with number: $number"</span><span class="p">)</span>
    
    <span class="kd">val</span> <span class="py">toStringTime</span> <span class="p">=</span> <span class="nf">measureNanoTime</span> <span class="p">{</span>
        <span class="nf">repeat</span><span class="p">(</span><span class="n">iterations</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">number</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">val</span> <span class="py">toPlainStringTime</span> <span class="p">=</span> <span class="nf">measureNanoTime</span> <span class="p">{</span>
        <span class="nf">repeat</span><span class="p">(</span><span class="n">iterations</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">number</span><span class="p">.</span><span class="nf">toPlainString</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nf">println</span><span class="p">(</span><span class="s">"toString() took: ${toStringTime / iterations} ns per call"</span><span class="p">)</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"toPlainString() took: ${toPlainStringTime / iterations} ns per call"</span><span class="p">)</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"toPlainString() is ${toPlainStringTime.toFloat() / toStringTime.toFloat()}x slower"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="results">Results</h3>

<p>The performance difference is significant and varies based on the number of digits:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Testing with number: 12345.56789
toString() took: 3 ns per call
toPlainString() took: 44 ns per call
toPlainString() is 11.48x slower

Testing with number: 1234567890.0123456789
toString() took: 3 ns per call
toPlainString() took: 241 ns per call
toPlainString() is 63.91x slower

Testing with number: 1E+20
toString() took: 1 ns per call
toPlainString() took: 61 ns per call
toPlainString() is 48.25x slower
</code></pre></div></div>

<h2 id="real-world-implications">Real-world Implications</h2>

<p>In our banking application, we frequently need to display the difference between two BigDecimal values, typically for scenarios like:</p>
<ul>
  <li>Showing account balance</li>
  <li>Calculating required additional deposits for purchases</li>
  <li>Displaying transaction amounts</li>
</ul>

<h3 id="edge-case-analysis">Edge Case Analysis</h3>

<p>We tested various scenarios to understand when scientific notation might appear:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">a</span> <span class="p">=</span> <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"1"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">b</span> <span class="p">=</span> <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"0.00000000000000100001"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="n">a</span> <span class="p">-</span> <span class="n">b</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"1 - 0.00000000000000100001 = ${result.toString()} (Plain: ${result.toPlainString()})"</span><span class="p">)</span>

<span class="kd">val</span> <span class="py">largeA</span> <span class="p">=</span> <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"1000000000000000000000"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">largeB</span> <span class="p">=</span> <span class="nc">BigDecimal</span><span class="p">(</span><span class="s">"999999999999999999999"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">largeResult</span> <span class="p">=</span> <span class="n">largeA</span> <span class="p">-</span> <span class="n">largeB</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Large number subtraction = ${largeResult.toString()} (Plain: ${largeResult.toPlainString()})"</span><span class="p">)</span>
</code></pre></div></div>

<p>Our tests revealed that <code class="language-plaintext highlighter-rouge">toString()</code> produces scientific notation primarily with very small differences:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Very small difference:
1.000000000000000000001 - 1 = 1E-21 (Plain: 0.000000000000000000001)
</code></pre></div></div>

<h2 id="the-solution">The Solution</h2>

<p>To balance performance with user experience, we implemented a hybrid approach:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">BigDecimal</span><span class="p">.</span><span class="nf">toStringWithoutE</span><span class="p">():</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">asString</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
    <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="n">asString</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="sc">'E'</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nf">toPlainString</span><span class="p">()</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">asString</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This solution:</p>
<ol>
  <li>Initially uses the faster <code class="language-plaintext highlighter-rouge">toString()</code></li>
  <li>Falls back to <code class="language-plaintext highlighter-rouge">toPlainString()</code> only when scientific notation is detected</li>
  <li>Ensures readable output for users while maintaining optimal performance in most cases</li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>While <code class="language-plaintext highlighter-rouge">toPlainString()</code> is significantly slower than <code class="language-plaintext highlighter-rouge">toString()</code>, the hybrid approach provides an excellent balance between performance and usability. For financial applications where user experience is crucial, this compromise ensures both efficient processing and clear, understandable number representation.</p>]]></content><author><name>Robert Wang</name></author><category term="BigDeciaml" /><category term="Bank" /><summary type="html"><![CDATA[The Trade-offs Between toString() and toPlainString() in BigDecimal]]></summary></entry><entry><title type="html">蘆洲廟會遶境的索討</title><link href="https://robertnotes.com/2024/07/21/luzhou-process-donation.html" rel="alternate" type="text/html" title="蘆洲廟會遶境的索討" /><published>2024-07-21T00:00:00+00:00</published><updated>2024-07-21T00:00:00+00:00</updated><id>https://robertnotes.com/2024/07/21/luzhou-process-donation</id><content type="html" xml:base="https://robertnotes.com/2024/07/21/luzhou-process-donation.html"><![CDATA[<p>今天騎車在蘆洲等紅燈的時候，一位穿著類似參加廟會遶境的人湊上前來，拿著糖果擺到我手上，然後說：幾霸摳！跟我要一百元的意思。</p>

<p>剛好我沒帶錢包出門，我回他說沒帶錢！他竟然把糖果拿走~~~~~</p>

<p>你不是要分享神明加持過的供物嗎？？？？結果是來要錢的啊！</p>

<p>如果我是他的話，一開始就會說清楚是分享神明加持過的糖果，備有多種口味的糖果，可以讓你自己選，然後金額隨喜，這樣的話我還可能會多給一些！</p>

<p>要錢就提供一些服務，有加持過的糖果、有選擇權、金額隨喜，這樣成功率不是更高嗎？</p>]]></content><author><name>Robert Wang</name></author><category term="蘆洲" /><category term="遶境" /><summary type="html"><![CDATA[今天騎車在蘆洲等紅燈的時候，一位穿著類似參加廟會遶境的人湊上前來，拿著糖果擺到我手上，然後說：幾霸摳！跟我要一百元的意思。]]></summary></entry><entry><title type="html">右傾的機車騎士</title><link href="https://robertnotes.com/2024/06/30/strange-scooter-driver.html" rel="alternate" type="text/html" title="右傾的機車騎士" /><published>2024-06-30T00:00:00+00:00</published><updated>2024-06-30T00:00:00+00:00</updated><id>https://robertnotes.com/2024/06/30/strange-scooter-driver</id><content type="html" xml:base="https://robertnotes.com/2024/06/30/strange-scooter-driver.html"><![CDATA[<p>今天在蘆洲環提大道上遇到一位騎著機車的阿伯，他以神奇的右傾姿勢快速前進，左手還叼著煙！</p>

<p>我追上去之後才發現，原來他是因為手機架偏右側，為了一路看車一路看手機，似乎已經跟手機融為一體，直接把身體向右傾，車身也向左偏的狀況下前進…</p>

<p>這種車，我還是閃遠一點以策安全！</p>]]></content><author><name>Robert Wang</name></author><category term="scooter" /><category term="driver" /><summary type="html"><![CDATA[今天在蘆洲環提大道上遇到一位騎著機車的阿伯，他以神奇的右傾姿勢快速前進，左手還叼著煙！]]></summary></entry><entry><title type="html">二度開發購買基金功能</title><link href="https://robertnotes.com/2024/06/28/implement-another-purchase-screen.html" rel="alternate" type="text/html" title="二度開發購買基金功能" /><published>2024-06-28T00:00:00+00:00</published><updated>2024-06-28T00:00:00+00:00</updated><id>https://robertnotes.com/2024/06/28/implement-another-purchase-screen</id><content type="html" xml:base="https://robertnotes.com/2024/06/28/implement-another-purchase-screen.html"><![CDATA[<p>最近要在寫一次購買基金的功能，為了記取上次過於貪心想要使用一個共用元件通吃三種購買方式的下場，在正式開發之前，我特別保留時間回去 trace 上次寫的程式，並設計一個功能耦合度最低的架構來實作。</p>

<p>購買基金一般來說有三種購買方式：單筆，定期定額及修改定期定額</p>

<p>之前：實做一個同時完成三種購買方式的元件</p>

<p>這次：三種購買方式改由三個獨立元件來達成，透過可共用的 Compose 元件來重新組合出不同的行為</p>

<p>上一次的實作決定讓所有行為都混雜在一起，導致超級複雜的 LaunchedEffect 來驅動試算結果，在後續需求變化下造成改了A壞了B，動線無法輕易切分開，是個十分慘痛的經驗！也可能是分工上討論不夠，同事各自做不同的功能，項目沒有被切分成可以好好分工的程度，所以第二次挑戰，希望可以好好做成好改好維護的版本，可長可久的開發下去。</p>]]></content><author><name>Robert Wang</name></author><category term="trust" /><category term="compose" /><category term="job" /><summary type="html"><![CDATA[最近要在寫一次購買基金的功能，為了記取上次過於貪心想要使用一個共用元件通吃三種購買方式的下場，在正式開發之前，我特別保留時間回去 trace 上次寫的程式，並設計一個功能耦合度最低的架構來實作。]]></summary></entry><entry><title type="html">社群網站帶來的病</title><link href="https://robertnotes.com/2024/06/20/social-network-disease.html" rel="alternate" type="text/html" title="社群網站帶來的病" /><published>2024-06-20T00:00:00+00:00</published><updated>2024-06-20T00:00:00+00:00</updated><id>https://robertnotes.com/2024/06/20/social-network-disease</id><content type="html" xml:base="https://robertnotes.com/2024/06/20/social-network-disease.html"><![CDATA[<p>今天又聽到同事提到把手機上的社群網站 app 移掉之後的舒暢跟平靜感，其實我也試過幾次，大約是兩三個禮拜後會用瀏覽器來看原本那些 app 的內容，再過一陣子覺得他們網頁很卡很不順，接著就會把 app 又裝回來了！</p>

<p>試過 <a href="https://one-sec.app/">One Sec</a> 這個工具來幫忙稍微延緩打開社群 app 的衝動，但似乎習慣了它的延緩動畫後，還是依然義無反顧的進去狂刷猛刷浪費了很多時間！</p>

<p>好像已經很難想起以前沒有這些思緒泥沼的時候，我會做什麼事？</p>

<p>以前只有 Google 的時候就是到處搜尋瀏覽爬文，讀多了還會寫些文章在無名小站，還記得同學說啊你好厲害怎麼寫了那麼多篇！後來無名掛了好像還備份了起來只是早已消失在多年的搬移之中。那時候還沒有純文字 markdown 這種語法，總是要在各家網誌的奇耙語法中折騰，也許是年少只知道賣弄語法，不知文字本身才是會留下來的重點，就像寫了 doc 檔你就是被微軟綁架還覺得 Word 好用！<a href="https://frdm.cyut.edu.tw/~ckhung/a/c_91.php">可參考洪朝貴老師的文章</a></p>

<p>社群網站就像是環繞在最表層思緒的那些念頭，手機通知的推播助瀾下，無時不刻的侵擾著我們的意識！天外飛來一筆的幻想、偶爾寫幾個字的動力都會被排擠掉，可怕的是連可以拿來運動或面對面講話的時間都被置換掉！</p>

<p>今天起刪除 Facebook, Instagram, Thread, Twitter(X)，每天 +1，看能不能撐到 D+30天！</p>

<hr />

<p>關於手機影響腦袋的研究有很多，參考如下：</p>

<ul>
  <li><a href="https://www.books.com.tw/products/0010911511">拯救手機腦：每天5分鐘，終結數位焦慮，找回快樂與專注力</a> 這本我讀電子書，可以想見我根本讀不完</li>
  <li><a href="https://www.hubermanlab.com/episode/dr-jonathan-haidt-how-smartphones-social-media-impact-mental-health-the-realistic-solutions">How Smartphones &amp; Social Media Impact Mental Health &amp; the Realistic Solutions</a> Andrew Huberman 是一位長青的作家/ Podcaster / Youtuber，專門研究神經學，有很多與其他專業人士的對談，非常推薦</li>
</ul>]]></content><author><name>Robert Wang</name></author><category term="social-network" /><category term="disease" /><category term="one-sec" /><summary type="html"><![CDATA[今天又聽到同事提到把手機上的社群網站 app 移掉之後的舒暢跟平靜感，其實我也試過幾次，大約是兩三個禮拜後會用瀏覽器來看原本那些 app 的內容，再過一陣子覺得他們網頁很卡很不順，接著就會把 app 又裝回來了！]]></summary></entry><entry><title type="html">兩個 thread 同時操作 List 造成 Crash</title><link href="https://robertnotes.com/2024/04/19/compose-threading.html" rel="alternate" type="text/html" title="兩個 thread 同時操作 List 造成 Crash" /><published>2024-04-19T00:00:00+00:00</published><updated>2024-04-19T00:00:00+00:00</updated><id>https://robertnotes.com/2024/04/19/compose-threading</id><content type="html" xml:base="https://robertnotes.com/2024/04/19/compose-threading.html"><![CDATA[<p>聽起來很糟糕對吧！這種常見的 threading 錯誤示範怎麼會通過 CodeReview 進到 PROD CODE呢？</p>

<p>請容我說來…</p>

<p>在某個版本 App 中有一個展示熱門商品列表的頁面，它的行為有：</p>

<ol>
  <li>直接載入前20筆熱門商品，捲到最後幾筆再呼叫 API 載入下一頁</li>
  <li>以 LazyColumn 實作整個頁面，含搜尋框</li>
  <li>以 stickyHeader 呈現關鍵字，讓它可以在手勢向上的時候收起來</li>
  <li>進入頁面時直接跳出鍵盤，方便直接搜尋</li>
</ol>

<p><img src="https://robertnotes.com/assets/images/Screenshot_20240416_094524.png" alt="screenshot" width="480px" /></p>

<p>這問題發生的原因主要是 UX 設計上版本更迭後造成的：第一個版本進到畫面時並不會呼叫 API，只有在按下搜尋才會呼叫 API。下一版本為了增加資訊量，加上了預設的呼叫 API 動作。</p>

<p>兩次迭代的行為變更，造成了疏忽！而且在我開發用的 Pixel 7a/Android 13 上並不會 Crash。</p>

<p>Crash 發生在不同手機上稍微不同：在 VIVO X80 上進到頁面後，輸入任何關鍵字在按下鍵盤上的搜尋，便會直接 crash；在其他手機上需要比較複雜的操作：</p>

<ol>
  <li>頁面載入後，輸入關鍵字（先不要按下搜尋）</li>
  <li>手勢向上快速滑動的同時，按下搜尋</li>
  <li>Crash</li>
</ol>

<h2 id="問題分析">問題分析</h2>

<p>從 Crashlytics 上的紀錄看起來是 IndexOutOfBound 造成的</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Fatal Exception: java.lang.IndexOutOfBoundsException

Index 11 out of bounds for length 0
</code></pre></div></div>

<p>我們的程式是以 MVVM 為主， VM 從 API 得到結果後，透過 mutableState 傳遞給定義在 Fragment 的 Compose 元件，如下所示：</p>

<pre><code class="language-mermaid">sequenceDiagram
Fragment-&gt;&gt;ViewModel: Call API
ViewModel--&gt;&gt;Fragment: Records in mutable state
Fragment-)Compose Element: Render these records

</code></pre>

<p>所以在第一次 Compose 拿到 20 筆資料後，並開始 Render 上螢幕的過程中（快速滑動），搜尋新關鍵字的動作會讓整個上述流程再跑一次，只是在另一個 Thread 操作，由那一個 Thread 將新的資料覆蓋到同一個 records 中！</p>

<p>這也就是問題：兩個 thread 有機會同時操作同一個 list：一個 thread 讀取的同時，另一個 thread 去寫！也就是上 crash log 寫的：UI thread 正在嘗試讀取 list 中的第 11 筆並繪製到螢幕上時， background thread 拿著剛拿到的新資料寫進了同一個 list 裡…剛剛要拿東西還有怎麼突然沒有了！</p>

<p>App 只能崩潰給你看…</p>

<h2 id="解決方案">解決方案</h2>

<p>重點是保護 list 的讀取/操作，至於要怎麼做？</p>

<p>一開始我用了 CopyOnWriteList，讓每次操作都可以 Copy 一份，可以解決上述的問題。但也產生另一個問題：隨著手勢向上滑載入下一頁，記憶體用量暴增，GC 不斷出現…有這個不友善的行為，決定不採用。</p>

<p>由於是兩個 thread 讀取同一個 list，所以模仿 CopyOnWriteList，是不是只要在 Compose 元件從新的 MutableState 裡拿到新的 list 時，從另一個複製出來的 list 去繪製資料就可以了呢？</p>

<pre><code class="language-mermaid">sequenceDiagram
Fragment-&gt;&gt;ViewModel: Call API
ViewModel--&gt;&gt;Fragment: Records in mutable state
Fragment-)Compose Element: Render these records with **copied list**

</code></pre>

<p>這個方式測試後也可以解決問題，記憶體也沒有像使用 CopyOnWriteList 一樣大幅度增加，GC 也就不會出現！</p>

<p>要注意到因為是 list 的複製，所以要自行處理 deep copy 的行為。如果你的 data class 有很多層，記得不能僅用 data class 提供的 copy function，因為它只會複製第一層的欄位，再往下一層去就只是 reference 的複製而已，又稱為 shallow copy（詳細可參考 [[Copy function of Data class]]）</p>

<p>這樣還是有機會讓兩個 thread 處不好喔！</p>

<h2 id="最終解法">最終解法</h2>

<p>自行實作一個 deepCopy function 讓 fragment 在轉手傳遞 list 到 Compose 元件時使用：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WMSProductSearchScreen</span><span class="p">(</span>  
    <span class="n">searchResult</span> <span class="p">=</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">searchResult</span><span class="o">?.</span><span class="nf">deepCopy</span><span class="p">()</span>  
        <span class="o">?:</span> <span class="nc">NCBGenericProductResult</span><span class="p">(</span><span class="n">_state</span> <span class="p">=</span> <span class="nc">GeneralState</span><span class="p">.</span><span class="nc">NoData</span><span class="p">),</span>  
    <span class="n">productClicked</span> <span class="p">=</span> <span class="p">{</span> <span class="n">code</span> <span class="p">-&gt;</span> <span class="nf">redirect</span><span class="p">(</span><span class="nc">XYZ</span><span class="p">)</span> <span class="p">},</span>  
    <span class="n">onScrollToEnd</span> <span class="p">=</span> <span class="p">{</span>  
        <span class="n">viewModel</span><span class="p">.</span><span class="nf">searchProduct</span><span class="p">(</span><span class="n">keyword</span> <span class="p">=</span> <span class="n">it</span><span class="p">,</span> <span class="n">isSearchNewKeyword</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
    <span class="p">},</span>  
    <span class="n">refreshClicked</span> <span class="p">=</span> <span class="p">{</span>  
        <span class="n">viewModel</span><span class="p">.</span><span class="nf">searchProduct</span><span class="p">(</span><span class="n">keyword</span> <span class="p">=</span> <span class="n">it</span><span class="p">,</span> <span class="n">isSearchNewKeyword</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>  
    <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>特別是 list 的複製：</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">GenericProductInfo</span><span class="p">.</span><span class="nf">deepCopy</span><span class="p">()</span> <span class="p">=</span>  
    <span class="nc">GenericProductInfo</span><span class="p">(</span>  
        <span class="n">products</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">GenericProduct</span><span class="p">&gt;().</span><span class="nf">also</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="n">products</span><span class="p">)</span> <span class="p">},</span>  
        <span class="n">pageInfo</span> <span class="p">=</span> <span class="n">pageInfo</span><span class="o">?.</span><span class="nf">copy</span><span class="p">()</span>  
    <span class="p">)</span>
</code></pre></div></div>

<h2 id="結語">結語</h2>

<p>儘管上面寫了技術解法，在 UI 設計上其實也是有解法：</p>

<ol>
  <li>進入頁面時不要讓鍵盤預設出現，讓使用者專心滑產品（搜尋降為第二步的操作）</li>
  <li>或者，進入頁面時還是讓鍵盤預設出現，加上像 iOS 一樣在整個畫面加上一層擷取觸擊的元件（輸入框以外），只要一觸擊到這層元件就將鍵盤關閉，減少兩個元件互相干擾的流程</li>
</ol>

<p>技術解法外，改變規格的做法都可以考慮看看。</p>

<p>當然，這要看你 Bargin 能力有多強囉！</p>]]></content><author><name>Robert Wang</name></author><category term="android" /><category term="compose" /><category term="threading" /><summary type="html"><![CDATA[聽起來很糟糕對吧！這種常見的 threading 錯誤示範怎麼會通過 CodeReview 進到 PROD CODE呢？]]></summary></entry><entry><title type="html">Long time no see</title><link href="https://robertnotes.com/2024/04/17/long-time-no-see.html" rel="alternate" type="text/html" title="Long time no see" /><published>2024-04-17T00:00:00+00:00</published><updated>2024-04-17T00:00:00+00:00</updated><id>https://robertnotes.com/2024/04/17/long-time-no-see</id><content type="html" xml:base="https://robertnotes.com/2024/04/17/long-time-no-see.html"><![CDATA[<p>不敢相信！</p>

<p>套句我家人的口頭禪，源自於某部韓劇？</p>

<p>自從上次這裡的更新, 已經是三年之前！我都無法相信自己是怎麼放過自己這樣廢耕文章的！</p>

<p>–more</p>

<p>罪己到此為止（好像不怎麼用力鞭）</p>

<p>近來發覺比起程式產出, 寫點文章當作產出似乎比較容易！特別是稍微戒除社群網路的症頭, 刪掉 Twitter, 裝上 <a href="https://one-sec.app/">OneSec</a>, 能夠找回一絲絲手癢想寫點東西、打點字的念頭（還只是念頭哦！）</p>

<p>從念頭落實到文章的距離可長可短, 只要你手握觸控手機, 很大機會當你醒來, 才意識到自己怎麼還在滑手機歐…本來想寫下來的那三言兩語早就被波濤不斷的社群推文淹沒, 僅存的意志力大概只夠我慢慢把身體擺到床上, 沉沉睡去</p>

<p>比起程式, 文章可長可短, 不需安裝開發環境或精準的編譯器版本設定, 還勞持續維護軟硬體搭配, 甚至得心甘情願的讓<strong>無良硬體廠商如蘋果</strong>剝削, 催眠自己正在實現夢想, 讓自己稍微有點產出, 對人類社會做出貢獻！</p>

<p>我的期許比較低, 只要能讓文字留下, 若干日後我或任何人偶然瞥見, 讀後感受到我曾經的一點我努力, 如同幾千年前在石壁上刻畫下打獵一幕的史前人類的不經意</p>

<p>41 歲男子的懸念是很那個的！</p>]]></content><author><name>Robert Wang</name></author><category term="life" /><summary type="html"><![CDATA[不敢相信！]]></summary></entry><entry><title type="html">Quickly mute and unmute mic on Mac</title><link href="https://robertnotes.com/2021/06/11/mute-unmute-mac.html" rel="alternate" type="text/html" title="Quickly mute and unmute mic on Mac" /><published>2021-06-11T00:00:00+00:00</published><updated>2021-06-11T00:00:00+00:00</updated><id>https://robertnotes.com/2021/06/11/mute-unmute-mac</id><content type="html" xml:base="https://robertnotes.com/2021/06/11/mute-unmute-mac.html"><![CDATA[<p>During #pandemic you’re #WFH , are you sick of constantly mute and unmute your microphone in the meeting? Keep looking for mute button in app after app? #MicrosoftTeams ? Or #GoogleMeet ? #CiscoWebEx ?</p>

<p>Don’t bother anymore. Here’s a quick setup then you’re ONE hotkey away from mute or unmute your microphone <strong>across entire Mac</strong>.</p>

<p>You only need system’s Automator app.</p>

<ol>
  <li>Create a Quick Action workflow</li>
  <li>Set “Workflow receives current” option to “no input”</li>
  <li>Add “Run Shell Script” action</li>
  <li>Paste: <code class="language-plaintext highlighter-rouge">osascript -e "set volume input volume 0</code>”</li>
  <li>(Repeat steps above and paste: <code class="language-plaintext highlighter-rouge">osascript -e "set volume input volume 100</code>”) to another workflow file called “Unmute systematically”</li>
  <li>Save this Quick Action as “Mute systematically”</li>
  <li>Set a global shortcut you want in  &gt; System Preferences &gt; Keyboard &gt; Shortcuts &gt; Services</li>
</ol>

<p>Done.</p>

<p>Enjoy your peace of mind and remember to unmute when someone starting to talk!</p>

<p>I’m enjoying my peace of mind. Hope it works for you too!</p>

<p>Wondering if there’s a hotkey for iPad?</p>]]></content><author><name>Robert Wang</name></author><category term="Mac" /><category term="mute" /><category term="hotkey" /><category term="wfh" /><category term="Automator" /><category term="script" /><summary type="html"><![CDATA[During #pandemic you’re #WFH , are you sick of constantly mute and unmute your microphone in the meeting? Keep looking for mute button in app after app? #MicrosoftTeams ? Or #GoogleMeet ? #CiscoWebEx ?]]></summary></entry><entry><title type="html">First week of WFH</title><link href="https://robertnotes.com/2021/05/23/pandemic_week1.html" rel="alternate" type="text/html" title="First week of WFH" /><published>2021-05-23T00:00:00+00:00</published><updated>2021-05-23T00:00:00+00:00</updated><id>https://robertnotes.com/2021/05/23/pandemic_week1</id><content type="html" xml:base="https://robertnotes.com/2021/05/23/pandemic_week1.html"><![CDATA[<h1 id="first-week-wfh-居家隔離第一週">First week WFH 居家隔離第一週</h1>

<p>5/14 那天週五下班，只是想說帶上公司筆電回家，方便週末抽空躲起來追一些要準備的資料，也刷刷題。沒想到台北及新北市防疫警戒突然就升到三級，開始兩週的 #WFH !</p>

<p>公司分兩班，原本就在 B 班的我，直接就在家工作，經過本週的嘗試跟家人的磨合（因為學校也一起停課在家自學了），看起來效率不差，溝通也還算順暢，決定下週也自請在家工作整週了！（幸好另一半可以扛整天，偶爾交換一下！）</p>

<p>在家工作的優點有：</p>
<ol>
  <li>省掉通勤時間（一天2小時一個月40小時）及成本（一個月近$1300）</li>
  <li>睡眠時間拉長</li>
  <li>跟小孩相處時間增加，多了很多可一同用餐的機會</li>
  <li>自己開伙的意願也增加，也真的做到</li>
  <li>不用費心穿著打扮</li>
  <li>整天都可以洗衣服晾衣服，甚至工作休息時起身打掃拖地動一動整理家務</li>
</ol>

<p>相對的缺點有：</p>
<ol>
  <li>工作與生活的界線模糊，容易工作超時（尤其當腦袋運轉起來思考解法時）</li>
  <li>疫情影響無法隨意外出，活動量/步行數大減會影響體能，得特意留心訓練</li>
  <li>電費增加：電腦/網路/冷氣都得自費</li>
  <li>若共處一室的夥伴不那麼好相處，會很辛苦</li>
  <li>多個小孩的課程很難同時兼顧，又容易互相干擾分心</li>
  <li>小孩沒上課時在家玩很容易亂入，而干擾到會議或專心的狀態</li>
</ol>

<p>這週有很多餐都自己煮，偶爾幾餐找外送反而遇到雷，雖然自己備餐比較累，尤其時一忙起來突然發現時已經中午12點了，還要趕快切換情境出來備餐，再忙一下開始用餐的時間可能都到了近下午1點。</p>

<p>在生活與工作模糊的地帶切換，需要的反而是極大的專注力，及時間管理的能力。下週在家工作，我需要準備的是 #番茄鐘 的工具，雖然早就知道這個用法，但之前在公司一直無法使用這個方式，因為被打斷討論的機會太高，討論後還要紀錄結論跟TODO，再回到專注的心流裡可能又是10分鐘了。</p>

<p>不過進到 #WFH 的情境，這次真的可以拿出來用用看了！明天起把 #番茄鐘 加進來工作流程中，看看有什麼變化。</p>]]></content><author><name>Robert Wang</name></author><summary type="html"><![CDATA[First week WFH 居家隔離第一週]]></summary></entry></feed>