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