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 android.content.Context;
20 import android.database.sqlite.SQLiteDatabase;
21 import android.database.sqlite.SQLiteOpenHelper;
22 import android.os.Build;
23 import android.util.Log;
24 
25 import androidx.annotation.VisibleForTesting;
26 
27 import java.util.Locale;
28 
29 /**
30  * Defines the schema for the Slices database.
31  */
32 public class SlicesDatabaseHelper extends SQLiteOpenHelper {
33 
34     private static final String TAG = "SlicesDatabaseHelper";
35 
36     private static final String DATABASE_NAME = "slices_index.db";
37     private static final String SHARED_PREFS_TAG = "slices_shared_prefs";
38 
39     private static final int DATABASE_VERSION = 10;
40 
41     public interface Tables {
42         String TABLE_SLICES_INDEX = "slices_index";
43     }
44 
45     public interface IndexColumns {
46         /**
47          * Primary key of the DB. Preference key from preference controllers.
48          */
49         String KEY = "key";
50 
51         /**
52          * Title of the Setting.
53          */
54         String TITLE = "title";
55 
56         /**
57          * Summary / Subtitle for the setting.
58          */
59         String SUMMARY = "summary";
60 
61         /**
62          * Title of the Setting screen on which the Setting lives.
63          */
64         String SCREENTITLE = "screentitle";
65 
66         /**
67          * String with a comma separated list of keywords relating to the Slice.
68          */
69         String KEYWORDS = "keywords";
70 
71         /**
72          * Resource ID for the icon of the setting. Should be 0 for no icon.
73          */
74         String ICON_RESOURCE = "icon";
75 
76         /**
77          * Classname of the fragment name of the page that hosts the setting.
78          */
79         String FRAGMENT = "fragment";
80 
81         /**
82          * Class name of the controller backing the setting. Must be a
83          * {@link com.android.settings.core.BasePreferenceController}.
84          */
85         String CONTROLLER = "controller";
86 
87         /**
88          * {@link SliceData.SliceType} representing the inline type of the result.
89          */
90         String SLICE_TYPE = "slice_type";
91 
92         /**
93          * Customized subtitle if it's a unavailable slice
94          */
95         String UNAVAILABLE_SLICE_SUBTITLE = "unavailable_slice_subtitle";
96 
97         /**
98          * The uri of slice.
99          */
100         String SLICE_URI = "slice_uri";
101 
102         /**
103          * Whether the slice should be exposed publicly.
104          */
105         String PUBLIC_SLICE = "public_slice";
106 
107         /**
108          * Resource ID for the menu entry of the setting.
109          */
110         String HIGHLIGHT_MENU_RESOURCE = "highlight_menu";
111 
112         /**
113          * The name of user restriction for the setting.
114          */
115         String USER_RESTRICTION = "user_restriction";
116     }
117 
118     private static final String CREATE_SLICES_TABLE =
119             "CREATE VIRTUAL TABLE " + Tables.TABLE_SLICES_INDEX + " USING fts4"
120                     + "("
121                     + IndexColumns.KEY
122                     + ", "
123                     + IndexColumns.SLICE_URI
124                     + ", "
125                     + IndexColumns.TITLE
126                     + ", "
127                     + IndexColumns.SUMMARY
128                     + ", "
129                     + IndexColumns.SCREENTITLE
130                     + ", "
131                     + IndexColumns.KEYWORDS
132                     + ", "
133                     + IndexColumns.ICON_RESOURCE
134                     + ", "
135                     + IndexColumns.FRAGMENT
136                     + ", "
137                     + IndexColumns.CONTROLLER
138                     + ", "
139                     + IndexColumns.SLICE_TYPE
140                     + ", "
141                     + IndexColumns.UNAVAILABLE_SLICE_SUBTITLE
142                     + ", "
143                     + IndexColumns.PUBLIC_SLICE
144                     + ", "
145                     + IndexColumns.HIGHLIGHT_MENU_RESOURCE
146                     + ", "
147                     + IndexColumns.USER_RESTRICTION
148                     + " INTEGER DEFAULT 0 "
149                     + ");";
150 
151     private final Context mContext;
152 
153     private static SlicesDatabaseHelper sSingleton;
154 
getInstance(Context context)155     public static synchronized SlicesDatabaseHelper getInstance(Context context) {
156         if (sSingleton == null) {
157             sSingleton = new SlicesDatabaseHelper(context.getApplicationContext());
158         }
159         return sSingleton;
160     }
161 
SlicesDatabaseHelper(Context context)162     private SlicesDatabaseHelper(Context context) {
163         super(context, DATABASE_NAME, null /* CursorFactor */, DATABASE_VERSION);
164         mContext = context;
165     }
166 
167     @Override
onCreate(SQLiteDatabase db)168     public void onCreate(SQLiteDatabase db) {
169         createDatabases(db);
170     }
171 
172     @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)173     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
174         if (oldVersion < DATABASE_VERSION) {
175             Log.d(TAG, "Reconstructing DB from " + oldVersion + " to " + newVersion);
176             reconstruct(db);
177         }
178     }
179 
180     /**
181      * Drops the currently stored databases rebuilds them.
182      * Also un-marks the state of the data such that any subsequent call to
183      * {@link#isNewIndexingState(Context)} will return {@code true}.
184      */
reconstruct(SQLiteDatabase db)185     void reconstruct(SQLiteDatabase db) {
186         mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
187                 .edit()
188                 .clear()
189                 .apply();
190         dropTables(db);
191         createDatabases(db);
192     }
193 
194     /**
195      * Marks the current state of the device for the validity of the data. Should be called after
196      * a full index of the TABLE_SLICES_INDEX.
197      */
setIndexedState()198     public void setIndexedState() {
199         setBuildIndexed();
200         setLocaleIndexed();
201     }
202 
203     /**
204      * Indicates if the indexed slice data reflects the current state of the phone.
205      *
206      * @return {@code true} if database should be rebuilt, {@code false} otherwise.
207      */
isSliceDataIndexed()208     public boolean isSliceDataIndexed() {
209         return isBuildIndexed() && isLocaleIndexed();
210     }
211 
createDatabases(SQLiteDatabase db)212     private void createDatabases(SQLiteDatabase db) {
213         db.execSQL(CREATE_SLICES_TABLE);
214         Log.d(TAG, "Created databases");
215     }
216 
dropTables(SQLiteDatabase db)217     private void dropTables(SQLiteDatabase db) {
218         db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SLICES_INDEX);
219     }
220 
setBuildIndexed()221     private void setBuildIndexed() {
222         mContext.getSharedPreferences(SHARED_PREFS_TAG, 0 /* mode */)
223                 .edit()
224                 .putBoolean(getBuildTag(), true /* value */)
225                 .apply();
226     }
227 
setLocaleIndexed()228     private void setLocaleIndexed() {
229         mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE)
230                 .edit()
231                 .putBoolean(Locale.getDefault().toString(), true /* value */)
232                 .apply();
233     }
234 
isBuildIndexed()235     private boolean isBuildIndexed() {
236         return mContext.getSharedPreferences(SHARED_PREFS_TAG,
237                 Context.MODE_PRIVATE)
238                 .getBoolean(getBuildTag(), false /* default */);
239     }
240 
isLocaleIndexed()241     private boolean isLocaleIndexed() {
242         return mContext.getSharedPreferences(SHARED_PREFS_TAG,
243                 Context.MODE_PRIVATE)
244                 .getBoolean(Locale.getDefault().toString(), false /* default */);
245     }
246 
247     @VisibleForTesting
getBuildTag()248     String getBuildTag() {
249         return Build.FINGERPRINT;
250     }
251 }