1 /*
2  * Copyright (C) 2008 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.launcher3;
18 
19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
21 
22 import android.appwidget.AppWidgetManager;
23 import android.content.ComponentName;
24 import android.content.ContentProvider;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.database.Cursor;
28 import android.database.sqlite.SQLiteQueryBuilder;
29 import android.net.Uri;
30 import android.os.Binder;
31 import android.os.Process;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.launcher3.LauncherSettings.Favorites;
36 import com.android.launcher3.model.ModelDbController;
37 import com.android.launcher3.widget.LauncherWidgetHolder;
38 
39 import java.io.FileDescriptor;
40 import java.io.PrintWriter;
41 import java.util.function.ToIntFunction;
42 
43 public class LauncherProvider extends ContentProvider {
44     private static final String TAG = "LauncherProvider";
45 
46     /**
47      * $ adb shell dumpsys activity provider com.android.launcher3
48      */
49     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)50     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
51         LauncherAppState.INSTANCE.executeIfCreated(appState -> {
52             if (appState.getModel().isModelLoaded()) {
53                 appState.getModel().dumpState("", fd, writer, args);
54             }
55         });
56     }
57 
58     @Override
onCreate()59     public boolean onCreate() {
60         return true;
61     }
62 
63     @Override
getType(Uri uri)64     public String getType(Uri uri) {
65         SqlArguments args = new SqlArguments(uri, null, null);
66         if (TextUtils.isEmpty(args.where)) {
67             return "vnd.android.cursor.dir/" + args.table;
68         } else {
69             return "vnd.android.cursor.item/" + args.table;
70         }
71     }
72 
73     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)74     public Cursor query(Uri uri, String[] projection, String selection,
75             String[] selectionArgs, String sortOrder) {
76         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
77         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
78         qb.setTables(args.table);
79 
80         Cursor[] result = new Cursor[1];
81         executeControllerTask(controller -> {
82             result[0] = controller.query(args.table, projection, args.where, args.args, sortOrder);
83             return 0;
84         });
85         return result[0];
86     }
87 
88     @Override
insert(Uri uri, ContentValues values)89     public Uri insert(Uri uri, ContentValues values) {
90         int rowId = executeControllerTask(controller -> {
91             // 1. Ensure that externally added items have a valid item id
92             int id = controller.generateNewItemId();
93             values.put(LauncherSettings.Favorites._ID, id);
94 
95             // 2. In the case of an app widget, and if no app widget id is specified, we
96             // attempt allocate and bind the widget.
97             Integer itemType = values.getAsInteger(Favorites.ITEM_TYPE);
98             if (itemType != null
99                     && itemType.intValue() == Favorites.ITEM_TYPE_APPWIDGET
100                     && !values.containsKey(Favorites.APPWIDGET_ID)) {
101 
102                 ComponentName cn = ComponentName.unflattenFromString(
103                         values.getAsString(Favorites.APPWIDGET_PROVIDER));
104                 if (cn == null) {
105                     return 0;
106                 }
107 
108                 LauncherWidgetHolder widgetHolder = LauncherWidgetHolder.newInstance(getContext());
109                 try {
110                     int appWidgetId = widgetHolder.allocateAppWidgetId();
111                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
112                     if (!AppWidgetManager.getInstance(getContext())
113                             .bindAppWidgetIdIfAllowed(appWidgetId, cn)) {
114                         widgetHolder.deleteAppWidgetId(appWidgetId);
115                         return 0;
116                     }
117                 } catch (RuntimeException e) {
118                     Log.e(TAG, "Failed to initialize external widget", e);
119                     return 0;
120                 } finally {
121                     // Necessary to destroy the holder to free up possible activity context
122                     widgetHolder.destroy();
123                 }
124             }
125 
126             SqlArguments args = new SqlArguments(uri);
127             return controller.insert(args.table, values);
128         });
129 
130         return rowId < 0 ? null : ContentUris.withAppendedId(uri, rowId);
131     }
132 
133     @Override
delete(Uri uri, String selection, String[] selectionArgs)134     public int delete(Uri uri, String selection, String[] selectionArgs) {
135         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
136         return executeControllerTask(c -> c.delete(args.table, args.where, args.args));
137     }
138 
139     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)140     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
141         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
142         return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
143     }
144 
executeControllerTask(ToIntFunction<ModelDbController> task)145     private int executeControllerTask(ToIntFunction<ModelDbController> task) {
146         if (Binder.getCallingPid() == Process.myPid()) {
147             throw new IllegalArgumentException("Same process should call model directly");
148         }
149         try {
150             return MODEL_EXECUTOR.submit(() -> {
151                 LauncherModel model = LauncherAppState.getInstance(getContext()).getModel();
152                 int count = task.applyAsInt(model.getModelDbController());
153                 if (count > 0) {
154                     MAIN_EXECUTOR.submit(model::forceReload);
155                 }
156                 return count;
157             }).get();
158         } catch (Exception e) {
159             throw new IllegalStateException(e);
160         }
161     }
162 
163     static class SqlArguments {
164         public final String table;
165         public final String where;
166         public final String[] args;
167 
SqlArguments(Uri url, String where, String[] args)168         SqlArguments(Uri url, String where, String[] args) {
169             if (url.getPathSegments().size() == 1) {
170                 this.table = url.getPathSegments().get(0);
171                 this.where = where;
172                 this.args = args;
173             } else if (url.getPathSegments().size() != 2) {
174                 throw new IllegalArgumentException("Invalid URI: " + url);
175             } else if (!TextUtils.isEmpty(where)) {
176                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
177             } else {
178                 this.table = url.getPathSegments().get(0);
179                 this.where = "_id=" + ContentUris.parseId(url);
180                 this.args = null;
181             }
182         }
183 
SqlArguments(Uri url)184         SqlArguments(Uri url) {
185             if (url.getPathSegments().size() == 1) {
186                 table = url.getPathSegments().get(0);
187                 where = null;
188                 args = null;
189             } else {
190                 throw new IllegalArgumentException("Invalid URI: " + url);
191             }
192         }
193     }
194 }
195