2021年4月5日 星期一

【Android】onConfigurationChanged的觸發(和Configuration)

有些情況下-主要是指「配置」發生改變-,Activity會被重啟...

這些重啟發生時,經常伴隨「onConfigurationChanged的觸發」。(但官方文件說法是「不會觸發」。)

但如果只希望「onConfigurationChanged的觸發」發生、但不要有Activity重啟時,可以在AndroidManifest中,找到會被重啟的Activity下增加一個屬性「android:configChanges」,並在後面增加對應的參數來決定哪些情況要觸發、哪些情況就任由Activity重啟吧!


Configuration在官方的定義是指「決定系統要取用哪些Resource檔的方式」,像「螢幕是橫式或直式時會取用不同的Layout檔(如果有設定)」。

所以偵測螢幕橫式直式最正確的方式(除了等待onConfigurationChanged觸發)是從Activity下呼叫「getResource().getConfiguration()」去取得Configuration,然後判斷orientation的參數。


這個屬性後面可以接收的參數有:(官方網頁將它列在「activity」的Manifest說明中。)

android:configChanges=["mcc", "mnc", "locale",
                                 
"touchscreen", "keyboard", "keyboardHidden",
                                 
"navigation", "screenLayout", "fontScale",
                                 
"uiMode", "orientation", "density",
                                 
"screenSize", "smallestScreenSize"]

(官網這裡的說明有點奇妙,XML檔無法用這種方式輸入tag的參數,必須要用類似「"mcc|mnc"」這樣的格式才可以。其他欄位的說明有遵照這個模式,唯獨「configChanges」卻是錯誤的。)

比較奇妙的是:如果要針對螢幕轉向做偵測,需要使用多個參數會比較保險。

一般來說,是使用「orientation」和「screenSize」,(甚至再加上「uiMode」。)



2020年5月3日 星期日

隱私權政策

非常感謝各位使用本APP。
本APP不會收集各位的使用行為,例如閱覽習慣或GPS定位。
除了使用Google帳號進行身分認證以外,不會使用到任何使用者的個人資料。

謝謝。

2020年3月8日 星期日

【不要仰賴Activity的Android開發】什麼事情都用CustomView解決





一般來說,製作一個這樣的介面會使用一組Activity和XML。



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
tools:context=".MainActivity"
tools:showIn="@layout/activity_main"> 

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" /> 

 <EditText
android:id="@+id/identify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="ID" />
<EditText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="Password" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"/>
</LinearLayout>



public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { 
 super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); 

 final EditText id = findViewById(R.id.identify);
final EditText pw = findViewById(R.id.password); findViewById(R.id.login).setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View view) { 

 Log.d("Hsu", "ID:" + id.getEditableText().toString());
 Log.d("Hsu", "PW:" + pw.getEditableText().toString()); 


 }
); 
 } 
}
(因為不需要,所以OnClickListener的內容只有意思性地Log。)


接下來的做法或許會比較麻煩,但概念上來說會提供程式比較多的擴充升級彈性。

因為它完全不受限於Activity,功能和介面可以隨時彈性重組,方便調整或擴充。

首先,整個Activity會變成只剩下這樣.......


public class MainActivity extends AppCompatActivity { 
 @Override 
 protected void onCreate(Bundle savedInstanceState) {             super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main); 

 } 
}

但XML會做出比較大幅度的改變...

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"> 

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" /> 

<com.myapplication.InputText
android:id="@+id/identify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="ID" /> 

<com.myapplication.InputText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="Password" /> 

<com.myapplication.LoginButton
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"/> 

</LinearLayout>


可以看到EditText和Button從Android標準元件變成APP專案客制的元件。

「com.myapplication」是專案的設定,有興趣的人可以自己彈性的修改命名,只要是「InputText」和「LoginButton」的路徑位置就好。(這兩個View物件的命名也可以自己彈性調整。)

InputText和LoginButton的內容則在下面。

public class InputText extends EditText { 
 public static InputText ID; 
 public static InputText PW; 

 public InputText(Context context, AttributeSet attrs) { 
 super(context, attrs); 
 String hint = getHint().toString();
 switch (hint){ 

 case "ID": ID = this;
 break;
 case "Password": 

 PW = this;
 break;
 } 

 } 
}



public class LoginButton extends Button implements View.OnClickListener { 
 public LoginButton(Context context, AttributeSet attrs) { 
 super(context, attrs);
 setOnClickListener(this);
 } 


 @Override
 public void onClick(View view) { 

 Log.d("Hsu", "ID:" + InputText.ID.getEditableText().toString());
 Log.d("Hsu", "PW:" + InputText.PW.getEditableText().toString()); 

 } 
}


因為這個介面還是算簡單的,所以這樣做的好處可能無法立刻顯現,要引入一些情境做說明。

這樣做的目的在於快速方便的大量產生擁有登入功能的介面。
登入這件事情越來越複雜,會產生各種登入模式,例如多帳號登入、例如彈出視窗登入、例如重新檢查使用者帳號密碼.....
所以快速的設計跟新增登入功能或許有其必要。

InputText可以確保針對登入的輸入功能可以快速重複利用,譬如「將輸入的內容加密」「將使用者帳號自動帶入ID欄」。
而LoginButton可以延生出各種版本,例如CheckPasswordButton可以用來讓使用者再次輸入密碼做保險性登入,等物件設計好後只要修改XML,將LoginButton換成CheckPasswordButton,一樣不需要修改Activity的任何內容,就可以完成新介面新功能。

