1 /*
2  * Copyright (C) 2017 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.settings.slices;
18 
19 import static com.android.settings.slices.SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX;
20 
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.database.sqlite.SQLiteDatabase;
24 import android.net.Uri;
25 import android.os.Binder;
26 import android.text.TextUtils;
27 import android.util.Pair;
28 
29 import androidx.slice.Slice;
30 
31 import com.android.settings.overlay.FeatureFactory;
32 import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 
37 /**
38  * Class used to map a {@link Uri} from {@link SettingsSliceProvider} to a Slice.
39  */
40 public class SlicesDatabaseAccessor {
41 
42     public static final String[] SELECT_COLUMNS_ALL = {
43             IndexColumns.KEY,
44             IndexColumns.TITLE,
45             IndexColumns.SUMMARY,
46             IndexColumns.SCREENTITLE,
47             IndexColumns.KEYWORDS,
48             IndexColumns.ICON_RESOURCE,
49             IndexColumns.FRAGMENT,
50             IndexColumns.CONTROLLER,
51             IndexColumns.SLICE_TYPE,
52             IndexColumns.UNAVAILABLE_SLICE_SUBTITLE,
53             IndexColumns.HIGHLIGHT_MENU_RESOURCE,
54             IndexColumns.USER_RESTRICTION,
55     };
56 
57     private final Context mContext;
58     private final SlicesDatabaseHelper mHelper;
59 
SlicesDatabaseAccessor(Context context)60     public SlicesDatabaseAccessor(Context context) {
61         mContext = context;
62         mHelper = SlicesDatabaseHelper.getInstance(mContext);
63     }
64 
65     /**
66      * Query the slices database and return a {@link SliceData} object corresponding to the row
67      * matching the key provided by the {@param uri}. Additionally adds the {@param uri} to the
68      * {@link SliceData} object so the {@link Slice} can bind to the {@link Uri}.
69      * Used when building a {@link Slice}.
70      */
getSliceDataFromUri(Uri uri)71     public SliceData getSliceDataFromUri(Uri uri) {
72         Pair<Boolean, String> pathData = SliceBuilderUtils.getPathData(uri);
73         if (pathData == null) {
74             throw new IllegalStateException("Invalid Slices uri: " + uri);
75         }
76         try (Cursor cursor = getIndexedSliceData(pathData.second /* key */)) {
77             return buildSliceData(cursor, uri, pathData.first /* isIntentOnly */);
78         }
79     }
80 
81     /**
82      * Query the slices database and return a {@link SliceData} object corresponding to the row
83      * matching the {@param key}.
84      * Used when handling the action of the {@link Slice}.
85      */
getSliceDataFromKey(String key)86     public SliceData getSliceDataFromKey(String key) {
87         try (Cursor cursor = getIndexedSliceData(key)) {
88             return buildSliceData(cursor, null /* uri */, false /* isIntentOnly */);
89         }
90     }
91 
92     /**
93      * @return a list of Slice {@link Uri}s based on their visibility {@param isPublicSlice } and
94      * {@param authority}.
95      */
getSliceUris(String authority, boolean isPublicSlice)96     public List<Uri> getSliceUris(String authority, boolean isPublicSlice) {
97         verifyIndexing();
98         final List<Uri> uris = new ArrayList<>();
99         final String whereClause = IndexColumns.PUBLIC_SLICE + (isPublicSlice ? "=1" : "=0");
100         final SQLiteDatabase database = mHelper.getReadableDatabase();
101         final String[] columns = new String[]{IndexColumns.SLICE_URI};
102         try (Cursor resultCursor = database.query(TABLE_SLICES_INDEX, columns,
103                 whereClause /* where */, null /* selection */, null /* groupBy */,
104                 null /* having */, null /* orderBy */)) {
105             if (!resultCursor.moveToFirst()) {
106                 return uris;
107             }
108 
109             do {
110                 final Uri uri = Uri.parse(resultCursor.getString(0 /* SLICE_URI */));
111                 if (TextUtils.isEmpty(authority)
112                         || TextUtils.equals(authority, uri.getAuthority())) {
113                     uris.add(uri);
114                 }
115             } while (resultCursor.moveToNext());
116         }
117         return uris;
118     }
119 
getIndexedSliceData(String path)120     private Cursor getIndexedSliceData(String path) {
121         verifyIndexing();
122 
123         final String whereClause = buildKeyMatchWhereClause();
124         final SQLiteDatabase database = mHelper.getReadableDatabase();
125         final String[] selection = new String[]{path};
126         final Cursor resultCursor = database.query(TABLE_SLICES_INDEX, SELECT_COLUMNS_ALL,
127                 whereClause, selection, null /* groupBy */, null /* having */, null /* orderBy */);
128 
129         int numResults = resultCursor.getCount();
130 
131         if (numResults == 0) {
132             resultCursor.close();
133             throw new IllegalStateException("Invalid Slices key from path: " + path);
134         }
135 
136         if (numResults > 1) {
137             resultCursor.close();
138             throw new IllegalStateException(
139                     "Should not match more than 1 slice with path: " + path);
140         }
141 
142         resultCursor.moveToFirst();
143         return resultCursor;
144     }
145 
buildKeyMatchWhereClause()146     private String buildKeyMatchWhereClause() {
147         return new StringBuilder(IndexColumns.KEY)
148                 .append(" = ?")
149                 .toString();
150     }
151 
buildSliceData(Cursor cursor, Uri uri, boolean isIntentOnly)152     private static SliceData buildSliceData(Cursor cursor, Uri uri, boolean isIntentOnly) {
153         final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY));
154         final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE));
155         final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY));
156         final String screenTitle = cursor.getString(
157                 cursor.getColumnIndex(IndexColumns.SCREENTITLE));
158         final String keywords = cursor.getString(cursor.getColumnIndex(IndexColumns.KEYWORDS));
159         final int iconResource = cursor.getInt(cursor.getColumnIndex(IndexColumns.ICON_RESOURCE));
160         final String fragmentClassName = cursor.getString(
161                 cursor.getColumnIndex(IndexColumns.FRAGMENT));
162         final String controllerClassName = cursor.getString(
163                 cursor.getColumnIndex(IndexColumns.CONTROLLER));
164         int sliceType = cursor.getInt(
165                 cursor.getColumnIndex(IndexColumns.SLICE_TYPE));
166         final String unavailableSliceSubtitle = cursor.getString(
167                 cursor.getColumnIndex(IndexColumns.UNAVAILABLE_SLICE_SUBTITLE));
168         final int highlightMenuRes = cursor.getInt(
169                 cursor.getColumnIndex(IndexColumns.HIGHLIGHT_MENU_RESOURCE));
170         final String userRestriction = cursor.getString(
171                 cursor.getColumnIndex(IndexColumns.USER_RESTRICTION));
172 
173         if (isIntentOnly) {
174             sliceType = SliceData.SliceType.INTENT;
175         }
176 
177         return new SliceData.Builder()
178                 .setKey(key)
179                 .setTitle(title)
180                 .setSummary(summary)
181                 .setScreenTitle(screenTitle)
182                 .setKeywords(keywords)
183                 .setIcon(iconResource)
184                 .setFragmentName(fragmentClassName)
185                 .setPreferenceControllerClassName(controllerClassName)
186                 .setUri(uri)
187                 .setSliceType(sliceType)
188                 .setUnavailableSliceSubtitle(unavailableSliceSubtitle)
189                 .setHighlightMenuRes(highlightMenuRes)
190                 .setUserRestriction(userRestriction)
191                 .build();
192     }
193 
verifyIndexing()194     private void verifyIndexing() {
195         final long uidToken = Binder.clearCallingIdentity();
196         try {
197             FeatureFactory.getFeatureFactory().getSlicesFeatureProvider().indexSliceData(mContext);
198         } finally {
199             Binder.restoreCallingIdentity(uidToken);
200         }
201     }
202 }