diff --git a/Android.bp b/Android.bp index 2608280c92..4b327020c3 100644 --- a/Android.bp +++ b/Android.bp @@ -28,3 +28,23 @@ java_library_static { ], platform_apis: true, } + + +android_library { + name: "icon-loader", + sdk_version: "28", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/com/android/launcher3/icons/BaseIconFactory.java", + "src/com/android/launcher3/icons/BitmapInfo.java", + "src/com/android/launcher3/icons/IconNormalizer.java", + "src/com/android/launcher3/icons/FixedScaleDrawable.java", + "src/com/android/launcher3/icons/ShadowGenerator.java", + "src/com/android/launcher3/icons/ColorExtractor.java", + ], +} diff --git a/Android.mk b/Android.mk index 15daf1fd2f..fbe19b0b63 100644 --- a/Android.mk +++ b/Android.mk @@ -66,7 +66,8 @@ LOCAL_MODULE_TAGS := optional LOCAL_STATIC_ANDROID_LIBRARIES := \ androidx.recyclerview_recyclerview \ - androidx.dynamicanimation_dynamicanimation + androidx.dynamicanimation_dynamicanimation \ + androidx.preference_preference LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 8f4d5bece5..5b00b7d027 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -156,7 +156,7 @@ The settings activity. To extend point settings_fragment_name to appropriate fragment class --> diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000000..a8c84eb2d7 --- /dev/null +++ b/OWNERS @@ -0,0 +1,4 @@ +adamcohen@google.com +hyunyoungs@google.com +sunnygoyal@google.com +winsonc@google.com diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto index 41dd0bda4f..b3ed365b88 100644 --- a/protos/launcher_log.proto +++ b/protos/launcher_log.proto @@ -112,6 +112,7 @@ enum ControlType { CANCEL_TARGET = 14; TASK_PREVIEW = 15; SPLIT_SCREEN_TARGET = 16; + REMOTE_ACTION_SHORTCUT = 17; } enum TipType { diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 72895167f8..59a937f4cc 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -20,21 +20,34 @@ import android.content.Context; import android.graphics.Matrix; import android.view.View; +import androidx.annotation.AnyThread; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.R; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.ResourceBasedOverride; +import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; -import androidx.annotation.AnyThread; +import java.util.ArrayList; +import java.util.List; /** * Factory class to create and add an overlays on the TaskView */ public class TaskOverlayFactory implements ResourceBasedOverride { - private static TaskOverlayFactory sInstance; + /** Note that these will be shown in order from top to bottom, if available for the task. */ + private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{ + new TaskSystemShortcut.AppInfo(), + new TaskSystemShortcut.SplitScreen(), + new TaskSystemShortcut.Pin(), + new TaskSystemShortcut.Install(), + }; + public static TaskOverlayFactory get(Context context) { Preconditions.assertUIThread(); if (sInstance == null) { @@ -55,9 +68,23 @@ public class TaskOverlayFactory implements ResourceBasedOverride { public static class TaskOverlay { - public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) { } + public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) { + } - public void reset() { } + public void reset() { + } + public List getEnabledShortcuts(TaskView taskView) { + final ArrayList shortcuts = new ArrayList<>(); + final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext()); + for (TaskSystemShortcut menuOption : MENU_OPTIONS) { + View.OnClickListener onClickListener = + menuOption.getOnClickListener(activity, taskView); + if (onClickListener != null) { + shortcuts.add(menuOption); + } + } + return shortcuts; + } } } diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java index c4c660c647..1c79f44755 100644 --- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -540,7 +540,7 @@ public class WindowTransformSwipeHandler { public void updateDisplacement(float displacement) { // We are moving in the negative x/y direction displacement = -displacement; - if (displacement > mTransitionDragLength) { + if (displacement > mTransitionDragLength && mTransitionDragLength > 0) { mCurrentShift.updateValue(1); if (!mBgLongSwipeMode) { @@ -815,8 +815,12 @@ public class WindowTransformSwipeHandler { long startMillis = SystemClock.uptimeMillis(); executeOnUiThread(() -> { // Animate the launcher components at the same time as the window, always on UI thread. - if (mLauncherTransitionController != null && !mWasLauncherAlreadyVisible - && start != end && duration > 0) { + if (mLauncherTransitionController == null) { + return; + } + if (start == end || duration <= 0) { + mLauncherTransitionController.getAnimationPlayer().end(); + } else { // Adjust start progress and duration in case we are on a different thread. long elapsedMillis = SystemClock.uptimeMillis() - startMillis; elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration); diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java index c4afad7c99..667165bf2e 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -43,6 +43,8 @@ import com.android.quickstep.TaskSystemShortcut; import com.android.quickstep.TaskUtils; import com.android.quickstep.views.IconView.OnScaleUpdateListener; +import java.util.List; + /** * Contains options for a recent task when long-pressing its icon. */ @@ -50,14 +52,6 @@ public class TaskMenuView extends AbstractFloatingView { private static final Rect sTempRect = new Rect(); - /** Note that these will be shown in order from top to bottom, if available for the task. */ - public static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[] { - new TaskSystemShortcut.AppInfo(), - new TaskSystemShortcut.SplitScreen(), - new TaskSystemShortcut.Pin(), - new TaskSystemShortcut.Install(), - }; - private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() { @Override public void onScaleUpdate(float scale) { @@ -197,11 +191,13 @@ public class TaskMenuView extends AbstractFloatingView { params.topMargin = (int) -mThumbnailTopMargin; mTaskIcon.setLayoutParams(params); - for (TaskSystemShortcut menuOption : MENU_OPTIONS) { - OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, taskView); - if (onClickListener != null) { - addMenuOption(menuOption, onClickListener); - } + final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext()); + final List shortcuts = + taskView.getTaskOverlay().getEnabledShortcuts(taskView); + final int count = shortcuts.size(); + for (int i = 0; i < count; ++i) { + final TaskSystemShortcut menuOption = shortcuts.get(i); + addMenuOption(menuOption, menuOption.getOnClickListener(activity, taskView)); } } diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java index 7223f97f44..ce65de1e09 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java @@ -173,6 +173,10 @@ public class TaskThumbnailView extends View { return 0; } + public TaskOverlay getTaskOverlay() { + return mOverlay; + } + @Override protected void onDraw(Canvas canvas) { drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index c1424c4751..56074f0f55 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -42,12 +42,12 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.Toast; -import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskSystemShortcut; import com.android.quickstep.TaskUtils; import com.android.quickstep.views.RecentsView.PageCallbacks; @@ -56,8 +56,9 @@ import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.Task.TaskCallbacks; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; - import com.android.systemui.shared.system.ActivityOptionsCompat; + +import java.util.List; import java.util.function.Consumer; /** @@ -190,6 +191,10 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback return mIconView; } + public TaskOverlayFactory.TaskOverlay getTaskOverlay() { + return mSnapshotView.getTaskOverlay(); + } + public void launchTask(boolean animate) { launchTask(animate, (result) -> { if (!result) { @@ -384,7 +389,11 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback final Context context = getContext(); final BaseDraggingActivity activity = fromContext(context); - for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + final List shortcuts = + mSnapshotView.getTaskOverlay().getEnabledShortcuts(this); + final int count = shortcuts.size(); + for (int i = 0; i < count; ++i) { + final TaskSystemShortcut menuOption = shortcuts.get(i); OnClickListener onClickListener = menuOption.getOnClickListener(activity, this); if (onClickListener != null) { info.addAction(menuOption.createAccessibilityAction(context)); @@ -407,7 +416,11 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback return true; } - for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + final List shortcuts = + mSnapshotView.getTaskOverlay().getEnabledShortcuts(this); + final int count = shortcuts.size(); + for (int i = 0; i < count; ++i) { + final TaskSystemShortcut menuOption = shortcuts.get(i); if (menuOption.hasHandlerForAction(action)) { OnClickListener onClickListener = menuOption.getOnClickListener( fromContext(getContext()), this); diff --git a/res/values/config.xml b/res/values/config.xml index 0efaccf194..85c2e65c51 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -123,6 +123,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index eb6b28413d..7e5784d0bf 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -347,4 +347,7 @@ Notifications and apps are off Close Closed + + + Failed: %1$s diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index 1df7c2fbaf..c55cc49855 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -14,9 +14,10 @@ limitations under the License. --> - + - - + - - + diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 7190f12779..851454b77b 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -480,7 +480,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { final LauncherAppState app = LauncherAppState.getInstance(mContext); // Set default values until proper values is loaded. appInfo.title = ""; - app.getIconCache().getDefaultIcon(user).applyTo(appInfo); + appInfo.applyFrom(app.getIconCache().getDefaultIcon(user)); final ShortcutInfo si = appInfo.makeShortcut(); if (Looper.myLooper() == LauncherModel.getWorkerLooper()) { app.getIconCache().getTitleAndIcon(si, activityInfo, false /* useLowResIcon */); @@ -495,7 +495,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } else if (shortcutInfo != null) { ShortcutInfo si = new ShortcutInfo(shortcutInfo, mContext); LauncherIcons li = LauncherIcons.obtain(mContext); - li.createShortcutIcon(shortcutInfo).applyTo(si); + si.applyFrom(li.createShortcutIcon(shortcutInfo)); li.recycle(); return Pair.create((ItemInfo) si, (Object) shortcutInfo); } else if (providerInfo != null) { @@ -656,7 +656,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { if (iconInfo == null) { iconInfo = app.getIconCache().getDefaultIcon(info.user); } - iconInfo.applyTo(info); + info.applyFrom(iconInfo); info.title = Utilities.trim(name); info.contentDescription = UserManagerCompat.getInstance(app.getContext()) diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java index 6d453c9b38..e29f92713d 100644 --- a/src/com/android/launcher3/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/ItemInfoWithIcon.java @@ -20,6 +20,8 @@ import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; import android.graphics.Bitmap; +import com.android.launcher3.icons.BitmapInfo; + /** * Represents an ItemInfo which also holds an icon. */ @@ -118,4 +120,10 @@ public abstract class ItemInfoWithIcon extends ItemInfo { public boolean usingLowResIcon() { return iconBitmap == LOW_RES_ICON; } + + public void applyFrom(BitmapInfo info) { + iconBitmap = info.icon; + iconColor = info.color; + } + } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index cbfde25596..8e9021f58b 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -657,7 +657,7 @@ public class LauncherModel extends BroadcastReceiver updateAndBindShortcutInfo(() -> { si.updateFromDeepShortcutInfo(info, mApp.getContext()); LauncherIcons li = LauncherIcons.obtain(mApp.getContext()); - li.createShortcutIcon(info).applyTo(si); + si.applyFrom(li.createShortcutIcon(info)); li.recycle(); return si; }); diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java index a18dfde310..a2538930e1 100644 --- a/src/com/android/launcher3/MainProcessInitializer.java +++ b/src/com/android/launcher3/MainProcessInitializer.java @@ -19,6 +19,7 @@ package com.android.launcher3; import android.content.Context; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.folder.FolderShape; import com.android.launcher3.graphics.IconShapeOverride; import com.android.launcher3.logging.FileLog; import com.android.launcher3.util.ResourceBasedOverride; @@ -39,5 +40,6 @@ public class MainProcessInitializer implements ResourceBasedOverride { FeatureFlags.initialize(context); IconShapeOverride.apply(context); SessionCommitReceiver.applyDefaultUserPrefs(context); + FolderShape.init(); } } diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java deleted file mode 100644 index a17f614192..0000000000 --- a/src/com/android/launcher3/SettingsActivity.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright (C) 2015 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; - -import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; -import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue; -import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.provider.Settings; -import android.text.TextUtils; -import android.view.View; -import android.widget.Adapter; -import android.widget.ListView; - -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.graphics.IconShapeOverride; -import com.android.launcher3.notification.NotificationListener; -import com.android.launcher3.util.ListViewHighlighter; -import com.android.launcher3.util.SecureSettingsObserver; -import com.android.launcher3.views.ButtonPreference; - -import java.util.Objects; - -/** - * Settings activity for Launcher. Currently implements the following setting: Allow rotation - */ -public class SettingsActivity extends Activity - implements PreferenceFragment.OnPreferenceStartFragmentCallback { - - private static final String FLAGS_PREFERENCE_KEY = "flag_toggler"; - - private static final String ICON_BADGING_PREFERENCE_KEY = "pref_icon_badging"; - /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ - private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners"; - - private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; - private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"; - private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600; - private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState == null) { - Fragment f = Fragment.instantiate(this, getString(R.string.settings_fragment_name)); - // Display the fragment as the main content. - getFragmentManager().beginTransaction() - .replace(android.R.id.content, f) - .commit(); - } - } - - protected PreferenceFragment getNewFragment() { - return new LauncherSettingsFragment(); - } - - @Override - public boolean onPreferenceStartFragment( - PreferenceFragment preferenceFragment, Preference pref) { - if (getFragmentManager().isStateSaved()) { - // Sometimes onClick can come after onPause because of being posted on the handler. - // Skip starting new fragments in that case. - return false; - } - Fragment f = Fragment.instantiate(this, pref.getFragment(), pref.getExtras()); - if (f instanceof DialogFragment) { - ((DialogFragment) f).show(getFragmentManager(), pref.getKey()); - } else { - getFragmentManager() - .beginTransaction() - .replace(android.R.id.content, f) - .addToBackStack(pref.getKey()) - .commit(); - } - return true; - } - - /** - * This fragment shows the launcher preferences. - */ - public static class LauncherSettingsFragment extends PreferenceFragment { - - private SecureSettingsObserver mIconBadgingObserver; - - private String mPreferenceKey; - private boolean mPreferenceHighlighted = false; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mPreferenceHighlighted = savedInstanceState.getBoolean(SAVE_HIGHLIGHTED_KEY); - } - - getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY); - addPreferencesFromResource(R.xml.launcher_preferences); - - // Only show flag toggler UI if this build variant implements that. - Preference flagToggler = findPreference(FLAGS_PREFERENCE_KEY); - if (flagToggler != null && !FeatureFlags.showFlagTogglerUi()) { - getPreferenceScreen().removePreference(flagToggler); - } - - ContentResolver resolver = getActivity().getContentResolver(); - - ButtonPreference iconBadgingPref = - (ButtonPreference) findPreference(ICON_BADGING_PREFERENCE_KEY); - if (!Utilities.ATLEAST_OREO) { - getPreferenceScreen().removePreference( - findPreference(SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY)); - getPreferenceScreen().removePreference(iconBadgingPref); - } else if (!getResources().getBoolean(R.bool.notification_badging_enabled)) { - getPreferenceScreen().removePreference(iconBadgingPref); - } else { - // Listen to system notification badge settings while this UI is active. - mIconBadgingObserver = newNotificationSettingsObserver( - getActivity(), new IconBadgingObserver(iconBadgingPref, resolver)); - mIconBadgingObserver.register(); - // Also listen if notification permission changes - mIconBadgingObserver.getResolver().registerContentObserver( - Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false, - mIconBadgingObserver); - mIconBadgingObserver.dispatchOnChange(); - } - - Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE); - if (iconShapeOverride != null) { - if (IconShapeOverride.isSupported(getActivity())) { - IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride); - } else { - getPreferenceScreen().removePreference(iconShapeOverride); - } - } - - // Setup allow rotation preference - Preference rotationPref = findPreference(ALLOW_ROTATION_PREFERENCE_KEY); - if (getResources().getBoolean(R.bool.allow_rotation)) { - // Launcher supports rotation by default. No need to show this setting. - getPreferenceScreen().removePreference(rotationPref); - } else { - // Initialize the UI once - rotationPref.setDefaultValue(getAllowRotationDefaultValue()); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); - } - - @Override - public void onResume() { - super.onResume(); - - Intent intent = getActivity().getIntent(); - mPreferenceKey = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY); - if (isAdded() && !mPreferenceHighlighted && !TextUtils.isEmpty(mPreferenceKey)) { - getView().postDelayed(this::highlightPreference, DELAY_HIGHLIGHT_DURATION_MILLIS); - } - } - - private void highlightPreference() { - Preference pref = findPreference(mPreferenceKey); - if (pref == null || getPreferenceScreen() == null) { - return; - } - PreferenceScreen screen = getPreferenceScreen(); - if (Utilities.ATLEAST_OREO) { - screen = selectPreferenceRecursive(pref, screen); - } - if (screen == null) { - return; - } - - View root = screen.getDialog() != null - ? screen.getDialog().getWindow().getDecorView() : getView(); - ListView list = root.findViewById(android.R.id.list); - if (list == null || list.getAdapter() == null) { - return; - } - Adapter adapter = list.getAdapter(); - - // Find the position - int position = -1; - for (int i = adapter.getCount() - 1; i >= 0; i--) { - if (pref == adapter.getItem(i)) { - position = i; - break; - } - } - new ListViewHighlighter(list, position); - mPreferenceHighlighted = true; - } - - @Override - public void onDestroy() { - if (mIconBadgingObserver != null) { - mIconBadgingObserver.unregister(); - mIconBadgingObserver = null; - } - super.onDestroy(); - } - - @TargetApi(Build.VERSION_CODES.O) - private PreferenceScreen selectPreferenceRecursive( - Preference pref, PreferenceScreen topParent) { - if (!(pref.getParent() instanceof PreferenceScreen)) { - return null; - } - - PreferenceScreen parent = (PreferenceScreen) pref.getParent(); - if (Objects.equals(parent.getKey(), topParent.getKey())) { - return parent; - } else if (selectPreferenceRecursive(parent, topParent) != null) { - ((PreferenceScreen) parent.getParent()) - .onItemClick(null, null, parent.getOrder(), 0); - return parent; - } else { - return null; - } - } - } - - /** - * Content observer which listens for system badging setting changes, - * and updates the launcher badging setting subtext accordingly. - */ - private static class IconBadgingObserver implements SecureSettingsObserver.OnChangeListener { - - private final ButtonPreference mBadgingPref; - private final ContentResolver mResolver; - - public IconBadgingObserver(ButtonPreference badgingPref, ContentResolver resolver) { - mBadgingPref = badgingPref; - mResolver = resolver; - } - - @Override - public void onSettingsChanged(boolean enabled) { - int summary = enabled ? R.string.icon_badging_desc_on : R.string.icon_badging_desc_off; - - boolean serviceEnabled = true; - if (enabled) { - // Check if the listener is enabled or not. - String enabledListeners = - Settings.Secure.getString(mResolver, NOTIFICATION_ENABLED_LISTENERS); - ComponentName myListener = - new ComponentName(mBadgingPref.getContext(), NotificationListener.class); - serviceEnabled = enabledListeners != null && - (enabledListeners.contains(myListener.flattenToString()) || - enabledListeners.contains(myListener.flattenToShortString())); - if (!serviceEnabled) { - summary = R.string.title_missing_notification_access; - } - } - mBadgingPref.setWidgetFrameVisible(!serviceEnabled); - mBadgingPref.setFragment( - serviceEnabled ? null : NotificationAccessConfirmation.class.getName()); - mBadgingPref.setSummary(summary); - - } - } - - public static class NotificationAccessConfirmation - extends DialogFragment implements DialogInterface.OnClickListener { - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Context context = getActivity(); - String msg = context.getString(R.string.msg_missing_notification_access, - context.getString(R.string.derived_app_name)); - return new AlertDialog.Builder(context) - .setTitle(R.string.title_missing_notification_access) - .setMessage(msg) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.title_change_settings, this) - .create(); - } - - @Override - public void onClick(DialogInterface dialogInterface, int i) { - ComponentName cn = new ComponentName(getActivity(), NotificationListener.class); - Bundle showFragmentArgs = new Bundle(); - showFragmentArgs.putString(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()); - - Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()) - .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, showFragmentArgs); - getActivity().startActivity(intent); - } - } -} diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 11e601c7f3..3f7d68d12f 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -127,8 +127,8 @@ public class Workspace extends PagedView private static final int DEFAULT_PAGE = 0; - private static final boolean MAP_NO_RECURSE = false; - private static final boolean MAP_RECURSE = true; + public static final boolean MAP_NO_RECURSE = false; + public static final boolean MAP_RECURSE = true; // The screen id used for the empty screen always present to the right. public static final int EXTRA_EMPTY_SCREEN_ID = -201; @@ -3121,7 +3121,7 @@ public class Workspace extends PagedView * @param recurse true: iterate over folder children. false: op get the folders themselves. * @param op the operator to map over the shortcuts */ - void mapOverItems(boolean recurse, ItemOperator op) { + public void mapOverItems(boolean recurse, ItemOperator op) { ArrayList containers = getAllShortcutAndWidgetContainers(); final int containerCount = containers.size(); for (int containerIdx = 0; containerIdx < containerCount; containerIdx++) { diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java index fb446600c3..82617fe7ee 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java @@ -139,7 +139,7 @@ public class LauncherAppsCompatVO extends LauncherAppsCompatVL { ShortcutInfo info = new ShortcutInfo(compat, context); // Apply the unbadged icon and fetch the actual icon asynchronously. LauncherIcons li = LauncherIcons.obtain(context); - li.createShortcutIcon(compat, false /* badged */).applyTo(info); + info.applyFrom(li.createShortcutIcon(compat, false /* badged */)); li.recycle(); LauncherAppState.getInstance(context).getModel() .updateAndBindShortcutInfo(info, compat); diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index 6d1efd5879..b6a8b50a95 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -204,7 +204,7 @@ public class DragView extends View { public void run() { LauncherAppState appState = LauncherAppState.getInstance(mLauncher); Object[] outObj = new Object[1]; - final Drawable dr = getFullDrawable(info, appState, outObj); + Drawable dr = getFullDrawable(info, appState, outObj); if (dr instanceof AdaptiveIconDrawable) { int w = mBitmap.getWidth(); @@ -220,10 +220,20 @@ public class DragView extends View { mBadge = getBadge(info, appState, outObj[0]); mBadge.setBounds(badgeBounds); - LauncherIcons li = LauncherIcons.obtain(mLauncher); - Utilities.scaleRectAboutCenter(bounds, - li.getNormalizer().getScale(dr, null, null, null)); - li.recycle(); + // Do not draw the background in case of folder as its translucent + mDrawBitmap = !(dr instanceof FolderAdaptiveIcon); + + try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) { + Drawable nDr; // drawable to be normalized + if (mDrawBitmap) { + nDr = dr; + } else { + // Since we just want the scale, avoid heavy drawing operations + nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); + } + Utilities.scaleRectAboutCenter(bounds, + li.getNormalizer().getScale(nDr, null, null, null)); + } AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr; // Shrink very tiny bit so that the clip path is smaller than the original bitmap @@ -259,9 +269,6 @@ public class DragView extends View { // Assign the variable on the UI thread to avoid race conditions. mScaledMaskPath = mask; - // Do not draw the background in case of folder as its translucent - mDrawBitmap = !(dr instanceof FolderAdaptiveIcon); - if (info.isDisabled()) { FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); d.setIsDisabled(true); diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index c4d1058005..94c8d4549d 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -26,6 +26,8 @@ import android.animation.AnimatorSet; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Path; import android.graphics.Rect; import android.text.InputType; import android.text.Selection; @@ -151,6 +153,8 @@ public class Folder extends AbstractFloatingView implements DragSource, // Cell ranks used for drag and drop @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; + private Path mClipPath; + @ViewDebug.ExportedProperty(category = "launcher", mapping = { @ViewDebug.IntToString(from = STATE_NONE, to = "STATE_NONE"), @@ -1476,4 +1480,25 @@ public class Folder extends AbstractFloatingView implements DragSource, sHintText = res.getString(R.string.folder_hint_text); } } + + /** + * Alternative to using {@link #getClipToOutline()} as it only works with derivatives of + * rounded rect. + */ + public void setClipPath(Path clipPath) { + mClipPath = clipPath; + invalidate(); + } + + @Override + public void draw(Canvas canvas) { + if (mClipPath != null) { + int count = canvas.save(); + canvas.clipPath(mClipPath); + super.draw(canvas); + canvas.restoreToCount(count); + } else { + super.draw(canvas); + } + } } diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java index 1277a20909..fa890b99df 100644 --- a/src/com/android/launcher3/folder/FolderAnimationManager.java +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -19,6 +19,7 @@ package com.android.launcher3.folder; import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; +import static com.android.launcher3.folder.FolderShape.getShape; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -40,7 +41,6 @@ import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PropertyResetListener; -import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.util.Themes; @@ -166,7 +166,6 @@ public class FolderAnimationManager { Math.round((totalOffsetX + initialSize) / initialScale), Math.round((paddingOffsetY + initialSize) / initialScale)); Rect endRect = new Rect(0, 0, lp.width, lp.height); - float initialRadius = initialSize / initialScale / 2f; float finalRadius = Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics()); // Create the animators. @@ -189,14 +188,8 @@ public class FolderAnimationManager { play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale)); play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor)); play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening)); - RoundedRectRevealOutlineProvider outlineProvider = new RoundedRectRevealOutlineProvider( - initialRadius, finalRadius, startRect, endRect) { - @Override - public boolean shouldRemoveElevationDuringAnimation() { - return true; - } - }; - play(a, outlineProvider.createRevealAnimator(mFolder, !mIsOpening)); + play(a, getShape().createRevealAnimator( + mFolder, startRect, endRect, finalRadius, !mIsOpening)); // Animate the elevation midway so that the shadow is not noticeable in the background. int midDuration = mDuration / 2; diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index d09f03612f..429d44fcb6 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -75,6 +75,7 @@ import androidx.annotation.NonNull; * An icon that can appear on in the workspace representing an {@link Folder}. */ public class FolderIcon extends FrameLayout implements FolderListener { + @Thunk Launcher mLauncher; @Thunk Folder mFolder; private FolderInfo mInfo; @@ -477,20 +478,9 @@ public class FolderIcon extends FrameLayout implements FolderListener { if (mFolder == null) return; if (mFolder.getItemCount() == 0 && !mAnimating) return; - final int saveCount; - - if (canvas.isHardwareAccelerated()) { - saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null); - } else { - saveCount = canvas.save(); - canvas.clipPath(mBackground.getClipPath()); - } - + final int saveCount = canvas.save(); + canvas.clipPath(mBackground.getClipPath()); mPreviewItemManager.draw(canvas); - - if (canvas.isHardwareAccelerated()) { - mBackground.clipCanvasHardware(canvas); - } canvas.restoreToCount(saveCount); if (!mBackground.drawingDelegated()) { diff --git a/src/com/android/launcher3/folder/FolderShape.java b/src/com/android/launcher3/folder/FolderShape.java new file mode 100644 index 0000000000..ae279cb3dc --- /dev/null +++ b/src/com/android/launcher3/folder/FolderShape.java @@ -0,0 +1,422 @@ +/* + * 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.folder; + +import static com.android.launcher3.Workspace.MAP_NO_RECURSE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.FloatArrayEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Region.Op; +import android.graphics.RegionIterator; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.view.ViewOutlineProvider; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; + +/** + * Abstract representation of the shape of a folder icon + */ +public abstract class FolderShape { + + private static FolderShape sInstance = new Circle(); + + public static FolderShape getShape() { + return sInstance; + } + + private static FolderShape[] getAllShapes() { + return new FolderShape[] { + new Circle(), + new RoundedSquare(8f / 50), // Ratios based on path defined in config_icon_mask + new RoundedSquare(30f / 50), + new Square(), + new TearDrop(), + new Squircle()}; + } + + public abstract void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, + Paint paint); + + public abstract void addShape(Path path, float offsetX, float offsetY, float radius); + + public abstract Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect, + float endRadius, boolean isReversed); + + /** + * Abstract shape where the reveal animation is a derivative of a round rect animation + */ + private static abstract class SimpleRectShape extends FolderShape { + + @Override + public final Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect, + float endRadius, boolean isReversed) { + return new RoundedRectRevealOutlineProvider( + getStartRadius(startRect), endRadius, startRect, endRect) { + @Override + public boolean shouldRemoveElevationDuringAnimation() { + return true; + } + }.createRevealAnimator(target, isReversed); + } + + protected abstract float getStartRadius(Rect startRect); + } + + /** + * Abstract shape which draws using {@link Path} + */ + private static abstract class PathShape extends FolderShape { + + private final Path mTmpPath = new Path(); + + @Override + public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, + Paint paint) { + mTmpPath.reset(); + addShape(mTmpPath, offsetX, offsetY, radius); + canvas.drawPath(mTmpPath, paint); + } + + protected abstract AnimatorUpdateListener newUpdateListener( + Rect startRect, Rect endRect, float endRadius, Path outPath); + + @Override + public final Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect, + float endRadius, boolean isReversed) { + Path path = new Path(); + AnimatorUpdateListener listener = + newUpdateListener(startRect, endRect, endRadius, path); + + ValueAnimator va = + isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f); + va.addListener(new AnimatorListenerAdapter() { + private ViewOutlineProvider mOldOutlineProvider; + + public void onAnimationStart(Animator animation) { + mOldOutlineProvider = target.getOutlineProvider(); + target.setOutlineProvider(null); + + target.setTranslationZ(-target.getElevation()); + } + + public void onAnimationEnd(Animator animation) { + target.setTranslationZ(0); + target.setClipPath(null); + target.setOutlineProvider(mOldOutlineProvider); + } + }); + + va.addUpdateListener((anim) -> { + path.reset(); + listener.onAnimationUpdate(anim); + target.setClipPath(path); + }); + + return va; + } + } + + public static final class Circle extends SimpleRectShape { + + @Override + public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) { + canvas.drawCircle(radius + offsetX, radius + offsetY, radius, p); + } + + @Override + public void addShape(Path path, float offsetX, float offsetY, float radius) { + path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW); + } + + @Override + protected float getStartRadius(Rect startRect) { + return startRect.width() / 2f; + } + } + + public static class Square extends SimpleRectShape { + + @Override + public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) { + float cx = radius + offsetX; + float cy = radius + offsetY; + canvas.drawRect(cx - radius, cy - radius, cx + radius, cy + radius, p); + } + + @Override + public void addShape(Path path, float offsetX, float offsetY, float radius) { + float cx = radius + offsetX; + float cy = radius + offsetY; + path.addRect(cx - radius, cy - radius, cx + radius, cy + radius, Path.Direction.CW); + } + + @Override + protected float getStartRadius(Rect startRect) { + return 0; + } + } + + public static class RoundedSquare extends SimpleRectShape { + + /** + * Ratio of corner radius to half size. Based on the + */ + private final float mRadiusFactor; + + public RoundedSquare(float radiusFactor) { + mRadiusFactor = radiusFactor; + } + + @Override + public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) { + float cx = radius + offsetX; + float cy = radius + offsetY; + float cr = radius * mRadiusFactor; + canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p); + } + + @Override + public void addShape(Path path, float offsetX, float offsetY, float radius) { + float cx = radius + offsetX; + float cy = radius + offsetY; + float cr = radius * mRadiusFactor; + path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, + Path.Direction.CW); + } + + @Override + protected float getStartRadius(Rect startRect) { + return (startRect.width() / 2f) * mRadiusFactor; + } + } + + public static class TearDrop extends PathShape { + + /** + * Radio of short radius to large radius, based on the shape options defined in the config. + */ + private static final float RADIUS_RATIO = 15f / 50; + + private final float[] mTempRadii = new float[8]; + + @Override + public void addShape(Path p, float offsetX, float offsetY, float r1) { + float r2 = r1 * RADIUS_RATIO; + float cx = r1 + offsetX; + float cy = r1 + offsetY; + + p.addRoundRect(cx - r1, cy - r1, cx + r1, cy + r1, getRadiiArray(r1, r2), + Path.Direction.CW); + } + + private float[] getRadiiArray(float r1, float r2) { + mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] = + mTempRadii[6] = mTempRadii[7] = r1; + mTempRadii[4] = mTempRadii[5] = r2; + return mTempRadii; + } + + @Override + protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect, + float endRadius, Path outPath) { + float r1 = startRect.width() / 2f; + float r2 = r1 * RADIUS_RATIO; + + float[] startValues = new float[] { + startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2}; + float[] endValues = new float[] { + endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius}; + + FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]); + + return (anim) -> { + float progress = (Float) anim.getAnimatedValue(); + float[] values = evaluator.evaluate(progress, startValues, endValues); + outPath.addRoundRect( + values[0], values[1], values[2], values[3], + getRadiiArray(values[4], values[5]), Path.Direction.CW); + }; + } + } + + public static class Squircle extends PathShape { + + /** + * Radio of radius to circle radius, based on the shape options defined in the config. + */ + private static final float RADIUS_RATIO = 10f / 50; + + @Override + public void addShape(Path p, float offsetX, float offsetY, float r) { + float cx = r + offsetX; + float cy = r + offsetY; + float control = r - r * RADIUS_RATIO; + + p.moveTo(cx, cy - r); + addLeftCurve(cx, cy, r, control, p); + addRightCurve(cx, cy, r, control, p); + addLeftCurve(cx, cy, -r, -control, p); + addRightCurve(cx, cy, -r, -control, p); + p.close(); + } + + private void addLeftCurve(float cx, float cy, float r, float control, Path path) { + path.cubicTo( + cx - control, cy - r, + cx - r, cy - control, + cx - r, cy); + } + + private void addRightCurve(float cx, float cy, float r, float control, Path path) { + path.cubicTo( + cx - r, cy + control, + cx - control, cy + r, + cx, cy + r); + } + + @Override + protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect, + float endR, Path outPath) { + + float startCX = startRect.exactCenterX(); + float startCY = startRect.exactCenterY(); + float startR = startRect.width() / 2f; + float startControl = startR - startR * RADIUS_RATIO; + float startHShift = 0; + float startVShift = 0; + + float endCX = endRect.exactCenterX(); + float endCY = endRect.exactCenterY(); + // Approximate corner circle using bezier curves + // http://spencermortensen.com/articles/bezier-circle/ + float endControl = endR * 0.551915024494f; + float endHShift = endRect.width() / 2f - endR; + float endVShift = endRect.height() / 2f - endR; + + return (anim) -> { + float progress = (Float) anim.getAnimatedValue(); + + float cx = (1 - progress) * startCX + progress * endCX; + float cy = (1 - progress) * startCY + progress * endCY; + float r = (1 - progress) * startR + progress * endR; + float control = (1 - progress) * startControl + progress * endControl; + float hShift = (1 - progress) * startHShift + progress * endHShift; + float vShift = (1 - progress) * startVShift + progress * endVShift; + + outPath.moveTo(cx, cy - vShift - r); + outPath.rLineTo(-hShift, 0); + + addLeftCurve(cx - hShift, cy - vShift, r, control, outPath); + outPath.rLineTo(0, vShift + vShift); + + addRightCurve(cx - hShift, cy + vShift, r, control, outPath); + outPath.rLineTo(hShift + hShift, 0); + + addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath); + outPath.rLineTo(0, -vShift - vShift); + + addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath); + outPath.close(); + }; + } + } + + /** + * Initializes the shape which is closest to closest to the {@link AdaptiveIconDrawable} + */ + public static void init() { + if (!Utilities.ATLEAST_OREO) { + return; + } + new MainThreadExecutor().execute(FolderShape::pickShapeInBackground); + } + + @TargetApi(Build.VERSION_CODES.O) + protected static void pickShapeInBackground() { + // Pick any large size + int size = 200; + + Region full = new Region(0, 0, size, size); + Region iconR = new Region(); + AdaptiveIconDrawable drawable = new AdaptiveIconDrawable( + new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK)); + drawable.setBounds(0, 0, size, size); + iconR.setPath(drawable.getIconMask(), full); + + Path shapePath = new Path(); + Region shapeR = new Region(); + Rect tempRect = new Rect(); + + // Find the shape with minimum area of divergent region. + int minArea = Integer.MAX_VALUE; + FolderShape closestShape = null; + for (FolderShape shape : getAllShapes()) { + shapePath.reset(); + shape.addShape(shapePath, 0, 0, size / 2f); + shapeR.setPath(shapePath, full); + shapeR.op(iconR, Op.XOR); + + RegionIterator itr = new RegionIterator(shapeR); + int area = 0; + + while (itr.next(tempRect)) { + area += tempRect.width() * tempRect.height(); + } + if (area < minArea) { + minArea = area; + closestShape = shape; + } + } + + if (closestShape != null) { + FolderShape shape = closestShape; + new MainThreadExecutor().execute(() -> updateFolderShape(shape)); + } + } + + private static void updateFolderShape(FolderShape shape) { + sInstance = shape; + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + return; + } + Launcher launcher = (Launcher) app.getModel().getCallback(); + if (launcher != null) { + launcher.getWorkspace().mapOverItems(MAP_NO_RECURSE, (i, v) -> { + if (v instanceof FolderIcon) { + v.invalidate(); + } + return false; + }); + } + } +} diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java index ceb1a8c372..8443953bd9 100644 --- a/src/com/android/launcher3/folder/PreviewBackground.java +++ b/src/com/android/launcher3/folder/PreviewBackground.java @@ -16,6 +16,8 @@ package com.android.launcher3.folder; +import static com.android.launcher3.folder.FolderShape.getShape; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; @@ -49,16 +51,6 @@ public class PreviewBackground { private static final int CONSUMPTION_ANIMATION_DURATION = 100; - private final PorterDuffXfermode mClipPorterDuffXfermode - = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); - // Create a RadialGradient such that it draws a black circle and then extends with - // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and - // just at the edge quickly change it to transparent. - private final RadialGradient mClipShader = new RadialGradient(0, 0, 1, - new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT }, - new float[] {0, 0.999f, 1}, - Shader.TileMode.CLAMP); - private final PorterDuffXfermode mShadowPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); private RadialGradient mShadowShader = null; @@ -208,8 +200,7 @@ public class PreviewBackground { mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(getBgColor()); - drawCircle(canvas, 0 /* deltaRadius */); - + getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint); drawShadow(canvas); } @@ -244,7 +235,7 @@ public class PreviewBackground { mPaint.setShader(null); if (canvas.isHardwareAccelerated()) { mPaint.setXfermode(mShadowPorterDuffXfermode); - canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint); + getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint); mPaint.setXfermode(null); } @@ -287,7 +278,10 @@ public class PreviewBackground { mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mStrokeWidth); - drawCircle(canvas, 1 /* deltaRadius */); + + float inset = 1f; + getShape().drawShape(canvas, + getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint); } public void drawLeaveBehind(Canvas canvas) { @@ -296,40 +290,17 @@ public class PreviewBackground { mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(Color.argb(160, 245, 245, 245)); - drawCircle(canvas, 0 /* deltaRadius */); + getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint); mScale = originalScale; } - private void drawCircle(Canvas canvas,float deltaRadius) { - float radius = getScaledRadius(); - canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(), - radius - deltaRadius, mPaint); - } - public Path getClipPath() { mPath.reset(); - float r = getScaledRadius(); - mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW); + getShape().addShape(mPath, getOffsetX(), getOffsetY(), getScaledRadius()); return mPath; } - // It is the callers responsibility to save and restore the canvas layers. - void clipCanvasHardware(Canvas canvas) { - mPaint.setColor(Color.BLACK); - mPaint.setStyle(Paint.Style.FILL); - mPaint.setXfermode(mClipPorterDuffXfermode); - - float radius = getScaledRadius(); - mShaderMatrix.setScale(radius, radius); - mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY()); - mClipShader.setLocalMatrix(mShaderMatrix); - mPaint.setShader(mClipShader); - canvas.drawPaint(mPaint); - mPaint.setXfermode(null); - mPaint.setShader(null); - } - private void delegateDrawing(CellLayout delegate, int cellX, int cellY) { if (mDrawingDelegate != delegate) { delegate.addFolderBackground(this); diff --git a/src/com/android/launcher3/graphics/IconShapeOverride.java b/src/com/android/launcher3/graphics/IconShapeOverride.java index cadc6e35eb..b636c6d478 100644 --- a/src/com/android/launcher3/graphics/IconShapeOverride.java +++ b/src/com/android/launcher3/graphics/IconShapeOverride.java @@ -26,9 +26,6 @@ import android.content.Intent; import android.content.res.Resources; import android.os.Build; import android.os.SystemClock; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -42,6 +39,9 @@ import com.android.launcher3.util.LooperExecutor; import java.lang.reflect.Field; import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; /** * Utility class to override shape of {@link android.graphics.drawable.AdaptiveIconDrawable}. diff --git a/src/com/android/launcher3/icons/BaseIconCache.java b/src/com/android/launcher3/icons/BaseIconCache.java index 6433103f94..9198c24825 100644 --- a/src/com/android/launcher3/icons/BaseIconCache.java +++ b/src/com/android/launcher3/icons/BaseIconCache.java @@ -261,7 +261,7 @@ public class BaseIconCache { protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) { info.title = Utilities.trim(entry.title); info.contentDescription = entry.contentDescription; - ((entry.icon == null) ? getDefaultIcon(info.user) : entry).applyTo(info); + info.applyFrom((entry.icon == null) ? getDefaultIcon(info.user) : entry); } public synchronized BitmapInfo getDefaultIcon(UserHandle user) { diff --git a/src/com/android/launcher3/icons/BaseIconFactory.java b/src/com/android/launcher3/icons/BaseIconFactory.java index db723b7208..c8c9618279 100644 --- a/src/com/android/launcher3/icons/BaseIconFactory.java +++ b/src/com/android/launcher3/icons/BaseIconFactory.java @@ -69,14 +69,14 @@ public class BaseIconFactory { public ShadowGenerator getShadowGenerator() { if (mShadowGenerator == null) { - mShadowGenerator = new ShadowGenerator(mContext); + mShadowGenerator = new ShadowGenerator(mIconBitmapSize); } return mShadowGenerator; } public IconNormalizer getNormalizer() { if (mNormalizer == null) { - mNormalizer = new IconNormalizer(mContext); + mNormalizer = new IconNormalizer(mContext, mIconBitmapSize); } return mNormalizer; } diff --git a/src/com/android/launcher3/icons/BitmapInfo.java b/src/com/android/launcher3/icons/BitmapInfo.java index ebe05113e8..245561ea53 100644 --- a/src/com/android/launcher3/icons/BitmapInfo.java +++ b/src/com/android/launcher3/icons/BitmapInfo.java @@ -18,8 +18,6 @@ package com.android.launcher3.icons; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; -import com.android.launcher3.ItemInfoWithIcon; - public class BitmapInfo { public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8); @@ -27,11 +25,6 @@ public class BitmapInfo { public Bitmap icon; public int color; - public void applyTo(ItemInfoWithIcon info) { - info.iconBitmap = icon; - info.iconColor = color; - } - public void applyTo(BitmapInfo info) { info.icon = icon; info.color = color; diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 434945550c..41a53e572f 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -139,7 +139,7 @@ public class IconCache extends BaseIconCache { // null info means not installed, but if we have a component from the intent then // we should still look in the cache for restored app icons. if (info.getTargetComponent() == null) { - getDefaultIcon(info.user).applyTo(info); + info.applyFrom(getDefaultIcon(info.user)); info.title = ""; info.contentDescription = ""; } else { diff --git a/src/com/android/launcher3/icons/IconNormalizer.java b/src/com/android/launcher3/icons/IconNormalizer.java index 73177825bd..4052a55147 100644 --- a/src/com/android/launcher3/icons/IconNormalizer.java +++ b/src/com/android/launcher3/icons/IconNormalizer.java @@ -28,14 +28,9 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.Log; -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.Utilities; -import com.android.launcher3.dragndrop.FolderAdaptiveIcon; - import java.nio.ByteBuffer; import androidx.annotation.NonNull; @@ -84,9 +79,9 @@ public class IconNormalizer { private final Matrix mMatrix; /** package private **/ - IconNormalizer(Context context) { + IconNormalizer(Context context, int iconBitmapSize) { // Use twice the icon size as maximum size to avoid scaling down twice. - mMaxSize = LauncherAppState.getIDP(context).iconBitmapSize * 2; + mMaxSize = iconBitmapSize * 2; mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); mCanvas = new Canvas(mBitmap); mPixels = new byte[mMaxSize * mMaxSize]; @@ -193,17 +188,13 @@ public class IconNormalizer { */ public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds, @Nullable Path path, @Nullable boolean[] outMaskShape) { - if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) { + if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) { if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) { if (outBounds != null) { outBounds.set(mAdaptiveIconBounds); } return mAdaptiveIconScale; } - if (d instanceof FolderAdaptiveIcon) { - // Since we just want the scale, avoid heavy drawing operations - d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); - } } int width = d.getIntrinsicWidth(); int height = d.getIntrinsicHeight(); @@ -314,7 +305,7 @@ public class IconNormalizer { float areaScale = area / (width * height); // Use sqrt of the final ratio as the images is scaled across both width and height. float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; - if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable && + if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable && mAdaptiveIconScale == SCALE_NOT_INITIALIZED) { mAdaptiveIconScale = scale; mAdaptiveIconBounds.set(mBounds); diff --git a/src/com/android/launcher3/icons/ShadowGenerator.java b/src/com/android/launcher3/icons/ShadowGenerator.java index 57d463a806..6491b7ec15 100644 --- a/src/com/android/launcher3/icons/ShadowGenerator.java +++ b/src/com/android/launcher3/icons/ShadowGenerator.java @@ -16,7 +16,6 @@ package com.android.launcher3.icons; -import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BlurMaskFilter; @@ -28,23 +27,19 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; -import com.android.launcher3.LauncherAppState; - import androidx.core.graphics.ColorUtils; /** * Utility class to add shadows to bitmaps. */ public class ShadowGenerator { - - // Percent of actual icon size - private static final float HALF_DISTANCE = 0.5f; public static final float BLUR_FACTOR = 0.5f/48; // Percent of actual icon size public static final float KEY_SHADOW_DISTANCE = 1f/48; private static final int KEY_SHADOW_ALPHA = 61; - + // Percent of actual icon size + private static final float HALF_DISTANCE = 0.5f; private static final int AMBIENT_SHADOW_ALPHA = 30; private final int mIconSize; @@ -53,8 +48,8 @@ public class ShadowGenerator { private final Paint mDrawPaint; private final BlurMaskFilter mDefaultBlurMaskFilter; - public ShadowGenerator(Context context) { - mIconSize = LauncherAppState.getIDP(context).iconBitmapSize; + public ShadowGenerator(int iconSize) { + mIconSize = iconSize; mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL); diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 94cf5c286a..ea4d32b282 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -153,7 +153,7 @@ public class LoaderCursor extends CursorWrapper { info.title = getTitle(); // the fallback icon if (!loadIcon(info)) { - mIconCache.getDefaultIcon(info.user).applyTo(info); + info.applyFrom(mIconCache.getDefaultIcon(info.user)); } // TODO: If there's an explicit component and we can't install that, delete it. @@ -176,7 +176,7 @@ public class LoaderCursor extends CursorWrapper { BitmapInfo iconInfo = li.createIconBitmap(info.iconResource); li.recycle(); if (iconInfo != null) { - iconInfo.applyTo(info); + info.applyFrom(iconInfo); return true; } } @@ -185,7 +185,7 @@ public class LoaderCursor extends CursorWrapper { // Failed to load from resource, try loading from DB. byte[] data = getBlob(iconIndex); try (LauncherIcons li = LauncherIcons.obtain(mContext)) { - li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)).applyTo(info); + info.applyFrom(li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length))); return true; } catch (Exception e) { Log.e(TAG, "Failed to load icon for info " + info, e); diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 1ec7af2bb8..405125e12d 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -498,8 +498,8 @@ public class LoaderTask implements Runnable { c.loadIcon(finalInfo) ? finalInfo.iconBitmap : null; LauncherIcons li = LauncherIcons.obtain(context); - li.createShortcutIcon(pinnedShortcut, - true /* badged */, fallbackIconProvider).applyTo(info); + info.applyFrom(li.createShortcutIcon(pinnedShortcut, + true /* badged */, fallbackIconProvider)); li.recycle(); if (pmHelper.isAppSuspended( pinnedShortcut.getPackage(), info.user)) { diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 201a63e116..0f67f0cd48 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -194,7 +194,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { BitmapInfo iconInfo = li.createIconBitmap(si.iconResource); li.recycle(); if (iconInfo != null) { - iconInfo.applyTo(si); + si.applyFrom(iconInfo); infoUpdated = true; } } diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java index 020bc41822..47fcd9e13b 100644 --- a/src/com/android/launcher3/model/ShortcutsChangedTask.java +++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java @@ -96,8 +96,8 @@ public class ShortcutsChangedTask extends BaseModelUpdateTask { // If the shortcut is pinned but no longer has an icon in the system, // keep the current icon instead of reverting to the default icon. LauncherIcons li = LauncherIcons.obtain(context); - li.createShortcutIcon(fullDetails, true, Provider.of(shortcutInfo.iconBitmap)) - .applyTo(shortcutInfo); + shortcutInfo.applyFrom(li.createShortcutIcon(fullDetails, true, + Provider.of(shortcutInfo.iconBitmap))); li.recycle(); updatedShortcutInfos.add(shortcutInfo); } diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java index 9f02d4fdad..40c1912252 100644 --- a/src/com/android/launcher3/model/UserLockStateChangedTask.java +++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java @@ -95,7 +95,7 @@ public class UserLockStateChangedTask extends BaseModelUpdateTask { // If the shortcut is pinned but no longer has an icon in the system, // keep the current icon instead of reverting to the default icon. LauncherIcons li = LauncherIcons.obtain(context); - li.createShortcutIcon(shortcut, true, Provider.of(si.iconBitmap)).applyTo(si); + si.applyFrom(li.createShortcutIcon(shortcut, true, Provider.of(si.iconBitmap))); li.recycle(); } else { si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java index c14c00eb87..61113b80fd 100644 --- a/src/com/android/launcher3/popup/PopupPopulator.java +++ b/src/com/android/launcher3/popup/PopupPopulator.java @@ -150,7 +150,7 @@ public class PopupPopulator { final ShortcutInfo si = new ShortcutInfo(shortcut, launcher); // Use unbadged icon for the menu. LauncherIcons li = LauncherIcons.obtain(launcher); - li.createShortcutIcon(shortcut, false /* badged */).applyTo(si); + si.applyFrom(li.createShortcutIcon(shortcut, false /* badged */)); li.recycle(); si.rank = i; diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java new file mode 100644 index 0000000000..af0d3daa58 --- /dev/null +++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java @@ -0,0 +1,72 @@ +/* + * 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.popup; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.userevent.nano.LauncherLogProto; + +public class RemoteActionShortcut extends SystemShortcut { + private static final String TAG = "RemoteActionShortcut"; + + private final RemoteAction mAction; + + public RemoteActionShortcut(RemoteAction action) { + super(action.getIcon(), action.getTitle(), action.getContentDescription(), + R.id.action_remote_action_shortcut); + mAction = action; + } + + @Override + public View.OnClickListener getOnClickListener( + final Launcher launcher, final ItemInfo itemInfo) { + return view -> { + AbstractFloatingView.closeAllOpenViews(launcher); + + try { + mAction.getActionIntent().send(0, + (pendingIntent, intent, resultCode, resultData, resultExtras) -> { + if (resultData != null && !resultData.isEmpty()) { + Log.e(TAG, "Remote action returned result: " + mAction.getTitle() + + " : " + resultData); + Toast.makeText(launcher, resultData, Toast.LENGTH_SHORT).show(); + } + }, new Handler(Looper.getMainLooper())); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Remote action canceled: " + mAction.getTitle(), e); + Toast.makeText(launcher, launcher.getString( + R.string.remote_action_failed, + mAction.getTitle()), + Toast.LENGTH_SHORT) + .show(); + } + + launcher.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP, + LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view); + }; + } +} diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java index b80ba8abe1..f9a2007930 100644 --- a/src/com/android/launcher3/popup/SystemShortcut.java +++ b/src/com/android/launcher3/popup/SystemShortcut.java @@ -6,8 +6,10 @@ import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; import android.content.Context; import android.content.Intent; import android.graphics.Rect; -import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; @@ -36,7 +38,7 @@ import java.util.List; public abstract class SystemShortcut extends ItemInfo { private final int mIconResId; private final int mLabelResId; - private final Drawable mIcon; + private final Icon mIcon; private final CharSequence mLabel; private final CharSequence mContentDescription; private final int mAccessibilityActionId; @@ -50,7 +52,7 @@ public abstract class SystemShortcut extends Ite mContentDescription = null; } - public SystemShortcut(Drawable icon, CharSequence label, CharSequence contentDescription, + public SystemShortcut(Icon icon, CharSequence label, CharSequence contentDescription, int accessibilityActionId) { mIcon = icon; mLabel = label; @@ -71,7 +73,9 @@ public abstract class SystemShortcut extends Ite public void setIconAndLabelFor(View iconView, TextView labelView) { if (mIcon != null) { - iconView.setBackground(mIcon); + mIcon.loadDrawableAsync(iconView.getContext(), + iconView::setBackground, + new Handler(Looper.getMainLooper())); } else { iconView.setBackgroundResource(mIconResId); } @@ -85,7 +89,9 @@ public abstract class SystemShortcut extends Ite public void setIconAndContentDescriptionFor(ImageView view) { if (mIcon != null) { - view.setImageDrawable(mIcon); + mIcon.loadDrawableAsync(view.getContext(), + view::setImageDrawable, + new Handler(Looper.getMainLooper())); } else { view.setImageResource(mIconResId); } diff --git a/src/com/android/launcher3/settings/IconBadgingPreference.java b/src/com/android/launcher3/settings/IconBadgingPreference.java new file mode 100644 index 0000000000..7c97b38d28 --- /dev/null +++ b/src/com/android/launcher3/settings/IconBadgingPreference.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 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.settings; + +import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; +import static com.android.launcher3.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGS; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.notification.NotificationListener; +import com.android.launcher3.util.SecureSettingsObserver; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +/** + * A {@link Preference} for indicating icon badging status. + * Also has utility methods for updating UI based on badging status changes. + */ +public class IconBadgingPreference extends Preference + implements SecureSettingsObserver.OnChangeListener { + + private boolean mWidgetFrameVisible = false; + + /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ + private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners"; + + public IconBadgingPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public IconBadgingPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public IconBadgingPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconBadgingPreference(Context context) { + super(context); + } + + private void setWidgetFrameVisible(boolean isVisible) { + if (mWidgetFrameVisible != isVisible) { + mWidgetFrameVisible = isVisible; + notifyChanged(); + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + View widgetFrame = holder.findViewById(android.R.id.widget_frame); + if (widgetFrame != null) { + widgetFrame.setVisibility(mWidgetFrameVisible ? View.VISIBLE : View.GONE); + } + } + + @Override + public void onSettingsChanged(boolean enabled) { + int summary = enabled ? R.string.icon_badging_desc_on : R.string.icon_badging_desc_off; + + boolean serviceEnabled = true; + if (enabled) { + // Check if the listener is enabled or not. + String enabledListeners = Settings.Secure.getString( + getContext().getContentResolver(), NOTIFICATION_ENABLED_LISTENERS); + ComponentName myListener = + new ComponentName(getContext(), NotificationListener.class); + serviceEnabled = enabledListeners != null && + (enabledListeners.contains(myListener.flattenToString()) || + enabledListeners.contains(myListener.flattenToShortString())); + if (!serviceEnabled) { + summary = R.string.title_missing_notification_access; + } + } + setWidgetFrameVisible(!serviceEnabled); + setFragment(serviceEnabled ? null : NotificationAccessConfirmation.class.getName()); + setSummary(summary); + } + + public static class NotificationAccessConfirmation + extends DialogFragment implements DialogInterface.OnClickListener { + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + String msg = context.getString(R.string.msg_missing_notification_access, + context.getString(R.string.derived_app_name)); + return new AlertDialog.Builder(context) + .setTitle(R.string.title_missing_notification_access) + .setMessage(msg) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.title_change_settings, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + ComponentName cn = new ComponentName(getActivity(), NotificationListener.class); + Bundle showFragmentArgs = new Bundle(); + showFragmentArgs.putString(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()); + + Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(EXTRA_FRAGMENT_ARG_KEY, cn.flattenToString()) + .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, showFragmentArgs); + getActivity().startActivity(intent); + } + } +} diff --git a/src/com/android/launcher3/settings/PreferenceHighlighter.java b/src/com/android/launcher3/settings/PreferenceHighlighter.java new file mode 100644 index 0000000000..4ed4cf1133 --- /dev/null +++ b/src/com/android/launcher3/settings/PreferenceHighlighter.java @@ -0,0 +1,127 @@ +/* + * 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.settings; + +import static androidx.core.graphics.ColorUtils.setAlphaComponent; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.Property; +import android.view.View; + +import com.android.launcher3.util.Themes; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ItemDecoration; +import androidx.recyclerview.widget.RecyclerView.State; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * Utility class for highlighting a preference + */ +public class PreferenceHighlighter extends ItemDecoration implements Runnable { + + private static final Property HIGHLIGHT_COLOR = + new Property(Integer.TYPE, "highlightColor") { + + @Override + public Integer get(PreferenceHighlighter highlighter) { + return highlighter.mHighlightColor; + } + + @Override + public void set(PreferenceHighlighter highlighter, Integer value) { + highlighter.mHighlightColor = value; + highlighter.mRv.invalidateItemDecorations(); + } + }; + + private static final long HIGHLIGHT_DURATION = 15000L; + private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L; + private static final long HIGHLIGHT_FADE_IN_DURATION = 200L; + private static final int END_COLOR = setAlphaComponent(Color.WHITE, 0); + + private final Paint mPaint = new Paint(); + private final RecyclerView mRv; + private final int mIndex; + + private boolean mHighLightStarted = false; + private int mHighlightColor = END_COLOR; + + + public PreferenceHighlighter(RecyclerView rv, int index) { + mRv = rv; + mIndex = index; + } + + @Override + public void run() { + mRv.addItemDecoration(this); + mRv.smoothScrollToPosition(mIndex); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, State state) { + ViewHolder holder = parent.findViewHolderForAdapterPosition(mIndex); + if (holder == null) { + return; + } + if (!mHighLightStarted && state.getRemainingScrollVertical() != 0) { + // Wait until scrolling stopped + return; + } + + if (!mHighLightStarted) { + // Start highlight + int colorTo = setAlphaComponent(Themes.getColorAccent(mRv.getContext()), 66); + ObjectAnimator anim = ObjectAnimator.ofArgb(this, HIGHLIGHT_COLOR, END_COLOR, colorTo); + anim.setDuration(HIGHLIGHT_FADE_IN_DURATION); + anim.setRepeatMode(ValueAnimator.REVERSE); + anim.setRepeatCount(4); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeHighlight(); + } + }); + anim.start(); + mHighLightStarted = true; + } + + View view = holder.itemView; + mPaint.setColor(mHighlightColor); + c.drawRect(0, view.getY(), parent.getWidth(), view.getY() + view.getHeight(), mPaint); + } + + private void removeHighlight() { + ObjectAnimator anim = ObjectAnimator.ofArgb( + this, HIGHLIGHT_COLOR, mHighlightColor, END_COLOR); + anim.setDuration(HIGHLIGHT_FADE_OUT_DURATION); + anim.setStartDelay(HIGHLIGHT_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRv.removeItemDecoration(PreferenceHighlighter.this); + } + }); + anim.start(); + } +} diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java new file mode 100644 index 0000000000..66420d079e --- /dev/null +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2015 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.settings; + +import static com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY; +import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; +import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue; +import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.graphics.IconShapeOverride; +import com.android.launcher3.util.SecureSettingsObserver; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceFragment.OnPreferenceStartFragmentCallback; +import androidx.preference.PreferenceFragment.OnPreferenceStartScreenCallback; +import androidx.preference.PreferenceGroup.PreferencePositionCallback; +import androidx.preference.PreferenceScreen; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Settings activity for Launcher. Currently implements the following setting: Allow rotation + */ +public class SettingsActivity extends Activity + implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback { + + private static final String FLAGS_PREFERENCE_KEY = "flag_toggler"; + + private static final String ICON_BADGING_PREFERENCE_KEY = "pref_icon_badging"; + /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ + private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners"; + + public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; + public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"; + private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600; + public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + Bundle args = new Bundle(); + String prefKey = getIntent().getStringExtra(EXTRA_FRAGMENT_ARG_KEY); + if (!TextUtils.isEmpty(prefKey)) { + args.putString(EXTRA_FRAGMENT_ARG_KEY, prefKey); + } + + Fragment f = Fragment.instantiate( + this, getString(R.string.settings_fragment_name), args); + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, f) + .commit(); + } + } + + private boolean startFragment(String fragment, Bundle args, String key) { + if (Utilities.ATLEAST_P && getFragmentManager().isStateSaved()) { + // Sometimes onClick can come after onPause because of being posted on the handler. + // Skip starting new fragments in that case. + return false; + } + Fragment f = Fragment.instantiate(this, fragment, args); + if (f instanceof DialogFragment) { + ((DialogFragment) f).show(getFragmentManager(), key); + } else { + getFragmentManager() + .beginTransaction() + .replace(android.R.id.content, f) + .addToBackStack(key) + .commit(); + } + return true; + } + + @Override + public boolean onPreferenceStartFragment( + PreferenceFragment preferenceFragment, Preference pref) { + return startFragment(pref.getFragment(), pref.getExtras(), pref.getKey()); + } + + @Override + public boolean onPreferenceStartScreen(PreferenceFragment caller, PreferenceScreen pref) { + Bundle args = new Bundle(); + args.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey()); + return startFragment(getString(R.string.settings_fragment_name), args, pref.getKey()); + } + + /** + * This fragment shows the launcher preferences. + */ + public static class LauncherSettingsFragment extends PreferenceFragment { + + private SecureSettingsObserver mIconBadgingObserver; + + private String mHighLightKey; + private boolean mPreferenceHighlighted = false; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + final Bundle args = getArguments(); + mHighLightKey = args == null ? null : args.getString(EXTRA_FRAGMENT_ARG_KEY); + if (rootKey == null && !TextUtils.isEmpty(mHighLightKey)) { + rootKey = getParentKeyForPref(mHighLightKey); + } + + if (savedInstanceState != null) { + mPreferenceHighlighted = savedInstanceState.getBoolean(SAVE_HIGHLIGHTED_KEY); + } + + getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY); + setPreferencesFromResource(R.xml.launcher_preferences, rootKey); + + PreferenceScreen screen = getPreferenceScreen(); + for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) { + Preference preference = screen.getPreference(i); + if (!initPreference(preference)) { + screen.removePreference(preference); + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); + } + + protected String getParentKeyForPref(String key) { + return null; + } + + /** + * Initializes a preference. This is called for every preference. Returning false here + * will remove that preference from the list. + */ + protected boolean initPreference(Preference preference) { + switch (preference.getKey()) { + case ICON_BADGING_PREFERENCE_KEY: + if (!Utilities.ATLEAST_OREO || + !getResources().getBoolean(R.bool.notification_badging_enabled)) { + return false; + } + + // Listen to system notification badge settings while this UI is active. + mIconBadgingObserver = newNotificationSettingsObserver( + getActivity(), (IconBadgingPreference) preference); + mIconBadgingObserver.register(); + // Also listen if notification permission changes + mIconBadgingObserver.getResolver().registerContentObserver( + Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false, + mIconBadgingObserver); + mIconBadgingObserver.dispatchOnChange(); + return true; + + case ADD_ICON_PREFERENCE_KEY: + return Utilities.ATLEAST_OREO; + + case IconShapeOverride.KEY_PREFERENCE: + if (!IconShapeOverride.isSupported(getActivity())) { + return false; + } + IconShapeOverride.handlePreferenceUi((ListPreference) preference); + return true; + + case ALLOW_ROTATION_PREFERENCE_KEY: + if (getResources().getBoolean(R.bool.allow_rotation)) { + // Launcher supports rotation by default. No need to show this setting. + return false; + } + // Initialize the UI once + preference.setDefaultValue(getAllowRotationDefaultValue()); + return true; + + case FLAGS_PREFERENCE_KEY: + // Only show flag toggler UI if this build variant implements that. + return FeatureFlags.showFlagTogglerUi(); + } + + return true; + } + + @Override + public void onResume() { + super.onResume(); + + if (isAdded() && !mPreferenceHighlighted) { + PreferenceHighlighter highlighter = createHighlighter(); + if (highlighter != null) { + getView().postDelayed(highlighter, DELAY_HIGHLIGHT_DURATION_MILLIS); + mPreferenceHighlighted = true; + } + } + } + + private PreferenceHighlighter createHighlighter() { + if (TextUtils.isEmpty(mHighLightKey)) { + return null; + } + + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + return null; + } + + RecyclerView list = getListView(); + PreferencePositionCallback callback = (PreferencePositionCallback) list.getAdapter(); + int position = callback.getPreferenceAdapterPosition(mHighLightKey); + return position >= 0 ? new PreferenceHighlighter(list, position) : null; + } + + @Override + public void onDestroy() { + if (mIconBadgingObserver != null) { + mIconBadgingObserver.unregister(); + mIconBadgingObserver = null; + } + super.onDestroy(); + } + } +} diff --git a/src/com/android/launcher3/util/ListViewHighlighter.java b/src/com/android/launcher3/util/ListViewHighlighter.java deleted file mode 100644 index c9fe228d58..0000000000 --- a/src/com/android/launcher3/util/ListViewHighlighter.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.util; - -import android.animation.ArgbEvaluator; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AbsListView.RecyclerListener; -import android.widget.ListView; - -import com.android.launcher3.R; - -import androidx.core.graphics.ColorUtils; - -/** - * Utility class to scroll and highlight a list view item - */ -public class ListViewHighlighter implements OnScrollListener, RecyclerListener, - OnLayoutChangeListener { - - private final ListView mListView; - private int mPosHighlight; - - private boolean mColorAnimated = false; - - public ListViewHighlighter(ListView listView, int posHighlight) { - mListView = listView; - mPosHighlight = posHighlight; - mListView.setOnScrollListener(this); - mListView.setRecyclerListener(this); - mListView.addOnLayoutChangeListener(this); - - mListView.post(this::tryHighlight); - } - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - mListView.post(this::tryHighlight); - } - - private void tryHighlight() { - if (mPosHighlight < 0 || mListView.getChildCount() == 0) { - return; - } - if (!highlightIfVisible(mListView.getFirstVisiblePosition(), - mListView.getLastVisiblePosition())) { - mListView.smoothScrollToPosition(mPosHighlight); - } - } - - @Override - public void onScrollStateChanged(AbsListView absListView, int i) { } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - highlightIfVisible(firstVisibleItem, firstVisibleItem + visibleItemCount - 1); - } - - private boolean highlightIfVisible(int start, int end) { - if (mPosHighlight < 0 || mListView.getChildCount() == 0) { - return false; - } - if (start > mPosHighlight || mPosHighlight > end) { - return false; - } - highlightView(mListView.getChildAt(mPosHighlight - start)); - - // finish highlight - mListView.setOnScrollListener(null); - mListView.removeOnLayoutChangeListener(this); - - mPosHighlight = -1; - return true; - } - - @Override - public void onMovedToScrapHeap(View view) { - unhighlightView(view); - } - - private void highlightView(View view) { - if (Boolean.TRUE.equals(view.getTag(R.id.view_highlighted))) { - // already highlighted - } else { - view.setTag(R.id.view_highlighted, true); - view.setTag(R.id.view_unhighlight_background, view.getBackground()); - view.setBackground(getHighlightBackground()); - view.postDelayed(() -> { - unhighlightView(view); - }, 15000L); - } - } - - private void unhighlightView(View view) { - if (Boolean.TRUE.equals(view.getTag(R.id.view_highlighted))) { - Object background = view.getTag(R.id.view_unhighlight_background); - if (background instanceof Drawable) { - view.setBackground((Drawable) background); - } - view.setTag(R.id.view_unhighlight_background, null); - view.setTag(R.id.view_highlighted, false); - } - } - - private ColorDrawable getHighlightBackground() { - int color = ColorUtils.setAlphaComponent(Themes.getColorAccent(mListView.getContext()), 26); - if (mColorAnimated) { - return new ColorDrawable(color); - } - mColorAnimated = true; - ColorDrawable bg = new ColorDrawable(Color.WHITE); - ObjectAnimator anim = ObjectAnimator.ofInt(bg, "color", Color.WHITE, color); - anim.setEvaluator(new ArgbEvaluator()); - anim.setDuration(200L); - anim.setRepeatMode(ValueAnimator.REVERSE); - anim.setRepeatCount(4); - anim.start(); - return bg; - } -} diff --git a/src/com/android/launcher3/views/ButtonPreference.java b/src/com/android/launcher3/views/ButtonPreference.java deleted file mode 100644 index fdcf2ca5ba..0000000000 --- a/src/com/android/launcher3/views/ButtonPreference.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2017 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.views; - -import android.content.Context; -import android.preference.Preference; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -/** - * Extension of {@link Preference} which makes the widget layout clickable. - * - * @see #setWidgetLayoutResource(int) - */ -public class ButtonPreference extends Preference { - - private boolean mWidgetFrameVisible = false; - - public ButtonPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public ButtonPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public ButtonPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ButtonPreference(Context context) { - super(context); - } - - public void setWidgetFrameVisible(boolean isVisible) { - if (mWidgetFrameVisible != isVisible) { - mWidgetFrameVisible = isVisible; - notifyChanged(); - } - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - - ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); - if (widgetFrame != null) { - widgetFrame.setVisibility(mWidgetFrameVisible ? View.VISIBLE : View.GONE); - } - } -}