---
# System prepended metadata

title: Learning Android app

---

# Learning Android app 基礎（建議先要有 OO 觀念）

## Android API (Crucial!!)
https://developer.android.com/reference

## 軟體設計基礎 （Design Pattern -- 設計模式）
https://hackmd.io/@ZacharyZhuo/SkqiBc_lK#Builder

## Android 設計技術

### ------進階------
Frameworks :
#### Retrofit ( 專為API連線來取得資料，而Retrofit與API連線的效率非常高，最特別的是其規範的REST框架讓程式高度解耦，好寫易維護 )
官方 :  https://square.github.io/retrofit/

https://ithelp.ithome.com.tw/articles/10299779?sc=rss.iron

#### RxJava2 ( 輕鬆的切換thread 簡化非同步(asynchronous)機制繁瑣的過程, 只要一行程式就能在 background 和 UI thread 之間切換。)
https://ithelp.ithome.com.tw/articles/10197115

#### Dagger 觀念 (Dependency Injection of Android 讓程式碼的依賴關係提升變成模組間的關係)

官方文件: https://dagger.dev/dev-guide/

快速: https://medium.com/@genchilu/%E4%BD%BF%E7%94%A8-dependency-injection-%E6%A1%86%E6%9E%B6-dagger2-%E5%BF%83%E5%BE%97-e7208d30f995

DI是啥？https://hackmd.io/@AlienHackMd/ByLeoMjSI#Dagger2

