1 /* 2 * Copyright (C) 2023 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.providers.media.photopicker.ui.settings; 18 19 import static android.provider.MediaStore.AUTHORITY; 20 21 import static com.android.providers.media.photopicker.util.CloudProviderUtils.fetchProviderAuthority; 22 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders; 23 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaCollectionInfo; 24 import static com.android.providers.media.photopicker.util.CloudProviderUtils.persistSelectedProvider; 25 26 import static java.util.Objects.requireNonNull; 27 28 import android.content.ContentProviderClient; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageManager; 33 import android.graphics.drawable.Drawable; 34 import android.os.Bundle; 35 import android.os.Looper; 36 import android.os.UserHandle; 37 import android.provider.CloudMediaProviderContract; 38 import android.util.Log; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.annotation.UiThread; 43 import androidx.annotation.VisibleForTesting; 44 import androidx.lifecycle.LiveData; 45 import androidx.lifecycle.MutableLiveData; 46 import androidx.lifecycle.ViewModel; 47 48 import com.android.providers.media.ConfigStore; 49 import com.android.providers.media.R; 50 import com.android.providers.media.photopicker.data.CloudProviderInfo; 51 import com.android.providers.media.photopicker.data.model.UserId; 52 import com.android.providers.media.util.ForegroundThread; 53 54 import java.util.ArrayList; 55 import java.util.List; 56 57 /** 58 * SettingsCloudMediaViewModel stores cloud media app settings data for each profile. 59 */ 60 public class SettingsCloudMediaViewModel extends ViewModel { 61 static final String NONE_PREF_KEY = "none"; 62 private static final String TAG = "SettingsFragVM"; 63 private static final long GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS = 10000L; 64 65 @NonNull 66 private final Context mContext; 67 @NonNull 68 private final MutableLiveData<CloudProviderMediaCollectionInfo> 69 mCurrentProviderMediaCollectionInfo; 70 @NonNull 71 private final List<CloudMediaProviderOption> mProviderOptions; 72 @NonNull 73 private final UserId mUserId; 74 @Nullable 75 private String mSelectedProviderAuthority; 76 SettingsCloudMediaViewModel( @onNull Context context, @NonNull UserId userId)77 SettingsCloudMediaViewModel( 78 @NonNull Context context, 79 @NonNull UserId userId) { 80 super(); 81 82 mContext = requireNonNull(context); 83 mUserId = requireNonNull(userId); 84 mProviderOptions = new ArrayList<>(); 85 mSelectedProviderAuthority = null; 86 mCurrentProviderMediaCollectionInfo = new MutableLiveData<>(); 87 } 88 89 @NonNull getProviderOptions()90 List<CloudMediaProviderOption> getProviderOptions() { 91 return mProviderOptions; 92 } 93 94 @Nullable getSelectedProviderAuthority()95 String getSelectedProviderAuthority() { 96 return mSelectedProviderAuthority; 97 } 98 99 @NonNull getCurrentProviderMediaCollectionInfo()100 LiveData<CloudProviderMediaCollectionInfo> getCurrentProviderMediaCollectionInfo() { 101 return mCurrentProviderMediaCollectionInfo; 102 } 103 104 @NonNull getSelectedPreferenceKey()105 String getSelectedPreferenceKey() { 106 return getPreferenceKey(mSelectedProviderAuthority); 107 } 108 109 /** 110 * Fetch and cache the available cloud provider options and the selected provider. 111 */ loadData(@onNull ConfigStore configStore)112 void loadData(@NonNull ConfigStore configStore) { 113 refreshProviderOptions(configStore); 114 refreshSelectedProvider(); 115 } 116 117 /** 118 * Updates the selected cloud provider on disk and in cache. 119 * Returns true if the update was successful. 120 */ updateSelectedProvider(@onNull String newPreferenceKey)121 boolean updateSelectedProvider(@NonNull String newPreferenceKey) { 122 final String newCloudProvider = getProviderAuthority(newPreferenceKey); 123 try (ContentProviderClient client = getContentProviderClient()) { 124 if (client == null) { 125 // This could happen when work profile is turned off after opening the Settings 126 // page. The work tab would still be visible but the MP process for work profile 127 // will not be running. 128 return false; 129 } 130 final boolean success = 131 persistSelectedProvider(client, newCloudProvider); 132 if (success) { 133 mSelectedProviderAuthority = newCloudProvider; 134 return true; 135 } 136 } catch (Exception e) { 137 Log.e(TAG, "Could not persist selected cloud provider", e); 138 } 139 return false; 140 } 141 142 @Nullable getProviderAuthority(@onNull String preferenceKey)143 private String getProviderAuthority(@NonNull String preferenceKey) { 144 // For None option, the provider auth should be null to disable cloud media provider. 145 return preferenceKey.equals(SettingsCloudMediaViewModel.NONE_PREF_KEY) 146 ? null : preferenceKey; 147 } 148 149 @NonNull getPreferenceKey(@ullable String providerAuthority)150 private String getPreferenceKey(@Nullable String providerAuthority) { 151 return providerAuthority == null 152 ? SettingsCloudMediaViewModel.NONE_PREF_KEY : providerAuthority; 153 } 154 refreshProviderOptions(@onNull ConfigStore configStore)155 private void refreshProviderOptions(@NonNull ConfigStore configStore) { 156 mProviderOptions.clear(); 157 mProviderOptions.addAll(fetchProviderOptions(configStore)); 158 mProviderOptions.add(getNoneProviderOption()); 159 } 160 refreshSelectedProvider()161 private void refreshSelectedProvider() { 162 try (ContentProviderClient client = getContentProviderClient()) { 163 if (client == null) { 164 // TODO(b/266927613): Handle the edge case where work profile is turned off 165 // while user is on the settings page but work tab's data is not fetched yet. 166 throw new IllegalArgumentException("Could not get selected cloud provider" 167 + " because Media Provider client is null."); 168 } 169 mSelectedProviderAuthority = fetchProviderAuthority(client); 170 } catch (Exception e) { 171 // Since displaying the current cloud provider is the core function of the Settings 172 // page, if we're not able to fetch this info, there is no point in displaying this 173 // activity. 174 throw new IllegalArgumentException("Could not get selected cloud provider", e); 175 } 176 } 177 178 @UiThread loadMediaCollectionInfoAsync()179 void loadMediaCollectionInfoAsync() { 180 if (!Looper.getMainLooper().isCurrentThread()) { 181 // This method should only be run from the UI thread so that fetch media collection info 182 // requests are executed serially. 183 Log.w(TAG, "loadMediaCollectionInfoAsync method needs to be called from the UI thread"); 184 return; 185 } 186 187 final String providerAuthority = getSelectedProviderAuthority(); 188 // Foreground thread internally uses a queue to execute each request in a serialized manner. 189 ForegroundThread.getExecutor().execute(() -> { 190 mCurrentProviderMediaCollectionInfo.postValue( 191 fetchMediaCollectionInfoFromProvider(providerAuthority)); 192 }); 193 } 194 195 @Nullable fetchMediaCollectionInfoFromProvider( @ullable String currentProviderAuthority)196 private CloudProviderMediaCollectionInfo fetchMediaCollectionInfoFromProvider( 197 @Nullable String currentProviderAuthority) { 198 // If the selected cloud provider preference is "None", the media collection info is not 199 // applicable. 200 if (currentProviderAuthority == null) { 201 return null; 202 } 203 204 Bundle cloudMediaCollectionInfo = null; 205 try { 206 final ContentResolver currentUserContentResolver = mUserId.getContentResolver(mContext); 207 cloudMediaCollectionInfo = getCloudMediaCollectionInfo(currentUserContentResolver, 208 currentProviderAuthority, GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS); 209 } catch (Exception e) { 210 Log.w(TAG, "Failed to fetch media collection info from the cloud media provider.", e); 211 } 212 213 if (cloudMediaCollectionInfo == null) { 214 return new CloudProviderMediaCollectionInfo(currentProviderAuthority); 215 } 216 217 final String accountName = cloudMediaCollectionInfo.getString( 218 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME); 219 final Intent cloudProviderSettingsActivityIntent = cloudMediaCollectionInfo.getParcelable( 220 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT); 221 return new CloudProviderMediaCollectionInfo(currentProviderAuthority, accountName, 222 cloudProviderSettingsActivityIntent); 223 } 224 225 @NonNull fetchProviderOptions(@onNull ConfigStore configStore)226 private List<CloudMediaProviderOption> fetchProviderOptions(@NonNull ConfigStore configStore) { 227 // Get info of available cloud providers. 228 List<CloudProviderInfo> cloudProviders = getAvailableCloudProviders( 229 mContext, configStore, UserHandle.of(mUserId.getIdentifier())); 230 231 return getProviderOptionsFromCloudProviderInfos(cloudProviders); 232 } 233 234 @NonNull getProviderOptionsFromCloudProviderInfos( @onNull List<CloudProviderInfo> cloudProviders)235 private List<CloudMediaProviderOption> getProviderOptionsFromCloudProviderInfos( 236 @NonNull List<CloudProviderInfo> cloudProviders) { 237 // TODO(b/195009187): In case current cloud provider is not part of the allow list, it will 238 // not be listed on the Settings page. Handle this case so that it does show up. 239 final List<CloudMediaProviderOption> providerOption = new ArrayList<>(); 240 for (CloudProviderInfo cloudProvider : cloudProviders) { 241 providerOption.add( 242 CloudMediaProviderOption 243 .fromCloudProviderInfo(cloudProvider, mContext, mUserId)); 244 } 245 return providerOption; 246 } 247 248 @NonNull getNoneProviderOption()249 private CloudMediaProviderOption getNoneProviderOption() { 250 final Drawable nonePrefIcon = mContext.getDrawable(R.drawable.ic_cloud_picker_off); 251 final String nonePrefLabel = mContext.getString(R.string.picker_settings_no_provider); 252 253 return new CloudMediaProviderOption(NONE_PREF_KEY, nonePrefLabel, nonePrefIcon); 254 } 255 256 @Nullable 257 @VisibleForTesting getContentProviderClient()258 public ContentProviderClient getContentProviderClient() 259 throws PackageManager.NameNotFoundException { 260 return mUserId 261 .getContentResolver(mContext) 262 .acquireUnstableContentProviderClient(AUTHORITY); 263 } 264 } 265