| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.providers.media.photopicker.ui.settings; |
| |
| import static android.provider.MediaStore.AUTHORITY; |
| |
| import static com.android.providers.media.photopicker.util.CloudProviderUtils.fetchProviderAuthority; |
| import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders; |
| import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaAccountName; |
| import static com.android.providers.media.photopicker.util.CloudProviderUtils.persistSelectedProvider; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.content.ContentProviderClient; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.graphics.drawable.Drawable; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.UiThread; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.lifecycle.LiveData; |
| import androidx.lifecycle.MutableLiveData; |
| import androidx.lifecycle.ViewModel; |
| |
| import com.android.providers.media.ConfigStore; |
| import com.android.providers.media.R; |
| import com.android.providers.media.photopicker.data.CloudProviderInfo; |
| import com.android.providers.media.photopicker.data.model.UserId; |
| import com.android.providers.media.util.ForegroundThread; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * SettingsCloudMediaViewModel stores cloud media app settings data for each profile. |
| */ |
| public class SettingsCloudMediaViewModel extends ViewModel { |
| static final String NONE_PREF_KEY = "none"; |
| private static final String TAG = "SettingsFragVM"; |
| private static final long GET_ACCOUNT_NAME_TIMEOUT_IN_MILLIS = 10000L; |
| |
| @NonNull |
| private final Context mContext; |
| @NonNull |
| private final MutableLiveData<CloudMediaProviderAccount> mCurrentProviderAccount; |
| @NonNull |
| private final List<CloudMediaProviderOption> mProviderOptions; |
| @NonNull |
| private final UserId mUserId; |
| @Nullable |
| private String mSelectedProviderAuthority; |
| |
| SettingsCloudMediaViewModel( |
| @NonNull Context context, |
| @NonNull UserId userId) { |
| super(); |
| |
| mContext = requireNonNull(context); |
| mUserId = requireNonNull(userId); |
| mProviderOptions = new ArrayList<>(); |
| mSelectedProviderAuthority = null; |
| mCurrentProviderAccount = new MutableLiveData<CloudMediaProviderAccount>(); |
| } |
| |
| @NonNull |
| List<CloudMediaProviderOption> getProviderOptions() { |
| return mProviderOptions; |
| } |
| |
| @Nullable |
| String getSelectedProviderAuthority() { |
| return mSelectedProviderAuthority; |
| } |
| |
| @NonNull |
| LiveData<CloudMediaProviderAccount> getCurrentProviderAccount() { |
| return mCurrentProviderAccount; |
| } |
| |
| @Nullable |
| String getSelectedPreferenceKey() { |
| return getPreferenceKey(mSelectedProviderAuthority); |
| } |
| |
| /** |
| * Fetch and cache the available cloud provider options and the selected provider. |
| */ |
| void loadData(@NonNull ConfigStore configStore) { |
| refreshProviderOptions(configStore); |
| refreshSelectedProvider(); |
| } |
| |
| /** |
| * Updates the selected cloud provider on disk and in cache. |
| * Returns true if the update was successful. |
| */ |
| boolean updateSelectedProvider(@NonNull String newPreferenceKey) { |
| final String newCloudProvider = getProviderAuthority(newPreferenceKey); |
| try (ContentProviderClient client = getContentProviderClient()) { |
| if (client == null) { |
| // This could happen when work profile is turned off after opening the Settings |
| // page. The work tab would still be visible but the MP process for work profile |
| // will not be running. |
| return false; |
| } |
| final boolean success = |
| persistSelectedProvider(client, newCloudProvider); |
| if (success) { |
| mSelectedProviderAuthority = newCloudProvider; |
| return true; |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Could not persist selected cloud provider", e); |
| } |
| return false; |
| } |
| |
| @Nullable |
| private String getProviderAuthority(@NonNull String preferenceKey) { |
| // For None option, the provider auth should be null to disable cloud media provider. |
| return preferenceKey.equals(SettingsCloudMediaViewModel.NONE_PREF_KEY) |
| ? null : preferenceKey; |
| } |
| |
| @Nullable |
| private String getPreferenceKey(@Nullable String providerAuthority) { |
| return providerAuthority == null |
| ? SettingsCloudMediaViewModel.NONE_PREF_KEY : providerAuthority; |
| } |
| |
| private void refreshProviderOptions(@NonNull ConfigStore configStore) { |
| mProviderOptions.clear(); |
| mProviderOptions.addAll(fetchProviderOptions(configStore)); |
| mProviderOptions.add(getNoneProviderOption()); |
| } |
| |
| private void refreshSelectedProvider() { |
| try (ContentProviderClient client = getContentProviderClient()) { |
| if (client == null) { |
| // TODO(b/266927613): Handle the edge case where work profile is turned off |
| // while user is on the settings page but work tab's data is not fetched yet. |
| throw new IllegalArgumentException("Could not get selected cloud provider" |
| + " because Media Provider client is null."); |
| } |
| mSelectedProviderAuthority = |
| fetchProviderAuthority(client, /* default */ NONE_PREF_KEY); |
| } catch (Exception e) { |
| // Since displaying the current cloud provider is the core function of the Settings |
| // page, if we're not able to fetch this info, there is no point in displaying this |
| // activity. |
| throw new IllegalArgumentException("Could not get selected cloud provider", e); |
| } |
| } |
| |
| @UiThread |
| void loadAccountNameAsync() { |
| if (!Looper.getMainLooper().isCurrentThread()) { |
| // This method should only be run from the UI thread so that fetch account name |
| // requests are executed serially. |
| Log.d(TAG, "loadAccountNameAsync method needs to be called from the UI thread"); |
| return; |
| } |
| |
| final String providerAuthority = getSelectedProviderAuthority(); |
| // Foreground thread internally uses a queue to execute each request in a serialized manner. |
| ForegroundThread.getExecutor().execute(() -> { |
| mCurrentProviderAccount.postValue( |
| fetchAccountFromProvider(providerAuthority)); |
| }); |
| } |
| |
| @Nullable |
| private CloudMediaProviderAccount fetchAccountFromProvider( |
| @Nullable String currentProviderAuthority) { |
| if (currentProviderAuthority == null) { |
| // If the selected cloud provider preference is "None", account name is not applicable. |
| return null; |
| } else { |
| try { |
| final String accountName = getCloudMediaAccountName( |
| mUserId.getContentResolver(mContext), currentProviderAuthority, |
| GET_ACCOUNT_NAME_TIMEOUT_IN_MILLIS); |
| return new CloudMediaProviderAccount(currentProviderAuthority, accountName); |
| } catch (Exception e) { |
| Log.w(TAG, "Failed to fetch account name from the cloud media provider.", e); |
| return null; |
| } |
| } |
| } |
| |
| @NonNull |
| private List<CloudMediaProviderOption> fetchProviderOptions(@NonNull ConfigStore configStore) { |
| // Get info of available cloud providers. |
| List<CloudProviderInfo> cloudProviders = getAvailableCloudProviders( |
| mContext, configStore, UserHandle.of(mUserId.getIdentifier())); |
| |
| return getProviderOptionsFromCloudProviderInfos(cloudProviders); |
| } |
| |
| @NonNull |
| private List<CloudMediaProviderOption> getProviderOptionsFromCloudProviderInfos( |
| @NonNull List<CloudProviderInfo> cloudProviders) { |
| // TODO(b/195009187): In case current cloud provider is not part of the allow list, it will |
| // not be listed on the Settings page. Handle this case so that it does show up. |
| final List<CloudMediaProviderOption> providerOption = new ArrayList<>(); |
| for (CloudProviderInfo cloudProvider : cloudProviders) { |
| providerOption.add( |
| CloudMediaProviderOption |
| .fromCloudProviderInfo(cloudProvider, mContext, mUserId)); |
| } |
| return providerOption; |
| } |
| |
| @NonNull |
| private CloudMediaProviderOption getNoneProviderOption() { |
| final Drawable nonePrefIcon = mContext.getDrawable(R.drawable.ic_cloud_picker_off); |
| final String nonePrefLabel = mContext.getString(R.string.picker_settings_no_provider); |
| |
| return new CloudMediaProviderOption(NONE_PREF_KEY, nonePrefLabel, nonePrefIcon); |
| } |
| |
| @Nullable |
| @VisibleForTesting |
| public ContentProviderClient getContentProviderClient() |
| throws PackageManager.NameNotFoundException { |
| return mUserId |
| .getContentResolver(mContext) |
| .acquireUnstableContentProviderClient(AUTHORITY); |
| } |
| } |