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.MediaColumns;
20 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_ANIMATED_WEBP;
21 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_GIF;
22 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_MOTION_PHOTO;
23 
24 import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
25 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorInt;
26 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
27 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
28 
29 import static java.util.Objects.requireNonNull;
30 
31 import android.content.Context;
32 import android.database.Cursor;
33 import android.net.Uri;
34 import android.text.format.DateUtils;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.providers.media.R;
40 import com.android.providers.media.photopicker.data.ItemsProvider;
41 import com.android.providers.media.photopicker.data.glide.GlideLoadable;
42 import com.android.providers.media.photopicker.util.DateTimeUtils;
43 import com.android.providers.media.util.MimeUtils;
44 
45 import java.util.Objects;
46 
47 /**
48  * Base class for representing a single media item (a picture, a video, etc.) in the PhotoPicker.
49  */
50 public class Item {
51     public static final Item EMPTY_VIEW = new Item("EMPTY_VIEW");
52     public static final String ROW_ID = "row_id";
53 
54     /**
55      * This id represents the cloud id or the local id of the media, with priority given to cloud id
56      * if present.
57      */
58     private String mId;
59     private long mDateTaken;
60     private long mGenerationModified;
61     private long mDuration;
62     private String mMimeType;
63     private Uri mUri;
64     private boolean mIsImage;
65     private boolean mIsVideo;
66     private int mSpecialFormat;
67 
68     private boolean mIsPreGranted;
69 
70     /**
71      * This is the row id for the item in the db.
72      */
73     private int mRowId;
74 
Item(@onNull Cursor cursor, @NonNull UserId userId)75     public Item(@NonNull Cursor cursor, @NonNull UserId userId) {
76         updateFromCursor(cursor, userId);
77     }
78 
79     @VisibleForTesting
Item(String id, String mimeType, long dateTaken, long generationModified, long duration, Uri uri, int specialFormat)80     public Item(String id, String mimeType, long dateTaken, long generationModified, long duration,
81             Uri uri, int specialFormat) {
82         mId = id;
83         mMimeType = mimeType;
84         mDateTaken = dateTaken;
85         mGenerationModified = generationModified;
86         mDuration = duration;
87         mUri = uri;
88         mSpecialFormat = specialFormat;
89         parseMimeType();
90     }
91 
Item(String id)92     private Item(String id) {
93         this(id, null, 0, 0, 0, null, 0);
94     }
95 
getId()96     public String getId() {
97         return mId;
98     }
99 
isImage()100     public boolean isImage() {
101         return mIsImage;
102     }
103 
isVideo()104     public boolean isVideo() {
105         return mIsVideo;
106     }
107 
isGifOrAnimatedWebp()108     public boolean isGifOrAnimatedWebp() {
109         return isGif() || isAnimatedWebp();
110     }
111 
isGif()112     public boolean isGif() {
113         return mSpecialFormat == _SPECIAL_FORMAT_GIF;
114     }
115 
isAnimatedWebp()116     public boolean isAnimatedWebp() {
117         return mSpecialFormat == _SPECIAL_FORMAT_ANIMATED_WEBP;
118     }
119 
isMotionPhoto()120     public boolean isMotionPhoto() {
121         return mSpecialFormat == _SPECIAL_FORMAT_MOTION_PHOTO;
122     }
123 
getContentUri()124     public Uri getContentUri() {
125         return mUri;
126     }
127 
getDuration()128     public long getDuration() {
129         return mDuration;
130     }
131 
getMimeType()132     public String getMimeType() {
133         return mMimeType;
134     }
135 
getDateTaken()136     public long getDateTaken() {
137         return mDateTaken;
138     }
139 
getGenerationModified()140     public long getGenerationModified() {
141         return mGenerationModified;
142     }
143 
144     @VisibleForTesting
getSpecialFormat()145     public int getSpecialFormat() {
146         return mSpecialFormat;
147     }
148 
getRowId()149     public int getRowId() {
150         return mRowId;
151     }
152 
153     /**
154      * Setting this represents that the item has READ_GRANT for the current package.
155      */
setPreGranted()156     public void setPreGranted() {
157         mIsPreGranted = true;
158     }
isPreGranted()159     public boolean isPreGranted() {
160         return mIsPreGranted;
161     }
162 
fromCursor(@onNull Cursor cursor, UserId userId)163     public static Item fromCursor(@NonNull Cursor cursor, UserId userId) {
164         return new Item(requireNonNull(cursor), userId);
165     }
166 
167     /**
168      * Update the item based on the cursor
169      *
170      * @param cursor the cursor to update the data
171      * @param userId the user id to create an {@link Item} for
172      */
updateFromCursor(@onNull Cursor cursor, @NonNull UserId userId)173     public void updateFromCursor(@NonNull Cursor cursor, @NonNull UserId userId) {
174         final String authority = getCursorString(cursor, MediaColumns.AUTHORITY);
175         mId = getCursorString(cursor, MediaColumns.ID);
176         mMimeType = getCursorString(cursor, MediaColumns.MIME_TYPE);
177         mDateTaken = getCursorLong(cursor, MediaColumns.DATE_TAKEN_MILLIS);
178         mGenerationModified = getCursorLong(cursor, MediaColumns.SYNC_GENERATION);
179         mDuration = getCursorLong(cursor, MediaColumns.DURATION_MILLIS);
180         mSpecialFormat = getCursorInt(cursor, MediaColumns.STANDARD_MIME_TYPE_EXTENSION);
181         mUri = ItemsProvider.getItemsUri(mId, authority, userId);
182         mRowId = getCursorInt(cursor, ROW_ID);
183 
184         parseMimeType();
185     }
186 
getContentDescription(@onNull Context context)187     public String getContentDescription(@NonNull Context context) {
188         if (isVideo()) {
189             return context.getString(R.string.picker_video_item_content_desc,
190                     DateTimeUtils.getDateTimeStringForContentDesc(getDateTaken()),
191                     getDurationText());
192         }
193 
194         final String itemType;
195         if (isGif() || isAnimatedWebp()) {
196             itemType = context.getString(R.string.picker_gif);
197         } else if (isMotionPhoto()) {
198             itemType = context.getString(R.string.picker_motion_photo);
199         } else {
200             itemType = context.getString(R.string.picker_photo);
201         }
202 
203         return context.getString(R.string.picker_item_content_desc, itemType,
204                 DateTimeUtils.getDateTimeStringForContentDesc(getDateTaken()));
205     }
206 
getDurationText()207     public String getDurationText() {
208         if (mDuration == -1) {
209             return "";
210         }
211         return DateUtils.formatElapsedTime(mDuration / 1000);
212     }
213 
parseMimeType()214     private void parseMimeType() {
215         if (MimeUtils.isImageMimeType(mMimeType)) {
216             mIsImage = true;
217         } else if (MimeUtils.isVideoMimeType(mMimeType)) {
218             mIsVideo = true;
219         }
220     }
221 
222     /**
223      * Compares this item with given {@code anotherItem} by comparing
224      * {@link Item#getDateTaken()} value. When {@link Item#getDateTaken()} is
225      * same, Items are compared based on {@link Item#getId}.
226      */
compareTo(Item anotherItem)227     public int compareTo(Item anotherItem) {
228         if (mDateTaken > anotherItem.getDateTaken()) {
229             return 1;
230         } else if (mDateTaken < anotherItem.getDateTaken()) {
231             return -1;
232         } else {
233             return mId.compareTo(anotherItem.getId());
234         }
235     }
236 
237     /**
238      * @return {@code true} iff this item is local (available on device), {@code false} otherwise.
239      */
isLocal()240     public boolean isLocal() {
241         return LOCAL_PICKER_PROVIDER_AUTHORITY.equals(mUri.getAuthority());
242     }
243 
244     @Override
equals(Object obj)245     public boolean equals(Object obj) {
246         if (this == obj) return true;
247         if (obj == null || !(obj instanceof Item)) return false;
248 
249         Item other = (Item) obj;
250         return mUri.equals(other.mUri);
251     }
252 
hashCode()253     @Override public int hashCode() {
254         return Objects.hash(mUri);
255     }
256 
257     /**
258      * Convert this item into a loadable object for Glide.
259      *
260      * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
261      */
toGlideLoadable()262     public GlideLoadable toGlideLoadable() {
263         return new GlideLoadable(mUri, String.valueOf(getGenerationModified()));
264     }
265 
266 }
267