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 }