Story:  https://darkaries.github.io/2019/05/15/androidDI/
![image](https://hackmd.io/_uploads/rkGMJjcCp.png)
![image](https://hackmd.io/_uploads/HkmFyjqC6.png)

----------------------------

Backgroud knowledge : 

#### ActivityThread的理解和APP的啟動過程 ( 就是我們說的 Main Thread 或 UI Thread , 其中的 main() 就是整個 App 的 Entry Point)
https://blog.csdn.net/hzwailll/article/details/85339714

#### Zygote 進程 （ Zygote 進程作為服務端 --> 主要負責創建 Java Vm、加載系統資源、啟動 SystemServer 進程 ）
https://hackmd.io/@AlienHackMd/BkvyAzNf9#%E9%80%8F%E9%81%8E-initrc-%E5%95%9F%E5%8B%95-Zygote

----------------
#### Android => ContentProvider 和 getContentResolver
官方 API 說明
```
ContentResolver: 
 This class provides applications access to the content model.

ContentProvider:
 Content providers are one of the primary building blocks of 
 Android applications, providing content to applications. 
 They encapsulate data and provide it to applications through 
 the single ContentResolver interface. 
 
     content provider is only required if you need to share 
     data between multiple applications. 
 
 For example, the contacts data 
 is used by multiple applications and must be stored in a 
 content provider. If you don't need to share data amongst
 multiple applications you can use a database directly via 
 SQLiteDatabase.
```


https://juejin.cn/post/6844904159246811143




### ------基礎------

#### Android app 四大元件
* Android 中有四個主要元件，分別是 Activity、Service、BroadcastReceiver，以及ContentProvider。前三個是使用 Intent 溝通，而 app 使用 ContentResolver 與 ContentProvider 溝通。每個元件都有不同的用途，且所有都會遵循自身由 Android 控制的生命週期。


1. Activity ( **搭配使用者介面的畫面** )
每個 Activity 都獨立執行，且內部和外部的 app 都可以直接啟動他們，以控制 app 可能會啟動他們，以控制 app 改變畫面的方法。例如，app 可能會啟動某個 Activity 顯示食譜清單，然後另一個 Activity 顯示食譜細節。使用者選擇搜尋結果時，第三個搜尋 Activity 也可能會啟用相同的食譜細節 Activity。

2. Service ( **長時間執行，或長期在背景執行的程式碼** )
例如，app 執行的 Service，可能在執行食譜備份任務，所以使用者能夠在 app 內持續做其他事情，甚至是切換 app，而無需中斷備份。另一個 Service 可能會持續執行，每隔一會兒便紀錄使用者的位置以供日後分析。
* Service原理︰
https://jasonblog.github.io/note/android_note/android_service_zhi_yi__start_service.html

3. BroadcastReceiver ( **接受系統範圍或內部廣播的 Intent** )
BroadcastReceiver 可能會顯示一個通知、啟動另一個元件，或執行其他短期工作。app 可能會使用 BroadcastReceiver 訂閱許多系統廣播，比如用於啟動，或是修改網路連線。

4. ContentProvider ( **儲存了元件之間的共享資料，使其可透過某個 API 取得** )
app 用它來公開分享資料，且有時也在某個 app 內部私下分享資料。比方說 Android 接觸 ContentProvider，以存取使用者的聯絡人。ContentProvider 的優點，是把**儲存機制**的細節抽象化，該機制是會使用讀取和寫入的方法。另外，還封裝了**資料存取**功能，因此可以更改儲存的內部方法，而無須改變外部介面。
* ContentProvider - ContentProvider 基本使用如下，可以透過 ContextProvider 來操作其他應用進程內的內部資料 (像是 DB 之類的)
https://hackmd.io/@AlienHackMd/B1ef20DQ9#%E5%9B%9B%E5%A4%A7%E7%B5%84%E4%BB%B6
* getContentResolver() API 的使用 （ 簡短代碼範例 ）
https://blog.csdn.net/daniel80110_1020/article/details/55260510
https://zhuanlan.zhihu.com/p/40146450





----------------------
## System Manager 程式使用範例（阿密思）
https://github.com/Morsmalleo/AhMyth/tree/master/AhMyth-Client/app/src/main/java/ahmyth/mine/king/ahmyth

## PackageManager 官方文件

https://developer.android.com/reference/android/content/pm/PackageManager

----------------------
## Android 機制

Android 8.0: API等級26，    版本號 "8.0.0"
Android 8.1: API等級27，    版本號 "8.1.0"
Android 9:   API等級28，    版本號 "9"
Android 10:  API等級29，    版本號 "10"
--------------------------------------------------- 權限分水嶺
        MANAGE_EXTERNAL_STORAGE
Android 11:  API等級30，    版本號 "11"
Android 12:  API等級31, 32，版本號 "12"
Android 13:  API等級33，    版本號 "13"
Android 14:  API等級34，    版本號 "14"


// 系統產生事件
android.intent.action.BATTERY_CHANGED // 持久廣播含充電狀態等...
android.intent.action.BATTERY_LOW // 顯示電量低

android.intent.action.BATTERY_OKAY
/* 
說明︰Broadcast Action: Indicates the battery is now okay after being low. This will be sent after ACTION_BATTERY_LOW once the battery has gone back up to an okay state. 
*/

android.intent.action.BOOT_COMPLETED
/* 
說明︰Broadcast Action: This is broadcast once, after the user has finished booting. It can be used to perform application-specific initialization, such as installing alarms. You must hold the Manifest.permission.RECEIVE_BOOT_COMPLETED permission in order to receive this broadcast.  
*/ 

android.intent.action.LOCKED_BOOT_COMPLETED
/*
說明︰This is broadcast once, after the user has finished booting, but while still in the "locked" state. It can be used to perform application-specific initialization, such as installing alarms. You must hold the Manifest.permission.RECEIVE_BOOT_COMPLETED permission in order to receive this broadcast.
*/


android.intent.action.BUG_REPORT // 顯示活動報告錯誤
android.intent.action.CALL
android.intent.action.CALL_BUTTON 
android.intent.action.DATE_CHANGED // 日期改變

android.intent.action.REBOOT // 有設備重啟
/* 說明︰Broadcast Action: Have the device reboot. This is only for use by system code.*/

### Android 14 ：Behavior changes: Apps targeting Android 14 or higher 
https://developer.android.com/about/versions/14/behavior-changes-14?hl=zh-tw

### 相片和影片的部份存取權
https://developer.android.com/about/versions/14/changes/partial-photo-video-access?hl=zh-tw
![image](https://hackmd.io/_uploads/S1lULucCa.png)


### Permission 列表
https://developer.android.com/reference/android/Manifest.permission

### Use of All files access
https://support.google.com/googleplay/android-developer/answer/10467955?utm_source=lint&utm_medium=lint&utm_campaign=lint&visit_id=638457259128833935-2375749783&rd=1#zippy=%2Cpermitted-uses-of-the-all-files-access-permission

### Permissions and APIs that Access Sensitive Information
https://support.google.com/googleplay/android-developer/answer/9888170

### Gradle 筆記
https://ithelp.ithome.com.tw/articles/10204791



### Android 混淆機制
https://hackmd.io/@YubUeGjDS8C4yMh0F9Fn1g/H1Ikm8FdL

# 軟體程式設計->基礎模式
## 1.Gateway (閘道)
1. 這是一個物件，它**封裝**了對外部系統或資源的存取。

`封裝簡單說就是把一個 Class A 納入在某一個 Class 內部使用，但是使用者在使用Class的時候不用理會 Class A 有什麼 Method，使用者就僅僅使用 Class 就好`
### 例如︰Facade(外觀), Adapter(配接器), Mediator(中介者)

介面︰通往介面的閘道，訊息服務傳送訊息
```
int send(String messageType, Object[] args);
```

期望的介面方法︰
```
public void send Confirmation(String orderID, int amount, String symbol);
```

Domain物件若需要發送訊息︰
```
class Order {
    public void confirm() {
        if (isValid()) 
        Environment.getMessageGateway()
                   .sendConfirmation(id, amount, symbol);
    }
}
```

錯誤代碼定義在訊息系統的介面之中
```
public static final int NULL_PARAMETER = -1;
public static final int UNKNOWN_MESSAGE_TYPE = -2;
public static final int SUCCESS = 0l;
```
範例
```
class MessageGateway {
	
	protected static final String CONFIRM = "CNFRM";
	private MessageSender sender;
	public void sendConfirmation(String orderID, int amount, String symbol) {
		Object[] args = new Object[] {orderID, new Integer(amount), symbol};
		send(CONFIRM, args);
	}
	private void send(String msg, Object[] args) {
		int returnCode = doSend(msg, args);
		if (returnCode == MessageSender.NULL_PARAMETER)
			throw new NullPointerException("Null Parameter passed for msg type: " + msg);
		if (returnCode == MessageSender.SUCCESS)
			throw new IllegalStateException(
				"Unexpected error from messaging system #:" + returnCode);
	}
	protected int doSend(String msg, Object[] args) {
		Assert.notNull(sender);
		return sender.send(msg, args);
	}
}

class MessageGatwayStub {
	
	protected int doSend(String messageType, Object[] args) {
		int returnCode = isMessageValid(messageType, args);
		if (returnCode == MessageSender.SUCCESS) {
			messageSent++;
		}
		return returnCode;
	}
	private int isMessageValid(String messageType, Object[] args) {
		if (shouldFailAllMessages) return -999;
		if (!legalMessageTypes().contains(messageType))
			return MessageSender.UNKNOWN_MESSAGE_TYPE;
		for (int i = 0; i < args.length; i++) {
			Object arg = args[i];
			if (arg == null) {
				return MessageSender.NULL_PARAMTER;
			}
		}
		return MessageSender.SUCCESS;
	}
	public static List legalMessageTypes() {
		List result = new ArrayList();
		result.add(CONFIRM);
		return result;
	}
	private boolean shouldFailAllMessages = false;
	public void failAllMeesages() {
		shouldFailAllMessages = true;
	}
	public int getNumberOfMessageSent() {
		return messageSent;
	}
}

class GatewayTester {
	public void testSendNullArg() {
		try {
			gate().sendConfirmation(null, 5, "US");
			fail("Didn't detect null argument");
		} catch (NullPointerException expected) {
			
		}
		assertEquals(0, gate().getNumberOfMessageSent());
	}
	private MessageGatewayStub gate() {
		return (MessageGatewayStub) Environment.getMessageGateway();
	}
	protected void setUp() throws Exception {
		Environment.testInit();
	}
}
```

## 2.Mapper (對應器)
1. 在兩個獨立的物件之間建立通訊的物件
![image](https://hackmd.io/_uploads/Bkh5DK-1A.png)
2. Mapper是子系統之間的絕緣層，它控制了系統之間的通訊細節，而且他的存在不會對子系統造成影響。
3. 將系統不同部份解耦合使用︰Mapper和Gateway。
4. Mapper將系統中不同部份解耦合，但比起Gatway而言，Mapper可確保子系統間不會互相「依賴」(Customer類別中封裝Lease, Asset類別，其餘也是)。


## 3.Layer Supertype (分層超級型別)
1. 這是一種型別，他在其分層中充當所有型別的「超級型別」
2. 在某一分層中，所有的物件都會有一些方法是你不想在整個系統內因複製而重複出現的。而你可以把這些方法放到一個Layer Supertype中。

範例
以通用的超級類別來處理ID
```
class DomainObject {
    private Long ID;
    public Long getID() {
        return ID;
    }
    public void setID(Long ID) {
        Assert.notNull("Cannot set a null ID", ID);
        this.ID = ID;
    }
    public DomainObject(Long ID) {
        this.ID = ID;
    }
}

```

## 4.Separated Interface (分離介面)
1. 在一個與實作分離的套件中定義一個介面
![image](https://hackmd.io/_uploads/HkCThKZ1C.png)
2. 將「類別」分組到不同的套件中，並在套件之間控制依賴關係
3. 在 domain 套件中設計一個介面(Unit of work)作為藍圖或者說是設計圖，Customer 和 Order 可以直接使用該介面的抽象方法進行自己的程式碼設計，不用知道 介面 事後被子類別(Impl)完成的具體Method要怎麼運行，
4. 範例︰以 JPA 作為 Separated Interface，以 Hibernate 作為 Impl，實現介面和實作之間的解耦合 => (1)可替換性 (2)模組化 (3)利於測試性

![image](https://hackmd.io/_uploads/rJxpDq-JA.png)

## 5.Registry (登錄表)
1. 一個眾所周知的物件，讓其他物件可以使用它，來尋找共用物件服務

## 6.Value Object (值物件)
1. 一個小而簡單的物件，例如金錢和日期範圍，不使用識別碼來判斷它們是否相等

## 7.Money (金錢)
1. 代表一個金錢值
## 8.Special Case (特殊情況)
1. 為特別情況提供特殊行為的一個子類別
## 9.Plugin (外掛)
1. 在組態期間(而非編譯期間)連結(link)類別
## 10.Service (服務替身)
1. 在測試期間移除對「有問題的服務」的依賴
## 11.Record Set (紀錄集)
1. 表格式資料在記憶體中的呈現方式



## Mapper

# 小工具
### Java 版本切換

#### 1. MacOS
https://juejin.cn/post/6871959224314757134
1.打開 Terminal
2.查看當前 Java 版本
3.切換到 JDK1.8
4.將其設置為預設 JDK 版本（Optional）
5.更新配置
6.查看 Java 版本
```
/usr/libexec/java_home -V

java -version

export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

open ~/.bash_profile

# SWITCH TO JAVA VERSION 8
export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

source .~/.bash_profile
```

#### 2. Ubuntu
https://askubuntu.com/questions/740757/switch-between-multiple-java-versions

For Java
```
update-java-alternatives --list

sudo update-java-alternatives --set /path/to/java/version
```
![image](https://hackmd.io/_uploads/BySniO5Cp.png)

------------

For 一般語言 ( `--config java` )

`sudo update-alternatives --config java`

![image](https://hackmd.io/_uploads/H1Gx3u5Rp.png)


-------------------
### Git

Git 時光機回復版本的 2 種方法 reset & checkout
https://www.maxlist.xyz/2020/05/03/git-reset-checkout/

Git 常見指令
https://jim1105.coderbridge.io/2022/08/21/git-commands/

![截圖 2024-04-08 下午1.32.37](https://hackmd.io/_uploads/Syt5Z-bxC.png)


#### 專案本地端指令

git init：建立 .git 資料夾進行追蹤（.git資料夾是隱藏起來的）。
git status：git 狀態。
git add index.html：上傳單一檔案到 staging area.
git add . ：上傳全部檔案到 staging area。
git add -u：只上傳修改過的檔案到 staging area.
git commit -m "填寫此版本資訊"：commit 到 Local Repository。
git log：commit 的歷史。
git reflog：commit 的歷史（包含被 reset 過的檔案）。
git reset HEAD^：還原 commit 並將檔案退回到工作目錄，一個^代表退回一個，也可用數字 HEAD^2。
git reset HEAD^ --hard：還原 commit 並刪除檔案。
git reset < commit 編號 > ：可以還原已被刪除的 commit。
git branch：查看分支。
git branch name：新增分支。
git branch - < old name >< new name >：更改分支名字。
git checkout：移動分支。
git merge：合併分支，有兩種情況。


#### 專案遠端指令

git remote：查看遠端數據庫列表。

git remote -v：查看遠端數據庫列表（含 url）。

git remote add <遠端數據庫名稱><遠端數據庫url> ：建立遠端數據庫。

git push origin <數據庫名稱，預設為 origin>：上傳到遠端數據庫。

git pull <數據庫名稱>＜分支名稱＞ ：下載同步更新本地數據庫。

git clone "url"：下載遠端數據庫。




-------------------

# API

## 第三方登入機制

* Back4app (Twitter, Facebook)
https://github.com/back4app/android-facebook-login
https://github.com/back4app/android-twitter-login

* Parse (Google, Twitter, Facebook)
https://docs.parseplatform.org/android/guide/#twitter-users
https://github.com/parse-community/Parse-SDK-Android?tab=readme-ov-file


## 專案範例

### Blood Bank︰

Features

    User Databases
    User Login & Sign up
    Donor Details
    Finding blood group easily
    Finding Near By Hospitals
    Achievements & Rewards

Tools used

    Firebase Database
    Firebase Authentication
    Google Maps Api
    Android Studio IDE
    Android version 4.0 or later
    Android SDK 17-28

https://github.com/imShakil/BloodBank
# Jetpack compose

## Initial
![image](https://hackmd.io/_uploads/H1g_3Uzx0.png)

```
package com.example.myfirstcomposeapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.myfirstcomposeapp.ui.theme.MyFirstComposeAppTheme
import java.time.LocalTime

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyFirstComposeAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Story("Android", "Cartoon network",
                        LocalTime.parse("18:00"))
                }
            }
        }
    }
}

@Composable
fun Story(content: String, from: String, time: LocalTime, modifier: Modifier = Modifier) {
    Column {
        Text(
            text = "The show of $from : $content!",
            fontSize = 20.sp,
            lineHeight = 30.sp,
            textAlign = TextAlign.Left,
            modifier = modifier
                .padding(top = 10.dp, bottom = 4.dp)
                .padding(start = 10.dp, end = 10.dp)
        )
        Text(
            text = "TV Time: $time",
            fontSize = 25.sp,
            textAlign = TextAlign.Center,
            modifier = modifier
                .padding(top = 4.dp)
                .padding(end = 10.dp)
                .padding(bottom = 10.dp)
                .align(alignment = Alignment.End)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun TVShowPreview() {
    MyFirstComposeAppTheme {
        Story("It's about an exciting story with Courage saving his mom ",
            "Cartoon network",
            LocalTime.parse("18:00"))
    }
}
```


# Kotlin


## 類似 Switch case 決定實際 value
```
fun main() {
    // 玩家出拳
    val playerChoice = "布"

    // 電腦出拳
    val computerChoice = when ((0..2).random()) {
        0 -> "剪刀"
        1 -> "石頭"
        else -> "布"
    }

    // 判斷勝負
    val result = when (playerChoice) {
        "剪刀" -> {
            when (computerChoice) {
                "剪刀" -> "平手"
                "石頭" -> "你輸了"
                else -> "你贏了"
            }
        }
        "石頭" -> {
            when (computerChoice) {
                "剪刀" -> "你贏了"
                "石頭" -> "平手"
                else -> "你輸了"
            }
        }
        else -> {
            when (computerChoice) {
                "剪刀" -> "你輸了"
                "石頭" -> "你贏了"
                else -> "平手"
            }
        }
    }

    // 輸出結果
    println("玩家出拳：$playerChoice")
    println("電腦出拳：$computerChoice")
    println("結果：$result")
}
```
改寫 A
```
val regretfulMsg: String = "為了生活必須忍，能屈能伸是英雄本色！"
val excuseMsg: String = "生活在現代，凡事還是要長點腦子"

fun main() {
    // 計畫
    val scheduledChoice = "倒立"

    // 當天出遊
    val outing = when (('a'..'d').random()) {
        'a' -> "走路"
        'b' -> "騎腳踏車"
        'c' -> "開車"
        else -> "倒立"
    }

    // 判斷當天出遊方式，產生不同藉口
    val result = when (scheduledChoice) {
        "走路" -> { // 原本計劃走路
            when (outing) {// 實際情形 
                "騎腳踏車" -> "因為腳踏車有優惠"
                "開車" -> "誰叫天要下雨"
                "倒立" -> "我想鍛鍊身體"
                else -> """
                	走路有益身體健康
                """
            }
        }
        "騎腳踏車" -> { // 原本計劃騎腳踏車
            when (outing) {// 實際情形
                "騎腳踏車" -> "照計畫執行"
                "開車" -> "誰叫天要下雨"
                "倒立" -> "我想鍛鍊身體"
                else -> "走路是最好的運動"
            }
        }
        "開車" -> { // 原本計劃開車
            when (outing) {// 實際情形
                "騎腳踏車" -> "車子故障了"
                "開車" -> "照計畫執行"
                "倒立" -> """
                	就當作是鍛鍊身體好惹
                    	凡是面對大風大浪
                        	${regretfulMsg}
                """
                else -> "走路有益身體健康"
            }
        }
        else -> { // 原本計劃倒立前進
            when (outing) {// 實際情形
                "騎腳踏車" -> 
                """$excuseMsg
                咦？${outing}會有大背頭嗎？
可惡，腳踏車並沒辦法大背頭，只能怒髮衝冠
                    """
                
                "開車" -> "倒立好累，還是${outing}好了 ʅ（´◔౪◔）ʃ"
                
                "倒立" -> """一定要這麼慘忍嗎？
為何要折磨自己
                可惡 ${regretfulMsg}
可是男子漢言出必行
"""
                
                else -> """哎呀，走路還是比較節能減碳
                	${excuseMsg}
                況且今天地板太熱，而拖鞋是個偉大的發明，要當一個善用工具的人
                """
            }
        }
    }
    printResult(scheduledChoice, result, outing)
}

fun printResult(scheduledChoice: String, result: String, outing: String) {
    // 輸出結果
    println("原本計劃要${scheduledChoice}出遊")
    println("但是$result")
    println("所以就$outing")
    println("")
}

```

改寫 B
```
fun getRandomPlan(list: List<String>): String {
    return list.random()
}

fun main() {
    // 計畫
    val myPlanList: List<String> = listOf("Plan A", "Plan B", "Plan C", "Plan D")
    val myPlan: String = getRandomPlan(myPlanList)
    
    // 當天出遊
    val outing = when (myPlan) {
        "Plan A" -> "走路"
        "Plan B" -> "騎腳踏車"
        "Plan C" -> "開車"
        else -> "倒立前進"
    }

    // 判斷當天出遊方式，產生不同藉口
    val result = when (val number: Int = (1..4).random()) {
        1-> { // 原本計劃走路
            println("今天星期一: 隨機數字為$number")
            when (outing) {// 實際情形 
                "騎腳踏車"  -> "因為腳踏車有優惠"
                "開車" -> "誰叫天要下雨"
                "倒立前進" -> "我想鍛鍊身體"
                else -> "照計畫執行"
            }
        }
        2 -> { // 原本計劃騎腳踏車
            println("今天星期二: 隨機數字為$number")
            when (outing) {// 實際情形
                "騎腳踏車" -> "照計畫執行"
                "開車" -> "誰叫天要下雨"
                "倒立前進"-> "我想鍛鍊身體"
                else -> "走路是最好的運動"
            }
        }
        3-> { // 原本計劃開車
            println("今天星期三: 隨機數字為$number")
            when (outing) {// 實際情形
                "騎腳踏車" -> "車子故障了"
                "開車" -> "照計畫執行"
                "倒立前進" -> "我想亂鍛鍊身體"
                else -> "走路有益身體健康"
            }
        }
        else -> { // 原本計劃倒立前進
            println("今天隨心所欲=> 隨機數字為$number")
            when (outing) {// 實際情形
                "騎腳踏車" -> "我可以倒立騎車啊"
                "開車" -> "倒立好累，還是開車好了"
                "倒立前進" -> "我想今天完蛋了"
                else -> "今天地板太熱，感謝拖鞋的發明"
            }
        }
    }

    // 輸出結果
    println("\n\n我的出遊方式是$outing($myPlan)")
    println("但是$result")
    println("""
最適合的方式果然是：
$outing
這是最好的選擇 ${showEmoji(outing)}
    """)
}

fun showEmoji(choice: String): String {
    return when (choice) {
        "騎腳踏車" -> "(≧∀≦)ゞ"
        "開車" -> "(*´∀`)~♥"
        "倒立前進" -> "(;´༎ຶД༎ຶ`)"
        "走路" -> "(ง๑ •̀_•́)ง"
        else -> "0.0"
    }
}
```
改寫C
```
enum class Choice {
    石頭, 布, 剪刀
}

fun main() {
    val playChoice = Choice.布
    val computerChoice = Choice.values().random()
    val result = (playerChoice.oridnal - computerChoice)
    
    

}


```

改寫成三戰兩勝的猜拳遊戲
![image](https://hackmd.io/_uploads/ryTz6IfeR.png)

![image](https://hackmd.io/_uploads/HJkU6IMgA.png)


```
package com.example.testkotlin

class Game(private val player1: Player, private val player2: Player) {
    private var state: GameState
    private var player1State: Int = 0
    private var player2State: Int = 0

    fun playGame(doesReportGame: Boolean = false) {
        state = GameState.PlayingGame
        println("The game is ready to start with two player randomly playing...\n")
        println("_______ State: ${(state as GameState.PlayingGame).name} _______")

        if (doesReportGame)
            println("The game is being reported...\n")

        while (player1State < 2 && player2State < 2) {
            val gesture1 = player1.chooseGesture()
            val gesture2 = player2.chooseGesture()
            val result = determineWinner(gesture1, gesture2, doesReportGame)
            print(result)
        }

        state = GameState.EndGame
        println("""
            
            $player1 : $player1State wins total
            $player2 : $player2State wins total
            
            The winner is ${if (player1State == 2) player1 else if (player2State == 2) player2 else "no one\nSystem malfunction!"}
            
            _______ State: ${(state as GameState.EndGame).name} _______
        """.trimIndent())

    }

    private fun determineWinner(gesture1: Gesture, gesture2: Gesture, doesReportGame: Boolean): String {
        when((gesture1.ordinal - gesture2.ordinal + 3) % 3) {
            1 -> {
                ++player1State
                if (doesReportGame)
                    return "\t$player1 wins this round with $gesture1 against $gesture2\n"
            }
            2 -> {
                ++player2State
                if (doesReportGame)
                    return "\t$player2 wins this round with $gesture2 against $gesture1\n"
            }
            else -> {
                if (doesReportGame)
                    return "\tIt's a tie with $gesture1 against $gesture2\n"
            }
        }
        return ""
    }

    init {
        state = GameState.StartGame
        println("_______ State: ${(state as GameState.StartGame).name} _______")
    }
}

interface Player {
    val name: String
    fun chooseGesture(): Gesture
}

enum class Gesture {
    ROCK, PAPER, SCISSORS
}

data class RockPaperScissorsPlayer(override val name: String): Player {
    override fun chooseGesture(): Gesture {
        return Gesture.entries.toTypedArray().random()
    }

    override fun toString(): String {
        return name
    }
}

sealed class GameState {
    object StartGame: GameState() {
        var name = "Waiting to start game..."
    }
    object PlayingGame: GameState() {
        var name = "Playing game..."
    }
    object EndGame: GameState() {
        var name = "Game ended!"
    }
}

fun main() {
    val player1 = RockPaperScissorsPlayer("Husky")
    val computerPlayer = RockPaperScissorsPlayer("Computer")
    val game = Game(player1, computerPlayer)
    game.playGame(true)
}
```

## Jetpack compose homework - Concurrency Converter

![image](https://hackmd.io/_uploads/rkVe3Py-0.png)



```
package com.example.currencyconverterapp

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.currencyconverterapp.ui.theme.CurrencyConverterAppTheme
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import java.io.IOException


data class ApiResponse(
    @SerializedName("date") val date: String,
    @SerializedName("jpy") val jpy: Map<String, Double>
)

interface CurrencyApiService {
    @GET("v1/currencies/jpy.json")
    suspend fun getJpyRates(): ApiResponse
}

object ApiClient {
    private var retrofit: Retrofit? = null
    val client: Retrofit?
        get() {
            if (retrofit == null) {
                retrofit = Retrofit.Builder()
                    .baseUrl("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
            }
            return retrofit
        }
}

class MainActivity : ComponentActivity() {

    private var apiService: CurrencyApiService? = null
    private var jpyRates: Double? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            CurrencyConverterAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                ) {
                    CurrencyConverterLayout(this@MainActivity)
                }
            }
        }
    }

    fun getCurrentRateOfJpyExchange(dollarSpecie: String, callback: (Double) -> Unit) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                apiService = ApiClient.client?.create(CurrencyApiService::class.java)
                val response = apiService?.getJpyRates()
                jpyRates = response?.jpy?.get(dollarSpecie)

                withContext(Dispatchers.Main) {
                    callback(jpyRates ?: 0.0)
                }
            } catch (e: IOException) {
                withContext(Dispatchers.Main) {
                    callback(0.0)
                    Toast.makeText(this@MainActivity, "IO Error", Toast.LENGTH_SHORT).show()
                }
            } catch (e: HttpException) {
                withContext(Dispatchers.Main) {
                    callback(0.0)
                    Toast.makeText(this@MainActivity, "HTTP Error", Toast.LENGTH_SHORT).show()
                }
            }

        }
    }

}

@Composable
fun StyledDollarSpecie(dollarSpecie: String, targetAmount: Double) {

    Text(
        buildAnnotatedString {
            withStyle(style = SpanStyle(fontSize = 30.sp)) {
                append(stringResource(R.string.target_amount))
            }
            append(" ")
            withStyle(style = SpanStyle(fontSize = 20.sp)) {
                append(dollarSpecie)
            }
            append("\n")
            withStyle(style = SpanStyle(fontSize = 36.sp)) {
                append(stringResource(R.string.target_amount_value, targetAmount))
            }

        },
        lineHeight = 40.sp,
        modifier = Modifier
            .padding(bottom = 32.dp)
    )
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CurrencyConverterLayout(mainActivity: MainActivity) {
    var amountInput by remember { mutableStateOf("") }
    var exchangeRateInput by remember { mutableStateOf("")}

    val amount = amountInput.toDoubleOrNull() ?: 0.0
    val exchangeRate = exchangeRateInput.toDoubleOrNull() ?: 0.0
    val targetAmount = amount * exchangeRate
    var dollarSpecie by remember { mutableStateOf("") }

    val keyboardController = LocalSoftwareKeyboardController.current

    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(20.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding()
            .onKeyEvent { keyEvent -> // hide keyboard
                if (keyEvent.key == Key.Enter) {
                    keyboardController?.hide()
                    true
                } else {
                    false
                }
            }
            .clickable { keyboardController?.hide() },
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,

    ) {
        Text(
            text = stringResource(R.string.jpy_currency_converter),
            fontSize = 30.sp,
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.CenterHorizontally)
        )
        TextField(
            value = amountInput,
            onValueChange = { amountInput = it},
            label = { Text(stringResource(R.string.jpy)) },
            singleLine = true,
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Next
            ),
            modifier = Modifier
                .padding(bottom = 32.dp)
                .fillMaxWidth()
        )
        TextField(
            value = exchangeRateInput,
            onValueChange = {
                exchangeRateInput = it
                dollarSpecie = if (it.isEmpty()) "" else "(Customized)"
            },
            label = { Text(
                        text = "JPY to dollars of other countries"
                      )
            },
            singleLine = true,
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            modifier = Modifier
                .padding(bottom = 32.dp)
                .fillMaxWidth()
        )
        StyledDollarSpecie(
            dollarSpecie = dollarSpecie,
            targetAmount = targetAmount,
        )
        Button(
            onClick = {
                keyboardController?.hide() // Hide the keyboard when button is clicked
                var rate = mainActivity.getCurrentRateOfJpyExchange("twd") { rate ->
                    exchangeRateInput = rate.toString()
                    dollarSpecie = "(TWD)"
                }
            },
            modifier = Modifier
                .padding(bottom = 20.dp)

        ) {
            Text(text = "JPY to TWD current rate",
                fontSize = 20.sp,
                fontFamily = FontFamily.Serif,
                fontWeight = FontWeight.Bold,
            )
        }
        Button(
            onClick = {
                keyboardController?.hide() // Hide the keyboard when button is clicked

                var rate = mainActivity.getCurrentRateOfJpyExchange("usd") { rate ->
                    exchangeRateInput = rate.toString()
                    dollarSpecie = "(USD)"

                }
            },
            modifier = Modifier
                .padding(bottom = 20.dp)
        ) {
            Text(
                text = "JPY to USD current rate",
                fontSize = 20.sp,
                fontFamily = FontFamily.Serif,
                fontWeight = FontWeight.Bold,
            )
        }
        Button(
            onClick = {
                keyboardController?.hide() // Hide the keyboard when button is clicked

                var rate = mainActivity.getCurrentRateOfJpyExchange("eur") { rate ->
                    exchangeRateInput = rate.toString()
                    dollarSpecie = "(EUR)"
                }
            },
            modifier = Modifier
                .padding(bottom = 20.dp)
        ) {
            Text(
                text = "JPY to EUR current rate",
                fontSize = 20.sp,
                fontFamily = FontFamily.Serif,
                fontWeight = FontWeight.Bold,
            )
        }
        Button(
            onClick = {
                keyboardController?.hide() // Hide the keyboard when button is clicked

                var rate = mainActivity.getCurrentRateOfJpyExchange("gbp") { rate ->
                    exchangeRateInput = rate.toString()
                    dollarSpecie = "(GBP)"
                }
            },
            modifier = Modifier
                .padding(bottom = 20.dp)
        ) {
            Text(
                text = "JPY to GBP current rate",
                fontSize = 20.sp,
                fontFamily = FontFamily.Serif,
                fontWeight = FontWeight.Bold,
            )
        }
    }
}


