Skip to content

Commit

Permalink
Merge branch 'release/v1.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
skgmn committed Aug 15, 2021
2 parents 8c42273 + c7c7e05 commit d33d70e
Show file tree
Hide file tree
Showing 64 changed files with 1,961 additions and 72 deletions.
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Setup

```
dependencies {
implementation "com.github.skgmn:startactivityx:1.0.0"
}
```
If you don't know how to access to GitHub Packges, please refer to [this](https://gist.github.com/skgmn/79da4a935e904078491e932bd5b327c7).

# Features

## startActivityForResult

```kotlin
lifecycleScope.launch {
val activityResult = startActivityForResult(intent)
}
```
That's it.
It provides some extension methods of `ActivityResult` for the convenience.

```kotlin
val ActivityResult.isOk: Boolean
val ActivityResult.isCanceled: Boolean
fun ActivityResult.getDataIfOk(): Intent?
```
So returned `ActivityResult` can be used like this in more kotlin way.

```kotlin
lifecycleScope.launch {
startActivityForResult(intent).getDataIfOk()?.let { open(it) }
}
```

## requestPermissions

```kotlin
lifecycleScope.launch {
if (requestPermissions(Manifest.permission.CAMERA).granted) {
// Permissions are granted here
startCamera()
}
}
```
Simple again.
This single method also handles rationale dialog and _do not ask again_ cases so there are no other things to acquire permissions.

There are some more features which are not documented yet. Please refer to source code and sample code to know about them.

## PermissionStatus

Sometimes there needs to show or hide views according to whether permissions are granted or not. `listenPermissionStatus()` has been introduced to manage this case. It returns `Flow<Boolean>` which infinitely emits boolean values that indicate whether required permissions are granted.
```kotlin
lifecycleScope.launch {
listenPermissonStatus(Manifest.permission.CAMERA).collect {
binding.permissionsGranted = it.granted
}
}
```
```xml
<Button
android:text="Grant permissions"
android:visibility="@{permissionsGranted ? View.GONE : View.VISIBLE}" />
```

Or `getPermissionStatus()` can be used to get `PermissionStatus` just once.

## startActivityForInstance

This is some kind of bonus feature. It starts an Activity and returns its instance.
```kotlin
lifecycleScope.launch {
val intent = ExplicitIntent(context, MyActivity::class.java)
val activity = startActivityForInstance(intent)
}
```

This file was deleted.

13 changes: 0 additions & 13 deletions app/src/main/AndroidManifest.xml

This file was deleted.

3 changes: 0 additions & 3 deletions app/src/main/res/values/strings.xml

This file was deleted.

This file was deleted.

10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.2"
classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

// NOTE: Do not place your application dependencies here; they belong
Expand All @@ -18,7 +18,13 @@ allprojects {
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
maven {
url "https://maven.pkg.github.com/skgmn/*"
credentials {
username GITHUB_ID
password GITHUB_PACKAGES_TOKEN
}
}
}
}

Expand Down
File renamed without changes.
23 changes: 15 additions & 8 deletions app/build.gradle → camerasample/build.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}

android {
compileSdkVersion 30

dataBinding {
enabled = true
}
defaultConfig {
applicationId "com.github.skgmn.rapidstartactivity"
minSdkVersion 16
applicationId "com.github.skgmn.startactivityx.camerasample"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
Expand All @@ -32,12 +36,15 @@ android {
}

dependencies {
implementation project(":library")

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.camera:camera-camera2:1.0.1"
implementation "androidx.camera:camera-lifecycle:1.0.1"
implementation "androidx.camera:camera-view:1.0.0-alpha27"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "com.github.skgmn:cameraxx:0.1.0"
implementation "com.github.skgmn:viewmodelevent:1.1.0"
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
File renamed without changes.
28 changes: 28 additions & 0 deletions camerasample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.skgmn.startactivityx.camerasample">

<uses-feature android:name="android.hardware.camera.any" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<application
android:name=".SampleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CameraSample">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.github.skgmn.startactivityx.camerasample

import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import com.github.skgmn.startactivityx.*
import com.github.skgmn.startactivityx.camerasample.databinding.ActivityMainBinding
import com.github.skgmn.viewmodelevent.handle
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

binding.owner = this
binding.lifecycleOwner = this
binding.viewModel = viewModel

lifecycleScope.launch {
requestPermissions(Manifest.permission.CAMERA)
viewModel.permissionsInitiallyRequested.value = true
init()
}
}

private fun init() {
handleEvents()

lifecycleScope.launch {
listenPermissionStatus(Manifest.permission.CAMERA).collect {
viewModel.cameraPermissionsGranted.value = it.granted
}
}
}

private fun handleEvents() = with(viewModel) {
handle(requestCameraPermissionsByUserEvent) {
lifecycleScope.launch {
val permissionRequest = PermissionRequest(listOf(Manifest.permission.CAMERA), true)
requestPermissions(permissionRequest)
}
}
handle(requestTakePhotoPermissionsEvent) {
lifecycleScope.launch {
val permissionRequest =
PermissionRequest(listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), true)
val permissionResult = requestPermissions(permissionRequest)
if (permissionResult.granted) {
// ImageCapture seems not work well right after a permission is granted.
// In this case recreate and rebind ImageCapture to workaround this issue.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P &&
permissionResult == GrantResult.JUST_GRANTED
) {
viewModel.replaceImageCapture()
binding.executePendingBindings()
}
whenStarted {
viewModel.takePhoto()
}
} else {
Toast.makeText(this@MainActivity, R.string.no_permissions, Toast.LENGTH_SHORT)
.show()
}
}
}
handle(showTakenPhotoEvent) { uri ->
if (uri != Uri.EMPTY) {
try {
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
return@handle
} catch (e: ActivityNotFoundException) {
// fall through
}
}
Toast.makeText(this@MainActivity, R.string.photo_saved, Toast.LENGTH_SHORT).show()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.github.skgmn.startactivityx.camerasample

import android.app.Application
import android.content.ContentValues
import android.net.Uri
import android.provider.MediaStore
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.github.skgmn.cameraxx.takePicture
import com.github.skgmn.viewmodelevent.publicEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

class MainViewModel(application: Application) : AndroidViewModel(application) {
private val imageCaptureUseCaseFlow = MutableStateFlow(newImageCapture())

val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val previewUseCase = Preview.Builder().build()
val imageCaptureUseCase =
imageCaptureUseCaseFlow.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)

val permissionsInitiallyRequested = MutableStateFlow(false)
val cameraPermissionsGranted = MutableStateFlow(false)

val requestCameraPermissionsByUserEvent = publicEvent<Any>()
val requestTakePhotoPermissionsEvent = publicEvent<Any>()
val showTakenPhotoEvent = publicEvent<Uri>()

fun requestCameraPermissions() {
requestCameraPermissionsByUserEvent.post(Unit)
}

fun requestTakePhotoPermissions() {
requestTakePhotoPermissionsEvent.post(Unit)
}

fun takePhoto() {
viewModelScope.launch {
val outputOptions = ImageCapture.OutputFileOptions.Builder(
getApplication<Application>().contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
ContentValues()
).build()

val result = imageCaptureUseCaseFlow.value.takePicture(outputOptions)
showTakenPhotoEvent.post(result.savedUri ?: Uri.EMPTY)
}
}

fun replaceImageCapture() {
imageCaptureUseCaseFlow.value = newImageCapture()
}

private fun newImageCapture() = ImageCapture.Builder().build()
}
Loading

0 comments on commit d33d70e

Please sign in to comment.