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