1 /*
2  * Copyright (C) 2022 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;
18 
19 import android.os.Build;
20 import android.util.ArrayMap;
21 import android.util.Log;
22 
23 import androidx.annotation.GuardedBy;
24 import androidx.annotation.NonNull;
25 import androidx.annotation.Nullable;
26 
27 import java.lang.annotation.Annotation;
28 import java.lang.reflect.Field;
29 import java.util.Objects;
30 
31 /**
32  * Utility class to handle projection columns across releases, database agnostic.
33  */
34 public class ProjectionHelper {
35 
36     private static final String TAG = "ProjectionHelper";
37     @Nullable
38     private final Class<? extends Annotation> mColumnAnnotation;
39     @Nullable
40     private final Class<? extends Annotation> mExportedSinceAnnotation;
41 
ProjectionHelper(@ullable Class<? extends Annotation> columnAnnotation, @Nullable Class<? extends Annotation> exportedSinceAnnotation)42     public ProjectionHelper(@Nullable Class<? extends Annotation> columnAnnotation,
43             @Nullable Class<? extends Annotation> exportedSinceAnnotation) {
44         mColumnAnnotation = columnAnnotation;
45         mExportedSinceAnnotation = exportedSinceAnnotation;
46     }
47 
48     @GuardedBy("mProjectionMapCache")
49     private final ArrayMap<Class<?>, ArrayMap<String, String>>
50             mProjectionMapCache = new ArrayMap<>();
51 
52     /**
53      * Return a projection map that represents the valid columns that can be
54      * queried the given contract class. The mapping is built automatically
55      * using the {@link android.provider.Column} annotation, and is designed to
56      * ensure that we always support public API commitments.
57      */
getProjectionMap(Class<?>.... clazzes)58     public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) {
59         ArrayMap<String, String> result = new ArrayMap<>();
60         synchronized (mProjectionMapCache) {
61             for (Class<?> clazz : clazzes) {
62                 ArrayMap<String, String> map = mProjectionMapCache.get(clazz);
63                 if (map == null) {
64                     map = new ArrayMap<>();
65                     try {
66                         for (Field field : clazz.getFields()) {
67                             if (Objects.equals(field.getName(), "_ID") || (mColumnAnnotation != null
68                                     && field.isAnnotationPresent(mColumnAnnotation))) {
69                                 boolean shouldIgnoreByOsVersion = shouldBeIgnoredByOsVersion(field);
70                                 if (!shouldIgnoreByOsVersion) {
71                                     final String column = (String) field.get(null);
72                                     map.put(column, column);
73                                 }
74                             }
75                         }
76                     } catch (ReflectiveOperationException e) {
77                         throw new RuntimeException(e);
78                     }
79                     mProjectionMapCache.put(clazz, map);
80                 }
81                 result.putAll(map);
82             }
83             return result;
84         }
85     }
86 
shouldBeIgnoredByOsVersion(@onNull Field field)87     private boolean shouldBeIgnoredByOsVersion(@NonNull Field field) {
88         if (mExportedSinceAnnotation == null) {
89             return false;
90         }
91 
92         if (!field.isAnnotationPresent(mExportedSinceAnnotation)) {
93             return false;
94         }
95 
96         try {
97             final Annotation annotation = field.getAnnotation(mExportedSinceAnnotation);
98             final int exportedSinceOSVersion = (int) annotation.annotationType().getMethod(
99                     "osVersion").invoke(annotation);
100             final boolean shouldIgnore = exportedSinceOSVersion > Build.VERSION.SDK_INT;
101             if (shouldIgnore) {
102                 Log.d(TAG, "Ignoring column " + field.get(null) + " with version "
103                         + exportedSinceOSVersion + " in OS version " + Build.VERSION.SDK_INT);
104             }
105             return shouldIgnore;
106         } catch (Exception e) {
107             Log.e(TAG, "Can't parse the OS version in ExportedSince annotation", e);
108             return false;
109         }
110     }
111 
112     /**
113      * @return whether a column annotation has been defined for the helper.
114      */
hasColumnAnnotation()115     public boolean hasColumnAnnotation() {
116         return mColumnAnnotation != null;
117     }
118 }
119