用這種方式寫程式,為的是可以同時滿足快速組合出功能並達到模組化的目的。


有些人可以從附上的程式碼中快速找到一些功能上的限制,但其實這些限制都可以藉由簡單幾行程式碼擴充來破解,但以後再寫了。

2019年8月1日 星期四

【Google】如何將SheetSpread表格內的資料轉換成JSON

其實Google自己有自己的工具,但轉換出來的資料複雜的不合理。




所以找了一下,發現有神人製作了一個工具「http://gsx2json.com/」...

可以將第一列視為「Tag」,然後將每一欄作為一筆資料,用該欄第一列的資料作為Tag進行資料分類索引。




利用工具後...「http://gsx2json.com/api?id=1bRM_dIcl67MgLinJfc_YBqifZ65vusun5XJLWHQZ_oM&sheet=1&columns=false

轉換出的資料變成「{"rows":[{"標題":"測試一","內容":"測試內容1","參數":100},{"標題":"測試二","內容":"測試內容2","參數":1002},{"標題":"測試三","內容":"測試內容3","參數":1002}]}」

這樣的規格明顯合理許多。

也有別的參數可以調整...「http://gsx2json.com/api?id=1bRM_dIcl67MgLinJfc_YBqifZ65vusun5XJLWHQZ_oM&sheet=1

{"columns":{"標題":["測試一","測試二","測試三"],"內容":["測試內容1","測試內容2","測試內容3"],"參數":[100,1002,1002]},"rows":[{"標題":"測試一","內容":"測試內容1","參數":100},{"標題":"測試二","內容":"測試內容2","參數":1002},{"標題":"測試三","內容":"測試內容3","參數":1002}]}」

2019年7月12日 星期五

【Android Html5】播放Youtube影片

目標是建立一個會佔滿整個WebView的YouTubePlayer。

必須注意到:基本上,開發時所有會遇到的問題都源於「網頁版Youtube撥放器在移動網頁上不能正常運作」。──所以Google才會另外開發一套移動式套件,但因為這套件非常不好用,所以終究還是要回來解決不能正常運作的問題。


基本建立方式在第二個回應。(測試過,可以使用。)

這個技巧播放檔案的秘訣在於監聽Player的State,所以當它Ready就開始播放,播放完可以開始再次重播、或撥下一檔。

如果要改撥放PlayList、而不是單一影片,可以使用這個方法修改範例。

範例內是使用在YT.Player內直接設定VideoId的方式,但其實有用播放指令指定ID的方法,同時也可以用清單播放。方法可以看標準官方範例。

官方範例內有player.nextVideo(),可以用來自動播放列表下一檔。


影片如果要不暫停、就退出或前進到下一層功能,這個地方有講解需要使用什麼樣的修改。簡而言之就是WebView也有自己的onPause和onResume,必須要在Fragment和Activity中相對應的地方呼叫。(同時需要使用Timer的啟動與暫停。)

另外,影片如果還沒開始撥放就被退到背景,YouTube Player這時可能會在背景被啟動。所以在執行「是否要開始撥放」前,同時要檢查WebView是否還在前景上。



2019年4月10日 星期三

【Android AES】SHA1PRING + Crypto

Google提供的範例檔案


好像是Android6以後,AES加解密如果是使用SecureRandom這個物件搭配演算法「SHA1PRING」和Provider「Crypto」來取得RawKey(用來產生SecretKeySpec時需要的導入參數),會發生失敗的情況。

雖然Google提供了一組叫做CryptoProvider的物件在網路上讓人下載(或複製貼上),但在Cipher進行解密(執行dofinal這個函數)時一樣會出錯。

Google這個超級天才就繼續提供了一組物件叫做「InsecureSHA1PRNGKeyDerivator」在網路上,(載點在上面,)來解決這個問題。


這個物件的用法很單純,就是呼叫一組static物件取得Instance然後再導出 RawKey即可。

方法大概是這樣...

byte[] raw = InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(seed, 16);

比較詭異、比較巧妙的是這個「16」,因為大多數使用這個物件的範例都使用「32」,可是解密部分一樣會在Cipher的地方出錯。這似乎是看你使用的Key長短而定,或是你設定的加密位元。

(32是256位元?16是128位元?)

其他部分都可以循著一般標準範例。

2019年2月19日 星期二

【Android】Mac Adress的可用求法(純Java、不含Android API)

根據官方文件的說法,所有可行的Android API都已經不可以取得Mac Address。

所以要取得Mac Address,必須要回歸原始Java方法。


public static String getMacAddr() {
    try {
        List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
        for (NetworkInterface nif : all) {
            if (!nif.getName().equalsIgnoreCase("wlan0")) continue;

            byte[] macBytes = nif.getHardwareAddress();
            if (macBytes == null) {
                return "";
            }

            StringBuilder res1 = new StringBuilder();
            for (byte b : macBytes) {
                String hex = Integer.toHexString(b & 0xFF);
                if (hex.length() == 1)
                    hex = "0".concat(hex);
                res1.append(hex.concat(":"));
            }

            if (res1.length() > 0) {
                res1.deleteCharAt(res1.length() - 1);
            }
            return res1.toString();
        }
    } catch (Exception ex) {
    }
    return "";

簡單來說就是去取得無法閱讀的編碼然後轉換成Mac Address格式。(但也不是真的無法閱讀,只是送出的資料為數值,要轉16進位、還要轉成統一文字格式。

方法很簡單,但是困死許多人。