iT邦幫忙

2021 iThome 鐵人賽

DAY 2
0
Mobile Development

重新瞭解Android硬體控制系列 第 2

110/02 - 只有 StartActivityForResult 可以用嗎?

前一天講到合約(Contracts)和啟動器(Launcher)取代StartActivityForResult,官方也幫我們建立了14種常見的合約模板,以下是官方的14種合約

ActivityResultContracts.CreateDocument()
ActivityResultContracts.GetContent()
ActivityResultContracts.GetMultipleContents()
ActivityResultContracts.OpenDocument()
ActivityResultContracts.OpenDocumentTree()
ActivityResultContracts.OpenMultipleDocuments()
ActivityResultContracts.PickContact()
ActivityResultContracts.RequestMultiplePermissions()
ActivityResultContracts.RequestPermission()
ActivityResultContracts.StartActivityForResult()
ActivityResultContracts.StartIntentSenderForResult()
ActivityResultContracts.TakePicture()
ActivityResultContracts.TakePicturePreview()
ActivityResultContracts.TakeVideo()

這篇先介紹

ActivityResultContracts.CreateDocument()

An ActivityResultContract to prompt the user to select a path for creating a new document, returning the content: Uri of the item that was created.
The input is the suggested name for the new file.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

CreateDocument()可以用在拍照前的建立空白檔案。

以下範例是建立空白的saberEat.jpg檔案,然後回傳檔案的content://uri,需要注意使用者可以更改檔案名稱。

createDocumentResultLauncher.launch("saberEat.jpg")
private val createDocumentResultLauncher =
    registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

實際執行程式後的Log

D/maho: 回傳: content://com.android.providers.downloads.documents/document/1692

ActivityResultContracts.GetContent()

An ActivityResultContract to prompt the user to pick a piece of content, receiving a content:// Uri for that content that allows you to use android.content.ContentResolver.openInputStream(Uri) to access the raw data. By default, this adds Intent.CATEGORY_OPENABLE to only return content that can be represented as a stream.

