[Android Studio] 저작권 무료 이미지 검색기 앱

jeunguri·2022년 4월 25일
0

토이 프로젝트

목록 보기
2/8
post-thumbnail


앱 소개


기능

  • Unsplash API를 활용하여 사진을 가져온다.
  • 검색한 사진 다운받기
  • 다운 받은 사진 배경화면으로 설정
  • 로딩할 때 Loading Shimmer 효과

활용 기술

  • Retrofit2
  • Coroutine
  • Glide
  • ShimmerLayout
  • WallpaperManager



사진 정보 불러오기


https://unsplash.com/developers 에서 API 사용 신청

의존성 추가

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.8.0'

access key와 baseUrl 정의

Authentication, Schema 참고

gradle.properties

UNSPLASH_ACCESS_KEY="access_key"

앱수준 build.gradle

android {
    defaultConfig {
        buildConfigField "String", "UNSPLASH_ACCESS_KEY", project.properties["UNSPLASH_ACCESS_KEY"]
    }

data/Url

object Url {
    const val UNSPLASH_BASE_URL = "https://api.unsplash.com/"
}


사진과 관련된 정보 가져오기 위해 네트워크 모델 추가

Get a random photo 참고

models 패키지에 kotlin data class file from JSON 통해서 네트워크 모델을 추가해주면 아래의 이미지처럼 파일들이 생성된다.


서비스 정의

data/UnsplashApiService

interface UnsplashApiService {

    @GET(
        "photos/random?" +
                "client_id=${BuildConfig.UNSPLASH_ACCESS_KEY}" +
                "&count=30"
    )

    suspend fun getRandomPhotos(
        @Query("query") query: String?
    ): Response<List<PhotoResponse>>
}

Repository 정의

data/Repository

object Repository {

	// 레트로핏으로 생성
    private val unsplashApiService: UnsplashApiService by lazy {
        Retrofit.Builder()
            .baseUrl(Url.UNSPLASH_BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(buildOkHttpClient())
            .build()
            .create()
    }
	// 실제로 호출할 수 있는 호출부 구현
    suspend fun getRandomPhotos(query: String?): List<PhotoResponse>? =
        unsplashApiService.getRandomPhotos(query).body()

	// 로깅 찍기 위해 OkHttpClient 추가
    private fun buildOkHttpClient(): OkHttpClient =
        OkHttpClient.Builder()
            .addInterceptor(
                HttpLoggingInterceptor().apply {
                    level = if (BuildConfig.DEBUG) {
                        HttpLoggingInterceptor.Level.BODY
                    } else {
                        HttpLoggingInterceptor.Level.NONE
                    }
                }
            )
            .build()
}

MainActivity에서 API 호출

API를 호출하기 위해서는 INTERNET 퍼미션이 필요

<uses-permission android:name="android.permission.INTERNET"/>

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private val scope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        fetchRandomPhotos()
    }

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }

    private fun fetchRandomPhotos(query: String? = null) = scope.launch {
        Repository.getRandomPhotos(query)?.let { photos ->
            photos
        }
    }
}


사진 보여주기


의존성 추가

// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

// SwipeRefreshLayout
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

레이아웃 구성

