1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.DeviceAsWebcam.view
18 
19 import android.app.AlertDialog
20 import android.app.Dialog
21 import android.hardware.camera2.CameraCharacteristics
22 import android.os.Bundle
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.ImageView
27 import android.widget.RadioButton
28 import android.widget.TextView
29 import androidx.fragment.app.DialogFragment
30 import androidx.recyclerview.widget.LinearLayoutManager
31 import androidx.recyclerview.widget.RecyclerView
32 import com.android.DeviceAsWebcam.CameraCategory
33 import com.android.DeviceAsWebcam.CameraId
34 import com.android.DeviceAsWebcam.CameraInfo
35 import com.android.DeviceAsWebcam.R
36 import java.util.function.Consumer
37 
38 /**
39  * Class to create an AlertDialog for the camera picker. This class handles the AlertDialog's
40  * lifecycle.
41  */
42 class CameraPickerDialog(private val mOnItemSelected: Consumer<CameraId>) : DialogFragment() {
43     // Map from lens facing to ListItems
44     private var mDialogItems: Map<Int, List<ListItem>> = mapOf()
45 
46     // List Adapter to be used by the Dialog's Recycler View. This lives as long
47     // as the activity so we don't re-populate the data every time the dialog is created.
48     private val mCameraPickerListAdapter: CameraPickerListAdapter =
49         CameraPickerListAdapter(listOf(), mOnItemSelected)
50 
51     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
52         val dialog = AlertDialog.Builder(activity!!).setCancelable(true).create()
53 
54         dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
55         val customView =
56             dialog.layoutInflater.inflate(R.layout.camera_picker_dialog, /*root=*/ null)
57 
58         val containerView: View = customView.findViewById(R.id.selector_container_view)!!
59         containerView.background.alpha = (0.75 * 0xFF).toInt()
60 
61         val recyclerView: RecyclerView = customView.findViewById(R.id.camera_selector_view)!!
62         recyclerView.layoutManager = LinearLayoutManager(dialog.context)
63         recyclerView.adapter = mCameraPickerListAdapter
64         dialog.setView(customView)
65         return dialog
66     }
67 
68     /**
69      * Updates the dialog's backing data structures with a new list of available cameras.
70      * This will clobber any previous state so callers should send the full list every time.
71      */
72     fun updateAvailableCameras(availableCameras: List<ListItem>, selectedCameraId: CameraId) {
73         // create a map by grouping the entries by their lensFacing values.
74         mDialogItems = availableCameras.groupBy({ it.lensFacing }, { it })
75         updateSelectedCamera(selectedCameraId)
76     }
77 
78     /**
79      * Updates the currently selected camera and updates the UI if necessary
80      */
81     fun updateSelectedCamera(selectedCameraId: CameraId) {
82         mDialogItems.forEach { entry ->
83             entry.value.find { it.isSelected }?.isSelected = false
84             entry.value.find { it.cameraId == selectedCameraId }?.isSelected = true
85         }
86 
87         updateListAdapter()
88     }
89 
90     /**
91      * Re-generates the information required by {@link CameraPickerListAdapter} from
92      * {@link #mDialogItems}, invalidating the recycler view if necessary.
93      */
94     private fun updateListAdapter() {
95         val lensFacingOrder =
96             listOf(CameraCharacteristics.LENS_FACING_BACK, CameraCharacteristics.LENS_FACING_FRONT)
97 
98         val dialogViewListItems: MutableList<CameraPickerListAdapter.ViewListItem> = mutableListOf()
99         for (lensFacing in lensFacingOrder) {
100             if (!mDialogItems.containsKey(lensFacing) || mDialogItems[lensFacing]!!.isEmpty()) {
101                 continue
102             }
103 
104             dialogViewListItems.add(getHeaderViewListItem(lensFacing))
105             mDialogItems[lensFacing]!!.stream().map(::getCameraViewListItem)
106                 .forEach(dialogViewListItems::add)
107         }
108         mCameraPickerListAdapter.updateBackingData(dialogViewListItems)
109     }
110 
111     /**
112      * Utility function to create a {@link ViewListItem} of type {@link ViewListItem.Type.HEADING}
113      * for a given {@code lensFacing}
114      */
115     private fun getHeaderViewListItem(lensFacing: Int): CameraPickerListAdapter.ViewListItem {
116         val drawable = when (lensFacing) {
117             CameraCharacteristics.LENS_FACING_BACK -> R.drawable.ic_camera_rear
118             else -> R.drawable.ic_camera_front
119         }
120         val text = when (lensFacing) {
121             CameraCharacteristics.LENS_FACING_BACK -> R.string.list_item_text_back_camera
122             else -> R.string.list_item_text_front_camera
123         }
124 
125         return CameraPickerListAdapter.ViewListItem(
126             type = CameraPickerListAdapter.ViewListItem.Type.HEADING,
127             cameraId = null,
128             textResource = text,
129             drawable = drawable,
130             selected = false
131         )
132     }
133 
134     /**
135      * Utility function to create the corresponding {@link ViewListItem} from the given
136      * {@link ListItem}
137      */
138     private fun getCameraViewListItem(listItem: ListItem): CameraPickerListAdapter.ViewListItem {
139         val text: Int
140         val drawable: Int
141         when (listItem.cameraCategory) {
142             CameraCategory.UNKNOWN -> {
143                 text = R.string.list_item_text_unknown_camera
144                 drawable = R.drawable.ic_camera_category_unknown
145             }
146 
147             CameraCategory.STANDARD -> {
148                 text = R.string.list_item_text_standard_camera
149                 drawable = R.drawable.ic_camera_category_standard
150             }
151 
152             CameraCategory.WIDE_ANGLE -> {
153                 text = R.string.list_item_text_wide_angle_camera
154                 drawable = R.drawable.ic_camera_category_wide_angle
155             }
156 
157             CameraCategory.ULTRA_WIDE -> {
158                 text = R.string.list_item_text_ultra_wide_camera
159                 drawable = R.drawable.ic_camera_category_wide_angle
160             }
161 
162             CameraCategory.TELEPHOTO -> {
163                 text = R.string.list_item_text_telephoto_camera
164                 drawable = R.drawable.ic_camera_category_telephoto
165             }
166 
167             CameraCategory.OTHER -> {
168                 text = R.string.list_item_text_other_camera
169                 drawable = R.drawable.ic_camera_category_other
170             }
171         }
172 
173         return CameraPickerListAdapter.ViewListItem(
174             type = CameraPickerListAdapter.ViewListItem.Type.ELEMENT,
175             cameraId = listItem.cameraId,
176             textResource = text,
177             drawable = drawable,
178             selected = listItem.isSelected
179         )
180     }
181 
182     /**
183      * Internal class to manage the AlertDialog's RecyclerView that acts as the Camera Picker.
184      */
185     private class CameraPickerListAdapter(
186         private var mViewListItems: List<ViewListItem>,
187         private val mOnItemSelectedListener: Consumer<CameraId>
188     ) : RecyclerView.Adapter<CameraPickerListAdapter.CameraPickerViewHolder>() {
189 
190         override fun onCreateViewHolder(
191             viewgroup: ViewGroup, viewType: Int
192         ): CameraPickerViewHolder {
193             val holderType = ViewListItem.Type.entries[viewType]
194             return CameraPickerViewHolder.getCameraPickerViewHolder(viewgroup, holderType)
195         }
196 
197         override fun getItemCount(): Int {
198             return mViewListItems.size
199         }
200 
201         override fun onBindViewHolder(viewHolder: CameraPickerViewHolder, position: Int) {
202             val item = mViewListItems[position]
203             viewHolder.setupViewHolder(item)
204 
205             if (item.type == ViewListItem.Type.ELEMENT) {
206                 viewHolder.mView.setOnClickListener {
207                     mOnItemSelectedListener.accept(item.cameraId!!)
208                 }
209             } else {
210                 viewHolder.mView.setOnClickListener(null)
211             }
212         }
213 
214         override fun onBindViewHolder(
215             holder: CameraPickerViewHolder, position: Int, payloads: MutableList<Any>
216         ) {
217             if (payloads.isEmpty()) {
218                 return onBindViewHolder(holder, position)
219             }
220 
221             // notifyItemChange is called with "true" payload if the diff between state is the
222             // "selected" value only;  otherwise the payload is false.
223 
224             // If the payload here contains any "false", then we don't know what all has changed,
225             // so defer to the base onBindViewHolder which re-inits the view
226             if (payloads.contains(false)) {
227                 return onBindViewHolder(holder, position)
228             }
229 
230             // Only "true" in payloads, just update the isChecked value of the view
231             holder.mRadioButton?.isChecked = mViewListItems[position].selected
232         }
233 
234         override fun getItemViewType(position: Int): Int {
235             return mViewListItems[position].type.ordinal
236         }
237 
238         fun updateBackingData(newData: List<ViewListItem>) {
239             if (mViewListItems.size != newData.size) {
240                 mViewListItems = newData
241                 notifyDataSetChanged()
242             }
243 
244             // Contains the index at which the item has changes, and if the only change
245             // at the position is the ViewListItem.selected value. This would happen when
246             // updateBackingData is called because the user picked a new camera to stream
247             // from.
248             val modifiedPositions: MutableList<Pair<Int, Boolean>> = mutableListOf()
249 
250             mViewListItems.zip(newData).forEachIndexed { idx, (currentItem, newItem) ->
251                 if (currentItem != newItem) {
252                     modifiedPositions.add(
253                         Pair(
254                             idx, ViewListItem.onlySelectedChanged(currentItem, newItem)
255                         )
256                     )
257                 }
258             }
259             mViewListItems = newData
260             modifiedPositions.forEach { (modifiedPos, onlySelectedChanged) ->
261                 notifyItemChanged(modifiedPos, /*payload=*/ onlySelectedChanged)
262             }
263         }
264 
265         /**
266          * Private class to mux header list items and the camera list items.
267          *
268          * {@link CameraPickerListAdapter} delegates to this class to populate and set up the
269          * actual view in the RecyclerView.
270          */
271         private class CameraPickerViewHolder private constructor(val mView: View) :
272             RecyclerView.ViewHolder(mView) {
273             companion object {
274                 /**
275                  * Static helper to construct an instance of this class for different
276                  * {@link ViewListItem}s.
277                  */
278                 fun getCameraPickerViewHolder(
279                     viewGroup: ViewGroup, type: ViewListItem.Type
280                 ): CameraPickerViewHolder {
281                     val layoutId = when (type) {
282                         ViewListItem.Type.HEADING -> R.layout.list_item_header
283                         ViewListItem.Type.ELEMENT -> R.layout.list_item_camera
284                     }
285 
286                     val view = LayoutInflater.from(viewGroup.context)
287                         .inflate(layoutId, viewGroup, /*attachToRoot=*/ false)
288                     return CameraPickerViewHolder(view)
289                 }
290             }
291 
292             private val mTextView: TextView = mView.findViewById(R.id.text_view)!!
293             private val mImageView: ImageView = mView.findViewById(R.id.image_view)!!
294             val mRadioButton: RadioButton? = mView.findViewById(R.id.radio_button)
295 
296             /**
297              * Method to set up the text views and drawables for the given ViewHolder.
298              *
299              * {@code item} is the {@link ViewListItem} that this View is displaying.
300              * {@code item.type} must match the type that was passed in
301              * {@link #getCameraPickerViewHolder}.
302              */
303             fun setupViewHolder(item: ViewListItem) {
304                 when (item.type) {
305                     ViewListItem.Type.HEADING -> setupHeader(item)
306                     ViewListItem.Type.ELEMENT -> setupElement(item)
307                 }
308             }
309 
310             fun setupHeader(item: ViewListItem) {
311                 mTextView.setText(item.textResource)
312                 mImageView.setImageResource(item.drawable)
313             }
314 
315             fun setupElement(item: ViewListItem) {
316                 mTextView.setText(item.textResource)
317                 mImageView.setImageResource(item.drawable)
318                 mRadioButton!!.isChecked = item.selected
319             }
320         }
321 
322         /**
323          * Internal representation used by the {@link CameraPickerListAdapter}. This closely tracks
324          * the information needed by the RecyclerView and Adapter to display the information on
325          * screen.
326          */
327         data class ViewListItem(
328             val type: Type,
329             val cameraId: CameraId?,
330             val textResource: Int,
331             val drawable: Int,
332             val selected: Boolean
333         ) {
334             enum class Type {
335                 HEADING, ELEMENT
336             }
337 
338             companion object {
339                 /**
340                  * returns true if the only change between two objects is their "selected" value
341                  */
342                 fun onlySelectedChanged(o1: ViewListItem, o2: ViewListItem): Boolean {
343                     return o1.type == o2.type
344                             && o1.cameraId == o2.cameraId
345                             && o1.textResource == o2.textResource
346                             && o1.drawable == o2.drawable
347                             && o1.selected != o2.selected
348                 }
349             }
350         }
351     }
352 
353     /**
354      * Utility class used to track the current state of Dialog.
355      */
356     data class ListItem(
357         val lensFacing: Int,
358         val cameraId: CameraId,
359         val cameraCategory: CameraCategory,
360         var isSelected: Boolean
361     ) {
362         constructor(cameraInfo: CameraInfo) : this(
363             cameraInfo.lensFacing,
364             cameraInfo.cameraId,
365             cameraInfo.cameraCategory,
366             isSelected = false
367         )
368     }
369 
370 
371 }