1 /*
2  * Copyright (C) 2024 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.v2;
18 
19 import static com.android.providers.media.PickerUriResolver.getPickerSegmentFromIntentAction;
20 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID;
21 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DATE_TAKEN_MS;
22 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DURATION_MS;
23 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_ID;
24 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
25 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_MIME_TYPE;
26 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_SIZE_BYTES;
27 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION;
28 
29 import static java.util.Objects.requireNonNull;
30 
31 import android.provider.CloudMediaProviderContract;
32 import android.provider.MediaStore;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 
37 import com.android.providers.media.photopicker.v2.model.MediaSource;
38 
39 import java.util.Arrays;
40 
41 /**
42  * Helper class that keeps track of Picker related Constants.
43  */
44 public class PickerSQLConstants {
45     /**
46      * An enum that holds the table names in Picker DB
47      */
48     enum Table {
49         MEDIA,
50         ALBUM_MEDIA,
51     }
52 
53     /**
54      * An enum that holds the columns names for the Available Providers query response.
55      */
56     enum AvailableProviderResponse {
57         AUTHORITY("authority"),
58         MEDIA_SOURCE("media_source"),
59         UID("uid");
60 
61         private final String mColumnName;
62 
AvailableProviderResponse(String columnName)63         AvailableProviderResponse(String columnName) {
64             this.mColumnName = columnName;
65         }
66 
getColumnName()67         public String getColumnName() {
68             return mColumnName;
69         }
70     }
71 
72     /**
73      * An enum that holds the DB columns names and projections for the Album SQL query response.
74      */
75     public enum AlbumResponse {
76         ALBUM_ID(CloudMediaProviderContract.AlbumColumns.ID),
77         PICKER_ID("picker_id"),
78         AUTHORITY("authority"),
79         DATE_TAKEN(CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS),
80         ALBUM_NAME(CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME),
81         UNWRAPPED_COVER_URI("unwrapped_cover_uri"),
82         COVER_MEDIA_SOURCE("media_source");
83 
84         private final String mColumnName;
85 
AlbumResponse(@onNull String columnName)86         AlbumResponse(@NonNull String columnName) {
87             requireNonNull(columnName);
88             this.mColumnName = columnName;
89         }
90 
getColumnName()91         public String getColumnName() {
92             return mColumnName;
93         }
94     }
95 
96     /**
97      * @param columnName Input album column name.
98      * @return Corresponding enum AlbumResponse to the given column name.
99      * @throws IllegalArgumentException if the column name does not correspond to a AlbumResponse
100      * enum.
101      */
mapColumnNameToAlbumResponseColumn(String columnName)102     public static AlbumResponse mapColumnNameToAlbumResponseColumn(String columnName)
103             throws IllegalArgumentException {
104         for (AlbumResponse albumResponseColumn : AlbumResponse.values()) {
105             if (albumResponseColumn.getColumnName().equalsIgnoreCase(columnName)) {
106                 return albumResponseColumn;
107             }
108         }
109         throw new IllegalArgumentException(columnName + " does not exist. Available data: "
110                 + Arrays.toString(PickerSQLConstants.AlbumResponse.values()));
111     }
112 
113     /**
114      * An enum that holds the DB columns names and projections for the Media SQL query response.
115      */
116     enum MediaResponse {
117         MEDIA_ID(CloudMediaProviderContract.MediaColumns.ID),
118         AUTHORITY(CloudMediaProviderContract.MediaColumns.AUTHORITY),
119         MEDIA_SOURCE("media_source"),
120         WRAPPED_URI("wrapped_uri"),
121         UNWRAPPED_URI("unwrapped_uri"),
122         PICKER_ID(KEY_ID, "picker_id"),
123         DATE_TAKEN_MS(KEY_DATE_TAKEN_MS, CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS),
124         SIZE_IN_BYTES(KEY_SIZE_BYTES, CloudMediaProviderContract.MediaColumns.SIZE_BYTES),
125         MIME_TYPE(KEY_MIME_TYPE, CloudMediaProviderContract.MediaColumns.MIME_TYPE),
126         STANDARD_MIME_TYPE(KEY_STANDARD_MIME_TYPE_EXTENSION,
127                 CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION),
128         DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS);
129 
130         private static final String DEFAULT_PROJECTION = "%s AS %s";
131         @Nullable
132         private final String mColumnName;
133         @NonNull
134         private final String mProjectedName;
135 
MediaResponse(@onNull String dbColumnName, @NonNull String projectedName)136         MediaResponse(@NonNull String dbColumnName, @NonNull String projectedName) {
137             this.mColumnName = dbColumnName;
138             this.mProjectedName = projectedName;
139         }
140 
MediaResponse(@onNull String projectedName)141         MediaResponse(@NonNull String projectedName) {
142             this.mColumnName = null;
143             this.mProjectedName = projectedName;
144         }
145 
146         @Nullable
getColumnName()147         public String getColumnName() {
148             return mColumnName;
149         }
150 
151         @NonNull
getProjectedName()152         public String getProjectedName() {
153             return mProjectedName;
154         }
155 
156         @NonNull
getProjection( @ullable String localAuthority, @Nullable String cloudAuthority, @NonNull String intentAction )157         public String getProjection(
158                 @Nullable String localAuthority,
159                 @Nullable String cloudAuthority,
160                 @NonNull String intentAction
161         ) {
162             switch (this) {
163                 case WRAPPED_URI:
164                     return String.format(
165                             DEFAULT_PROJECTION,
166                             getWrappedUri(localAuthority, cloudAuthority, intentAction),
167                             mProjectedName
168                     );
169                 default:
170                     return getProjection(localAuthority, cloudAuthority);
171             }
172         }
173 
174         @NonNull
getProjection( @ullable String localAuthority, @Nullable String cloudAuthority )175         public String getProjection(
176                 @Nullable String localAuthority,
177                 @Nullable String cloudAuthority
178         ) {
179             switch (this) {
180                 case AUTHORITY:
181                     return String.format(
182                             DEFAULT_PROJECTION,
183                             getAuthority(localAuthority, cloudAuthority),
184                             mProjectedName
185                     );
186                 case UNWRAPPED_URI:
187                     return String.format(
188                             DEFAULT_PROJECTION,
189                             getUnwrappedUri(localAuthority, cloudAuthority),
190                             mProjectedName
191                     );
192                 default:
193                     return getProjection();
194             }
195         }
196 
197         @NonNull
getProjection()198         public String getProjection() {
199             switch (this) {
200                 case MEDIA_ID:
201                     return String.format(
202                             DEFAULT_PROJECTION,
203                             getMediaId(),
204                             mProjectedName
205                     );
206                 case MEDIA_SOURCE:
207                     return String.format(
208                             DEFAULT_PROJECTION,
209                             getMediaSource(),
210                             mProjectedName
211                     );
212                 default:
213                     if (mColumnName == null) {
214                         throw new IllegalArgumentException(
215                                 "Could not get projection for " + this.name()
216                         );
217                     }
218                     return String.format(DEFAULT_PROJECTION, mColumnName, mProjectedName);
219             }
220         }
221 
getMediaId()222         private String getMediaId() {
223             return String.format(
224                     "IFNULL(%s, %s)",
225                     KEY_CLOUD_ID,
226                     KEY_LOCAL_ID
227             );
228         }
229 
getMediaSource()230         private String getMediaSource() {
231             return String.format(
232                     "CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END",
233                     KEY_CLOUD_ID,
234                     MediaSource.LOCAL,
235                     MediaSource.REMOTE
236             );
237         }
238 
getAuthority( @ullable String localAuthority, @Nullable String cloudAuthority )239         private String getAuthority(
240                 @Nullable String localAuthority,
241                 @Nullable String cloudAuthority
242         ) {
243             return String.format(
244                     "CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END",
245                     KEY_CLOUD_ID,
246                     localAuthority,
247                     cloudAuthority
248             );
249         }
250 
getWrappedUri( @ullable String localAuthority, @Nullable String cloudAuthority, @NonNull String intentAction )251         private String getWrappedUri(
252                 @Nullable String localAuthority,
253                 @Nullable String cloudAuthority,
254                 @NonNull String intentAction
255         ) {
256             // The format is:
257             // content://media/picker/<user-id>/<cloud-provider-authority>/media/<media-id>
258             return String.format(
259                     "'content://%s/%s/%s/' || %s || '/media/' || %s",
260                     MediaStore.AUTHORITY,
261                     getPickerSegmentFromIntentAction(intentAction),
262                     MediaStore.MY_USER_ID,
263                     getAuthority(localAuthority, cloudAuthority),
264                     getMediaId()
265             );
266         }
267 
getUnwrappedUri( @ullable String localAuthority, @Nullable String cloudAuthority )268         private String getUnwrappedUri(
269                 @Nullable String localAuthority,
270                 @Nullable String cloudAuthority
271         ) {
272             // The format is:
273             // content://<cloud-provider-authority>/media/<media-id>
274             return String.format(
275                     "'content://%s@' || %s || '/media/' || %s",
276                     MediaStore.MY_USER_ID,
277                     getAuthority(localAuthority, cloudAuthority),
278                     getMediaId()
279             );
280         }
281     }
282 
283     enum MediaResponseExtras {
284         PREV_PAGE_ID("prev_page_picker_id"),
285         PREV_PAGE_DATE_TAKEN("prev_page_date_taken"),
286         NEXT_PAGE_ID("next_page_picker_id"),
287         NEXT_PAGE_DATE_TAKEN("next_page_date_taken");
288 
289         private final String mKey;
290 
MediaResponseExtras(String key)291         MediaResponseExtras(String key) {
292             mKey = key;
293         }
294 
getKey()295         public String getKey() {
296             return mKey;
297         }
298     }
299 }
300