layout/item_photo.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingHorizontal="12dp"
    android:paddingVertical="6dp">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="8dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/contentsContainer"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            tools:layout_height="500dp">

            <ImageView
                android:id="@+id/photoImageView"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1.0"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.0"
                tools:background="@color/teal_200"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/profileImageView"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_marginStart="12dp"
                android:layout_marginBottom="12dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                tools:background="@color/teal_700"
                tools:ignore="ContentDescription" />

            <TextView
                android:id="@+id/authorTextView"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="6dp"
                android:layout_marginEnd="12dp"
                android:ellipsize="end"
                android:maxLines="1"
                android:shadowColor="#60000000"
                android:shadowDx="1"
                android:shadowDy="1"
                android:shadowRadius="5"
                android:textColor="@color/white"
                app:layout_constraintBottom_toTopOf="@id/descriptionTextView"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/profileImageView"
                app:layout_constraintTop_toTopOf="@id/profileImageView"
                app:layout_constraintVertical_chainStyle="packed"
                tools:text="Author" />

            <TextView
                android:id="@+id/descriptionTextView"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:alpha="0.8"
                android:ellipsize="end"
                android:maxLines="1"
                android:shadowColor="#60000000"
                android:shadowDx="1"
                android:shadowDy="1"
                android:shadowRadius="5"
                android:textColor="@color/white"
                android:textSize="12sp"
                android:visibility="gone"
                app:layout_constraintBottom_toBottomOf="@id/profileImageView"
                app:layout_constraintEnd_toEndOf="@id/authorTextView"
                app:layout_constraintStart_toStartOf="@id/authorTextView"
                app:layout_constraintTop_toBottomOf="@id/authorTextView"
                tools:text="Description" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</FrameLayout>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
    	android:id="@+id/searchEditText"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:background="@color/white"
        android:drawableStart="@drawable/ic_baseline_search_24"
        android:drawablePadding="6dp"
        android:elevation="8dp"
        android:hint="@string/search_image"
        android:imeOptions="actionSearch"
        android:importantForAutofill="no"
        android:inputType="text"
        android:paddingHorizontal="12dp"
        android:textSize="14sp" />

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:paddingVertical="6dp"
            tools:listitem="@layout/item_photo" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>


drawable/shape_profile_placeholder.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#FFDDDDDD" />
</shape>


Adapter 구현


PhotoAdapter

class PhotoAdapter : RecyclerView.Adapter<PhotoAdapter.ViewHolder>() {

    var photos: List<PhotoResponse> = emptyList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(
            ItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(photos[position])
    }

    override fun getItemCount(): Int = photos.size

    inner class ViewHolder(
        private val binding: ItemPhotoBinding
    ): RecyclerView.ViewHolder(binding.root) {

        fun bind(photo: PhotoResponse) {
            val dimensionRatio = photo.height / photo.width.toFloat()
            val targetWidth = binding.root.resources.displayMetrics.widthPixels -
                    (binding.root.paddingStart + binding.root.paddingEnd)
            val targetHeight = (targetWidth * dimensionRatio).toInt()

            binding.contentsContainer.layoutParams =
                binding.contentsContainer.layoutParams.apply {
                    height = targetHeight
                }

            Glide.with(binding.root)
                .load(photo.urls?.regular)
                .thumbnail(
                    Glide.with(binding.root)
                        .load(photo.urls?.thumb)
                        .transition(DrawableTransitionOptions.withCrossFade())
                )
                .override(targetWidth, targetHeight)
                .into(binding.photoImageView)

            Glide.with(binding.root)
                .load(photo.user?.profileImageUrls?.small)
                .placeholder(R.drawable.shape_profile_placeholder)
                .transition(DrawableTransitionOptions.withCrossFade())
                .circleCrop()
                .into(binding.profileImageView)

            if (photo.user?.name.isNullOrBlank()) {
                binding.authorTextView.visibility = View.GONE
            } else {
                binding.authorTextView.visibility = View.VISIBLE
                binding.authorTextView.text = photo.user?.name
            }
            if (photo.description.isNullOrBlank()) {
                binding.descriptionTextView.visibility = View.GONE
            } else {
                binding.descriptionTextView.visibility = View.VISIBLE
                binding.descriptionTextView.text = photo.description
            }
        }
    }
}

Thumbnail requests : 인터넷 속도에 따라 고화질의 이미지를 불러올 때 오래 걸리는 경우가 있기 때문에 해상도가 조금 떨어지는 이미지를 먼저 불러와 보여준 후, 고화질의 이미지가 완전히 로딩되면 교체해주는 식의 api를 말함.

MainActivity에 연동

MainActivity

private fun initViews() {
        binding.recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        binding.recyclerView.adapter = PhotoAdapter()
    }

   