@Preview(showBackground = true)
@Composable
fun CurrencyConverterLayoutPreview() {
    CurrencyConverterAppTheme {
        CurrencyConverterLayout(MainActivity())
    }
}



```
strings.xml
```
<resources>
    <string name="app_name">CurrencyConverterApp</string>
    <string name="jpy_currency_converter">JPY Currency Converter</string>
    <string name="jpy">JPY</string>
    <string name="target_amount">Target Amount</string>
    <string name="target_amount_value">%1$s dollars</string>
</resources>
```

# Simple Demo + 片段程式碼

## 存取照片 ( 混了一些測試 lifecycle 的程式碼 )
![image](https://hackmd.io/_uploads/ryZR--ekA.png)

```
package com.example.testrequestpm;

import android.Manifest;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;

import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;


public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_CODE_PERMISSION = 100;
    private ListView listView;
    private SimpleCursorAdapter adapter;

    private Button button1;
    private static final String TAG = "Sivan";
    //private final String osVersion = android.os.Build.VERSION.RELEASE;
    private final int apiLevel = android.os.Build.VERSION.SDK_INT;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        getLifecycle().addObserver(new MyObserver()); // inner class

        init();

        int pid = android.os.Process.myPid();
        Log.w(TAG, "onCreate(): ending!!: MyApplication is oncreate=====" + "pid=" + pid);


    }

    private boolean ifAPILowerThan29() {
        return apiLevel < 29 ? true : false;
    }

    private void init() {
        listView = findViewById(R.id.listView);
        button1 = findViewById(R.id.button1);
        Log.w(TAG, "onCreate(): init(): start and show apiLevel -> " + apiLevel );

        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

            if (ifAPILowerThan29()) {
                ActivityCompat.requestPermissions(this,
                        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE
                        },
                        REQUEST_CODE_PERMISSION);
            } else {
                ActivityCompat.requestPermissions(this,
                        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE,
                                      Manifest.permission.READ_MEDIA_IMAGES},
                        REQUEST_CODE_PERMISSION);
            }

            Log.w(TAG, "onCreate(): init(): requestPermissions: Permission is asking!!!");

            adapter = new SimpleCursorAdapter(
                    this,
                    R.layout.list_item_layout,
                    null,
                    new String[] {MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATA},
                    new int[] {R.id.photoNameTextView, R.id.photoImageView},
                    0);
            listView.setAdapter(adapter);
            Log.w(TAG, "onCreate(): init(): match ListView with adapter : Permission is asking!!!");

        } else {
            Log.w(TAG, "Permission is granted!!!");
        }

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        Log.e(TAG, "onRequestPermissionsResult(): Permission is granted, and fetchLocalPhotos() is not called yet!!!");
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSION && grantResults.length > 0
                /*&& grantResults[0] == PackageManager.PERMISSION_GRANTED*/) {
            fetchLocalPhotos();
            Log.e(TAG, "onRequestPermissionsResult(): Permission is granted, and fetchLocalPhotos() is called!!!");
        }
    }

    // 取得本機相片
    private void fetchLocalPhotos() {
        Log.v(TAG, "fetchLocalPhotos(): Action is starting!!!");
        // 建立一個 Cursor 來查詢 MediaStore
        Cursor cursor = getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // 查詢外部存儲的圖片
                new String[]{
                        MediaStore.Images.Media._ID,
                        MediaStore.Images.Media.DISPLAY_NAME,
                        MediaStore.Images.Media.DATA
                },
                null,
                null,
                MediaStore.Images.Media.DATE_ADDED + " DESC" // 按照添加時間降序排序
        );

        // 如果 Cursor 不為 null，則將其設定為 adapter 的 Cursor
        if (cursor != null) {
            adapter.changeCursor(cursor);
        }

        Toast.makeText(this,
                "apiVersion: " + apiLevel,
                Toast.LENGTH_SHORT)
             .show();
        Log.v(TAG, "fetchLocalPhotos(): ending to show Toast: Action is over!!!");
    }

    public void sentToMyProcessActivity(View view) {
        Intent intent = new Intent(MainActivity.this, MyProcessActivity.class);
        startActivity(intent);
    }

    @Override
    protected void onResume() {
        super.onResume();

        Toast.makeText(this,
                        "apiVersion: " + apiLevel,
                        Toast.LENGTH_SHORT)
                .show();
        Log.i(TAG, "onResume(): MainActivity onResume, and show Toast");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.i(TAG, "onPause(): MainActivity onPause");
    }

    public void sentToMessengerActivity(View view) {
        Intent intent = new Intent(MainActivity.this, MessengerActivity.class);
        startActivity(intent);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: MainActivity onDestroy");
    }

    public class MyObserver implements LifecycleObserver {

        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        void onResume() {
            Log.d(TAG, "MyObserver: Lifecycle call onResume");
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        void onPause() {
            Log.d(TAG, "MyObserver: Lifecycle call onPause");
        }
    }
}
```
#### 索取版本號
`
int apiLevel = android.os.Build.VERSION.SDK_INT;
`

```
Android 8.0: API等級26，    版本號 "8.0.0"
Android 8.1: API等級27，    版本號 "8.1.0"
Android 9:   API等級28，    版本號 "9"
Android 10:  API等級29，    版本號 "10"
--------------------------------------------------- 權限分水嶺
        MANAGE_EXTERNAL_STORAGE
