Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.7.1'
implementation 'com.squareup.picasso:picasso:2.71828'
}
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package otus.gpb.homework.activities

import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Test

class ProfileEditingTest {
@Test
fun editingPrefillsCurrentProfileAndCancelDoesNotApplyChanges() {
ActivityScenario.launch(EditProfileActivity::class.java).use {
onView(withId(R.id.buttonEditProfile)).perform(click())
onView(withId(R.id.nameEditText)).perform(replaceText("Иван"))
onView(withId(R.id.surnameEditText)).perform(replaceText("Петров"))
onView(withId(R.id.ageEditText)).perform(replaceText("30"), closeSoftKeyboard())
onView(withId(R.id.applyButton)).perform(click())

onView(withId(R.id.buttonEditProfile)).perform(click())
onView(withId(R.id.nameEditText)).check(matches(withText("Иван")))
onView(withId(R.id.surnameEditText)).check(matches(withText("Петров")))
onView(withId(R.id.ageEditText)).check(matches(withText("30")))
onView(withId(R.id.nameEditText)).perform(replaceText("Пётр"), closeSoftKeyboard())
onView(withId(R.id.applyButton)).perform(click())
onView(withId(R.id.textview_name)).check(matches(withText("Пётр")))
onView(withId(R.id.textview_surname)).check(matches(withText("Петров")))
onView(withId(R.id.textview_age)).check(matches(withText("30")))

onView(withId(R.id.buttonEditProfile)).perform(click())
onView(withId(R.id.nameEditText)).check(matches(withText("Пётр")))
onView(withId(R.id.ageEditText)).perform(replaceText("99"), closeSoftKeyboard())
pressBack()
onView(withId(R.id.textview_age)).check(matches(withText("30")))
}
}

@Test
fun formRecreationKeepsDraftInsteadOfResettingToInitialProfile() {
val intent = Intent(ApplicationProvider.getApplicationContext(), FillFormActivity::class.java).apply {
putExtra(ProfileExtras.NAME, "Иван")
putExtra(ProfileExtras.SURNAME, "Петров")
putExtra(ProfileExtras.AGE, "30")
}
ActivityScenario.launch<FillFormActivity>(intent).use { scenario ->
onView(withId(R.id.nameEditText)).perform(replaceText("Анна"))
onView(withId(R.id.surnameEditText)).perform(replaceText("Сидорова"))
onView(withId(R.id.ageEditText)).perform(replaceText("31"), closeSoftKeyboard())
scenario.recreate()
onView(withId(R.id.nameEditText)).check(matches(withText("Анна")))
onView(withId(R.id.surnameEditText)).check(matches(withText("Сидорова")))
onView(withId(R.id.ageEditText)).check(matches(withText("31")))
}
}
}
12 changes: 11 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<queries>
<package android:name="org.telegram.messenger" />
</queries>

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -13,6 +19,10 @@
android:theme="@style/Theme.Activities"
tools:targetApi="31">

<activity
android:name=".FillFormActivity"
android:exported="false" />

<activity
android:name=".EditProfileActivity"
android:exported="true">
Expand All @@ -24,4 +34,4 @@
</activity>
</application>

</manifest>
</manifest>
184 changes: 170 additions & 14 deletions app/src/main/java/otus/gpb/homework/activities/EditProfileActivity.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,71 @@
package otus.gpb.homework.activities

import android.graphics.BitmapFactory
import android.Manifest
import android.content.Intent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.widget.ImageView
import android.provider.Settings
import android.widget.Toast
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import otus.gpb.homework.activities.databinding.ActivityEditProfileBinding

class EditProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityEditProfileBinding
private var selectedPhotoUri: Uri? = null
private var showingCameraPlaceholder = false

private lateinit var imageView: ImageView
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
showCameraPlaceholder()
} else if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
showSettingsDialog()
}
}

private val photoPickerLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let {
selectedPhotoUri = it
populateImage(it)
}
}

private val editProfileLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.let { data ->
binding.textviewName.text = data.getStringExtra(ProfileExtras.NAME).orEmpty()
binding.textviewSurname.text = data.getStringExtra(ProfileExtras.SURNAME).orEmpty()
binding.textviewAge.text = data.getStringExtra(ProfileExtras.AGE).orEmpty()
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit_profile)
imageView = findViewById(R.id.imageview_photo)
binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
selectedPhotoUri = savedInstanceState?.getString(STATE_PHOTO)?.let(Uri::parse)
showingCameraPlaceholder = savedInstanceState?.getBoolean(STATE_CAMERA) ?: false
selectedPhotoUri?.let(::populateImage)
if (showingCameraPlaceholder) binding.imageviewPhoto.setImageResource(R.drawable.cat)
if (savedInstanceState != null) {
binding.textviewName.text = savedInstanceState.getString(ProfileExtras.NAME).orEmpty()
binding.textviewSurname.text = savedInstanceState.getString(ProfileExtras.SURNAME).orEmpty()
binding.textviewAge.text = savedInstanceState.getString(ProfileExtras.AGE).orEmpty()
}