@SuppressLint("NotifyDataSetChanged")
private fun fetchRandomPhotos(query: String? = null) = scope.launch {
        Repository.getRandomPhotos(query)?.let { photos ->
            (binding.recyclerView.adapter as? PhotoAdapter)?.apply {
                this.photos = photos
                notifyDataSetChanged()
            }
        }
    }

이미지 갱신 및 새로운 이미지 검색해서 갱신

MainActivity

private fun bindView() {
        binding.searchEditText.setOnEditorActionListener { editText, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) { // SEARCH 버튼 눌렀을때만 처리
                currentFocus?.let { view ->
                    // search 가 끝나면 키보드 사라지게 함
                    val inputMethodManager =
                        getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
                    inputMethodManager?.hideSoftInputFromWindow(view.windowToken, 0)

                    view.clearFocus() // search 가 끝나면 커셔 깜빡거림 사라지게 함
                }
                // editText 의 값을 전달, fetch 하고 데이터가 변경되면서 갱신됨
                fetchRandomPhotos(editText.text.toString())
            }
            true
        }
        binding.refreshLayout.setOnRefreshListener {
            // 검색한 데이터에 대해서 refresh 하면, 검색한 데이터에 대한 것만 갱신하게끔
            fetchRandomPhotos(binding.searchEditText.text.toString())
        }
    }

    @SuppressLint("NotifyDataSetChanged")
    private fun fetchRandomPhotos(query: String? = null) = scope.launch {
        Repository.getRandomPhotos(query)?.let { photos ->
            (binding.recyclerView.adapter as? PhotoAdapter)?.apply {
                this.photos = photos
                notifyDataSetChanged()
            }

            // refresh 의 경우, fetch 가 끝나면 숨겨줘야 함
            binding.refreshLayout.isRefreshing = false
        }
    }



Loading Shimmer 보여주기, 에러처리


facebook/shimmer-android 라이브러리 추가

dependencies {
  implementation 'com.facebook.shimmer:shimmer:0.5.0'
}

activity_main.xml

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <androidx.recyclerview.widget.RecyclerView/>

            <TextView/>

            <com.facebook.shimmer.ShimmerFrameLayout
                android:id="@+id/shimmerLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">

                    <include layout="@layout/view_shimmer_item_photo" />

                    <include layout="@layout/view_shimmer_item_photo" />

                </LinearLayout>

            </com.facebook.shimmer.ShimmerFrameLayout>

        </FrameLayout>

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

FrameLayout으로 감싸주고 ShimmerLayout과 LinearLayout을 추가해주고 include로 레이아웃을 추가해준다.

layout/view_shimmer_item_photo.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingHorizontal="12dp"
    android:paddingVertical="6dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/contentsContainer"
        android:layout_width="match_parent"
        android:layout_height="400dp">

        <View
            android:id="@+id/photoImageView"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:background="#FFEEEEEE"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />

        <View
            android:id="@+id/profileImageView"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="12dp"
            android:layout_marginBottom="12dp"
            android:background="@drawable/shape_profile_placeholder"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <View
            android:id="@+id/authorTextView"
            android:layout_width="0dp"
            android:layout_height="10dp"
            android:layout_marginStart="6dp"
            android:layout_marginEnd="12dp"
            android:background="#FFDDDDDD"
            app:layout_constraintBottom_toTopOf="@id/descriptionTextView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/profileImageView"
            app:layout_constraintTop_toTopOf="@id/profileImageView"
            app:layout_constraintVertical_chainStyle="packed" />

        <View
            android:id="@+id/descriptionTextView"
            android:layout_width="0dp"
            android:layout_height="10dp"
            android:layout_marginTop="5dp"
            android:background="#FFDDDDDD"
            app:layout_constraintBottom_toBottomOf="@id/profileImageView"
            app:layout_constraintEnd_toEndOf="@id/authorTextView"
            app:layout_constraintStart_toStartOf="@id/authorTextView"
            app:layout_constraintTop_toBottomOf="@id/authorTextView" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

MainActivity