Android 11:  API等級30，    版本號 "11"
Android 12:  API等級31, 32，版本號 "12"
Android 13:  API等級33，    版本號 "13"
Android 14:  API等級34，    版本號 "14"
```
### 測試機 (使用 Android 8.0.0 及 Android 14)



## 觀察 Logcat
![image](https://hackmd.io/_uploads/SJFSctK06.png)

### API 26
![image](https://hackmd.io/_uploads/HkPm0KFAp.png)

### API 34
![image](https://hackmd.io/_uploads/SJHE0tFAa.png)



----------------

## Fragment 切換
![image](https://hackmd.io/_uploads/rk29KxxkA.png)

```
package com.example.ry_fragmenttest;

import android.os.Bundle;
import android.view.View;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

public class MainActivity extends AppCompatActivity {
    private FragmentManager fragmentManager;
    private F1Fragment f1;
    private F2Fragment f2;
    private F3Fragment f3;
    private F4Fragment f4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        fragmentManager = getSupportFragmentManager();
        f1 = new F1Fragment();
        f2 = new F2Fragment();
        f3 = new F3Fragment();
        f4 = new F4Fragment();

        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.add(R.id.container, f1);
        transaction.commit();
    }

    public void setF1(View view) {
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.container, f1);
        transaction.commit();
    }

    public F1Fragment getF1() { return f1;}

    public void setF2(View view) {
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.container, f2);
        transaction.commit();
    }

    public void setF3(View view) {
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.container, f3);
        transaction.commit();
    }

    public void setF4(View view) {
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.container, f4);
        transaction.commit();
    }
}
```
-------------------
## 音樂播放器
![image](https://hackmd.io/_uploads/SJzHKxl1A.png)

MainActivity.java
```
package com.example.ry_museplayer1;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    private Button playPause;
    private boolean isPlaying;
    private SeekBar seekBar;
    private MyReceiver myReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        playPause = findViewById(R.id.playOrPause);
        isPlaying = false;
        playPause.setText("Play");

        seekBar = findViewById(R.id.seekBar);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    Intent intent = new Intent(MainActivity.this, PlayMusicService.class);
                    intent.putExtra("action", PlayMusicService.ACTION_SEEKTO);
                    intent.putExtra("whereto", progress);
                    startService(intent);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        myReceiver = new MyReceiver();
        IntentFilter filter = new IntentFilter("fromService");
        registerReceiver(myReceiver, filter);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unregisterReceiver(myReceiver);
    }

    public void playOrPause(View view) {
        isPlaying = !isPlaying;
        if (!isPlaying) {
            playPause.setText("Play");
        } else {
            playPause.setText("Pause");
        }
        Intent intent = new Intent(this, PlayMusicService.class);
        intent.putExtra("action", isPlaying ? PlayMusicService.ACTION_PLAY : PlayMusicService.ACTION_PAUSE);
        startService(intent);
    }

    public void stopPlay(View view) {
        isPlaying = false;
        playPause.setText("Play");
        Intent intent = new Intent(this, PlayMusicService.class);
        stopService(intent);
    }

    private class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            int max = intent.getIntExtra("max", -1);
            if (max >= 0) {
                seekBar.setMax(max);
            }
            int wherenow = intent.getIntExtra("wherenow", -1);
            if (wherenow >= 0) {
                seekBar.setProgress(wherenow);
            }
        }
    }
}
```
PlayMusicService.java
```
package com.example.ry_museplayer1;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;

