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