The input is the mime type to filter by, e.g. image/*.
This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

以下範例是輸入一個MIME type指定類型,然後選擇一個檔案,回傳檔案的content://uri,不可以輸入null

getContentResultLauncher.launch("image/*")
private val getContentResultLauncher =
    registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

實際執行程式後的Log

D/maho: 回傳: content://com.android.providers.downloads.documents/document/1688

ActivityResultContracts.GetMultipleContents()

An ActivityResultContract to prompt the user to pick one or more a pieces of content, receiving a content:// Uri for each piece of content that allows you to use android.content.ContentResolver.openInputStream(Uri) to access the raw data. By default, this adds Intent.CATEGORY_OPENABLE to only return content that can be represented as a stream.

The input is the mime type to filter by, e.g. image/*.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

以下範例是輸入一個MIME type指定類型,然後長按選擇多個檔案,用陣列的形式回傳檔案的content://uri,不可以輸入null

getMultipleContentsResultLauncher.launch("image/*")
private val getMultipleContentsResultLauncher =
    registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

實際執行程式後的Log

D/maho: 回傳: [
content://com.android.providers.downloads.documents/document/1688,
content://com.android.providers.downloads.documents/document/1677
]

ActivityResultContracts.OpenDocument()

An ActivityResultContract to prompt the user to open a document, receiving its contents as a file:/http:/content: Uri.

The input is the mime types to filter by, e.g. image/*.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

See Also: DocumentsContract

以下範例是輸入多個MIME type指定類型,然後選擇一個檔案,回傳檔案的content://uri,可以輸入null表示不指定類型。

openDocumentResultLauncher.launch(arrayOf("image/jpeg", "video/mp4"))
private val openDocumentResultLauncher =
    registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

實際執行程式後的Log

D/maho: 回傳: content://com.android.providers.downloads.documents/document/1677

ActivityResultContracts.OpenDocumentTree()

An ActivityResultContract to prompt the user to select a directory, returning the user selection as a Uri. Apps can fully manage documents within the returned directory.

The input is an optional Uri of the initial starting location.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

See Also:
Intent.ACTION_OPEN_DOCUMENT_TREE, DocumentsContract.buildDocumentUriUsingTree, DocumentsContract.buildChildDocumentsUriUsingTree

以下範例是選擇資料夾,然後回傳資料夾的content://uri,可以輸入null表示不指定路徑。

openDocumentTreeResultLauncher.launch(null)
private val openDocumentTreeResultLauncher = 
    registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

選擇Download/Duo資料夾,實際執行程式後的Log

D/maho: 回傳: content://com.android.externalstorage.documents/tree/primary%3ADownload%2FDuo

ActivityResultContracts.OpenMultipleDocuments()

An ActivityResultContract to prompt the user to open (possibly multiple) documents, receiving their contents as file:/http:/content: Uris.

The input is the mime types to filter by, e.g. image/*.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

See Also:
DocumentsContract

以下範例是輸入多個MIME type指定類型,然後選擇多個檔案,用陣列的形式回傳檔案的content://uri,不可以輸入null

openMultipleDocumentsResultLauncher.launch(arrayOf("image/jpeg", "video/mp4"))
private val openMultipleDocumentsResultLauncher =
    registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { list ->
        Log.d("maho", "回傳: $list")
    }

實際執行程式後的Log

D/maho: 回傳: [
content://com.android.providers.media.documents/document/image%3A5789, 
content://com.android.providers.media.documents/document/image%3A5791
]

ActivityResultContracts.PickContact()

An ActivityResultContract to request the user to pick a contact from the contacts app.

The result is a content: Uri.

See Also:
ContactsContract

選擇單個連絡人,回傳content://uri

pickContactResultLauncher.launch(null)
private val pickContactResultLauncher =
    registerForActivityResult(ActivityResultContracts.PickContact()) { uri ->
        Log.d("maho", "回傳: $uri")
    }

實際執行程式後的Log

D/maho: 回傳: content://com.android.contacts/contacts/lookup/1519iaad44c18aa14d9a/10

ActivityResultContracts.RequestMultiplePermissions()

An Intent action for making a permission request via a regular Activity.startActivityForResult API. Caller must provide a String[] extra EXTRA_PERMISSIONS Result will be delivered via Activity.onActivityResult(int, int, Intent) with String[] EXTRA_PERMISSIONS and int[] EXTRA_PERMISSION_GRANT_RESULTS, similar to Activity.onRequestPermissionsResult(int, String[], int[])

See Also:
Activity.requestPermissions(String[], int), Activity.onRequestPermissionsResult(int, String[], int[])

Key for the extra containing all the requested permissions.
See Also:
ACTION_REQUEST_PERMISSIONS

Key for the extra containing whether permissions were granted.
See Also:
ACTION_REQUEST_PERMISSIONS

以下範例是輸入多個系統權限,用map的形式回傳每個權限的truefalse,可以輸入null,不過沒意義。

requestMultiplePermissionsResultLauncher.launch(
    arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    )
)
private val requestMultiplePermissionsResultLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { map ->
        Log.d("maho", "回傳: $map")
    }

實際執行程式後的Log

D/maho: 回傳: {
android.permission.CAMERA=false, 
android.permission.WRITE_EXTERNAL_STORAGE=false
}

ActivityResultContracts.RequestPermission()

An ActivityResultContract to request a permission

以下範例是輸入一個系統權限,回傳權限的truefalse,可以輸入null,不過沒意義。

requestPermissionResultLauncher.launch(Manifest.permission.CAMERA)
private val requestPermissionResultLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { boolean ->
        Log.d("maho", "回傳: $boolean")
    }

實際執行程式後的Log

D/maho: 回傳: false

ActivityResultContracts.StartIntentSenderForResult()

An ActivityResultContract that calls Activity.startIntentSender(IntentSender, Intent, int, int, int). This ActivityResultContract takes an IntentSenderRequest, which must be constructed using an IntentSenderRequest.Builder. If the call to Activity.startIntentSenderForResult(IntentSender, int, Intent, int, int, int) throws an IntentSender.SendIntentException the androidx.activity.result.ActivityResultCallback will receive an ActivityResult with an Activity.RESULT_CANCELED resultCode and whose intent has the action of ACTION_INTENT_SENDER_REQUEST and an extra EXTRA_SEND_INTENT_EXCEPTION that contains the thrown exception.

我不知道這個是做什麼用的,Google後才知道是Phone Selector Api,可以用來做簡訊驗證之類的功能。

//implementation 'com.google.android.gms:play-services-auth:19.2.0'

val hintRequest = HintRequest
    .Builder()
    .setPhoneNumberIdentifierSupported(true)
    .build()
    
val credentialsOptions = CredentialsOptions
    .Builder()
    .forceEnableSaveDialog()
    .build()
    
val credentials = Credentials
    .getClient(this, credentialsOptions)
    .getHintPickerIntent(hintRequest)
    
startIntentSenderForResultResultLauncher.launch(
    IntentSenderRequest
        .Builder(credentials)
        .build()
)
private val startIntentSenderForResultResultLauncher =
    registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
        if (RESULT_OK == result.resultCode) {
            val credential: Credential? = result
                .data
                ?.getParcelableExtra(Credential.EXTRA_KEY)
            Log.d(
                "maho",
                "id: ${credential?.id} " +
                        "\naccountType: ${credential?.accountType} " +
                        "\nfamilyName: ${credential?.familyName} " +
                        "ngivenName: ${credential?.givenName} " +
                        "\nidTokens: ${credential?.idTokens} " +
                        "\nname: ${credential?.name} " +
                        "\npassword: ${credential?.password} " +
                        "\nprofilePictureUri: ${credential?.profilePictureUri}"
            )
        }
    }

實際執行程式後的Log

D/maho: id: +886910123456 
    accountType: null 
    familyName: null 
    givenName: null 
    idTokens: [] 
    name: null 
    password: null 
    profilePictureUri: null

ActivityResultContracts.TakePicture()

An ActivityResultContract to take a picture saving it into the provided content-Uri.

Returns true if the image was saved into the given Uri.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

以下範例是建立空白檔案後再開啟相機拍攝照片,回傳true表示儲存成功,false表示儲存失敗。

第一種寫法:用ActivityResultContracts.CreateDocument()建立檔案,取得檔案的uri後再使用TakePicture()拍照

takePictureCreateDocumentResultLauncher.launch("002.jpg")
private val takePictureCreateDocumentResultLauncher =
    registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
        takePictureResultLauncher.launch(uri)
    }
    
private val takePictureResultLauncher =
    registerForActivityResult(ActivityResultContracts.TakePicture()) { boolean ->
        Log.d("maho", "回傳: $boolean")
    }    

實際執行程式後的Log

//ActivityResultContracts.CreateDocument()的Log
D/maho: 回傳: content://com.android.providers.downloads.documents/document/1701

//ActivityResultContracts.TakePicture()的Log
D/maho: 回傳: true

第二種寫法:使用File()建立檔案,再使用getUriForFile()取得檔案的uri後再使用TakePicture()拍照

val picturePath = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "003.jpg")
val uri = getUriForFile(this, "$packageName.fileprovider", picturePath)

takePictureResultLauncher.launch(uri)
private val takePictureResultLauncher =
    registerForActivityResult(ActivityResultContracts.TakePicture()) { boolean ->
        Log.d("maho", "回傳: $boolean")
    }    

如果要用第二種寫法還要前置作業

  1. res 資料夾底下新增 xml 資料夾
  2. 建立 tool_provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="lens_picture"
        path="." />
</paths>
  1. AndroidManifest 新增
<?xml version="1.0" encoding="utf-8"?>
<manifest>
    <application>
    
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/tool_provider_paths" />
        </provider>
        
    </application>
</manifest>

實際執行程式後的Log

D/maho: 回傳: true

ActivityResultContracts.TakePicturePreview()

An ActivityResultContract to take small a picture preview, returning it as a Bitmap.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

以下範例是開啟相機,拍攝照片後回傳相片縮圖的bitmap,因為是縮圖,所以圖片會非常小,官方文件表示 take small a picture preview,所以我也不太懂這個可以用來做什麼功能。

takePicturePreviewResultLauncher.launch(null)
private val takePicturePreviewResultLauncher =
    registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->    
        amIvTakePicturePreview.setImageBitmap(bitmap)
        
        //把縮圖存起來的程式碼
        val picturePath = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "004.jpg")
        val fileOutputStream = FileOutputStream(picturePath)
        val bufferedOutputStream = BufferedOutputStream(fileOutputStream)
        
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bufferedOutputStream)
        bufferedOutputStream.flush()
        bufferedOutputStream.close()
    }

實際執行程式後的Log

D/maho: 回傳: android.graphics.Bitmap@93f6ede

ActivityResultContracts.TakeVideo()

An ActivityResultContract to take a video saving it into the provided content-Uri.
Returns a thumbnail.

This can be extended to override createIntent if you wish to pass additional extras to the Intent created by super.createIntent().

以下範例是建立空白檔案後再開啟相機錄製影片,錄製完後回傳影片縮圖的bitmap

//路徑為空,會儲存到 DCIM 資料夾
takeVideoResultLauncher.launch(null)

//指定儲存路徑
val picturePath = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "005.mp4")
val uri = getUriForFile(this, "$packageName.fileprovider", picturePath)
takeVideoResultLauncher.launch(uri)
private val takeVideoResultLauncher =
    registerForActivityResult(ActivityResultContracts.TakeVideo()) { bitmap ->
        amIvTakeVideo.setImageBitmap(bitmap)
    }

實際執行程式後的Log,影片有儲存成功,理論上要回傳影片縮圖的bitmap,但我在Android 11一直試不出來,StackOverflow也有一樣的問題,也還沒解決,只能說是官方的坑

D/maho: 回傳: null

總結

我自己覺得官方合約的命名方式滿混亂的,開啟檔案就有GetOpen,然後檔案本身也有DocumentContent,所以來分類一下

建立檔案

  • CreateDocument()

取得檔案

  • GetContent()
  • OpenDocument()
  • GetMultipleContents()
  • OpenMultipleDocuments()
輸入單個檔案類型 輸入多個檔案類型
回傳單個檔案uri GetContent() OpenDocument()
回傳陣列檔案uri GetMultipleContents() OpenMultipleDocuments()

取得資料夾

  • OpenDocumentTree()

取得連絡人

  • PickContact()

取得電話驗證

  • StartIntentSenderForResult()

權限驗證

  • RequestMultiplePermissions()
  • RequestPermission()
輸入單個權限 輸入多個權限
回傳單個權限驗證結果 RequestPermission() X
回傳陣列權限驗證結果 X RequestMultiplePermissions()

頁面跳轉

  • StartActivityForResult()

鏡頭控制

  • TakePicture()
  • TakePicturePreview()
  • TakeVideo()
相片 相片縮圖 影片
回傳布林值 TakePicture() X X
回傳相片縮圖bitmap X TakePicturePreview() X
回傳影片縮圖bitmap X X TakeVideo()

程式碼放在feature/resultTemplate分支
https://github.com/AndyAWD/AndroidSystem/tree/feature/resultTemplate


上一篇
110/01 - 什麼!startActivityForResult 被標記棄用?
下一篇
110/07 - 建立自己的 ResultContracts
系列文
重新瞭解Android硬體控制14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言