diff options
17 files changed, 995 insertions, 608 deletions
diff --git a/Android.mk b/Android.mk index 15daf1fd2..fbe19b0b6 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 8f4d5bece..5b00b7d02 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 --> <activity - android:name="com.android.launcher3.SettingsActivity" + android:name="com.android.launcher3.settings.SettingsActivity" android:label="@string/settings_button_text" android:theme="@android:style/Theme.DeviceDefault.Settings" android:autoRemoveFromRecents="true"> diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index 1df7c2fba..c55cc4985 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -14,9 +14,10 @@ limitations under the License. --> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> +<androidx.preference.PreferenceScreen + xmlns:android="http://schemas.android.com/apk/res/android"> - <com.android.launcher3.views.ButtonPreference + <com.android.launcher3.settings.IconBadgingPreference android:key="pref_icon_badging" android:title="@string/icon_badging_title" android:persistent="false" @@ -27,7 +28,7 @@ android:name=":settings:fragment_args_key" android:value="notification_badging" /> </intent> - </com.android.launcher3.views.ButtonPreference> + </com.android.launcher3.settings.IconBadgingPreference> <SwitchPreference android:key="pref_add_icon_to_home" @@ -52,10 +53,10 @@ android:defaultValue="" android:persistent="false" /> - <PreferenceScreen + <androidx.preference.PreferenceScreen android:fragment="com.android.launcher3.config.FlagTogglerPreferenceFragment" android:key="flag_toggler" android:persistent="false" android:title="Feature flags"/> -</PreferenceScreen> +</androidx.preference.PreferenceScreen> diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java index a18dfde31..a2538930e 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 a17f61419..000000000 --- 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 11e601c7f..3f7d68d12 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -127,8 +127,8 @@ public class Workspace extends PagedView<WorkspacePageIndicator> 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<WorkspacePageIndicator> * @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<ShortcutAndWidgetContainer> containers = getAllShortcutAndWidgetContainers(); final int containerCount = containers.size(); for (int containerIdx = 0; containerIdx < containerCount; containerIdx++) { diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index c4d105800..94c8d4549 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 1277a2090..fa890b99d 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 d09f03612..429d44fcb 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 000000000..ae279cb3d --- /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 ceb1a8c37..8443953bd 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 cadc6e35e..b636c6d47 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/settings/IconBadgingPreference.java b/src/com/android/launcher3/settings/IconBadgingPreference.java new file mode 100644 index 000000000..7c97b38d2 --- /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 000000000..4ed4cf113 --- /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<PreferenceHighlighter, Integer> HIGHLIGHT_COLOR = + new Property<PreferenceHighlighter, Integer>(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 000000000..66420d079 --- /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 c9fe228d5..000000000 --- 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 fdcf2ca5b..000000000 --- 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); - } - } -} |