• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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