import java.util.Timer;
import java.util.TimerTask;

public class PlayMusicService extends Service {
    private MediaPlayer mediaPlayer;
    public static final int ACTION_PLAY = 1;
    public static final int ACTION_PAUSE = 2;
    public static final int ACTION_SEEKTO = 3;

    private Timer timer;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mediaPlayer = MediaPlayer.create(this, R.raw.test1);

        Intent intent = new Intent("fromService");
        intent.putExtra("max", mediaPlayer.getDuration());
        sendBroadcast(intent);

        timer = new Timer();
        timer.schedule(new UpdateTask(), 0, 400);
    }

    private class UpdateTask extends TimerTask {
        @Override
        public void run() {
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                Intent intent = new Intent("fromService");
                intent.putExtra("wherenow", mediaPlayer.getCurrentPosition());
                sendBroadcast(intent);
            }
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int action = intent.getIntExtra("action", -1);
        switch (action) {
            case ACTION_PLAY:
                if (!mediaPlayer.isPlaying())  mediaPlayer.start();
                break;
            case ACTION_PAUSE:
                if (mediaPlayer.isPlaying()) mediaPlayer.pause();
                break;
            case ACTION_SEEKTO:
                int whereto = intent.getIntExtra("whereto", -1);
                if (whereto >= 0) mediaPlayer.seekTo(whereto);
                break;
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (mediaPlayer != null) {
            if (mediaPlayer.isPlaying()) mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null;
        }

        if (timer != null) {
            timer.cancel();
            timer.purge();
            timer = null;
        }
    }
}
```
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:id="@+id/main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <Button
            android:id="@+id/playOrPause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Play"
            android:onClick="playOrPause"
            />
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Stop"
            android:onClick="stopPlay"
            />
    </LinearLayout>
    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>
```
-------------------------------
## 測試自定義 View （MyView）-> 螢幕保護程式彈牆動畫
```
package com.example.ry_myview2;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.Timer;
import java.util.TimerTask;

public class MyView extends View {
    private Bitmap ball;
    private Resources resources;
    private boolean isInit;
    private int viewW, viewH;
    private float ballX, ballY, dx, dy, ballW, ballH;
    private Timer timer;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        resources = context.getResources();
        ball = BitmapFactory.decodeResource(resources, R.drawable.ball);

        timer = new Timer();

//        Matrix matrix = new Matrix();
//        matrix.postScale()
    }

    private void init() {
        viewW = getWidth(); viewH = getHeight();
        ballW = viewW / 6f; ballH = ballW;

        Matrix matrix = new Matrix();
        matrix.postScale(ballW / ball.getWidth(), ballH / ball.getHeight());
        ball = Bitmap.createBitmap(ball, 0, 0,
                ball.getWidth(), ball.getHeight(), matrix, false);
        dx = 16; dy = 16;

        timer.schedule(new Balltask(), 1000, 80);
        isInit = true;
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);

        if (!isInit) init();

        canvas.drawBitmap(ball, ballX, ballY, null);
    }

    private class Balltask extends TimerTask {
        @Override
        public void run() {
            if (ballX < 0 || ballX + ballW > viewW) {
                dx *= -1;
            }
            if (ballY < 0 || ballY + ballH > viewH) {
                dy *= -1;
            }

            ballX += dx; ballY += dy;

            postInvalidate();
        }
    }
}
```
並利用任意的 Layout 取用 "MyView"
```
<?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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.ry_myview2.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>
```

