• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.pm.PackageManager;
28 import android.database.Cursor;
29 import android.net.Uri;
30 import android.os.Binder;
31 import android.os.Bundle;
32 import android.os.Process;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import android.util.Pair;
36 
37 import com.android.launcher3.LauncherSettings.Favorites;
38 import com.android.launcher3.model.ModelDbController;
39 import com.android.launcher3.util.LayoutImportExportHelper;
40 import com.android.launcher3.widget.LauncherWidgetHolder;
41 
42 import java.io.FileDescriptor;
43 import java.io.PrintWriter;
44 import java.util.concurrent.CompletableFuture;
45 import java.util.concurrent.ExecutionException;
46 import java.util.function.ToIntFunction;
47 
48 public class LauncherProvider extends ContentProvider {
49     private static final String TAG = "LauncherProvider";
50 
51     // Method API For Provider#call method.
52     private static final String METHOD_EXPORT_LAYOUT_XML = "EXPORT_LAYOUT_XML";
53     private static final String METHOD_IMPORT_LAYOUT_XML = "IMPORT_LAYOUT_XML";
54     private static final String KEY_RESULT = "KEY_RESULT";
55     private static final String KEY_LAYOUT = "KEY_LAYOUT";
56     private static final String SUCCESS = "success";
57     private static final String FAILURE = "failure";
58 
59     /**
60      * $ adb shell dumpsys activity provider com.android.launcher3
61      */
62     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)63     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
64         LauncherModel model = LauncherAppState.INSTANCE.get(getContext()).getModel();
65         if (model.isModelLoaded()) {
66             model.dumpState("", fd, writer, args);
67         }
68     }
69 
70     @Override
onCreate()71     public boolean onCreate() {
72         return true;
73     }
74 
75     @Override
getType(Uri uri)76     public String getType(Uri uri) {
77         if (TextUtils.isEmpty(parseUri(uri, null, null).first)) {
78             return "vnd.android.cursor.dir/" + Favorites.TABLE_NAME;
79         } else {
80             return "vnd.android.cursor.item/" + Favorites.TABLE_NAME;
81         }
82     }
83 
84     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)85     public Cursor query(Uri uri, String[] projection, String selection,
86             String[] selectionArgs, String sortOrder) {
87         Pair<String, String[]> args = parseUri(uri, selection, selectionArgs);
88         Cursor[] result = new Cursor[1];
89         executeControllerTask(controller -> {
90             result[0] = controller.query(projection, args.first, args.second, sortOrder);
91             return 0;
92         });
93         return result[0];
94     }
95 
96     @Override
insert(Uri uri, ContentValues values)97     public Uri insert(Uri uri, ContentValues values) {
98         int rowId = executeControllerTask(controller -> {
99             // 1. Ensure that externally added items have a valid item id
100             int id = controller.generateNewItemId();
101             values.put(LauncherSettings.Favorites._ID, id);
102 
103             // 2. In the case of an app widget, and if no app widget id is specified, we
104             // attempt allocate and bind the widget.
105             Integer itemType = values.getAsInteger(Favorites.ITEM_TYPE);
106             if (itemType != null
107                     && itemType == Favorites.ITEM_TYPE_APPWIDGET
108                     && !values.containsKey(Favorites.APPWIDGET_ID)) {
109 
110                 ComponentName cn = ComponentName.unflattenFromString(
111                         values.getAsString(Favorites.APPWIDGET_PROVIDER));
112                 if (cn == null) {
113                     return 0;
114                 }
115 
116                 LauncherWidgetHolder widgetHolder = LauncherWidgetHolder.newInstance(getContext());
117                 try {
118                     int appWidgetId = widgetHolder.allocateAppWidgetId();
119                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
120                     if (!AppWidgetManager.getInstance(getContext())
121                             .bindAppWidgetIdIfAllowed(appWidgetId, cn)) {
122                         widgetHolder.deleteAppWidgetId(appWidgetId);
123                         return 0;
124                     }
125                 } catch (RuntimeException e) {
126                     Log.e(TAG, "Failed to initialize external widget", e);
127                     return 0;
128                 } finally {
129                     // Necessary to destroy the holder to free up possible activity context
130                     widgetHolder.destroy();
131                 }
132             }
133 
134             return controller.insert(values);
135         });
136 
137         return rowId < 0 ? null : ContentUris.withAppendedId(uri, rowId);
138     }
139 
140     @Override
delete(Uri uri, String selection, String[] selectionArgs)141     public int delete(Uri uri, String selection, String[] selectionArgs) {
142         Pair<String, String[]> args = parseUri(uri, selection, selectionArgs);
143         return executeControllerTask(c -> c.delete(args.first, args.second));
144     }
145 
146     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)147     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
148         Pair<String, String[]> args = parseUri(uri, selection, selectionArgs);
149         return executeControllerTask(c -> c.update(values, args.first, args.second));
150     }
151 
152     @Override
call(String method, String arg, Bundle extras)153     public Bundle call(String method, String arg, Bundle extras) {
154         Bundle b = new Bundle();
155 
156         // The caller must have the read or write permission for this content provider to
157         // access the "call" method at all. We also enforce the appropriate per-method permissions.
158         switch(method) {
159             case METHOD_EXPORT_LAYOUT_XML:
160                 if (getContext().checkCallingOrSelfPermission(getReadPermission())
161                         != PackageManager.PERMISSION_GRANTED) {
162                     throw new SecurityException("Caller doesn't have read permission");
163                 }
164 
165                 CompletableFuture<String> resultFuture = LayoutImportExportHelper.INSTANCE
166                         .exportModelDbAsXmlFuture(getContext());
167                 try {
168                     b.putString(KEY_LAYOUT, resultFuture.get());
169                     b.putString(KEY_RESULT, SUCCESS);
170                 } catch (ExecutionException | InterruptedException e) {
171                     b.putString(KEY_RESULT, FAILURE);
172                 }
173                 return b;
174 
175             case METHOD_IMPORT_LAYOUT_XML:
176                 if (getContext().checkCallingOrSelfPermission(getWritePermission())
177                         != PackageManager.PERMISSION_GRANTED) {
178                     throw new SecurityException("Caller doesn't have write permission");
179                 }
180 
181                 LayoutImportExportHelper.INSTANCE.importModelFromXml(getContext(), arg);
182                 b.putString(KEY_RESULT, SUCCESS);
183                 return b;
184             default:
185                 return null;
186         }
187     }
188 
executeControllerTask(ToIntFunction<ModelDbController> task)189     private int executeControllerTask(ToIntFunction<ModelDbController> task) {
190         if (Binder.getCallingPid() == Process.myPid()) {
191             throw new IllegalArgumentException("Same process should call model directly");
192         }
193         try {
194             return MODEL_EXECUTOR.submit(() -> {
195                 LauncherModel model = LauncherAppState.getInstance(getContext()).getModel();
196                 int count = task.applyAsInt(model.getModelDbController());
197                 if (count > 0) {
198                     MAIN_EXECUTOR.submit(model::forceReload);
199                 }
200                 return count;
201             }).get();
202         } catch (Exception e) {
203             throw new IllegalStateException(e);
204         }
205     }
206 
207     /**
208      * Parses the uri and returns the where and arg clause.
209      *
210      * Note: This should be called on the binder thread (before posting on any executor) so that
211      * any parsing error gets propagated to the caller.
212      */
parseUri(Uri url, String where, String[] args)213     private static Pair<String, String[]> parseUri(Uri url, String where, String[] args) {
214         switch (url.getPathSegments().size()) {
215             case 1 -> {
216                 return Pair.create(where, args);
217             }
218             case 2 -> {
219                 if (!TextUtils.isEmpty(where)) {
220                     throw new UnsupportedOperationException("WHERE clause not supported: " + url);
221                 }
222                 return Pair.create("_id=" + ContentUris.parseId(url), null);
223             }
224             default -> throw new IllegalArgumentException("Invalid URI: " + url);
225         }
226     }
227 }
228