private fun fetchRandomPhotos(query: String? = null) = scope.launch {
        Repository.getRandomPhotos(query)?.let { photos ->
            (binding.recyclerView.adapter as? PhotoAdapter)?.apply {
                this.photos = photos
                notifyDataSetChanged()
            }
            binding.recyclerView.visibility = View.VISIBLE
            binding.shimmerLayout.visibility = View.GONE
            binding.refreshLayout.isRefreshing = false
        }
    }

fetch가 끝나면, GONE처리 되어있던 recyclerView를 VISIBLE로 바꿔주고, shimmerLayout은 GONE처리해준다.

에러 처리

MainActivity

private fun fetchRandomPhotos(query: String? = null) = scope.launch {
        try {
            Repository.getRandomPhotos(query)?.let { photos ->

                binding.errorDescriptionTextView.visibility = View.GONE
                (binding.recyclerView.adapter as? PhotoAdapter)?.apply {
                    this.photos = photos
                    notifyDataSetChanged()
                }
            }
            binding.recyclerView.visibility = View.VISIBLE
        } catch (exception: Exception) {
            binding.recyclerView.visibility = View.INVISIBLE
            binding.errorDescriptionTextView.visibility = View.VISIBLE
        } finally {
            binding.shimmerLayout.visibility = View.GONE
            binding.refreshLayout.isRefreshing = false
        }
    }



사진 저장하기


PhotoAdapter

class PhotoAdapter : RecyclerView.Adapter<PhotoAdapter.ViewHolder>() {

    var onClickPhoto: (PhotoResponse) -> Unit = {}
    
// 내부클래스에 inner 붙여주면 상위 클래스에서 정의한 프로퍼티 접근 가능
inner class ViewHolder(
        private val binding: ItemPhotoBinding
    ): RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                // 아이템 클릭 시, 클릭 당시 adapterPosition 에 있는 photo 를 이벤트로 전달
                onClickPhoto(photos[adapterPosition])
            }
        }

MainActivity

private fun bindView() {
	(binding.recyclerView.adapter as? PhotoAdapter)?.onClickPhoto = { photo ->
            showDownloadPhotoConfirmationDialog(photo)
        }
 }

이미지 저장 전 한번 더 확인해주는 Dialog 구현

private fun showDownloadPhotoConfirmationDialog(photo: PhotoResponse) {
        AlertDialog.Builder(this)
            .setMessage("이 사진을 저장하시겠습니까?")
            .setPositiveButton("저장") { dialog, _ ->
                downloadPhoto(photo.urls?.full)
                dialog.dismiss()
            }
            .setNegativeButton("취소") { dialog, _ ->
                dialog.dismiss()
            }
            .create()
            .show()
    }

이미지 다운로드 관련

private fun downloadPhoto(photoUrl: String?) {
        photoUrl ?: return

        Glide.with(this)
            .asBitmap()
            .load(photoUrl)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .into(
                object : CustomTarget<Bitmap>(SIZE_ORIGINAL, SIZE_ORIGINAL) {
                    @RequiresApi(Build.VERSION_CODES.M)
                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition<in Bitmap>?
                    ) { // 다운로드가 다 끝난 상태
                        saveBitmapToMediaStore(resource)

                        val wallpaperManager = WallpaperManager.getInstance(this@MainActivity)

                        val snackbar = Snackbar.make(
                            binding.root,
                            "다운로드 완료",
                            Snackbar.LENGTH_SHORT
                        )

                        if (wallpaperManager.isWallpaperSupported
                            && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                                    && wallpaperManager.isSetWallpaperAllowed)
                        ) {
                            snackbar.setAction("배경화면으로 저장") {
                                try {
                                    wallpaperManager.setBitmap(resource)
                                } catch (exception: Exception) {
                                    Snackbar.make(binding.root, "배경화면 저장 실패", Snackbar.LENGTH_SHORT)
                                }
                            }
                            snackbar.duration = Snackbar.LENGTH_INDEFINITE
                        }
                        snackbar.show()
                    }

                    override fun onLoadStarted(placeholder: Drawable?) {
                        super.onLoadStarted(placeholder)
                        Snackbar.make(
                            binding.root,
                            "다운로드 중...",
                            Snackbar.LENGTH_INDEFINITE
                        ).show()
                    }

                    // 불러오는게 취소되어 리소스가 해제되었을 때
                    override fun onLoadCleared(placeholder: Drawable?) {}

                    override fun onLoadFailed(errorDrawable: Drawable?) {
                        super.onLoadFailed(errorDrawable)
                        Snackbar.make(
                            binding.root,
                            "다운로드 실패",
                            Snackbar.LENGTH_SHORT
                        ).show()
                    }
                }
            )
    }

