diff --git a/Android.mk b/Android.mk index 727c541c4d..b8e6c858f9 100644 --- a/Android.mk +++ b/Android.mk @@ -28,6 +28,35 @@ LOCAL_UNINSTALLABLE_MODULE := true LOCAL_SDK_VERSION := current include $(BUILD_PREBUILT) +include $(CLEAR_VARS) +LOCAL_MODULE := libPluginCore +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_SRC_FILES := libs/plugin_core.jar +LOCAL_UNINSTALLABLE_MODULE := true +LOCAL_SDK_VERSION := current +include $(BUILD_PREBUILT) + +# +# Build rule for plugin lib (needed to write a plugin). +# +include $(CLEAR_VARS) +LOCAL_USE_AAPT2 := true +LOCAL_AAPT2_ONLY := true +LOCAL_MODULE_TAGS := optional + +LOCAL_STATIC_JAVA_LIBRARIES := libPluginCore + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src_plugins) + +LOCAL_SDK_VERSION := current +LOCAL_MIN_SDK_VERSION := 28 +LOCAL_MODULE := LauncherPluginLib +LOCAL_PRIVILEGED_MODULE := true + +include $(BUILD_STATIC_JAVA_LIBRARY) + # # Build rule for Launcher3 dependencies lib. # @@ -40,6 +69,8 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \ androidx.recyclerview_recyclerview \ androidx.dynamicanimation_dynamicanimation +LOCAL_STATIC_JAVA_LIBRARIES := libPluginCore + LOCAL_SRC_FILES := \ $(call all-proto-files-under, protos) \ $(call all-proto-files-under, proto_overrides) diff --git a/libs/plugin_core.jar b/libs/plugin_core.jar new file mode 100644 index 0000000000..dd27f86fa3 Binary files /dev/null and b/libs/plugin_core.jar differ diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar index 7ff664d41c..c5a7c051e9 100644 Binary files a/quickstep/libs/sysui_shared.jar and b/quickstep/libs/sysui_shared.jar differ diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java new file mode 100644 index 0000000000..e9fac26bdb --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.launcher3.uioverrides.plugins; + +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; + +import com.android.launcher3.Utilities; +import com.android.systemui.shared.plugins.PluginEnabler; + +public class PluginEnablerImpl implements PluginEnabler { + + final private SharedPreferences mSharedPrefs; + + public PluginEnablerImpl(Context context) { + mSharedPrefs = Utilities.getDevicePrefs(context); + } + + @Override + public void setEnabled(ComponentName component, boolean enabled) { + mSharedPrefs.edit().putBoolean(toPrefString(component), enabled).apply(); + } + + @Override + public boolean isEnabled(ComponentName component) { + return mSharedPrefs.getBoolean(toPrefString(component), true); + } + + private String toPrefString(ComponentName component) { + return "PLUGIN_ENABLED_" + component.flattenToString(); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java new file mode 100644 index 0000000000..8a6aa050cd --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.launcher3.uioverrides.plugins; + +import android.content.Context; +import android.os.Looper; + +import com.android.launcher3.LauncherModel; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginInitializer; + +public class PluginInitializerImpl implements PluginInitializer { + @Override + public Looper getBgLooper() { + return LauncherModel.getWorkerLooper(); + } + + @Override + public void onPluginManagerInit() { + } + + @Override + public String[] getWhitelistedPlugins(Context context) { + return new String[0]; + } + + @Override + public PluginEnabler getPluginEnabler(Context context) { + return new PluginEnablerImpl(context); + } + + @Override + public void handleWtfs() { + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java new file mode 100644 index 0000000000..ca12951c73 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.launcher3.uioverrides.plugins; + +import android.content.Context; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginInitializer; +import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.shared.plugins.PluginManagerImpl; + +public class PluginManagerWrapper { + + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(PluginManagerWrapper::new); + + private final PluginManager mPluginManager; + private final PluginEnabler mPluginEnabler; + + private PluginManagerWrapper(Context c) { + PluginInitializer pluginInitializer = new PluginInitializerImpl(); + mPluginManager = new PluginManagerImpl(c, pluginInitializer); + mPluginEnabler = pluginInitializer.getPluginEnabler(c); + } + + PluginEnabler getPluginEnabler() { + return mPluginEnabler; + } + + public void addPluginListener(PluginListener listener, Class pluginClass) { + mPluginManager.addPluginListener(listener, pluginClass); + } + + public void removePluginListener(PluginListener listener) { + mPluginManager.removePluginListener(listener); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java new file mode 100644 index 0000000000..3da4f84274 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.launcher3.uioverrides.plugins; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.View; + +import com.android.launcher3.R; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.shared.plugins.PluginPrefs; + +import java.util.List; +import java.util.Set; + +/** + * This class is copied from System UI Tuner, except using our PluginEnablerImpl. The reason we + * can't share a common base class in the shared lib is because the androidx preference dependency + * interferes with our recyclerview and fragment dependencies. + */ +public class PluginPreferencesFragment extends PreferenceFragment { + public static final String ACTION_PLUGIN_SETTINGS + = "com.android.systemui.action.PLUGIN_SETTINGS"; + + private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; + + private PluginPrefs mPluginPrefs; + private PluginEnabler mPluginEnabler; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + getContext().registerReceiver(mReceiver, filter); + filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); + getContext().registerReceiver(mReceiver, filter); + + mPluginEnabler = PluginManagerWrapper.INSTANCE.get(getContext()).getPluginEnabler(); + loadPrefs(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getContext().unregisterReceiver(mReceiver); + } + + private void loadPrefs() { + PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getContext()); + screen.setOrderingAsAdded(false); + Context prefContext = getContext(); + mPluginPrefs = new PluginPrefs(getContext()); + PackageManager pm = getContext().getPackageManager(); + + Set pluginActions = mPluginPrefs.getPluginList(); + ArrayMap> plugins = new ArrayMap<>(); + for (String action : pluginActions) { + String name = toName(action); + List result = pm.queryIntentServices( + new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS); + for (ResolveInfo info : result) { + String packageName = info.serviceInfo.packageName; + if (!plugins.containsKey(packageName)) { + plugins.put(packageName, new ArraySet<>()); + } + plugins.get(packageName).add(name); + } + } + + List apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION}, + PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES); + apps.forEach(app -> { + if (!plugins.containsKey(app.packageName)) return; + SwitchPreference pref = new PluginPreference(prefContext, app, mPluginEnabler); + pref.setSummary("Plugins: " + toString(plugins.get(app.packageName))); + screen.addPreference(pref); + }); + setPreferenceScreen(screen); + } + + private String toString(ArraySet plugins) { + StringBuilder b = new StringBuilder(); + for (String string : plugins) { + if (b.length() != 0) { + b.append(", "); + } + b.append(string); + } + return b.toString(); + } + + private String toName(String action) { + String str = action.replace("com.android.systemui.action.PLUGIN_", ""); + StringBuilder b = new StringBuilder(); + for (String s : str.split("_")) { + if (b.length() != 0) { + b.append(' '); + } + b.append(s.substring(0, 1)); + b.append(s.substring(1).toLowerCase()); + } + return b.toString(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + loadPrefs(); + } + }; + + private static class PluginPreference extends SwitchPreference { + private final boolean mHasSettings; + private final PackageInfo mInfo; + private final PluginEnabler mPluginEnabler; + + public PluginPreference(Context prefContext, PackageInfo info, PluginEnabler pluginEnabler) { + super(prefContext); + PackageManager pm = prefContext.getPackageManager(); + mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS) + .setPackage(info.packageName), 0) != null; + mInfo = info; + mPluginEnabler = pluginEnabler; + setTitle(info.applicationInfo.loadLabel(pm)); + setChecked(isPluginEnabled()); + setWidgetLayoutResource(R.layout.switch_preference_with_settings); + } + + private boolean isPluginEnabled() { + for (int i = 0; i < mInfo.services.length; i++) { + ComponentName componentName = new ComponentName(mInfo.packageName, + mInfo.services[i].name); + if (!mPluginEnabler.isEnabled(componentName)) { + return false; + } + } + return true; + } + + @Override + protected boolean persistBoolean(boolean isEnabled) { + boolean shouldSendBroadcast = false; + for (int i = 0; i < mInfo.services.length; i++) { + ComponentName componentName = new ComponentName(mInfo.packageName, + mInfo.services[i].name); + + if (mPluginEnabler.isEnabled(componentName) != isEnabled) { + mPluginEnabler.setEnabled(componentName, isEnabled); + shouldSendBroadcast = true; + } + } + if (shouldSendBroadcast) { + final String pkg = mInfo.packageName; + final Intent intent = new Intent(PluginManager.PLUGIN_CHANGED, + pkg != null ? Uri.fromParts("package", pkg, null) : null); + getContext().sendBroadcast(intent); + } + setChecked(isEnabled); + return true; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + view.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + view.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + view.findViewById(R.id.settings).setOnClickListener(v -> { + ResolveInfo result = v.getContext().getPackageManager().resolveActivity( + new Intent(ACTION_PLUGIN_SETTINGS).setPackage( + mInfo.packageName), 0); + if (result != null) { + v.getContext().startActivity(new Intent().setComponent( + new ComponentName(result.activityInfo.packageName, + result.activityInfo.name))); + } + }); + view.setOnLongClickListener(v -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", mInfo.packageName, null)); + getContext().startActivity(intent); + return true; + }); + } + } +} diff --git a/res/layout/switch_preference_with_settings.xml b/res/layout/switch_preference_with_settings.xml new file mode 100644 index 0000000000..d3195610d7 --- /dev/null +++ b/res/layout/switch_preference_with_settings.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src_plugins/README.md b/src_plugins/README.md new file mode 100644 index 0000000000..c7ce1daca3 --- /dev/null +++ b/src_plugins/README.md @@ -0,0 +1,3 @@ +This directory contains plugin interfaces that launcher listens for and plugins implement. In other words, these are the hooks that specify what plugins launcher currently supports. + +Details about how to create a new plugin interface, or to use existing interfaces to write a plugin can be found at go/gnl/plugins. diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java new file mode 100644 index 0000000000..fcb2abe3ec --- /dev/null +++ b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.launcher3.uioverrides.plugins; + +import android.content.Context; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginListener; + +public class PluginManagerWrapper { + + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(PluginManagerWrapper::new); + + private PluginManagerWrapper(Context c) { + } + + public void addPluginListener(PluginListener listener, Class pluginClass) { + } + + public void removePluginListener(PluginListener listener) { + } +}