• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.viewmodel;
18 
19 import static com.android.providers.media.util.MimeUtils.isImageMimeType;
20 import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
21 
22 import android.app.Application;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.database.Cursor;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 import androidx.lifecycle.AndroidViewModel;
35 import androidx.lifecycle.LiveData;
36 import androidx.lifecycle.MutableLiveData;
37 
38 import com.android.internal.logging.InstanceId;
39 import com.android.internal.logging.InstanceIdSequence;
40 import com.android.providers.media.photopicker.data.ItemsProvider;
41 import com.android.providers.media.photopicker.data.MuteStatus;
42 import com.android.providers.media.photopicker.data.Selection;
43 import com.android.providers.media.photopicker.data.UserIdManager;
44 import com.android.providers.media.photopicker.data.model.Category;
45 import com.android.providers.media.photopicker.data.model.Item;
46 import com.android.providers.media.photopicker.data.model.UserId;
47 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
48 import com.android.providers.media.photopicker.util.DateTimeUtils;
49 import com.android.providers.media.util.ForegroundThread;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 /**
55  * PickerViewModel to store and handle data for PhotoPickerActivity.
56  */
57 public class PickerViewModel extends AndroidViewModel {
58     public static final String TAG = "PhotoPicker";
59 
60     private static final int RECENT_MINIMUM_COUNT = 12;
61 
62     private static final int INSTANCE_ID_MAX = 1 << 15;
63 
64     private final Selection mSelection;
65     private final MuteStatus mMuteStatus;
66 
67     // TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the
68     // data set to reduce memories.
69     // The list of Items with all photos and videos
70     private MutableLiveData<List<Item>> mItemList;
71     // The list of Items with all photos and videos in category
72     private MutableLiveData<List<Item>> mCategoryItemList;
73     // The list of categories.
74     private MutableLiveData<List<Category>> mCategoryList;
75 
76     private ItemsProvider mItemsProvider;
77     private UserIdManager mUserIdManager;
78 
79     private InstanceId mInstanceId;
80     private PhotoPickerUiEventLogger mLogger;
81 
82     private String mMimeTypeFilter = null;
83     private int mBottomSheetState;
84 
85     private Category mCurrentCategory;
86 
PickerViewModel(@onNull Application application)87     public PickerViewModel(@NonNull Application application) {
88         super(application);
89         final Context context = application.getApplicationContext();
90         mItemsProvider = new ItemsProvider(context);
91         mSelection = new Selection();
92         mUserIdManager = UserIdManager.create(context);
93         mMuteStatus = new MuteStatus();
94         mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId();
95         mLogger = new PhotoPickerUiEventLogger();
96     }
97 
98     @VisibleForTesting
setItemsProvider(@onNull ItemsProvider itemsProvider)99     public void setItemsProvider(@NonNull ItemsProvider itemsProvider) {
100         mItemsProvider = itemsProvider;
101     }
102 
103     @VisibleForTesting
setUserIdManager(@onNull UserIdManager userIdManager)104     public void setUserIdManager(@NonNull UserIdManager userIdManager) {
105         mUserIdManager = userIdManager;
106     }
107 
108     /**
109      * @return {@link UserIdManager} for this context.
110      */
getUserIdManager()111     public UserIdManager getUserIdManager() {
112         return mUserIdManager;
113     }
114 
115     /**
116      * @return {@code mSelection} that manages the selection
117      */
getSelection()118     public Selection getSelection() {
119         return mSelection;
120     }
121 
122 
123     /**
124      * @return {@code mMuteStatus} that tracks the volume mute status of the video preview
125      */
getMuteStatus()126     public MuteStatus getMuteStatus() {
127         return mMuteStatus;
128     }
129 
130     /**
131      * Reset to personal profile mode.
132      */
resetToPersonalProfile()133     public void resetToPersonalProfile() {
134         // 1. Clear Selected items
135         mSelection.clearSelectedItems();
136         // 2. Change profile to personal user
137         mUserIdManager.setPersonalAsCurrentUserProfile();
138         // 3. Update Item and Category lists
139         updateItems();
140         updateCategories();
141     }
142 
143     /**
144      * @return the list of Items with all photos and videos {@link #mItemList} on the device.
145      */
getItems()146     public LiveData<List<Item>> getItems() {
147         if (mItemList == null) {
148             updateItems();
149         }
150         return mItemList;
151     }
152 
loadItems(Category category, UserId userId)153     private List<Item> loadItems(Category category, UserId userId) {
154         final List<Item> items = new ArrayList<>();
155 
156         try (Cursor cursor = mItemsProvider.getItems(category, /* offset */ 0,
157                 /* limit */ -1, mMimeTypeFilter, userId)) {
158             if (cursor == null || cursor.getCount() == 0) {
159                 Log.d(TAG, "Didn't receive any items for " + category
160                         + ", either cursor is null or cursor count is zero");
161                 return items;
162             }
163 
164             // We only add the RECENT header on the PhotosTabFragment with CATEGORY_DEFAULT. In this
165             // case, we call this method {loadItems} with null category. When the category is not
166             // empty, we don't show the RECENT header.
167             final boolean showRecent = category.isDefault();
168 
169             int recentSize = 0;
170             long currentDateTaken = 0;
171 
172             if (showRecent) {
173                 // add Recent date header
174                 items.add(Item.createDateItem(0));
175             }
176             while (cursor.moveToNext()) {
177                 // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it
178                 // here again.
179                 final Item item = Item.fromCursor(cursor, userId);
180                 final long dateTaken = item.getDateTaken();
181                 // the minimum count of items in recent is not reached
182                 if (showRecent && recentSize < RECENT_MINIMUM_COUNT) {
183                     recentSize++;
184                     currentDateTaken = dateTaken;
185                 }
186 
187                 // The date taken of these two images are not on the
188                 // same day, add the new date header.
189                 if (!DateTimeUtils.isSameDate(currentDateTaken, dateTaken)) {
190                     items.add(Item.createDateItem(dateTaken));
191                     currentDateTaken = dateTaken;
192                 }
193                 items.add(item);
194             }
195         }
196 
197         Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user "
198                 + userId.toString());
199         return items;
200     }
201 
loadItemsAsync()202     private void loadItemsAsync() {
203         final UserId userId = mUserIdManager.getCurrentUserProfileId();
204         ForegroundThread.getExecutor().execute(() -> {
205                     mItemList.postValue(loadItems(Category.DEFAULT, userId));
206         });
207     }
208 
209     /**
210      * Update the item List {@link #mItemList}
211      */
updateItems()212     public void updateItems() {
213         if (mItemList == null) {
214             mItemList = new MutableLiveData<>();
215         }
216         loadItemsAsync();
217     }
218 
219     /**
220      * Get the list of all photos and videos with the specific {@code category} on the device.
221      *
222      * In our use case, we only keep the list of current category {@link #mCurrentCategory} in
223      * {@link #mCategoryItemList}. If the {@code category} and {@link #mCurrentCategory} are
224      * different, we will create the new LiveData to {@link #mCategoryItemList}.
225      *
226      * @param category the category we want to be queried
227      * @return the list of all photos and videos with the specific {@code category}
228      *         {@link #mCategoryItemList}
229      */
getCategoryItems(@onNull Category category)230     public LiveData<List<Item>> getCategoryItems(@NonNull Category category) {
231         if (mCategoryItemList == null || !TextUtils.equals(mCurrentCategory.getId(),
232                 category.getId())) {
233             mCategoryItemList = new MutableLiveData<>();
234             mCurrentCategory = category;
235         }
236         updateCategoryItems();
237         return mCategoryItemList;
238     }
239 
loadCategoryItemsAsync()240     private void loadCategoryItemsAsync() {
241         final UserId userId = mUserIdManager.getCurrentUserProfileId();
242         ForegroundThread.getExecutor().execute(() -> {
243             mCategoryItemList.postValue(loadItems(mCurrentCategory, userId));
244         });
245     }
246 
247     /**
248      * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemList}
249      *
250      * @throws IllegalStateException category and category items is not initiated before calling
251      *     this method
252      */
253     @VisibleForTesting
updateCategoryItems()254     public void updateCategoryItems() {
255         if (mCategoryItemList == null || mCurrentCategory == null) {
256             throw new IllegalStateException("mCurrentCategory and mCategoryItemList are not"
257                     + " initiated. Please call getCategoryItems before calling this method");
258         }
259         loadCategoryItemsAsync();
260     }
261 
262     /**
263      * @return the list of Categories {@link #mCategoryList}
264      */
getCategories()265     public LiveData<List<Category>> getCategories() {
266         if (mCategoryList == null) {
267             updateCategories();
268         }
269         return mCategoryList;
270     }
271 
loadCategories(UserId userId)272     private List<Category> loadCategories(UserId userId) {
273         final List<Category> categoryList = new ArrayList<>();
274         try (final Cursor cursor = mItemsProvider.getCategories(mMimeTypeFilter, userId)) {
275             if (cursor == null || cursor.getCount() == 0) {
276                 Log.d(TAG, "Didn't receive any categories, either cursor is null or"
277                         + " cursor count is zero");
278                 return categoryList;
279             }
280 
281             while (cursor.moveToNext()) {
282                 final Category category = Category.fromCursor(cursor, userId);
283                 categoryList.add(category);
284             }
285 
286             Log.d(TAG,
287                     "Loaded " + categoryList.size() + " categories for user " + userId.toString());
288         }
289         return categoryList;
290     }
291 
loadCategoriesAsync()292     private void loadCategoriesAsync() {
293         final UserId userId = mUserIdManager.getCurrentUserProfileId();
294         ForegroundThread.getExecutor().execute(() -> {
295             mCategoryList.postValue(loadCategories(userId));
296         });
297     }
298 
299     /**
300      * Update the category List {@link #mCategoryList}
301      */
updateCategories()302     public void updateCategories() {
303         if (mCategoryList == null) {
304             mCategoryList = new MutableLiveData<>();
305         }
306         loadCategoriesAsync();
307     }
308 
309     /**
310      * Return whether the {@link #mMimeTypeFilter} is {@code null} or not
311      */
hasMimeTypeFilter()312     public boolean hasMimeTypeFilter() {
313         return !TextUtils.isEmpty(mMimeTypeFilter);
314     }
315 
316     /**
317      * Parse values from {@code intent} and set corresponding fields
318      */
parseValuesFromIntent(Intent intent)319     public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException {
320         mUserIdManager.setIntentAndCheckRestrictions(intent);
321 
322         final String mimeType = intent.getType();
323         if (isMimeTypeMedia(mimeType)) {
324             mMimeTypeFilter = mimeType;
325         }
326 
327         mSelection.parseSelectionValuesFromIntent(intent);
328     }
329 
isMimeTypeMedia(@ullable String mimeType)330     private static boolean isMimeTypeMedia(@Nullable String mimeType) {
331         return isImageMimeType(mimeType) || isVideoMimeType(mimeType);
332     }
333 
334     /**
335      * Set BottomSheet state
336      */
setBottomSheetState(int state)337     public void setBottomSheetState(int state) {
338         mBottomSheetState = state;
339     }
340 
341     /**
342      * @return BottomSheet state
343      */
getBottomSheetState()344     public int getBottomSheetState() {
345         return mBottomSheetState;
346     }
347 
logPickerOpened(String callingPackage)348     public void logPickerOpened(String callingPackage) {
349         if (getUserIdManager().isManagedUserSelected()) {
350             mLogger.logPickerOpenWork(mInstanceId, callingPackage);
351         } else {
352             mLogger.logPickerOpenPersonal(mInstanceId, callingPackage);
353         }
354     }
355 
getInstanceId()356     public InstanceId getInstanceId() {
357         return mInstanceId;
358     }
359 
setInstanceId(InstanceId parcelable)360     public void setInstanceId(InstanceId parcelable) {
361         mInstanceId = parcelable;
362     }
363 }
364