findViewById<Toolbar>(R.id.toolbar).apply {
binding.toolbar.apply {
inflateMenu(R.menu.menu)
setOnMenuItemClickListener {
when (it.itemId) {
Expand All @@ -28,17 +77,124 @@ class EditProfileActivity : AppCompatActivity() {
}
}
}
binding.imageviewPhoto.setOnClickListener { showPhotoActionDialog() }
binding.buttonEditProfile.setOnClickListener {
val intent = Intent(this, FillFormActivity::class.java).apply {
putExtra(ProfileExtras.NAME, binding.textviewName.text.toString())
putExtra(ProfileExtras.SURNAME, binding.textviewSurname.text.toString())
putExtra(ProfileExtras.AGE, binding.textviewAge.text.toString())
}
editProfileLauncher.launch(intent)
}
}

/**
* Используйте этот метод чтобы отобразить картинку полученную из медиатеки в ImageView
*/
private fun populateImage(uri: Uri) {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
imageView.setImageBitmap(bitmap)
showingCameraPlaceholder = false
binding.imageviewPhoto.setImageURI(uri)
}

private fun showCameraPlaceholder() {
showingCameraPlaceholder = true
selectedPhotoUri = null
binding.imageviewPhoto.setImageResource(R.drawable.cat)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(STATE_PHOTO, selectedPhotoUri?.toString())
outState.putBoolean(STATE_CAMERA, showingCameraPlaceholder)
outState.putString(ProfileExtras.NAME, binding.textviewName.text.toString())
outState.putString(ProfileExtras.SURNAME, binding.textviewSurname.text.toString())
outState.putString(ProfileExtras.AGE, binding.textviewAge.text.toString())
}

private fun openSenderApp() {
TODO("В качестве реализации метода отправьте неявный Intent чтобы поделиться профилем. В качестве extras передайте заполненные строки и картинку")
if (selectedPhotoUri == null) {
Toast.makeText(this, R.string.choose_photo_before_sending, Toast.LENGTH_LONG).show()
return
}
val profileDescription = listOf(
binding.textviewName.text,
binding.textviewSurname.text,
binding.textviewAge.text
).joinToString(separator = " ")
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
setPackage(TELEGRAM_PACKAGE)
putExtra(Intent.EXTRA_TEXT, profileDescription)
putExtra(ProfileExtras.NAME, binding.textviewName.text.toString())
putExtra(ProfileExtras.SURNAME, binding.textviewSurname.text.toString())
putExtra(ProfileExtras.AGE, binding.textviewAge.text.toString())
selectedPhotoUri?.let { uri ->
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri("Profile photo", uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
val target = intent.resolveActivity(packageManager)
if (target == null) {
Toast.makeText(this, R.string.telegram_unavailable, Toast.LENGTH_LONG).show()
return
}
intent.component = target
try {
startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(this, R.string.telegram_unavailable, Toast.LENGTH_LONG).show()
}
}

private fun showPhotoActionDialog() {
MaterialAlertDialogBuilder(this)
.setItems(arrayOf(getString(R.string.take_photo), getString(R.string.select_photo))) { _, index ->
when (index) {
TAKE_PHOTO_INDEX -> requestCameraPermission()
SELECT_PHOTO_INDEX -> photoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
.show()
}

private fun requestCameraPermission() {
when {
checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
showCameraPlaceholder()
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> showRationaleDialog()
else -> cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}

private fun showRationaleDialog() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.camera_rationale)
.setPositiveButton(R.string.grant_access) { _, _ ->
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
.setNegativeButton(R.string.cancel, null)
.show()
}

private fun showSettingsDialog() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.camera_settings_message)
.setPositiveButton(R.string.open_settings) { _, _ ->
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null)
)
startActivity(intent)
}
.show()
}

private companion object {
const val STATE_PHOTO = "selected_photo"
const val STATE_CAMERA = "camera_placeholder"
const val TAKE_PHOTO_INDEX = 0
const val SELECT_PHOTO_INDEX = 1
const val TELEGRAM_PACKAGE = "org.telegram.messenger"
}
}
}
33 changes: 33 additions & 0 deletions app/src/main/java/otus/gpb/homework/activities/FillFormActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package otus.gpb.homework.activities

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import otus.gpb.homework.activities.databinding.ActivityFillFormBinding

class FillFormActivity : AppCompatActivity() {
private lateinit var binding: ActivityFillFormBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFillFormBinding.inflate(layoutInflater)
setContentView(binding.root)

if (savedInstanceState == null) {
binding.nameEditText.setText(intent.getStringExtra(ProfileExtras.NAME).orEmpty())
binding.surnameEditText.setText(intent.getStringExtra(ProfileExtras.SURNAME).orEmpty())
binding.ageEditText.setText(intent.getStringExtra(ProfileExtras.AGE).orEmpty())
}

binding.applyButton.setOnClickListener {
val result = Intent().apply {
putExtra(ProfileExtras.NAME, binding.nameEditText.text.toString())
putExtra(ProfileExtras.SURNAME, binding.surnameEditText.text.toString())
putExtra(ProfileExtras.AGE, binding.ageEditText.text.toString())
}
setResult(Activity.RESULT_OK, result)
finish()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package otus.gpb.homework.activities

object ProfileExtras {
const val NAME = "name"
const val SURNAME = "surname"
const val AGE = "age"
}
12 changes: 6 additions & 6 deletions app/src/main/res/layout/activity_edit_profile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:hint="Имя"
android:textSize="18dp"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@+id/imageview_photo"
app:layout_constraintTop_toTopOf="@+id/imageview_photo" />

Expand All @@ -46,7 +46,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:hint="Фамилия"
android:textSize="18dp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@+id/textview_name"
app:layout_constraintTop_toBottomOf="@+id/textview_name" />

Expand All @@ -56,18 +56,18 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:hint="Возраст"
android:textSize="18dp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@+id/textview_surname"
app:layout_constraintTop_toBottomOf="@+id/textview_surname" />

<Button
android:id="@+id/button4"
android:id="@+id/buttonEditProfile"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Редактировать профиль"
android:text="@string/edit_profile"
app:layout_constraintStart_toStartOf="@+id/textview_age"
app:layout_constraintTop_toBottomOf="@+id/textview_age" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Loading