## 測試 Handler
```
package com.example.ry_timertask;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
    private Timer timer;
    private Task1 task1;
    private TextView message;
    private int counter;
    private UIHandler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        message = findViewById(R.id.message);
        handler = new UIHandler();
    }

    @Override
    protected void onStart() {
        super.onStart();

        timer = new Timer();
    }

    @Override
    protected void onPause() {
        super.onPause();

        if (timer != null) {
            timer.cancel();
            timer.purge();
            timer = null;
        }
    }


    public void test1(View view) {
        Log.v("brad", "start");
        timer.schedule(new Task1(), 3*1000);
    }

    public void test2(View view) {
        task1 = new Task1();
        timer.schedule(task1, 1 * 1000, 1 * 1000);
    }

    public void test3(View view) {
        if (task1 != null) {
            task1.cancel();
            task1 = null;
        }
    }

    public void test4(View view) {
        Intent intent = new Intent(this, Page2Activity.class);
        startActivity(intent);
    }

    private class Task1 extends TimerTask {
        @Override
        public void run() {
            //Log.v("brad", "OK");
            counter++;
//            Message msg = new Message();
//            Bundle data = new Bundle();
//            data.putInt("counter", counter);
//            msg.setData(data);
//            handler.sendMessage(msg);
            handler.sendEmptyMessage(0);
        }
    }

    private class UIHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == 0) {
                message.setText("" + counter);
            }
        }
    }
}
```
------------------------------------
* 開場動畫

Intro.java
```
package com.example.testrequestpm;


import static android.content.ContentValues.TAG;

import android.content.Intent;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class Intro extends AppCompatActivity {

    private AnimationDrawable animationDrawable;
    private ImageView imageView1;
    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE); // 設定畫面不顯示標題
        getWindow().setFlags(   // 設定畫面全螢幕顯示，第一個參數是要設定的標誌，第二個參數是要設定的標誌的值
                WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN
        );
        super.onCreate(savedInstanceState);  // 呼叫父類別的onCreate方法
        EdgeToEdge.enable(this); // 准許畫面全螢幕顯示
        setContentView(R.layout.activity_intro); // 設定畫面的layout
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { // 設定畫面的padding
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); // 取得系統狀態列的尺寸
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); // 設定畫面的padding
            return insets; // 回傳系統狀態列的尺寸
        });

        imageView1 = (ImageView) findViewById(R.id.myBackImageView01); // 取得畫面上的ImageView元件
        // 取得res/drawable目錄下的animation.xml檔案，並轉換成AnimationDrawable物件
        animationDrawable = (AnimationDrawable) getResources().getDrawable(R.drawable.animation);
        imageView1.setImageDrawable(animationDrawable); // 設定ImageView元件的圖片

        new Handler().postDelayed(new Runnable() { // 設定一個延遲的工作
            @Override
            public void run() {
                Intent intent = new Intent(Intro.this, MainActivity.class);
                try {
                    startActivity(intent); // 啟動MainActivity
                } catch (Exception e) {
                    finish(); // 結束畫面
                }
                aniStart(); // 呼叫aniStart方法
            }
        }, 4000); // 設定延遲的時間
    }

    private Runnable runnableStart = new Runnable() {
        @Override
        public void run() {
            Intent intent = new Intent(Intro.this, MainActivity.class);// 建立一個Intent物件
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 設定Intent物件的旗標
            try {
                startActivity(intent); // 啟動MainActivity
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, e.toString());
            }
        }
    };

    private void animationStart() {
        animationDrawable.start(); // 啟動動畫
    }

    private Runnable runnableAnimation = new Runnable() {
        @Override
        public void run() {
            animationStart(); // 呼叫animationStart方法
        }
    };

    private void aniStart() {
        if (animationDrawable.isRunning()) {
            animationDrawable.stop();
            animationDrawable.start();
        } else {
            animationDrawable.start();
        }
    }

    @Override
    protected void onResume() {
        handler.postDelayed(runnableAnimation, 1000); // 設定一個延遲的工作，用以啟動動畫
        handler.postDelayed(runnableStart, 6000); // 設定一個延遲的工作，用以啟動MainActivity
        super.onResume();
    }

    @Override
    protected void onPause() {
        handler.removeCallbacks(runnableAnimation); // 移除延遲的工作

        super.onPause();
    }
}
```
activity_intro.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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".Intro">

    <ImageView
        android:id="@+id/myBackImageView01"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        app:srcCompat="@drawable/sbar_1"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
```
添加動畫圖片

![image](https://hackmd.io/_uploads/Ska32eeJ0.png)


animation.xml
```
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/sbar_1" android:duration="150" />
    <item android:drawable="@drawable/sbar_2" android:duration="150" />
    <item android:drawable="@drawable/sbar_3" android:duration="150" />
    <item android:drawable="@drawable/sbar_4" android:duration="150" />
    <item android:drawable="@drawable/sbar_5" android:duration="150" />
    <item android:drawable="@drawable/sbar_6" android:duration="150" />
    <item android:drawable="@drawable/sbar_7" android:duration="1100" />
    <item android:drawable="@drawable/sbar_8" android:duration="2000" />
</animation-list>
```

list_item_layout.xml
![image](https://hackmd.io/_uploads/ry99ClekA.png)

```
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/photoImageView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        />
    <TextView
        android:id="@+id/photoNameTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginStart="16dp"
        android:textSize="16sp"
        />

</LinearLayout>
```