이미지 저장

private fun saveBitmapToMediaStore(bitmap: Bitmap) {
        val fileName = "${System.currentTimeMillis()}.jpg"
        val resolver = applicationContext.contentResolver
        val imageCollectionUri =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                MediaStore.Images.Media.getContentUri(
                    MediaStore.VOLUME_EXTERNAL_PRIMARY
                )
            } else {
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            }
        val imageDetails = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                // 이미지 저장하는 동안은(0으로 바뀌기 전까진) 이미지 파일에 접근하지 못하도록
                put(MediaStore.Images.Media.IS_PENDING, 1)
            }
        }
        // 실제로 이미지파일을 저장할 uri 를 가져와야 함
        val imageUri = resolver.insert(imageCollectionUri, imageDetails)

        imageUri ?: return // imageUri 가 null 일 경우 저장하지 않고 리턴

        resolver.openOutputStream(imageUri).use { outputStream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            imageDetails.clear()
            // 이미지가 저장되었으면 1이었던 팬딩을 다시 0으로 바꿔줌
            imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
            // imageUri 를 imageDetails 로 업데이터
            resolver.update(imageUri, imageDetails, null, null)
        }
    }

이렇게 하면 안드로이드 10버전 이상에서는 이미지가 잘 저장되지만, 그 아래 28이하부터는 권한 요청을 하지 않았기 때문에 이미지 저장에 있어 에러가 발생한다. 이부분을 추가로 권한 요청 해줘야 한다.

AndroidManifest.xml

<uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"
        tools:ignore="ScopedStorage" />

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            fetchRandomPhotos()
        } else {
            requestWriteStoragePermission()
        }

    }
    
private fun requestWriteStoragePermission() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION
        )
    } 
    
override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        val writeExternalStoragePermissionGranted =
            requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED
        if (writeExternalStoragePermissionGranted) {
            fetchRandomPhotos()
        }
    }



다운받은 사진 배경화면으로 설정


이미지를 배경화면을 지정하기 위해 WallpaperManager를 사용했다.

"다운로드 완료" snackBar를 OnResourceReady() {} 로 옮기고, wallpaperManager를 정의해준다.

MainActivity

override fun onResourceReady(
     resource: Bitmap,
     transition: Transition<in Bitmap>?
) { // 다운로드가 다 끝난 상태
    saveBitmapToMediaStore(resource)

    val wallpaperManager = WallpaperManager.getInstance(this@MainActivity)

    val snackbar = Snackbar.make(
                   binding.root,
                   "다운로드 완료",
                   Snackbar.LENGTH_SHORT
    )

    if (wallpaperManager.isWallpaperSupported
             && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                     && wallpaperManager.isSetWallpaperAllowed)
       ) {
           snackbar.setAction("배경화면으로 저장") {
           try {
              wallpaperManager.setBitmap(resource)
           } catch (exception: Exception) {
               Snackbar.make(binding.root, "배경화면 저장 실패", Snackbar.LENGTH_SHORT)
           }
     }
     snackbar.duration = Snackbar.LENGTH_INDEFINITE
  }
  snackbar.show()
}

wallpaper를 set하려고 시도하다 에러 발생시 IOException이 뜨기 때문에 try-catch로 감싸주었다.

0개의 댓글