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