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.data.model;
18 
19 import static android.provider.CloudMediaProviderContract.AlbumColumns;
20 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
21 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
22 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
23 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
24 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
25 
26 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorInt;
27 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
28 
29 import android.content.Context;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.provider.MediaStore;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.providers.media.R;
41 import com.android.providers.media.photopicker.data.ItemsProvider;
42 import com.android.providers.media.photopicker.data.glide.GlideLoadable;
43 
44 import java.util.List;
45 import java.util.Locale;
46 
47 /**
48  * Defines each category (which is group of items) for the photo picker.
49  */
50 public class Category {
51     public static final String TAG = "PhotoPicker";
52     public static final Category DEFAULT = new Category();
53     public static final Category EMPTY_VIEW = new Category("EMPTY_VIEW");
54     private static final List<String> TRANSLATABLE_CATEGORIES = List.of(ALBUM_ID_VIDEOS,
55             ALBUM_ID_CAMERA, ALBUM_ID_SCREENSHOTS, ALBUM_ID_DOWNLOADS, ALBUM_ID_FAVORITES);
56 
57     private final String mId;
58     private final String mAuthority;
59     private final String mDisplayName;
60     private final boolean mIsLocal;
61     private final Uri mCoverUri;
62     private final int mItemCount;
63 
Category()64     private Category() {
65         this(null, null, null, null, 0, false);
66     }
67 
Category(String id)68     private Category(String id) {
69         this(id, null, null, null, 0, false);
70     }
71     @VisibleForTesting
Category(String id, String authority, String displayName, Uri coverUri, int itemCount, boolean isLocal)72     public Category(String id, String authority, String displayName, Uri coverUri, int itemCount,
73             boolean isLocal) {
74         mId = id;
75         mAuthority = authority;
76         mDisplayName = displayName;
77         mIsLocal = isLocal;
78         mCoverUri = coverUri;
79         mItemCount = itemCount;
80     }
81 
82     @Override
toString()83     public String toString() {
84         return String.format(Locale.ROOT, "Category: {mId: %s, mAuthority: %s, mDisplayName: %s, " +
85                         "mCoverUri: %s, mItemCount: %d, mIsLocal: %b",
86                 mId, mAuthority, mDisplayName, mCoverUri, mItemCount, mIsLocal);
87     }
88 
getId()89     public String getId() {
90         return mId;
91     }
92 
getAuthority()93     public String getAuthority() {
94         return mAuthority;
95     }
96 
getDisplayName(Context context)97     public String getDisplayName(Context context) {
98         if (TRANSLATABLE_CATEGORIES.contains(mId)) {
99             return getLocalizedDisplayName(context, mId);
100         }
101         return mDisplayName;
102     }
103 
isLocal()104     public boolean isLocal() {
105         return mIsLocal;
106     }
107 
getCoverUri()108     public Uri getCoverUri() {
109         return mCoverUri;
110     }
111 
getItemCount()112     public int getItemCount() {
113         return mItemCount;
114     }
115 
isDefault()116     public boolean isDefault() {
117         return TextUtils.isEmpty(mId);
118     }
119 
120     /**
121      * Write the {@link Category} to the given {@code bundle}.
122      */
toBundle(@onNull Bundle bundle)123     public void toBundle(@NonNull Bundle bundle) {
124         bundle.putString(AlbumColumns.ID, mId);
125         bundle.putString(AlbumColumns.AUTHORITY, mAuthority);
126         bundle.putString(AlbumColumns.DISPLAY_NAME, mDisplayName);
127         // Re-using the 'media_cover_id' to store the media_cover_uri for lack of
128         // a different constant
129         bundle.putParcelable(AlbumColumns.MEDIA_COVER_ID, mCoverUri);
130         bundle.putInt(AlbumColumns.MEDIA_COUNT, mItemCount);
131         bundle.putBoolean(AlbumColumns.IS_LOCAL, mIsLocal);
132     }
133 
134     /**
135      * Create a {@link Category} from the {@code bundle}.
136      */
fromBundle(@onNull Bundle bundle)137     public static Category fromBundle(@NonNull Bundle bundle) {
138         return new Category(bundle.getString(AlbumColumns.ID),
139                 bundle.getString(AlbumColumns.AUTHORITY),
140                 bundle.getString(AlbumColumns.DISPLAY_NAME),
141                 bundle.getParcelable(AlbumColumns.MEDIA_COVER_ID),
142                 bundle.getInt(AlbumColumns.MEDIA_COUNT),
143                 bundle.getBoolean(AlbumColumns.IS_LOCAL));
144     }
145 
146     /**
147      * Create a {@link Category} from the {@code cursor}.
148      */
fromCursor(@onNull Cursor cursor, @NonNull UserId userId)149     public static Category fromCursor(@NonNull Cursor cursor, @NonNull UserId userId) {
150         String authority = getCursorString(cursor, AlbumColumns.AUTHORITY);
151         if (authority == null) {
152             // Authority will be null for cloud albums in cursor.
153             String cloudProvider = cursor.getExtras().getString(MediaStore.EXTRA_CLOUD_PROVIDER);
154             if (cloudProvider == null) {
155                 // If cloud provider is null, cloud albums will not show up properly.
156                 Log.e(TAG, "Cloud provider is set by the user but not passed in album media cursor"
157                         + " extras.");
158             } else {
159                 authority = cloudProvider;
160             }
161         }
162         final boolean isLocal = authority != null
163                 && authority.equals(cursor.getExtras().getString(MediaStore.EXTRA_LOCAL_PROVIDER));
164         final Uri coverUri = ItemsProvider.getItemsUri(
165                 getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID), authority, userId);
166 
167         return new Category(getCursorString(cursor, AlbumColumns.ID),
168                 authority,
169                 getCursorString(cursor, AlbumColumns.DISPLAY_NAME),
170                 getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID) != null ? coverUri : null,
171                 getCursorInt(cursor, AlbumColumns.MEDIA_COUNT),
172                 isLocal);
173     }
174 
getLocalizedDisplayName(Context context, String albumId)175     private static String getLocalizedDisplayName(Context context, String albumId) {
176         switch (albumId) {
177             case ALBUM_ID_VIDEOS:
178                 return context.getString(R.string.picker_category_videos);
179             case ALBUM_ID_CAMERA:
180                 return context.getString(R.string.picker_category_camera);
181             case ALBUM_ID_SCREENSHOTS:
182                 return context.getString(R.string.picker_category_screenshots);
183             case ALBUM_ID_DOWNLOADS:
184                 return context.getString(R.string.picker_category_downloads);
185             case ALBUM_ID_FAVORITES:
186                 return context.getString(R.string.picker_category_favorites);
187             default:
188                 return albumId;
189         }
190     }
191 
192     /**
193      * Convert this category into a loadable object for Glide.
194      *
195      * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
196      */
toGlideLoadable()197     public GlideLoadable toGlideLoadable() {
198         return new GlideLoadable(getCoverUri());
199     }
200 }
201