diff --git a/lawnchair/res/drawable/ic_smartspace_preferences.xml b/lawnchair/res/drawable/ic_smartspace_preferences.xml new file mode 100644 index 0000000000..bcf14ef1d4 --- /dev/null +++ b/lawnchair/res/drawable/ic_smartspace_preferences.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/lawnchair/res/layout/search_container_all_apps.xml b/lawnchair/res/layout/search_container_all_apps.xml new file mode 100644 index 0000000000..729a87d990 --- /dev/null +++ b/lawnchair/res/layout/search_container_all_apps.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/lawnchair/res/layout/search_container_workspace.xml b/lawnchair/res/layout/search_container_workspace.xml new file mode 100644 index 0000000000..31342acced --- /dev/null +++ b/lawnchair/res/layout/search_container_workspace.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/lawnchair/res/layout/smart_space_date_view.xml b/lawnchair/res/layout/smart_space_date_view.xml new file mode 100644 index 0000000000..75f93b5d24 --- /dev/null +++ b/lawnchair/res/layout/smart_space_date_view.xml @@ -0,0 +1,27 @@ + + + + diff --git a/lawnchair/res/values/dimens.xml b/lawnchair/res/values/dimens.xml new file mode 100644 index 0000000000..9ce30883b2 --- /dev/null +++ b/lawnchair/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + 70dp + 0dp + 72dp + + 24dp + 56dp + \ No newline at end of file diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 95e7ad3466..508cce7894 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -36,4 +36,6 @@ Make Colored Backgrounds For generated Adaptive icons. Notification Dots + + Preferences diff --git a/lawnchair/src/ch/deletescape/nexuslauncher/HotseatQsb.kt b/lawnchair/src/ch/deletescape/nexuslauncher/HotseatQsb.kt new file mode 100644 index 0000000000..a7888963e0 --- /dev/null +++ b/lawnchair/src/ch/deletescape/nexuslauncher/HotseatQsb.kt @@ -0,0 +1,162 @@ +package ch.deletescape.nexuslauncher + +import android.content.Context +import android.graphics.Rect +import android.text.Selection +import android.text.Spannable +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.method.TextKeyListener +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.View +import android.view.animation.Interpolator +import android.widget.EditText +import com.android.launcher3.* +import com.android.launcher3.allapps.AllAppsContainerView +import com.android.launcher3.allapps.AllAppsStore +import com.android.launcher3.allapps.AlphabeticalAppsList +import com.android.launcher3.allapps.SearchUiManager +import com.android.launcher3.allapps.search.AllAppsSearchBarController +import com.android.launcher3.allapps.search.DefaultAppSearchAlgorithm +import com.android.launcher3.anim.Interpolators +import com.android.launcher3.anim.PropertySetter +import com.android.launcher3.graphics.TintedDrawableSpan +import com.android.launcher3.qsb.QsbContainerView +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.views.ActivityContext +import java.util.* + +class HotseatQsb @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + QsbContainerView(context, attrs, defStyleAttr), Insettable, SearchUiManager, AllAppsSearchBarController.Callbacks, AllAppsStore.OnUpdateListener { + private val mActivity: ActivityContext = ActivityContext.lookupContext(context) + private val mFixedTranslationY: Int = resources.getDimensionPixelSize(R.dimen.search_widget_hotseat_height) / 2 + private val mMarginTopAdjusting: Int = resources.getDimensionPixelSize(R.dimen.search_widget_top_shift) + private val mSearchBarController: AllAppsSearchBarController = AllAppsSearchBarController() + private val mSearchQueryBuilder: SpannableStringBuilder = SpannableStringBuilder() + private var mApps: AlphabeticalAppsList? = null + private var mAppsView: AllAppsContainerView? = null + private var mSearchWrapperView: View? = null + private var mFallbackSearchView: ExtendedEditText? = null + + override fun onFinishInflate() { + super.onFinishInflate() + mFallbackSearchView = findViewById(R.id.fallback_search_view) + mSearchWrapperView = findViewById(R.id.search_wrapper_view) + val spanned = SpannableString(" " + mFallbackSearchView?.hint) + spanned.setSpan(TintedDrawableSpan(context, R.drawable.ic_allapps_search), + 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE) + mFallbackSearchView?.hint = spanned + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mAppsView?.appsStore?.addUpdateListener(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mAppsView?.appsStore?.removeUpdateListener(this) + } + + override fun setInsets(insets: Rect) { + visibility = if (mActivity.deviceProfile.isVerticalBarLayout) View.GONE else View.VISIBLE + val mlp: MarginLayoutParams = layoutParams as MarginLayoutParams + mlp.topMargin = (-mFixedTranslationY).coerceAtLeast(insets.top - mMarginTopAdjusting) + val padding: Rect = mActivity.deviceProfile.hotseatLayoutPadding + setPaddingUnchecked(padding.left, 0, padding.right, 0) + requestLayout() + } + + override fun initialize(appsView: AllAppsContainerView) { + mApps = appsView.apps + mAppsView = appsView + mSearchBarController.initialize(DefaultAppSearchAlgorithm(mApps?.apps), + mFallbackSearchView, Launcher.cast(mActivity), this) + } + + override fun onAppsUpdated() { + mSearchBarController.refreshSearchResult() + } + + override fun resetSearch() { + mSearchBarController.reset() + } + + override fun preDispatchKeyEvent(event: KeyEvent) { + if (!mSearchBarController.isSearchFieldFocused && + event.action == KeyEvent.ACTION_DOWN) { + val unicodeChar = event.unicodeChar + val isKeyNotWhitespace = unicodeChar > 0 && + !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar) + if (isKeyNotWhitespace) { + val gotKey: Boolean = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, + event.keyCode, event) + if (gotKey && mSearchQueryBuilder.isNotEmpty()) { + mSearchBarController.focusSearchField() + } + } + } + } + + override fun onSearchResult(query: String, apps: ArrayList) { + mApps?.setOrderedFilter(apps) + notifyResultChanged() + mAppsView?.setLastSearchQuery(query) + } + + override fun clearSearchResult() { + if (mApps?.setOrderedFilter(null) == true) { + notifyResultChanged() + } + mSearchQueryBuilder.clear() + mSearchQueryBuilder.clearSpans() + Selection.setSelection(mSearchQueryBuilder, 0) + mAppsView?.onClearSearchResult() + } + + private fun notifyResultChanged() { + mAppsView?.onSearchResultsChanged() + } + + override fun getScrollRangeDelta(insets: Rect): Float { + return if (mActivity.deviceProfile.isVerticalBarLayout) { + 0.0f + } else { + val dp: DeviceProfile = mActivity.deviceProfile + val percentageOfAvailSpaceFromBottom = 0.45f + val center = ((dp.hotseatBarSizePx - dp.hotseatCellHeightPx + - layoutParams.height - insets.bottom) * percentageOfAvailSpaceFromBottom).toInt() + val bottomMargin = insets.bottom + center + val topMargin = (-mFixedTranslationY).coerceAtLeast(insets.top - mMarginTopAdjusting) + val myBot: Int = layoutParams.height + topMargin + mFixedTranslationY + (bottomMargin + myBot).toFloat() + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + offsetTopAndBottom(mFixedTranslationY) + } + + override fun setContentVisibility(visibleElements: Int, setter: PropertySetter, + interpolator: Interpolator) { + val showAllAppsMode = visibleElements and LauncherState.ALL_APPS_CONTENT != 0 + setter.setViewAlpha(mSearchWrapperView, if (showAllAppsMode) 0f else 1f, Interpolators.LINEAR) + setter.setViewAlpha(mFallbackSearchView, if (showAllAppsMode) 1f else 0f, Interpolators.LINEAR) + } + + override fun setTextSearchEnabled(isEnabled: Boolean): EditText? { + return mFallbackSearchView + } + + class HotseatQsbFragment : QsbFragment() { + override fun isQsbEnabled(): Boolean { + return true + } + } + + init { + Selection.setSelection(mSearchQueryBuilder, 0) + } +} diff --git a/lawnchair/src/ch/deletescape/nexuslauncher/SmartSpaceHostView.kt b/lawnchair/src/ch/deletescape/nexuslauncher/SmartSpaceHostView.kt new file mode 100644 index 0000000000..3631a225cd --- /dev/null +++ b/lawnchair/src/ch/deletescape/nexuslauncher/SmartSpaceHostView.kt @@ -0,0 +1,119 @@ +package ch.deletescape.nexuslauncher + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.graphics.RectF +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.View.OnLongClickListener +import android.view.ViewGroup +import com.android.launcher3.CheckLongPressHelper +import com.android.launcher3.Launcher +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager.EventEnum +import com.android.launcher3.qsb.QsbWidgetHostView +import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener +import com.android.launcher3.views.OptionsPopupView +import com.android.launcher3.views.OptionsPopupView.OptionItem + +class SmartSpaceHostView(context: Context?) : QsbWidgetHostView(context), OnLongClickListener, TouchCompleteListener { + private val mLauncher: Launcher = Launcher.getLauncher(context) + private val mLongPressHelper: CheckLongPressHelper = CheckLongPressHelper(this, this) + + override fun getErrorView(): View { + return SmartspaceQsb.getDateView(this) + } + + override fun onLongClick(view: View): Boolean { + if (!hasSettings(view.context)) { + return false + } + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + val pos = Rect() + mLauncher.dragLayer.getDescendantRectRelativeToSelf(this, pos) + val centerPos = RectF() + centerPos.right = pos.exactCenterX() + centerPos.left = centerPos.right + centerPos.top = 0f + centerPos.bottom = pos.bottom.toFloat() + centerPos.bottom = findBottomRecur(this, pos.top, pos).toFloat().coerceAtMost(centerPos.bottom) + val item = OptionItem(R.string.smartspace_preferences, + R.drawable.ic_smartspace_preferences, NexusLauncherEnum.SMARTSPACE_TAP_OR_LONGPRESS, { v: View -> openSettings(v) }) + OptionsPopupView.show(mLauncher, centerPos, listOf(item)) + return true + } + + private fun findBottomRecur(view: View, max: Int, tempRect: Rect): Int { + var ret = max + if (view.visibility != VISIBLE) { + return ret + } + if (view is ViewGroup) { + for (i in view.childCount - 1 downTo 0) { + ret = findBottomRecur(view.getChildAt(i), ret, tempRect).coerceAtLeast(ret) + } + } + if (!view.willNotDraw()) { + mLauncher.dragLayer.getDescendantRectRelativeToSelf(view, tempRect) + return ret.coerceAtLeast(tempRect.bottom) + } + return ret + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + mLongPressHelper.onTouchEvent(ev) + return mLongPressHelper.hasPerformedLongPress() + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + mLongPressHelper.onTouchEvent(ev) + return true + } + + override fun cancelLongPress() { + super.cancelLongPress() + mLongPressHelper.cancelLongPress() + } + + override fun onTouchComplete() { + if (!mLongPressHelper.hasPerformedLongPress()) { + mLongPressHelper.cancelLongPress() + } + } + + private fun openSettings(v: View): Boolean { + v.context.startActivity(createSettingsIntent()) + return true + } + + companion object { + private const val SETTINGS_INTENT_ACTION = "com.google.android.apps.gsa.smartspace.SETTINGS" + fun hasSettings(context: Context): Boolean { + val info = context.packageManager + .resolveActivity(createSettingsIntent(), 0) + return info != null + } + + fun createSettingsIntent(): Intent { + return Intent(SETTINGS_INTENT_ACTION) + .setPackage(SmartspaceQsb.WIDGET_PACKAGE_NAME) + .setFlags(Intent.FLAG_RECEIVER_FOREGROUND + or Intent.FLAG_ACTIVITY_NO_HISTORY + or Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_NEW_DOCUMENT) + } + } + +} + +enum class NexusLauncherEnum(private val mId: Int) : EventEnum { + SMARTSPACE_TAP_OR_LONGPRESS(520); + + override fun getId(): Int { + return mId + } +} diff --git a/lawnchair/src/ch/deletescape/nexuslauncher/SmartspaceQsb.kt b/lawnchair/src/ch/deletescape/nexuslauncher/SmartspaceQsb.kt new file mode 100644 index 0000000000..0e1fc491af --- /dev/null +++ b/lawnchair/src/ch/deletescape/nexuslauncher/SmartspaceQsb.kt @@ -0,0 +1,62 @@ +package ch.deletescape.nexuslauncher + +import android.annotation.SuppressLint +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.os.Bundle +import android.os.Process +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.android.launcher3.R +import com.android.launcher3.qsb.QsbContainerView + +class SmartspaceQsb @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : QsbContainerView(context, attrs, defStyleAttr) { + class SmartSpaceFragment : QsbFragment() { + override fun createHost(): QsbWidgetHost { + return QsbWidgetHost(context, SMART_SPACE_WIDGET_HOST_ID + ) { c: Context? -> SmartSpaceHostView(c) } + } + + @SuppressLint("NewApi") + override fun getSearchWidgetProvider(): AppWidgetProviderInfo? { + for (info in AppWidgetManager.getInstance(context) + .getInstalledProvidersForPackage(WIDGET_PACKAGE_NAME, Process.myUserHandle())) { + if (WIDGET_CLASS_NAME == info.provider.className) { + return info + } + } + return null + } + + override fun getDefaultView(container: ViewGroup, showSetupIcon: Boolean): View { + return getDateView(container) + } + + override fun createBindOptions(): Bundle { + val opts = super.createBindOptions() + opts.putString("attached-launcher-identifier", context.packageName) + opts.putBoolean("com.google.android.apps.gsa.widget.PREINSTALLED", true) + return opts + } + + companion object { + private const val SMART_SPACE_WIDGET_HOST_ID = 1027 + } + + init { + mKeyWidgetId = "smart_space_widget_id" + } + } + + companion object { + const val WIDGET_PACKAGE_NAME = "com.google.android.googlequicksearchbox" + private const val WIDGET_CLASS_NAME = "com.google.android.apps.gsa.staticplugins.smartspace.widget.SmartspaceWidgetProvider" + fun getDateView(parent: ViewGroup): View { + return LayoutInflater.from(parent.context) + .inflate(R.layout.smart_space_date_view, parent, false) + } + } +}