1 /*
2 * Copyright (C) 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 package com.android.providers.media.tools.photopickerv2.photopicker
17 
18 import android.annotation.SuppressLint
19 import android.app.Application
20 import android.content.ActivityNotFoundException
21 import android.content.Intent
22 import android.net.Uri
23 import android.provider.MediaStore
24 import android.widget.Toast
25 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.isPhotoPickerAvailable
26 import androidx.lifecycle.AndroidViewModel
27 import com.android.providers.media.tools.photopickerv2.utils.LaunchLocation
28 import kotlinx.coroutines.flow.MutableStateFlow
29 import kotlinx.coroutines.flow.StateFlow
30 
31 /**
32  * PhotoPickerViewModel is responsible for managing the state and logic
33  * of the PhotoPicker feature.
34  */
35 @SuppressLint("NewApi")
36 class PhotoPickerViewModel(
37     application: Application,
38 ) : AndroidViewModel(application) {
39 
40     private val _selectedMedia = MutableStateFlow<List<Uri>>(emptyList())
41     val selectedMedia: StateFlow<List<Uri>> = _selectedMedia
42 
43     private val _pickImagesMaxSelectionLimit: Int
44 
45     init{
46         // If the PhotoPicker is available on the device, getPickImagesMaxLimit is there but not
47         // always visible on the SDK (only from Android 13+)
48         _pickImagesMaxSelectionLimit = if (isPhotoPickerAvailable(application)){
49             val maxLimit = MediaStore.getPickImagesMaxLimit()
50             if (maxLimit > 0) maxLimit else Int.MAX_VALUE
51         } else {
52             Int.MAX_VALUE
53         }
54     }
55 
updateSelectedMediaListnull56     fun updateSelectedMediaList(uris: List<Uri>) {
57         _selectedMedia.value = uris
58     }
59 
validateAndLaunchPickernull60     fun validateAndLaunchPicker(
61         isActionGetContentSelected: Boolean,
62         allowMultiple: Boolean,
63         maxMediaItemsDisplayed: Int,
64         selectedMimeType: String,
65         allowCustomMimeType: Boolean,
66         customMimeTypeInput: String,
67         isOrderSelectionEnabled: Boolean,
68         selectedLaunchTab: LaunchLocation,
69         accentColor: String,
70         isPreSelectionEnabled: Boolean,
71         launcher: (Intent) -> Unit
72     ): String? {
73         if (!isActionGetContentSelected && allowMultiple){
74             if (maxMediaItemsDisplayed <= 1) {
75                 return "Enter a valid count greater than one"
76             }
77 
78             if (maxMediaItemsDisplayed > _pickImagesMaxSelectionLimit) {
79                 return "Set media item limit within $_pickImagesMaxSelectionLimit items"
80             }
81         }
82 
83         if (accentColor == "") {
84             return "Enter an accent color"
85         }
86 
87         val accentColorLong: Long = try {
88             android.graphics.Color.parseColor(accentColor).toLong()
89         } catch (e: IllegalArgumentException) {
90             android.graphics.Color.parseColor("#FF6200EE").toLong() // Default color
91         }
92 
93         val intent = if (isActionGetContentSelected) {
94             // ACTION_GET_CONTENT supports only images and videos in the PhotoPicker tab
95             Intent(Intent.ACTION_GET_CONTENT).apply {
96                 if (selectedMimeType == "image/*") type = "image/*"
97                 else if (selectedMimeType == "video/*") type = "video/*"
98                 else {
99                     type = "image/*,video/*"
100                     putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
101                 }
102                 putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
103                 addCategory(Intent.CATEGORY_OPENABLE)
104             }
105         } else {
106             Intent(MediaStore.ACTION_PICK_IMAGES).apply {
107                 if (allowCustomMimeType) type = customMimeTypeInput
108                 else if (selectedMimeType != "") type = selectedMimeType
109 
110                 putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
111                 if (allowMultiple) {
112                     putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxMediaItemsDisplayed)
113                 }
114                 putExtra(
115                     MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
116                     if (selectedLaunchTab == LaunchLocation.ALBUMS_TAB) 0 else 1
117                 )
118                 putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, isOrderSelectionEnabled)
119                 putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, accentColorLong)
120                 if (isPreSelectionEnabled){
121                     Intent(putParcelableArrayListExtra(
122                         "android.provider.extra.PICKER_PRE_SELECTION_URIS",
123                         ArrayList(_selectedMedia.value)
124                     ))
125                 }
126             }
127         }
128 
129         try {
130             launcher(intent)
131         } catch (e: ActivityNotFoundException) {
132             val errorMessage =
133                 "No Activity found to handle Intent with type \"" + intent.type + "\""
134             Toast.makeText(getApplication(), errorMessage, Toast.LENGTH_SHORT).show()
135         }
136         return null
137     }
138 }