Skip to content
Extraits de code Groupes Projets
Valider 5b469699 rédigé par Monnot's avatar Monnot
Parcourir les fichiers

Taking picture from TD6 works

parent 5ae0e39c
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
Affichage de
avec 383 ajouts et 19 suppressions
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.72' // vérifier que votre version de kotlin est la même
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" // vérifier que votre version de kotlin est la même
}
android {
......@@ -70,4 +70,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Coil
implementation("io.coil-kt:coil:1.1.0")
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.clemhaowen.dm_td2">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
......@@ -9,7 +14,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DM_td2">
<activity android:name=".task.TaskActivity"></activity>
<activity android:name=".userInfo.UserInfoActivity"></activity>
<activity android:name=".task.TaskActivity" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
......
......@@ -44,4 +44,8 @@ object Api {
val userService: UserService by lazy {
retrofit.create(UserService::class.java)
}
val tasksWebService: TasksWebService by lazy {
retrofit.create(TasksWebService::class.java)
}
}
\ No newline at end of file
package com.clemhaowen.dm_td2.network
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.clemhaowen.dm_td2.tasklist.Task
import retrofit2.http.Body
import retrofit2.http.Path
class TasksRepository {
private val tasksWebService = Api.tasksWebService
// Ces deux variables encapsulent la même donnée:
// [_taskList] est modifiable mais privée donc inaccessible à l'extérieur de cette classe
private val _taskList = MutableLiveData<List<Task>>()
// [taskList] est publique mais non-modifiable:
// On pourra seulement l'observer (s'y abonner) depuis d'autres classes
public val taskList: LiveData<List<Task>> = _taskList
suspend fun refresh() {
// Call HTTP (opération longue):
val tasksResponse = tasksWebService.getTasks()
// À la ligne suivante, on a reçu la réponse de l'API:
if (tasksResponse.isSuccessful) {
val fetchedTasks = tasksResponse.body()
// on modifie la valeur encapsulée, ce qui va notifier ses Observers et donc déclencher leur callback
_taskList.value = fetchedTasks!!
}
}
suspend fun deleteTask(task : Task){
val taskResponse = tasksWebService.deleteTask(id = task.id)
if (taskResponse.isSuccessful){
val editableList = _taskList.value.orEmpty().toMutableList()
val position = editableList.indexOfFirst { task.id == it.id }
editableList.removeAt(position)
_taskList.value = editableList
}
}
suspend fun addTask(task: Task){
val taskResponse = tasksWebService.createTask(task)
if (taskResponse.isSuccessful){
val editableList = _taskList.value.orEmpty().toMutableList()
editableList.add(taskResponse.body()!!)
_taskList.value = editableList
}
}
suspend fun updateTask(task: Task) {
// TODO: do update request and check response
// DONE
val taskResponse = tasksWebService.updateTask(task, task.id)
if (taskResponse.isSuccessful){
val editableList = _taskList.value.orEmpty().toMutableList()
val updatedTask = taskResponse.body()
val position = editableList.indexOfFirst { updatedTask?.id == it.id }
editableList[position] = updatedTask!!
_taskList.value = editableList
}
}
}
\ No newline at end of file
package com.clemhaowen.dm_td2.network
import android.net.Uri
import androidx.core.net.toFile
import com.clemhaowen.dm_td2.tasklist.Task
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import retrofit2.Response
import retrofit2.http.*
interface TasksWebService {
@GET("tasks")
suspend fun getTasks(): Response<List<Task>>
@DELETE("tasks/{id}")
suspend fun deleteTask(@Path("id") id: String?): Response<Unit>
@POST("tasks")
suspend fun createTask(@Body task: Task): Response<Task>
@PATCH("tasks/{id}")
suspend fun updateTask(@Body task: Task, @Path("id") id: String? = task.id): Response<Task>
}
\ No newline at end of file
......@@ -10,5 +10,7 @@ data class UserInfo (
@SerialName("firstname")
val firstName: String,
@SerialName("lastname")
val lastName: String
val lastName: String,
@SerialName("avatar")
val avatar: String = ""
)
\ No newline at end of file
package com.clemhaowen.dm_td2.network
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.Part
interface UserService {
@GET("users/info")
suspend fun getInfo(): Response<UserInfo>
@Multipart
@PATCH("users/update_avatar")
suspend fun updateAvatar(@Part avatar: MultipartBody.Part): Response<UserInfo>
}
\ No newline at end of file
......@@ -21,7 +21,7 @@ class TaskActivity : AppCompatActivity() {
createButton.setOnClickListener{
val titleText = findViewById<EditText>(R.id.titleEditText).text
val descriptionText = findViewById<EditText>(R.id.descriptionEditText).text
val newTask = Task(id = UUID.randomUUID().toString(), title = titleText.toString(), description = descriptionText.toString())
val newTask = Task(id = java.util.UUID.randomUUID().toString(), title = titleText.toString(), description = descriptionText.toString())
intent.putExtra("newTask", newTask)
setResult(666, intent)
TASK_KEY = "newTask"
......
package com.clemhaowen.dm_td2.tasklist
import java.io.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Task (
var id: String = "id",
var title: String = "Task",
var description: String = "description"
) : Serializable
\ No newline at end of file
@SerialName("id")
val id: String = "id",
@SerialName("title")
val title: String = "Task",
@SerialName("description")
val description: String = "description"
) : java.io.Serializable
\ No newline at end of file
......@@ -9,9 +9,10 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.clemhaowen.dm_td2.R
class TaskListAdapter(private val taskList: List<Task>) : RecyclerView.Adapter<TaskListAdapter.TaskViewHolder>() {
class TaskListAdapter(/*private val*/var taskList: List<Task>) : RecyclerView.Adapter<TaskListAdapter.TaskViewHolder>() {
var onDeleteClickListener: ((Task) -> Unit)? = null
var onEditClickListener: ((Task) -> Unit)? = null
override fun getItemCount(): Int = taskList.size
......@@ -33,6 +34,12 @@ class TaskListAdapter(private val taskList: List<Task>) : RecyclerView.Adapter<T
val deleteButton = findViewById<ImageButton>(R.id.delete_task)
deleteButton.setOnClickListener {
onDeleteClickListener?.invoke(taskTitle)}
val editButton = findViewById<ImageButton>(R.id.editButton)
editButton.setOnClickListener {
// TODO invoke editClickListener
// DONE
onEditClickListener?.invoke(taskTitle)
}
}
......
......@@ -5,13 +5,23 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.clemhaowen.dm_td2.R
import com.clemhaowen.dm_td2.network.Api
import com.clemhaowen.dm_td2.network.TasksRepository
import com.clemhaowen.dm_td2.task.TaskActivity
import com.clemhaowen.dm_td2.userInfo.UserInfoActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch
import java.util.*
class TaskListFragment : Fragment() {
......@@ -24,6 +34,8 @@ class TaskListFragment : Fragment() {
Task(id = "id_3", title = "Task 3")
)
private val tasksRepository = TasksRepository()
companion object {
const val ADD_TASK_REQUEST_CODE = 666
}
......@@ -37,16 +49,38 @@ class TaskListFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var recyclerView = view?.findViewById<RecyclerView>(R.id.fragment_tasklist_recyclerView)
var recyclerView = view.findViewById<RecyclerView>(R.id.fragment_tasklist_recyclerView)
recyclerView?.layoutManager = LinearLayoutManager(activity)
// En utilisant les synthetics, on écrit juste l'id directement (c'est magique ✨):
//R.id.fragment_tasklist_recyclerView.layoutManager = LinearLayoutManager(activity) // ça marche pas
// TODO uncomment when TaskListAdapter is created
// DONE
recyclerView?.adapter = TaskListAdapter(taskList)
(recyclerView?.adapter as TaskListAdapter).onDeleteClickListener = { task ->
// Supprimer la tâche
taskList.remove(task)
(recyclerView?.adapter as TaskListAdapter).notifyDataSetChanged()
//taskList.remove(task)
//(recyclerView?.adapter as TaskListAdapter).notifyDataSetChanged()
lifecycleScope.launch {
tasksRepository.deleteTask(task)
}
}
val createButton = view?.findViewById<FloatingActionButton>(R.id.floatingActionButton)
(recyclerView?.adapter as TaskListAdapter).onEditClickListener = { task ->
lifecycleScope.launch {
tasksRepository.updateTask(task)
}
}
tasksRepository.taskList.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
val editableList = (recyclerView?.adapter as TaskListAdapter).taskList.toMutableList()
editableList.clear()
editableList.addAll(it)
(recyclerView?.adapter as TaskListAdapter).taskList = editableList
(recyclerView?.adapter as TaskListAdapter).notifyDataSetChanged()
})
val createButton = view.findViewById<FloatingActionButton>(R.id.floatingActionButton)
createButton.setOnClickListener {
val intent = Intent(activity, TaskActivity::class.java)
startActivityForResult(intent, ADD_TASK_REQUEST_CODE)
......@@ -54,12 +88,33 @@ class TaskListFragment : Fragment() {
(recyclerView?.adapter as TaskListAdapter).notifyDataSetChanged()
}
// En utilisant les synthetics, on écrit juste l'id directement (c'est magique ✨):
//R.id.fragment_tasklist_recyclerView.layoutManager = LinearLayoutManager(activity) // ça marche pas
val profilPhoto = view.findViewById<ImageView>(R.id.imageViewProfilPhoto)
profilPhoto.setOnClickListener {
val intent = Intent(activity, UserInfoActivity::class.java)
startActivity(intent)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val task = data!!.getSerializableExtra(TaskActivity.TASK_KEY) as Task
taskList.add(task)
}
override fun onResume() {
super.onResume()
val profilPhoto = view?.findViewById<ImageView>(R.id.imageViewProfilPhoto)
profilPhoto?.load("https://goo.gl/gEgYUd") {
crossfade(true)
transformations(CircleCropTransformation())
}
lifecycleScope.launch {
tasksRepository.refresh()
}
lifecycleScope.launch {
val userInfo = Api.userService.getInfo().body()!!
val textView = view?.findViewById<TextView>(R.id.textViewInfo)
textView?.text = "${userInfo.firstName} ${userInfo.lastName}"
}
}
}
\ No newline at end of file
package com.clemhaowen.dm_td2.userInfo
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.launch
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import coil.load
import com.clemhaowen.dm_td2.BuildConfig
import com.clemhaowen.dm_td2.R
import com.clemhaowen.dm_td2.network.Api
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserInfoActivity : AppCompatActivity() {
private val userService = Api.userService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_info)
val buttonToTakePicture = findViewById<Button>(R.id.take_picture_button)
buttonToTakePicture.setOnClickListener{
askCameraPermissionAndOpenCamera()
}
lifecycleScope.launch {
val userInfoResponse = userService.getInfo()
if (userInfoResponse.isSuccessful){
val imageView = findViewById<ImageView>(R.id.image_view)
imageView.load(userInfoResponse.body()?.avatar)
}
}
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) openCamera()
else showExplanationDialog()
}
private fun requestCameraPermission() =
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
private fun askCameraPermissionAndOpenCamera() {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED -> openCamera()
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> showExplanationDialog()
else -> requestCameraPermission()
}
}
private fun showExplanationDialog() {
AlertDialog.Builder(this).apply {
setMessage("On a besoin de la caméra sivouplé ! 🥺")
setPositiveButton("Bon, ok") { _, _ ->
requestCameraPermission()
}
setCancelable(true)
show()
}
}
// convert
private fun convert(uri: Uri) =
MultipartBody.Part.createFormData(
name = "avatar",
filename = "temp.jpeg",
body = contentResolver.openInputStream(uri)!!.readBytes().toRequestBody()
)
private suspend fun handleImage(photoUri: Uri){
userService.updateAvatar(convert(photoUri))
}
// use
private fun openCamera() = takePicture.launch()
// register
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
val tmpFile = File.createTempFile("avatar", "jpeg")
tmpFile.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
}
lifecycleScope.launch { handleImage(tmpFile.toUri()) }
}
/*
// create a temp file and get a uri for it
private val photoUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID +".fileProvider",
File.createTempFile("avatar", "jpeg")
)
// register
private val takePicture =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) lifecycleScope.launch { handleImage(photoUri) }
else Toast.makeText(this, "Si vous refusez, on peux pas prendre de photo ! 😢", Toast.LENGTH_LONG).show()
}
// use
private fun openCamera() = takePicture.launch(photoUri)
// convert
private fun convert(uri: Uri) =
MultipartBody.Part.createFormData(
name = "avatar",
filename = "temp.jpeg",
body = uri.toFile().asRequestBody()
)
// register
private val pickInGallery = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> lifecycleScope.launch { handleImage(uri) } }
// use*/
//private fun uploadFromGallery() = pickInGallery.launch("image/*")
}
\ No newline at end of file
<?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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".userInfo.UserInfoActivity">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/upload_image_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Choisir une Image" />
<Button
android:id="@+id/take_picture_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Prendre une photo" />
</LinearLayout>
\ No newline at end of file
......@@ -6,14 +6,38 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageViewProfilPhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/textViewInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:text="TextView"
app:layout_constraintBottom_toTopOf="@+id/fragment_tasklist_recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageViewProfilPhoto"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.513" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fragment_tasklist_recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/imageViewProfilPhoto" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
......
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
ext.kotlin_version = "1.4.20"
repositories {
google()
jcenter()
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter