1 /* 2 * Copyright (C) 2019 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.widget.custom; 18 19 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget; 20 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID; 21 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 22 import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX; 23 24 import android.appwidget.AppWidgetManager; 25 import android.appwidget.AppWidgetProviderInfo; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.os.Parcel; 29 import android.os.Process; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.launcher3.R; 37 import com.android.launcher3.dagger.ApplicationContext; 38 import com.android.launcher3.dagger.LauncherAppSingleton; 39 import com.android.launcher3.dagger.LauncherBaseAppComponent; 40 import com.android.launcher3.util.DaggerSingletonObject; 41 import com.android.launcher3.util.DaggerSingletonTracker; 42 import com.android.launcher3.util.PluginManagerWrapper; 43 import com.android.launcher3.util.SafeCloseable; 44 import com.android.launcher3.widget.LauncherAppWidgetHostView; 45 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 46 import com.android.systemui.plugins.CustomWidgetPlugin; 47 import com.android.systemui.plugins.PluginListener; 48 49 import java.lang.reflect.InvocationTargetException; 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.concurrent.CopyOnWriteArrayList; 55 import java.util.stream.Stream; 56 57 import javax.inject.Inject; 58 59 /** 60 * CustomWidgetManager handles custom widgets implemented as a plugin. 61 */ 62 @LauncherAppSingleton 63 public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin> { 64 65 public static final DaggerSingletonObject<CustomWidgetManager> INSTANCE = 66 new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager); 67 68 private static final String TAG = "CustomWidgetManager"; 69 private static final String PLUGIN_PKG = "android"; 70 private final Context mContext; 71 private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins; 72 private final List<CustomAppWidgetProviderInfo> mCustomWidgets; 73 private final List<Runnable> mWidgetRefreshCallbacks = new CopyOnWriteArrayList<>(); 74 private final @NonNull AppWidgetManager mAppWidgetManager; 75 76 @Inject CustomWidgetManager(@pplicationContext Context context, PluginManagerWrapper pluginManager, DaggerSingletonTracker tracker)77 CustomWidgetManager(@ApplicationContext Context context, PluginManagerWrapper pluginManager, 78 DaggerSingletonTracker tracker) { 79 this(context, pluginManager, AppWidgetManager.getInstance(context), tracker); 80 } 81 82 @VisibleForTesting CustomWidgetManager(@pplicationContext Context context, PluginManagerWrapper pluginManager, @NonNull AppWidgetManager widgetManager, DaggerSingletonTracker tracker)83 CustomWidgetManager(@ApplicationContext Context context, 84 PluginManagerWrapper pluginManager, 85 @NonNull AppWidgetManager widgetManager, 86 DaggerSingletonTracker tracker) { 87 mContext = context; 88 mAppWidgetManager = widgetManager; 89 mPlugins = new HashMap<>(); 90 mCustomWidgets = new ArrayList<>(); 91 92 pluginManager.addPluginListener(this, CustomWidgetPlugin.class, true); 93 if (enableSmartspaceAsAWidget()) { 94 for (String s: context.getResources() 95 .getStringArray(R.array.custom_widget_providers)) { 96 try { 97 Class<?> cls = Class.forName(s); 98 CustomWidgetPlugin plugin = (CustomWidgetPlugin) 99 cls.getDeclaredConstructor(Context.class).newInstance(context); 100 MAIN_EXECUTOR.execute(() -> onPluginConnected(plugin, context)); 101 } catch (ClassNotFoundException | InstantiationException 102 | IllegalAccessException 103 | ClassCastException | NoSuchMethodException 104 | InvocationTargetException e) { 105 Log.e(TAG, "Exception found when trying to add custom widgets: " + e); 106 } 107 } 108 } 109 tracker.addCloseable(() -> pluginManager.removePluginListener(this)); 110 } 111 112 @Override onPluginConnected(CustomWidgetPlugin plugin, Context context)113 public void onPluginConnected(CustomWidgetPlugin plugin, Context context) { 114 CustomAppWidgetProviderInfo info = getAndAddInfo(new ComponentName( 115 PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName())); 116 if (info != null) { 117 plugin.updateWidgetInfo(info, mContext); 118 mPlugins.put(info.provider, plugin); 119 mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute); 120 } 121 } 122 123 @Override onPluginDisconnected(CustomWidgetPlugin plugin)124 public void onPluginDisconnected(CustomWidgetPlugin plugin) { 125 // Leave the providerInfo as plugins can get disconnected/reconnected multiple times 126 mPlugins.values().remove(plugin); 127 mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute); 128 } 129 130 @VisibleForTesting 131 @NonNull getPlugins()132 Map<ComponentName, CustomWidgetPlugin> getPlugins() { 133 return mPlugins; 134 } 135 136 /** 137 * Inject a callback function to refresh the widgets. 138 * @return a closeable to remove this callback 139 */ addWidgetRefreshCallback(Runnable callback)140 public SafeCloseable addWidgetRefreshCallback(Runnable callback) { 141 mWidgetRefreshCallbacks.add(callback); 142 return () -> mWidgetRefreshCallbacks.remove(callback); 143 } 144 145 /** 146 * Callback method to inform a plugin it's corresponding widget has been created. 147 */ onViewCreated(LauncherAppWidgetHostView view)148 public void onViewCreated(LauncherAppWidgetHostView view) { 149 CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo(); 150 CustomWidgetPlugin plugin = mPlugins.get(info.provider); 151 if (plugin != null) { 152 plugin.onViewCreated(view); 153 } 154 } 155 156 /** 157 * Returns the stream of custom widgets. 158 */ 159 @NonNull stream()160 public Stream<CustomAppWidgetProviderInfo> stream() { 161 return mCustomWidgets.stream(); 162 } 163 164 /** 165 * Returns the widget provider in respect to given widget id. 166 */ 167 @Nullable getWidgetProvider(ComponentName cn)168 public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName cn) { 169 LauncherAppWidgetProviderInfo info = mCustomWidgets.stream() 170 .filter(w -> w.getComponent().equals(cn)).findAny().orElse(null); 171 if (info == null) { 172 // If the info is not present, add a placeholder info since the 173 // plugin might get loaded later 174 info = getAndAddInfo(cn); 175 } 176 return info; 177 } 178 179 /** 180 * Returns an id to set as the appWidgetId for a custom widget. 181 */ allocateCustomAppWidgetId(ComponentName componentName)182 public int allocateCustomAppWidgetId(ComponentName componentName) { 183 return CUSTOM_WIDGET_ID - mCustomWidgets.indexOf(getWidgetProvider(componentName)); 184 } 185 186 @Nullable getAndAddInfo(ComponentName cn)187 private CustomAppWidgetProviderInfo getAndAddInfo(ComponentName cn) { 188 for (CustomAppWidgetProviderInfo info : mCustomWidgets) { 189 if (info.provider.equals(cn)) return info; 190 } 191 192 List<AppWidgetProviderInfo> providers = mAppWidgetManager 193 .getInstalledProvidersForProfile(Process.myUserHandle()); 194 if (providers.isEmpty()) return null; 195 Parcel parcel = Parcel.obtain(); 196 providers.get(0).writeToParcel(parcel, 0); 197 parcel.setDataPosition(0); 198 CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false); 199 parcel.recycle(); 200 201 info.provider = cn; 202 info.initialLayout = 0; 203 mCustomWidgets.add(info); 204 return info; 205 } 206 } 207