├── app
├── .gitignore
├── web_hi_res_512.png
├── src
│ └── main
│ │ ├── java
│ │ └── tk
│ │ │ └── zwander
│ │ │ └── widgetdrawer
│ │ │ ├── misc
│ │ │ ├── BaseInfo.kt
│ │ │ ├── AppInfo.kt
│ │ │ ├── ShortcutData.kt
│ │ │ ├── WidgetInfo.kt
│ │ │ ├── WidgetSizeInfo.kt
│ │ │ ├── ShortcutIdManager.kt
│ │ │ ├── BaseWidgetInfo.kt
│ │ │ └── DividerItemDecoration.kt
│ │ │ ├── observables
│ │ │ ├── SizeObservable.kt
│ │ │ ├── EditingObservable.kt
│ │ │ ├── SelectionObservable.kt
│ │ │ └── TransparentObservable.kt
│ │ │ ├── views
│ │ │ ├── CustomCard.kt
│ │ │ ├── ButtonImageView.kt
│ │ │ ├── DrawerHostView.kt
│ │ │ ├── ToolbarAnimHolder.kt
│ │ │ ├── DrawerRecycler.kt
│ │ │ ├── Handle.kt
│ │ │ └── Drawer.kt
│ │ │ ├── App.kt
│ │ │ ├── activities
│ │ │ ├── TriggerActivity.kt
│ │ │ ├── WidgetSelectActivity.kt
│ │ │ └── PermConfigActivity.kt
│ │ │ ├── services
│ │ │ ├── DrawerToggleTile.kt
│ │ │ ├── EnhancedViewService.kt
│ │ │ └── DrawerService.kt
│ │ │ ├── utils
│ │ │ ├── GsonHelpers.kt
│ │ │ ├── EventManager.kt
│ │ │ ├── Utils.kt
│ │ │ └── PrefsManager.kt
│ │ │ ├── host
│ │ │ ├── WidgetHost12.kt
│ │ │ ├── WidgetHostInterface.kt
│ │ │ ├── WidgetHostClass.kt
│ │ │ └── WidgetHostCompat.kt
│ │ │ ├── adapters
│ │ │ ├── AppListAdapter.kt
│ │ │ ├── WidgetListAdapter.kt
│ │ │ └── DrawerAdapter.kt
│ │ │ └── MainActivity.kt
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── ids.xml
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ ├── values_pref.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable
│ │ │ ├── card_background.xml
│ │ │ ├── toolbar_background.xml
│ │ │ ├── divider.xml
│ │ │ ├── add_skel.xml
│ │ │ ├── arrow_up.xml
│ │ │ ├── back.xml
│ │ │ ├── expand_horiz.xml
│ │ │ ├── expand_vert.xml
│ │ │ ├── collapse_vert.xml
│ │ │ ├── handle_left.xml
│ │ │ ├── handle_right.xml
│ │ │ ├── reorder.xml
│ │ │ ├── collapse_horiz.xml
│ │ │ ├── add.xml
│ │ │ ├── edit.xml
│ │ │ ├── visibility.xml
│ │ │ └── grid.xml
│ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── header_layout.xml
│ │ │ ├── widget_holder.xml
│ │ │ ├── activity_widget_select.xml
│ │ │ ├── shortcut_holder.xml
│ │ │ ├── app_item.xml
│ │ │ ├── widget_item.xml
│ │ │ └── drawer_layout.xml
│ │ └── xml
│ │ │ ├── accessibility.xml
│ │ │ └── prefs_main.xml
│ │ └── AndroidManifest.xml
├── Widget Drawer-feature-graphic.png
├── proguard-rules.pro
├── widgeticon.svg
├── build.gradle
└── google-services.json
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .idea
├── encodings.xml
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── misc.xml
├── gradle.xml
└── jarRepositories.xml
├── .gitignore
├── PRIVACY.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/web_hi_res_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/web_hi_res_512.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/BaseInfo.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | abstract class BaseInfo
--------------------------------------------------------------------------------
/app/Widget Drawer-feature-graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/Widget Drawer-feature-graphic.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/WidgetDrawer/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':spannedlm'
2 |
3 | project(':spannedlm').projectDir = new File("../SpannedGridLayoutManager/spannedgridlayoutmanager")
4 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 | /app/release
9 | /app/src/main/res/values/lvl_key.xml
10 | /.idea/
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4dp
4 | 4dp
5 | 8dp
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat May 16 10:47:14 EDT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/observables/SizeObservable.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.observables
2 |
3 | import java.util.*
4 |
5 | class SizeObservable : Observable() {
6 | fun setSize(id: Int) {
7 | setChanged()
8 | notifyObservers(id)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/observables/EditingObservable.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.observables
2 |
3 | import java.util.*
4 |
5 | class EditingObservable : Observable() {
6 | fun setEditing(editing: Boolean) {
7 | setChanged()
8 | notifyObservers(editing)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/observables/SelectionObservable.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.observables
2 |
3 | import java.util.*
4 |
5 | class SelectionObservable : Observable() {
6 | fun setSelection(selection: Int) {
7 | setChanged()
8 | notifyObservers(selection)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #343434
4 | #1ed760
5 | #343434
6 |
7 | #aa000000
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/observables/TransparentObservable.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.observables
2 |
3 | import java.util.*
4 |
5 | class TransparentObservable : Observable() {
6 | fun setTransparent(transparent: Boolean) {
7 | setChanged()
8 | notifyObservers(transparent)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/card_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/toolbar_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/add_skel.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/values_pref.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | 0dp
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_up.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/expand_horiz.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/expand_vert.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/collapse_vert.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/handle_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/handle_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reorder.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/AppInfo.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.pm.ApplicationInfo
4 |
5 | data class AppInfo(
6 | var appName: String,
7 | var appInfo: ApplicationInfo,
8 | var widgets: ArrayList = ArrayList()
9 | ) : BaseInfo(), Comparable {
10 | override fun compareTo(other: AppInfo) =
11 | appName.compareTo(other.appName)
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/collapse_horiz.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/add.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/accessibility.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/edit.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/ShortcutData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.Intent
4 | import android.content.pm.ActivityInfo
5 | import android.os.Parcelable
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class ShortcutData(
10 | var label: String?,
11 | var iconRes: Intent.ShortcutIconResource?,
12 | var activityInfo: ActivityInfo?
13 | ) : Parcelable {
14 | override fun describeContents(): Int {
15 | return 0
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/WidgetInfo.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.pm.ApplicationInfo
4 | import android.os.Parcelable
5 |
6 | data class WidgetInfo(
7 | var widgetName: String,
8 | var previewImg: Int,
9 | var component: Parcelable,
10 | var appInfo: ApplicationInfo
11 | ) : BaseInfo(), Comparable {
12 | val isShortcut: Boolean
13 | get() = component is ShortcutData
14 |
15 | override fun compareTo(other: WidgetInfo) =
16 | widgetName.compareTo(other.widgetName)
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/visibility.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grid.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/header_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | Widget Drawer does not collect or store any information beyond what's necessary for crash logs.
4 |
5 | ## Permissions
6 |
7 | ### Accessibility
8 | This is an optional permission to allow Widget Drawer to work on the lock screen and over the notification center.
9 |
10 | No data is collected and no actions are performed on the device using this service.
11 |
12 | ### SYSTEM_ALERT_WINDOW
13 | This is the normal permission used by Widget Drawer to show the handle and drawer over other apps.
14 |
15 | ### FOREGROUND_SERVICE
16 | This is used to start a foreground service to keep Widget Drawer running when needed.
17 |
18 | ### QUERY_ALL_PACKAGES
19 | This is used to show all widgets from all apps.
20 |
21 | ### BIND_APPWIDGET
22 | This is used to actually create and display widgets.
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/CustomCard.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.GradientDrawable
5 | import android.util.AttributeSet
6 | import android.widget.FrameLayout
7 | import androidx.appcompat.content.res.AppCompatResources
8 | import tk.zwander.widgetdrawer.R
9 |
10 | class CustomCard : FrameLayout {
11 | constructor(context: Context) : super(context)
12 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
13 |
14 | init {
15 | background = AppCompatResources.getDrawable(context, R.drawable.card_background)
16 | }
17 |
18 | fun setCardBackgroundColor(color: Int) {
19 | (background as GradientDrawable).apply {
20 | setColor(color)
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/WidgetSizeInfo.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.Context
4 | import tk.zwander.widgetdrawer.utils.prefs
5 |
6 | data class WidgetSizeInfo(
7 | private var widthSpanSize: Int,
8 | private var heightSpanSize: Int,
9 | val id: Int
10 | ) {
11 | var safeWidthSpanSize: Int
12 | get() = widthSpanSize.coerceAtLeast(1)
13 | set(value) {
14 | widthSpanSize = value.coerceAtLeast(1)
15 | }
16 |
17 | var safeHeightSpanSize: Int
18 | get() = heightSpanSize.coerceAtLeast(1)
19 | set(value) {
20 | heightSpanSize = value.coerceAtLeast(1)
21 | }
22 |
23 | fun getSafeWidthSpanSize(context: Context): Int {
24 | return safeWidthSpanSize.coerceAtMost(context.prefs.columnCount)
25 | }
26 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 |
17 | android.useAndroidX=true
18 | android.enableJetifier=true
19 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_holder.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/ButtonImageView.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.content.Context
4 | import android.content.res.ColorStateList
5 | import android.graphics.Color
6 | import android.util.AttributeSet
7 | import android.widget.ImageView
8 | import androidx.appcompat.widget.AppCompatImageView
9 | import tk.zwander.helperlib.dpAsPx
10 |
11 | class ButtonImageView : AppCompatImageView {
12 | constructor(context: Context) : super(context)
13 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
14 |
15 | init {
16 | val attr = intArrayOf(android.R.attr.selectableItemBackgroundBorderless)
17 | val array = context.obtainStyledAttributes(attr)
18 | val drawable = array.getDrawable(0)
19 | array.recycle()
20 |
21 | isClickable = true
22 | isFocusable = true
23 | background = drawable
24 |
25 | val padding = context.dpAsPx(8)
26 | setPadding(padding, padding, padding, padding)
27 |
28 | imageTintList = ColorStateList.valueOf(Color.WHITE)
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/App.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer
2 |
3 | import android.app.Application
4 | import android.content.*
5 | import android.os.Build
6 | import org.lsposed.hiddenapibypass.HiddenApiBypass
7 | import tk.zwander.widgetdrawer.services.DrawerService
8 | import tk.zwander.widgetdrawer.utils.PrefsManager
9 |
10 | class App : Application(), SharedPreferences.OnSharedPreferenceChangeListener {
11 | val prefs by lazy { PrefsManager.getInstance(this) }
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 |
16 | if (prefs.enabled) {
17 | DrawerService.start(this@App)
18 | }
19 |
20 | prefs.addPrefListener(this@App)
21 |
22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
23 | HiddenApiBypass.addHiddenApiExemptions("")
24 | }
25 | }
26 |
27 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
28 | when (key) {
29 | PrefsManager.ENABLED -> {
30 | if (prefs.enabled) DrawerService.start(this)
31 | else DrawerService.stop(this)
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/activities/TriggerActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.activities
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import tk.zwander.widgetdrawer.R
8 | import tk.zwander.widgetdrawer.utils.Event
9 | import tk.zwander.widgetdrawer.utils.eventManager
10 |
11 | class TriggerActivity : AppCompatActivity() {
12 | @Suppress("DEPRECATION")
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 |
16 | if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
17 | val shortcutIntent = Intent(this, TriggerActivity::class.java)
18 |
19 | val resultIntent = Intent()
20 | resultIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
21 | resultIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, resources.getString(R.string.open_widget_drawer))
22 |
23 | val iconRes = Intent.ShortcutIconResource.fromContext(
24 | this, R.mipmap.ic_launcher
25 | )
26 |
27 | intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes)
28 |
29 | setResult(Activity.RESULT_OK, resultIntent)
30 | finish()
31 | } else {
32 | eventManager.sendEvent(Event.ShowDrawer())
33 |
34 | finish()
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_widget_select.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/ShortcutIdManager.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import tk.zwander.widgetdrawer.host.WidgetHostCompat
6 | import tk.zwander.widgetdrawer.utils.PrefsManager
7 | import kotlin.random.Random
8 |
9 | class ShortcutIdManager private constructor(private val context: Context, private val host: WidgetHostCompat) {
10 | companion object {
11 | @SuppressLint("StaticFieldLeak")
12 | private var instance: ShortcutIdManager? = null
13 |
14 | fun getInstance(context: Context, host: WidgetHostCompat): ShortcutIdManager {
15 | if (instance == null) instance = ShortcutIdManager(context.applicationContext, host)
16 | return instance!!
17 | }
18 | }
19 |
20 | private val prefs by lazy { PrefsManager.getInstance(context) }
21 |
22 | @SuppressLint("NewApi")
23 | fun allocateShortcutId(): Int {
24 | val current = prefs.shortcutIds
25 |
26 | val random = Random(System.currentTimeMillis())
27 | var id = random.nextInt()
28 |
29 | //AppWidgetHost.appWidgetIds has existed since at least 5.1.1, just hidden
30 | while (current.contains(id.toString()) && host.appWidgetIds.contains(id))
31 | id = random.nextInt()
32 |
33 | prefs.addShortcutId(id.toString())
34 |
35 | return id
36 | }
37 |
38 | fun removeShortcutId(id: Int) {
39 | prefs.removeShortcutId(id.toString())
40 | prefs.widgetSizes = prefs.widgetSizes.apply { remove(id) }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/services/DrawerToggleTile.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.services
2 |
3 | import android.annotation.TargetApi
4 | import android.content.SharedPreferences
5 | import android.os.Build
6 | import android.service.quicksettings.Tile
7 | import android.service.quicksettings.TileService
8 | import tk.zwander.widgetdrawer.utils.PrefsManager
9 |
10 | @TargetApi(Build.VERSION_CODES.N)
11 | class DrawerToggleTile : TileService(), SharedPreferences.OnSharedPreferenceChangeListener {
12 | private val prefs by lazy { PrefsManager.getInstance(this) }
13 |
14 | override fun onCreate() {
15 | super.onCreate()
16 | prefs.addPrefListener(this)
17 | }
18 |
19 | override fun onStartListening() {
20 | setState(prefs.enabled)
21 | }
22 |
23 | override fun onClick() {
24 | val newState = !prefs.enabled
25 | prefs.enabled = newState
26 | setState(newState)
27 |
28 | if (newState) DrawerService.start(this)
29 | else DrawerService.stop(this)
30 | }
31 |
32 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
33 | when (key) {
34 | PrefsManager.ENABLED -> setState(prefs.enabled)
35 | }
36 | }
37 |
38 | override fun onDestroy() {
39 | super.onDestroy()
40 | prefs.removePrefListener(this)
41 | }
42 |
43 | private fun setState(enabled: Boolean) {
44 | qsTile?.apply {
45 | state = if (enabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
46 | updateTile()
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/BaseWidgetInfo.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.Intent
4 | import android.os.Parcelable
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | data class BaseWidgetInfo(
9 | var type: Int,
10 | var label: String? = null,
11 | var iconBmpEncoded: String? = null,
12 | var iconRes: Intent.ShortcutIconResource? = null,
13 | var id: Int,
14 | var shortcutIntent: Intent? = null
15 | ) : Parcelable {
16 | companion object {
17 | fun shortcut(
18 | label: String?,
19 | icon: String?,
20 | iconRes: Intent.ShortcutIconResource?,
21 | id: Int,
22 | intent: Intent?
23 | ): BaseWidgetInfo {
24 | return BaseWidgetInfo(
25 | TYPE_SHORTCUT,
26 | label,
27 | icon,
28 | iconRes,
29 | id,
30 | intent
31 | )
32 | }
33 |
34 | fun widget(
35 | id: Int
36 | ): BaseWidgetInfo {
37 | return BaseWidgetInfo(
38 | TYPE_WIDGET,
39 | null,
40 | null,
41 | null,
42 | id
43 | )
44 | }
45 |
46 | fun header() =
47 | BaseWidgetInfo(
48 | TYPE_HEADER,
49 | null,
50 | null,
51 | null,
52 | -1
53 | )
54 |
55 | const val TYPE_WIDGET = 0
56 | const val TYPE_SHORTCUT = 1
57 | const val TYPE_HEADER = 2
58 | }
59 |
60 | override fun describeContents(): Int {
61 | return 0
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/services/EnhancedViewService.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.services
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.content.Context
5 | import android.view.WindowManager
6 | import android.view.accessibility.AccessibilityEvent
7 | import tk.zwander.widgetdrawer.utils.*
8 |
9 | class EnhancedViewService : AccessibilityService(), EventObserver {
10 | private val wm by lazy { getSystemService(Context.WINDOW_SERVICE) as WindowManager }
11 |
12 | override fun onServiceConnected() {
13 | super.onServiceConnected()
14 |
15 | accessibilityConnected = true
16 |
17 | eventManager.addObserver(this)
18 | eventManager.sendEvent(Event.AccessibilityConnected)
19 | }
20 |
21 | override fun onDestroy() {
22 | super.onDestroy()
23 |
24 | eventManager.sendEvent(Event.AccessibilityDisconnected)
25 | eventManager.removeObserver(this)
26 | }
27 |
28 | override fun onInterrupt() {}
29 | override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
30 |
31 | override fun onEvent(event: Event) {
32 | when (event) {
33 | Event.OpenDrawerFromAccessibility -> {
34 | eventManager.sendEvent(Event.ShowDrawer(
35 | wm,
36 | WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
37 | ))
38 | }
39 | Event.AddHandleFromAccessibility -> {
40 | eventManager.sendEvent(Event.ShowHandle(
41 | wm,
42 | WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
43 | ))
44 | }
45 | else -> {}
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/utils/GsonHelpers.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.utils
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import com.google.gson.*
6 | import java.lang.reflect.Type
7 |
8 |
9 | class CrashFixExclusionStrategy : ExclusionStrategy {
10 | private val fieldsToAvoid = setOf(
11 | "IS_ELASTIC_ENABLED",
12 | "isElasticEnabled"
13 | )
14 |
15 | override fun shouldSkipClass(clazz: Class<*>?): Boolean {
16 | return false
17 | }
18 |
19 | override fun shouldSkipField(fieldAttributes: FieldAttributes): Boolean {
20 | val fieldName = fieldAttributes.name
21 |
22 | return fieldsToAvoid.contains(fieldName)
23 | }
24 | }
25 |
26 | class GsonUriHandler : JsonDeserializer, JsonSerializer {
27 | override fun deserialize(
28 | src: JsonElement, srcType: Type,
29 | context: JsonDeserializationContext
30 | ): Uri? {
31 | return try {
32 | Uri.parse(src.asString)
33 | } catch (e: Exception) {
34 | null
35 | }
36 | }
37 |
38 | override fun serialize(src: Uri, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
39 | return JsonPrimitive(src.toString())
40 | }
41 | }
42 |
43 | class GsonIntentHandler : JsonSerializer, JsonDeserializer {
44 | override fun serialize(src: Intent, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
45 | return JsonPrimitive(src.toUri(0))
46 | }
47 |
48 | override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Intent? {
49 | return try {
50 | Intent.parseUri(json.asString, 0)
51 | } catch (e: Exception) {
52 | null
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/DrawerHostView.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.appwidget.AppWidgetHostView
5 | import android.appwidget.AppWidgetProviderInfo
6 | import android.content.Context
7 | import android.view.MotionEvent
8 | import android.view.ViewGroup
9 | import androidx.core.view.NestedScrollingChild
10 | import tk.zwander.widgetdrawer.R
11 |
12 | @SuppressLint("ViewConstructor")
13 | class DrawerHostView(context: Context) : AppWidgetHostView(context), NestedScrollingChild {
14 | private val recView by lazy { parent.parent.parent as DrawerRecycler }
15 |
16 | init {
17 | id = R.id.drawer_view
18 | }
19 |
20 | override fun onAttachedToWindow() {
21 | super.onAttachedToWindow()
22 |
23 | layoutParams = layoutParams.apply {
24 | width = ViewGroup.LayoutParams.MATCH_PARENT
25 | height = ViewGroup.LayoutParams.MATCH_PARENT
26 | }
27 |
28 | enableNestedScrolling(this)
29 | isNestedScrollingEnabled = true
30 | }
31 |
32 | override fun setAppWidget(appWidgetId: Int, info: AppWidgetProviderInfo?) {
33 | super.setAppWidget(appWidgetId, info)
34 |
35 | setPadding(0, 0, 0, 0)
36 | setPaddingRelative(0, 0, 0, 0)
37 | }
38 |
39 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
40 | return if (recView.allowReorder) {
41 | callOnClick()
42 | true
43 | } else {
44 | super.onInterceptTouchEvent(ev)
45 | }
46 | }
47 |
48 | private fun enableNestedScrolling(parent: ViewGroup) {
49 | for (i in 0 until parent.childCount) {
50 | val child = parent.getChildAt(i)
51 | child.isNestedScrollingEnabled = true
52 |
53 | if (child is ViewGroup) enableNestedScrolling(child)
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/shortcut_holder.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
21 |
22 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
21 |
22 |
29 |
30 |
40 |
41 |
42 |
43 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/host/WidgetHost12.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.host
2 |
3 | import android.appwidget.AppWidgetHost
4 | import android.annotation.SuppressLint
5 | import android.app.ActivityOptions
6 | import android.app.PendingIntent
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.view.View
10 | import android.widget.RemoteViews
11 | import java.lang.reflect.InvocationHandler
12 | import java.lang.reflect.Method
13 | import java.lang.reflect.Proxy
14 |
15 | /**
16 | * An implementation of [AppWidgetHost] used on devices that don't have [RemoteViews.OnClickHandler]
17 | * (i.e., Android 12+). Instead, this uses [RemoteViews.InteractionHandler], which is just a renamed version
18 | * of [RemoteViews.OnClickHandler].
19 | */
20 | @SuppressLint("PrivateApi")
21 | class WidgetHost12(context: Context, id: Int) : WidgetHostCompat(
22 | context, id, Proxy.newProxyInstance(
23 | Class.forName("android.widget.RemoteViews\$InteractionHandler").classLoader,
24 | arrayOf(Class.forName("android.widget.RemoteViews\$InteractionHandler")),
25 | InnerOnClickHandler12(context)
26 | )
27 | ) {
28 | class InnerOnClickHandler12(context: Context) : BaseInnerOnClickHandler(context),
29 | InvocationHandler {
30 | @SuppressLint("BlockedPrivateApi", "PrivateApi")
31 | override fun invoke(proxy: Any?, method: Method?, args: Array): Any {
32 | val view = args[0] as View
33 | val pi = args[1] as PendingIntent
34 | val response = args[2]
35 |
36 | val responseClass = Class.forName("android.widget.RemoteViews\$RemoteResponse")
37 |
38 | val getLaunchOptions = responseClass.getDeclaredMethod("getLaunchOptions", View::class.java)
39 | val startPendingIntent = RemoteViews::class.java.getDeclaredMethod(
40 | "startPendingIntent", View::class.java, PendingIntent::class.java, android.util.Pair::class.java)
41 |
42 | @Suppress("UNCHECKED_CAST")
43 | val launchOptions = getLaunchOptions.invoke(response, view) as android.util.Pair
44 |
45 | checkPendingIntent(pi)
46 |
47 | return startPendingIntent.invoke(null, view, pi, launchOptions) as Boolean
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/host/WidgetHostInterface.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.host
2 |
3 | import android.appwidget.AppWidgetHost
4 | import android.annotation.SuppressLint
5 | import android.app.ActivityOptions
6 | import android.app.PendingIntent
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.view.View
10 | import android.widget.RemoteViews
11 | import java.lang.reflect.InvocationHandler
12 | import java.lang.reflect.Method
13 | import java.lang.reflect.Proxy
14 |
15 | /**
16 | * An implementation of [AppWidgetHost] used on devices where the hidden API object
17 | * [RemoteViews.OnClickHandler] is an interface (i.e. Android 10 and above). Java makes
18 | * it pretty easy to implement interfaces through reflection, so no ByteBuddy needed here.
19 | */
20 | @SuppressLint("PrivateApi")
21 | class WidgetHostInterface(context: Context, id: Int)
22 | : WidgetHostCompat(
23 | context, id, Proxy.newProxyInstance(
24 | Class.forName("android.widget.RemoteViews\$OnClickHandler").classLoader,
25 | arrayOf(Class.forName("android.widget.RemoteViews\$OnClickHandler")),
26 | InnerOnClickHandlerQ(context)
27 | )
28 | ) {
29 | class InnerOnClickHandlerQ(context: Context) : BaseInnerOnClickHandler(context), InvocationHandler {
30 | @SuppressLint("BlockedPrivateApi", "PrivateApi")
31 | override fun invoke(proxy: Any?, method: Method?, args: Array): Any {
32 | val view = args[0] as View
33 | val pi = args[1] as PendingIntent
34 | val response = args[2]
35 |
36 | val responseClass = Class.forName("android.widget.RemoteViews\$RemoteResponse")
37 |
38 | val getLaunchOptions = responseClass.getDeclaredMethod("getLaunchOptions", View::class.java)
39 | val startPendingIntent = RemoteViews::class.java.getDeclaredMethod(
40 | "startPendingIntent", View::class.java, PendingIntent::class.java, android.util.Pair::class.java)
41 |
42 | @Suppress("UNCHECKED_CAST")
43 | val launchOptions = getLaunchOptions.invoke(response, view) as android.util.Pair
44 |
45 | checkPendingIntent(pi)
46 |
47 | return startPendingIntent.invoke(null, view, pi, launchOptions) as Boolean
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
22 |
23 |
26 |
27 |
37 |
38 |
45 |
46 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
32 |
33 |
36 |
37 |
43 |
44 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/host/WidgetHostClass.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.host
2 |
3 | import android.annotation.SuppressLint
4 | import android.appwidget.AppWidgetHost
5 | import android.app.PendingIntent
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Build
9 | import android.view.View
10 | import android.widget.RemoteViews
11 | import net.bytebuddy.ByteBuddy
12 | import net.bytebuddy.android.AndroidClassLoadingStrategy
13 | import net.bytebuddy.implementation.MethodDelegation
14 | import net.bytebuddy.implementation.SuperMethodCall
15 |
16 | /**
17 | * An implementation of [AppWidgetHost] used on devices where the hidden API object
18 | * [RemoteViews.OnClickHandler] is a class (i.e. Android Pie and below).
19 | * The handler is implemented through dynamic bytecode generation using ByteBuddy.
20 | * Since Lockscreen Widgets targets an API level above Pie, the [RemoteViews.OnClickHandler]
21 | * visible to it is an interface, so we can't just create a stub class.
22 | */
23 | @SuppressLint("PrivateApi")
24 | class WidgetHostClass(context: Context, id: Int)
25 | : WidgetHostCompat(
26 | context, id, ByteBuddy()
27 | .subclass(Class.forName("android.widget.RemoteViews\$OnClickHandler"))
28 | .name("OnClickHandlerPieIntercept")
29 | .defineMethod("onClickHandler", Boolean::class.java)
30 | .withParameters(View::class.java, PendingIntent::class.java, Intent::class.java)
31 | .intercept(
32 | MethodDelegation.to(InnerOnClickHandlerPie(context))
33 | .andThen(SuperMethodCall.INSTANCE)
34 | )
35 | .apply {
36 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
37 | defineMethod("onClickHandler", Boolean::class.java)
38 | .withParameters(View::class.java, PendingIntent::class.java, Intent::class.java, Int::class.java)
39 | .intercept(
40 | MethodDelegation.to(InnerOnClickHandlerPie(context))
41 | .andThen(SuperMethodCall.INSTANCE)
42 | )
43 | }
44 | }
45 | .make()
46 | .load(WidgetHostCompat::class.java.classLoader, AndroidClassLoadingStrategy.Wrapping(context.cacheDir))
47 | .loaded
48 | .newInstance()
49 | ) {
50 | class InnerOnClickHandlerPie(context: Context): BaseInnerOnClickHandler(context) {
51 | @Suppress("UNUSED_PARAMETER")
52 | fun onClickHandler(
53 | view: View,
54 | pendingIntent: PendingIntent,
55 | fillInIntent: Intent
56 | ): Boolean {
57 | checkPendingIntent(pendingIntent)
58 |
59 | return true
60 | }
61 |
62 | @Suppress("UNUSED_PARAMETER")
63 | fun onClickHandler(
64 | view: View,
65 | pendingIntent: PendingIntent,
66 | fillInIntent: Intent,
67 | windowingMode: Int
68 | ): Boolean {
69 | checkPendingIntent(pendingIntent)
70 |
71 | return true
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/prefs_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
11 |
16 |
17 |
23 |
24 |
29 |
30 |
36 |
37 |
43 |
44 |
53 |
54 |
63 |
64 |
72 |
73 |
80 |
81 |
89 |
90 |
--------------------------------------------------------------------------------
/app/widgeticon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
84 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'com.google.firebase.crashlytics'
6 |
7 | apply plugin: 'kotlin-parcelize'
8 |
9 | def version = 32
10 |
11 | android {
12 | compileSdkVersion 31
13 | defaultConfig {
14 | applicationId "tk.zwander.widgetdrawer"
15 | minSdkVersion 22
16 | targetSdkVersion 31
17 | versionCode version
18 | versionName version.toString()
19 | }
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | packagingOptions {
27 | exclude 'META-INF/atomicfu.kotlin_module'
28 | }
29 | lintOptions {
30 | abortOnError = false
31 | }
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = JavaVersion.VERSION_1_8.toString()
39 | }
40 | buildFeatures {
41 | viewBinding true
42 | }
43 | }
44 |
45 | dependencies {
46 | implementation fileTree(dir: 'libs', include: ['*.jar'])
47 | implementation 'com.android.support.test:runner:1.0.2'
48 |
49 | //Kotlin
50 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
51 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
52 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
53 |
54 | //AndroidX
55 | implementation 'androidx.appcompat:appcompat:1.4.1'
56 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
57 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
58 | implementation 'androidx.preference:preference-ktx:1.2.0'
59 | implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
60 | implementation "androidx.core:core-ktx:1.7.0"
61 |
62 | //Google
63 | implementation 'com.google.code.gson:gson:2.8.9'
64 | implementation 'com.google.firebase:firebase-core:21.0.0'
65 | implementation 'com.google.android.material:material:1.7.0-alpha01'
66 |
67 | //Other
68 | implementation 'com.github.zacharee:SeekBarPreference:b0b9567cd0'
69 | implementation 'com.github.zacharee:HelperLib:0317cbd35b'
70 | implementation 'com.github.zacharee:colorpicker:9ea5085260'
71 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
72 | // implementation 'com.github.zacharee:SpannedGridLayoutManager:b44693ba2d'
73 | implementation project(':spannedlm')
74 | // implementation 'com.jaredrummler:colorpicker:1.1.0'
75 | implementation 'com.github.agrawalsuneet:DotLoadersPack-Android:v1.4'
76 | implementation 'com.github.tingyik90:snackprogressbar:6.1'
77 | implementation 'com.squareup.picasso:picasso:2.71828'
78 | implementation 'net.bytebuddy:byte-buddy-android:1.11.12'
79 | implementation 'com.google.firebase:firebase-crashlytics:18.2.10'
80 | implementation 'com.google.firebase:firebase-analytics:21.0.0'
81 | }
82 |
83 | apply plugin: 'com.google.gms.google-services'
84 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Widget Drawer
3 |
4 |
5 | Enable
6 | Handle Height
7 | Handle Width
8 | Handle Color
9 | Handle Shadow
10 | Add Widget
11 | Unlicensed App!
12 | Show Handle
13 | Open Drawer
14 | Tap Empty Space to Close Drawer
15 | Undo
16 | Shortcut
17 | Drawer Background Color
18 | Enhanced View Mode
19 | Column Count
20 |
21 |
22 | Please allow Widget Drawer to draw over other apps
23 | It seems you have an unlicensed copy of Widget Drawer. If you recently purchased the app, wait a few minutes and try again.
24 | Press and hold widgets to reorder.\nSwipe widgets left or right to remove.
25 | Widget removed
26 | Unable to configure widget
27 | Allow Widget Drawer to display above everything (apps, status bar, lock screen, etc).
28 | Allow Widget Drawer to display above everything (apps, status bar, lock screen, etc). Widget Drawer does not collect any personal information using this service. It is simply used to allow the drawer to show over everything.
29 | Please enable the Enhanced View Mode service.
30 | How many columns of widgets the drawer should have. Increase this for greater widget width granularity.
31 | Enhanced View Mode makes use of an Accessibility Service to display Widget Drawer on the lock screen. Press \"OK\" to go to Settings to grant Widget Drawer accessibility access. No personal information is collected.
32 |
33 |
34 | Edit Mode
35 | Open Widget Drawer
36 | Toggle Opaque Backgrounds
37 | Close Drawer
38 | Open/Close Toolbar
39 | Expand Widget Vertically
40 | Expand Widget Horizontally
41 | Collapse Widget Vertically
42 | Collapse Widget Horizontally
43 | Back
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/host/WidgetHostCompat.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.host
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.PendingIntent
5 | import android.appwidget.AppWidgetHost
6 | import android.content.Context
7 | import android.widget.RemoteViews
8 | import tk.zwander.widgetdrawer.utils.Event
9 | import tk.zwander.widgetdrawer.utils.eventManager
10 | import tk.zwander.widgetdrawer.utils.prefs
11 |
12 | /**
13 | * Base widget host class. [WidgetHostClass], [WidgetHostInterface], and [WidgetHost12] extend this class and
14 | * are used conditionally, depending on whether [RemoteViews.OnClickHandler] is a class or interface
15 | * or [RemoteViews.InteractionHandler] is used on the device.
16 | *
17 | * @param context a Context object
18 | * @param id the ID of this widget host
19 | * @param onClickHandler the [RemoteViews.OnClickHandler] or [RemoteViews.InteractionHandler]
20 | * implementation defined in the subclass
21 | */
22 | abstract class WidgetHostCompat(
23 | val context: Context,
24 | id: Int,
25 | onClickHandler: Any
26 | ) : AppWidgetHost(context, id) {
27 | companion object {
28 | @SuppressLint("StaticFieldLeak")
29 | private var instance: WidgetHostCompat? = null
30 |
31 | @SuppressLint("PrivateApi")
32 | fun getInstance(context: Context, id: Int): WidgetHostCompat {
33 | return instance ?: run {
34 | if (!onClickHandlerExists) {
35 | WidgetHost12(context.applicationContext ?: context, id)
36 | } else {
37 | (if (Class.forName("android.widget.RemoteViews\$OnClickHandler").isInterface) {
38 | WidgetHostInterface(context.applicationContext ?: context, id)
39 | } else {
40 | WidgetHostClass(context.applicationContext ?: context, id)
41 | }).also {
42 | instance = it
43 | }
44 | }
45 | }
46 | }
47 |
48 | private val onClickHandlerExists: Boolean
49 | @SuppressLint("PrivateApi")
50 | get() = try {
51 | Class.forName("android.widget.RemoteViews\$OnClickHandler")
52 | true
53 | } catch (e: ClassNotFoundException) {
54 | //Should crash if neither exists
55 | Class.forName("android.widget.RemoteViews\$InteractionHandler")
56 | false
57 | }
58 | }
59 |
60 | init {
61 | AppWidgetHost::class.java
62 | .getDeclaredField(if (!onClickHandlerExists) "mInteractionHandler" else "mOnClickHandler")
63 | .apply {
64 | isAccessible = true
65 | set(this@WidgetHostCompat, onClickHandler)
66 | }
67 | }
68 |
69 | open class BaseInnerOnClickHandler(internal val context: Context) {
70 | @SuppressLint("NewApi")
71 | fun checkPendingIntent(pendingIntent: PendingIntent) {
72 | if (pendingIntent.isActivity) {
73 | context.eventManager.sendEvent(Event.CloseDrawer)
74 | }
75 | }
76 | }
77 |
78 | override fun deleteAppWidgetId(appWidgetId: Int) {
79 | super.deleteAppWidgetId(appWidgetId)
80 |
81 | context.prefs.apply {
82 | widgetSizes = widgetSizes.apply { remove(appWidgetId) }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/utils/EventManager.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.utils
2 |
3 | import android.annotation.SuppressLint
4 | import android.appwidget.AppWidgetProviderInfo
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.graphics.Bitmap
8 | import android.view.WindowManager
9 | import tk.zwander.widgetdrawer.misc.ShortcutData
10 |
11 | class EventManager private constructor(private val context: Context) {
12 | companion object {
13 | @SuppressLint("StaticFieldLeak")
14 | private var _instance: EventManager? = null
15 |
16 | fun getInstance(context: Context): EventManager {
17 | return _instance ?: EventManager(context.applicationContext ?: context).apply {
18 | _instance = this
19 | }
20 | }
21 | }
22 |
23 | private val listeners: MutableList> = ArrayList()
24 | private val observers: MutableList = ArrayList()
25 |
26 | inline fun addListener(noinline listener: (T) -> Unit) {
27 | addListener(
28 | ListenerInfo(
29 | T::class.java,
30 | listener
31 | )
32 | )
33 | }
34 |
35 | fun addListener(listenerInfo: ListenerInfo) {
36 | listeners.add(listenerInfo as ListenerInfo)
37 | }
38 |
39 | fun addObserver(observer: EventObserver) {
40 | observers.add(observer)
41 | }
42 |
43 | inline fun removeListener(noinline listener: (T) -> Unit) {
44 | removeListener(
45 | ListenerInfo(
46 | T::class.java,
47 | listener
48 | )
49 | )
50 | }
51 |
52 | fun removeListener(listenerInfo: ListenerInfo) {
53 | listeners.remove(listenerInfo as ListenerInfo)
54 | }
55 |
56 | fun removeObserver(observer: EventObserver) {
57 | observers.remove(observer)
58 | }
59 |
60 | fun sendEvent(event: Event) {
61 | observers.forEach {
62 | it.onEvent(event)
63 | }
64 |
65 | listeners.filter { it.listenerClass == event::class.java }
66 | .forEach {
67 | it.listener.invoke(event)
68 | }
69 | }
70 | }
71 |
72 | sealed class Event {
73 | data class PermissionResult(val success: Boolean, val widgetId: Int) : Event()
74 | data class WidgetConfigResult(val success: Boolean, val widgetId: Int) : Event()
75 | data class ShortcutConfigResult(val success: Boolean, val data: ShortcutData?, val intent: Intent?, val name: String?, val iconRes: Intent.ShortcutIconResource?, val iconBmp: Bitmap?) : Event()
76 | data class PickWidgetResult(val success: Boolean, val providerInfo: AppWidgetProviderInfo) : Event()
77 | data class PickShortcutResult(val success: Boolean, val shortcutData: ShortcutData) : Event()
78 |
79 | data class ShowDrawer(val wm: WindowManager? = null, val type: Int = getProperWLPType()) : Event()
80 | data class ShowHandle(val wm: WindowManager? = null, val type: Int = getProperWLPType()) : Event()
81 |
82 | object PickFailedResult : Event()
83 | object OpenDrawerFromAccessibility : Event()
84 | object AddHandleFromAccessibility : Event()
85 | object AccessibilityConnected : Event()
86 | object AccessibilityDisconnected : Event()
87 | object CloseDrawer : Event()
88 | }
89 |
90 | interface EventObserver {
91 | fun onEvent(event: Event)
92 | }
93 |
94 | data class ListenerInfo(
95 | val listenerClass: Class,
96 | val listener: (T) -> Unit
97 | )
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | xmlns:android
17 | ^$
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | xmlns:.*
27 | ^$
28 |
29 |
30 | BY_NAME
31 |
32 |
33 |
34 |
35 |
36 |
37 | .*:id
38 | http://schemas.android.com/apk/res/android
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | .*:name
48 | http://schemas.android.com/apk/res/android
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | name
58 | ^$
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | style
68 | ^$
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | .*
78 | ^$
79 |
80 |
81 | BY_NAME
82 |
83 |
84 |
85 |
86 |
87 |
88 | .*
89 | http://schemas.android.com/apk/res/android
90 |
91 |
92 | ANDROID_ATTRIBUTE_ORDER
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 | .*
101 |
102 |
103 | BY_NAME
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/adapters/AppListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.adapters
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Parcelable
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.recyclerview.widget.RecyclerView
10 | import androidx.recyclerview.widget.SortedList
11 | import com.squareup.picasso.Picasso
12 | import tk.zwander.widgetdrawer.R
13 | import tk.zwander.widgetdrawer.databinding.AppItemBinding
14 | import tk.zwander.widgetdrawer.misc.AppInfo
15 | import tk.zwander.widgetdrawer.misc.DividerItemDecoration
16 |
17 | class AppListAdapter(private val context: Context, private val selectionCallback: (provider: Parcelable) -> Unit) : RecyclerView.Adapter() {
18 | private val items = SortedList(AppInfo::class.java, object : SortedList.Callback() {
19 | override fun areItemsTheSame(item1: AppInfo?, item2: AppInfo?): Boolean {
20 | return false
21 | }
22 |
23 | override fun areContentsTheSame(oldItem: AppInfo?, newItem: AppInfo?): Boolean {
24 | return false
25 | }
26 |
27 | override fun compare(o1: AppInfo, o2: AppInfo): Int {
28 | return o1.compareTo(o2)
29 | }
30 |
31 | override fun onMoved(fromPosition: Int, toPosition: Int) {
32 | notifyItemMoved(fromPosition, toPosition)
33 | }
34 |
35 | override fun onChanged(position: Int, count: Int) {
36 | notifyItemRangeChanged(position, count)
37 | }
38 |
39 | override fun onInserted(position: Int, count: Int) {
40 | notifyItemRangeInserted(position, count)
41 | }
42 |
43 | override fun onRemoved(position: Int, count: Int) {
44 | notifyItemRangeRemoved(position, count)
45 | }
46 | })
47 |
48 | private val picasso = Picasso.Builder(context)
49 | .addRequestHandler(WidgetListAdapter.AppIconRequestHandler(context))
50 | .addRequestHandler(WidgetListAdapter.RemoteResourcesIconHandler(context))
51 | .build()
52 |
53 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
54 | AppVH(
55 | LayoutInflater.from(parent.context).inflate(
56 | R.layout.app_item,
57 | parent,
58 | false
59 | ),
60 | picasso,
61 | selectionCallback
62 | )
63 |
64 | override fun onBindViewHolder(holder: AppVH, position: Int) {
65 | holder.parseInfo(items.get(holder.adapterPosition))
66 | holder.setIsRecyclable(false)
67 | }
68 |
69 | override fun getItemCount() = items.size()
70 |
71 | fun addItem(item: AppInfo) {
72 | items.add(item)
73 | }
74 |
75 | fun addItems(items: MutableCollection) {
76 | items.forEach { addItem(it) }
77 | }
78 |
79 | class AppVH(view: View, private val picasso: Picasso, selectionCallback: (provider: Parcelable) -> Unit) : RecyclerView.ViewHolder(view) {
80 | private val adapter = WidgetListAdapter(picasso, selectionCallback)
81 | private val binding = AppItemBinding.bind(itemView)
82 |
83 | fun parseInfo(info: AppInfo) {
84 | binding.widgetHolder.adapter = adapter
85 | binding.widgetHolder.addItemDecoration(DividerItemDecoration(itemView.context, RecyclerView.HORIZONTAL))
86 |
87 | binding.appName.text = info.appName
88 | info.widgets.forEach {
89 | adapter.addItem(it)
90 | }
91 |
92 | picasso
93 | .load(Uri.parse("${WidgetListAdapter.AppIconRequestHandler.SCHEME}:${info.appInfo.packageName}"))
94 | .fit()
95 | .into(binding.appIcon)
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
11 |
14 |
15 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
38 |
45 |
50 |
55 |
56 |
57 |
58 |
59 |
60 |
65 |
72 |
73 |
74 |
75 |
76 |
80 |
81 |
82 |
83 |
84 |
87 |
88 |
89 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/misc/DividerItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.misc
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Rect
6 | import android.graphics.drawable.Drawable
7 | import android.view.View
8 | import android.widget.LinearLayout
9 | import androidx.core.content.ContextCompat
10 | import androidx.recyclerview.widget.RecyclerView
11 | import tk.zwander.widgetdrawer.R
12 | import kotlin.math.roundToInt
13 |
14 | /**
15 | * Modified version of Android's DividerItemDecoration
16 | * that doesn't draw the divider after the last item
17 | * @see androidx.recyclerview.widget.DividerItemDecoration
18 | */
19 | class DividerItemDecoration(context: Context, orientation: Int) : RecyclerView.ItemDecoration() {
20 | var divider: Drawable? = null
21 | set(drawable) {
22 | if (drawable == null) {
23 | throw IllegalArgumentException("Divider cannot be nulll")
24 | }
25 |
26 | field = drawable
27 | }
28 |
29 | /**
30 | * Current orientation. Either [.HORIZONTAL] or [.VERTICAL].
31 | */
32 | var orientation: Int = 0
33 | set(orientation) {
34 | if (orientation != HORIZONTAL && orientation != VERTICAL) {
35 | throw IllegalArgumentException(
36 | "Invalid orientation. It should be either HORIZONTAL or VERTICAL"
37 | )
38 | }
39 |
40 | field = orientation
41 | }
42 |
43 | private val bounds = Rect()
44 |
45 | init {
46 | divider = ContextCompat.getDrawable(context, R.drawable.divider)
47 | this.orientation = orientation
48 | }
49 |
50 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
51 | if (parent.layoutManager == null || divider == null) {
52 | return
53 | }
54 | if (orientation == VERTICAL) {
55 | drawVertical(c, parent)
56 | } else {
57 | drawHorizontal(c, parent)
58 | }
59 | }
60 |
61 | private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
62 | canvas.save()
63 | val left: Int
64 | val right: Int
65 |
66 | if (parent.clipToPadding) {
67 | left = parent.paddingLeft
68 | right = parent.width - parent.paddingRight
69 | canvas.clipRect(
70 | left, parent.paddingTop, right,
71 | parent.height - parent.paddingBottom
72 | )
73 | } else {
74 | left = 0
75 | right = parent.width
76 | }
77 |
78 | val childCount = parent.childCount
79 | for (i in 0 until childCount - 1) {
80 | val child = parent.getChildAt(i)
81 | parent.getDecoratedBoundsWithMargins(child, bounds)
82 | val bottom = bounds.bottom + child.translationY.roundToInt()
83 | val top = bottom - divider!!.intrinsicHeight
84 | divider!!.setBounds(left, top, right, bottom)
85 | divider!!.draw(canvas)
86 | }
87 | canvas.restore()
88 | }
89 |
90 | private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
91 | canvas.save()
92 | val top: Int
93 | val bottom: Int
94 |
95 | if (parent.clipToPadding) {
96 | top = parent.paddingTop
97 | bottom = parent.height - parent.paddingBottom
98 | canvas.clipRect(
99 | parent.paddingLeft, top,
100 | parent.width - parent.paddingRight, bottom
101 | )
102 | } else {
103 | top = 0
104 | bottom = parent.height
105 | }
106 |
107 | val childCount = parent.childCount
108 | for (i in 0 until childCount - 1) {
109 | val child = parent.getChildAt(i)
110 | parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds)
111 | val right = bounds.right + child.translationX.roundToInt()
112 | val left = right - divider!!.intrinsicWidth
113 | divider!!.setBounds(left, top, right, bottom)
114 | divider!!.draw(canvas)
115 | }
116 | canvas.restore()
117 | }
118 |
119 | override fun getItemOffsets(
120 | outRect: Rect, view: View, parent: RecyclerView,
121 | state: RecyclerView.State
122 | ) {
123 | if (divider == null) {
124 | outRect.set(0, 0, 0, 0)
125 | return
126 | }
127 | if (orientation == VERTICAL) {
128 | outRect.set(0, 0, 0, divider!!.intrinsicHeight)
129 | } else {
130 | outRect.set(0, 0, divider!!.intrinsicWidth, 0)
131 | }
132 | }
133 |
134 | companion object {
135 | const val HORIZONTAL = LinearLayout.HORIZONTAL
136 | const val VERTICAL = LinearLayout.VERTICAL
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.content.SharedPreferences
6 | import android.os.Bundle
7 | import android.provider.Settings
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.Toast
11 | import androidx.appcompat.app.AppCompatActivity
12 | import androidx.preference.*
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
15 | import tk.zwander.widgetdrawer.services.DrawerService
16 | import tk.zwander.widgetdrawer.utils.Event
17 | import tk.zwander.widgetdrawer.utils.PrefsManager
18 | import tk.zwander.widgetdrawer.utils.accessibilityEnabled
19 | import tk.zwander.widgetdrawer.utils.eventManager
20 |
21 | class MainActivity : AppCompatActivity() {
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_main)
25 |
26 | supportFragmentManager
27 | .beginTransaction()
28 | .replace(R.id.root, Prefs())
29 | .commit()
30 | }
31 |
32 | class Prefs : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
33 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
34 | setPreferencesFromResource(R.xml.prefs_main, rootKey)
35 | preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
36 |
37 | findPreference("open_drawer")?.setOnPreferenceClickListener {
38 | requireContext().eventManager.sendEvent(Event.ShowDrawer())
39 | true
40 | }
41 |
42 | findPreference("enhanced_view_mode")?.setOnPreferenceChangeListener { pref, newValue ->
43 | if (newValue.toString().toBoolean() && !requireContext().accessibilityEnabled) {
44 | MaterialAlertDialogBuilder(requireContext())
45 | .setTitle(R.string.enhanced_view_mode)
46 | .setMessage(R.string.enhanced_view_mode_grant_desc)
47 | .setPositiveButton(android.R.string.ok) { _, _ ->
48 | Toast.makeText(requireContext(), R.string.enable_accessibility, Toast.LENGTH_SHORT).show()
49 | startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
50 | }
51 | .setNegativeButton(android.R.string.cancel) { _, _ ->
52 | (pref as SwitchPreference).isChecked = false
53 | }
54 | .setOnCancelListener {
55 | (pref as SwitchPreference).isChecked = false
56 | }
57 | .show()
58 | }
59 |
60 | true
61 | }
62 | }
63 |
64 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
65 | when (key) {
66 | PrefsManager.ENABLED ->
67 | findPreference(PrefsManager.ENABLED)?.isChecked =
68 | preferenceManager.sharedPreferences?.getBoolean(PrefsManager.ENABLED, false) ?: false
69 | }
70 | }
71 |
72 | @SuppressLint("RestrictedApi")
73 | override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> {
74 | return object : PreferenceGroupAdapter(preferenceScreen) {
75 | @SuppressLint("RestrictedApi")
76 | override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
77 | super.onBindViewHolder(holder, position)
78 | val preference = getItem(position)
79 | if (preference is PreferenceCategory)
80 | setZeroPaddingToLayoutChildren(holder.itemView)
81 | else
82 | holder.itemView.findViewById(R.id.icon_frame)?.visibility = if (preference?.icon == null) View.GONE else View.VISIBLE
83 | }
84 | }
85 | }
86 |
87 | private fun setZeroPaddingToLayoutChildren(view: View) {
88 | if (view !is ViewGroup)
89 | return
90 | val childCount = view.childCount
91 | for (i in 0 until childCount) {
92 | setZeroPaddingToLayoutChildren(view.getChildAt(i))
93 | view.setPaddingRelative(0, view.paddingTop, view.paddingEnd, view.paddingBottom)
94 | }
95 | }
96 |
97 | override fun onDestroy() {
98 | super.onDestroy()
99 |
100 | preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.content.res.Resources
7 | import android.graphics.Bitmap
8 | import android.graphics.BitmapFactory
9 | import android.graphics.Point
10 | import android.graphics.drawable.BitmapDrawable
11 | import android.graphics.drawable.Drawable
12 | import android.os.Build
13 | import android.os.VibrationEffect
14 | import android.os.Vibrator
15 | import android.provider.Settings
16 | import android.util.Base64
17 | import android.util.Log
18 | import android.view.View
19 | import android.view.WindowManager
20 | import androidx.core.content.res.ResourcesCompat
21 | import com.arasthel.spannedgridlayoutmanager.SpanSize
22 | import tk.zwander.helperlib.dpAsPx
23 | import tk.zwander.widgetdrawer.App
24 | import java.io.ByteArrayOutputStream
25 | import java.io.IOException
26 |
27 | var accessibilityConnected = false
28 |
29 | fun View.calculateWidgetWidth(frameWidth: Int, size: SpanSize?): Int {
30 | return frameWidth / context.prefs.columnCount *
31 | (size?.width ?: 1)
32 | }
33 |
34 | fun View.calculateWidgetHeight(size: SpanSize?): Int {
35 | return (size?.height ?: 1) * context.widgetHeightUnit
36 | }
37 |
38 | val Context.widgetHeightUnit: Int
39 | get() = dpAsPx(50)
40 |
41 | val Context.canDrawOverlays
42 | get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.M
43 | || Settings.canDrawOverlays(this)
44 |
45 | val Context.app: App
46 | get() = applicationContext as App
47 |
48 | val Context.accessibilityEnabled: Boolean
49 | get() = Settings.Secure.getString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)?.contains(packageName) == true
50 |
51 | val Context.statusBarHeight: Int
52 | get() = resources.getDimensionPixelSize(resources.getIdentifier("status_bar_height", "dimen", "android"))
53 |
54 | val Context.prefs: PrefsManager
55 | get() = PrefsManager.getInstance(this)
56 |
57 | fun getProperWLPType(): Int {
58 | return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
59 | else WindowManager.LayoutParams.TYPE_PRIORITY_PHONE
60 | }
61 |
62 | fun Context.screenSize(): Point {
63 | val display = (getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
64 | return Point().apply { display.getRealSize(this) }
65 | }
66 |
67 | fun Context.vibrate(len: Long) {
68 | val vib = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
69 |
70 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
71 | val effect = VibrationEffect.createOneShot(len, VibrationEffect.DEFAULT_AMPLITUDE)
72 | vib.vibrate(effect)
73 | } else {
74 | vib.vibrate(len)
75 | }
76 | }
77 |
78 | fun Context.pxAsDp(pxVal: Number) =
79 | pxVal.toFloat() / resources.displayMetrics.density
80 |
81 | fun Bitmap?.toBitmapDrawable(resources: Resources): BitmapDrawable? {
82 | return if (this != null) BitmapDrawable(resources, this) else null
83 | }
84 |
85 | fun Intent.ShortcutIconResource?.loadToDrawable(context: Context): Drawable? {
86 | return if (this != null) {
87 | try {
88 | context.packageManager.getResourcesForApplication(packageName)
89 | .run {
90 | ResourcesCompat.getDrawable(this, getIdentifier(resourceName, "drawable", packageName), newTheme())
91 | }
92 | } catch (e: PackageManager.NameNotFoundException) {
93 | null
94 | } catch (e: Resources.NotFoundException) {
95 | null
96 | }
97 | } else {
98 | null
99 | }
100 | }
101 |
102 | fun Bitmap?.toByteArray(): ByteArray? {
103 | if (this == null) return null
104 |
105 | val size: Int = width * height * 4
106 | val out = ByteArrayOutputStream(size)
107 | return try {
108 | compress(Bitmap.CompressFormat.PNG, 100, out)
109 | out.flush()
110 | out.close()
111 | out.toByteArray()
112 | } catch (e: IOException) {
113 | Log.w("WidgetDrawer", "Could not write bitmap")
114 | null
115 | }
116 | }
117 |
118 | fun Bitmap?.toBase64(): String? {
119 | return toByteArray()?.toBase64()
120 | }
121 |
122 | fun ByteArray?.toBase64(): String? {
123 | if (this == null) return null
124 |
125 | return Base64.encodeToString(this, 0, size, Base64.DEFAULT)
126 | }
127 |
128 | fun ByteArray?.toBitmap(): Bitmap? {
129 | if (this == null) return null
130 |
131 | return BitmapFactory.decodeByteArray(this, 0, size)
132 | }
133 |
134 | fun String?.base64ToByteArray(): ByteArray? {
135 | if (this == null) return null
136 |
137 | return Base64.decode(this, Base64.DEFAULT)
138 | }
139 |
140 | fun String?.base64ToBitmap(): Bitmap? {
141 | return base64ToByteArray()?.toBitmap()
142 | }
143 |
144 | val Context.eventManager: EventManager
145 | get() = EventManager.getInstance(this)
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/activities/WidgetSelectActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.activities
2 |
3 | import android.appwidget.AppWidgetManager
4 | import android.appwidget.AppWidgetProviderInfo
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.os.Bundle
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.core.view.isVisible
10 | import kotlinx.coroutines.*
11 | import tk.zwander.widgetdrawer.adapters.AppListAdapter
12 | import tk.zwander.widgetdrawer.databinding.ActivityWidgetSelectBinding
13 | import tk.zwander.widgetdrawer.misc.AppInfo
14 | import tk.zwander.widgetdrawer.misc.ShortcutData
15 | import tk.zwander.widgetdrawer.misc.WidgetInfo
16 | import tk.zwander.widgetdrawer.utils.Event
17 | import tk.zwander.widgetdrawer.utils.eventManager
18 |
19 | class WidgetSelectActivity : AppCompatActivity(), CoroutineScope by MainScope() {
20 | private val appWidgetManager by lazy { AppWidgetManager.getInstance(this) }
21 | private val adapter by lazy {
22 | AppListAdapter(this) {
23 | val event = when (it) {
24 | is AppWidgetProviderInfo -> Event.PickWidgetResult(
25 | success = true,
26 | providerInfo = it
27 | )
28 | is ShortcutData -> Event.PickShortcutResult(
29 | success = true,
30 | shortcutData = it
31 | )
32 | else -> null
33 | }
34 |
35 | event?.let {
36 | eventManager.sendEvent(event)
37 | }
38 |
39 | finish()
40 | }
41 | }
42 | private val binding by lazy { ActivityWidgetSelectBinding.inflate(layoutInflater) }
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | setContentView(binding.root)
47 |
48 | binding.selectionList.adapter = adapter
49 |
50 | populateAsync()
51 | }
52 |
53 | override fun onBackPressed() {
54 | eventManager.sendEvent(Event.PickFailedResult)
55 | finish()
56 | }
57 |
58 | private fun populateAsync() = launch {
59 | val apps = withContext(Dispatchers.Main) {
60 | val apps = HashMap()
61 |
62 | appWidgetManager.installedProviders.forEach {
63 | val appInfo = packageManager.getApplicationInfo(it.provider.packageName, 0)
64 |
65 | val appName = packageManager.getApplicationLabel(appInfo)
66 | val widgetName = it.loadLabel(packageManager)
67 |
68 | var app = apps[appInfo.packageName]
69 | if (app == null) {
70 | apps[appInfo.packageName] = AppInfo(appName.toString(), appInfo)
71 | app = apps[appInfo.packageName]!!
72 | }
73 |
74 | app.widgets.add(
75 | WidgetInfo(
76 | widgetName,
77 | it.previewImage.run { if (this != 0) this else appInfo.icon },
78 | it, appInfo
79 | )
80 | )
81 | }
82 |
83 | val shortcuts = packageManager.queryIntentActivities(
84 | Intent(Intent.ACTION_CREATE_SHORTCUT),
85 | PackageManager.GET_RESOLVED_FILTER
86 | )
87 |
88 | shortcuts.forEach {
89 | val appInfo = it.activityInfo.applicationInfo
90 |
91 | val appName = appInfo.loadLabel(packageManager)
92 | val shortcutName = it.loadLabel(packageManager)
93 |
94 | var app = apps[appInfo.packageName]
95 | if (app == null) {
96 | val new = AppInfo(appName.toString(), appInfo)
97 | apps[appInfo.packageName] = new
98 | app = new
99 | }
100 |
101 | app!!.widgets.add(
102 | WidgetInfo(
103 | shortcutName.toString(),
104 | it.activityInfo.iconResource,
105 | ShortcutData(
106 | shortcutName.toString(),
107 | Intent.ShortcutIconResource()
108 | .apply {
109 | packageName = appInfo.packageName
110 | resourceName =
111 | packageManager.getResourcesForApplication(appInfo)
112 | .getResourceName(it.iconResource)
113 | },
114 | it.activityInfo
115 | ),
116 | appInfo
117 | )
118 | )
119 | }
120 |
121 | apps
122 | }
123 |
124 | adapter.addItems(apps.values)
125 | binding.progress.isVisible = false
126 | binding.selectionList.isVisible = true
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/ToolbarAnimHolder.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import android.view.MotionEvent
7 | import android.view.View
8 | import android.view.ViewConfiguration
9 | import android.widget.LinearLayout
10 | import android.widget.ViewFlipper
11 | import androidx.dynamicanimation.animation.DynamicAnimation
12 | import androidx.dynamicanimation.animation.SpringAnimation
13 | import androidx.dynamicanimation.animation.SpringForce
14 | import tk.zwander.helperlib.dpAsPx
15 | import tk.zwander.widgetdrawer.R
16 | import tk.zwander.widgetdrawer.utils.vibrate
17 | import kotlin.math.absoluteValue
18 |
19 | class ToolbarAnimHolder : LinearLayout {
20 | constructor(context: Context) : super(context)
21 | constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
22 |
23 | private val closedTranslation: Int
24 | get() = findViewById(R.id.action_bar_wrapper).height
25 | private val openedTranslation = -context.dpAsPx(16)
26 | private val threshold: Float
27 | get() = (openedTranslation + closedTranslation) / 2f
28 | private val touchListener = TouchListener()
29 |
30 | private val openAnim by lazy {
31 | SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, openedTranslation.toFloat()).apply {
32 | spring = SpringForce(openedTranslation.toFloat())
33 | spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
34 | }
35 | }
36 | private val closeAnim by lazy {
37 | SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, closedTranslation.toFloat()).apply {
38 | spring = SpringForce(closedTranslation.toFloat())
39 | spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
40 | }
41 | }
42 |
43 | private var wasDragging = false
44 | private var isOpen = false
45 | private var currentlyTransitioning = false
46 |
47 | init {
48 | orientation = VERTICAL
49 | }
50 |
51 | @SuppressLint("ClickableViewAccessibility")
52 | override fun onFinishInflate() {
53 | super.onFinishInflate()
54 |
55 | findViewById(R.id.open_close_toolbar).setOnTouchListener(touchListener)
56 | findViewById(R.id.action_bar_wrapper).setOnTouchListener(touchListener)
57 | }
58 |
59 | private fun transition(isOpen: Boolean = this.isOpen) {
60 | if (!currentlyTransitioning) {
61 |
62 | currentlyTransitioning = true
63 |
64 | (if (isOpen) closeAnim else openAnim).apply {
65 | addEndListener { _, _, _, _ ->
66 | currentlyTransitioning = false
67 | }
68 | }.start()
69 |
70 | this.isOpen = !isOpen
71 | }
72 | }
73 |
74 | private inner class TouchListener : OnTouchListener {
75 | private var prevY = -1f
76 | private var downY = -1f
77 |
78 | override fun onTouch(v: View, event: MotionEvent?): Boolean {
79 | v.onTouchEvent(event)
80 |
81 | return when (event?.action) {
82 | MotionEvent.ACTION_DOWN -> {
83 | prevY = event.rawY
84 | downY = event.rawY
85 |
86 | if (!isOpen) context.vibrate(1)
87 |
88 | true
89 | }
90 |
91 | MotionEvent.ACTION_MOVE -> {
92 | val dist = prevY - event.rawY
93 | prevY = event.rawY
94 |
95 | if ((event.rawY - downY).absoluteValue > ViewConfiguration.get(context).scaledTouchSlop) {
96 | wasDragging = true
97 | val newTranslation = translationY - dist
98 |
99 | if (newTranslation <= closedTranslation && newTranslation >= openedTranslation) {
100 | translationY = newTranslation
101 | } else if (newTranslation > closedTranslation) {
102 | translationY = closedTranslation.toFloat()
103 | } else if (newTranslation < openedTranslation) {
104 | translationY -= dist / 2f //TODO make this an actual deceleration
105 | }
106 |
107 | true
108 | } else false
109 | }
110 |
111 | MotionEvent.ACTION_UP -> {
112 | if (wasDragging && translationY >= openedTranslation) {
113 | transition(translationY >= threshold)
114 | } else if (translationY < openedTranslation) {
115 | transition(isOpen)
116 | } else if (v.id != R.id.action_bar_wrapper) {
117 | transition()
118 | v.performClick()
119 | }
120 |
121 | wasDragging = false
122 | true
123 | }
124 | else -> false
125 | }
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/activities/PermConfigActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.appwidget.AppWidgetManager
6 | import android.content.ComponentName
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.os.Bundle
10 | import android.os.ServiceManager
11 | import androidx.activity.result.IntentSenderRequest
12 | import androidx.activity.result.contract.ActivityResultContracts
13 | import androidx.appcompat.app.AppCompatActivity
14 | import com.android.internal.appwidget.IAppWidgetService
15 | import tk.zwander.widgetdrawer.host.WidgetHostCompat
16 | import tk.zwander.widgetdrawer.misc.ShortcutData
17 | import tk.zwander.widgetdrawer.utils.Event
18 | import tk.zwander.widgetdrawer.utils.eventManager
19 | import tk.zwander.widgetdrawer.views.Drawer
20 |
21 | class PermConfigActivity : AppCompatActivity() {
22 | companion object {
23 | private const val CONFIGURE_REQ = 1000
24 | }
25 |
26 | private val widgetBindLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
27 | val id = result.data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) ?: -1
28 |
29 | eventManager.sendEvent(Event.PermissionResult(
30 | success = result.resultCode == Activity.RESULT_OK && id != -1,
31 | widgetId = id
32 | ))
33 | finish()
34 | }
35 |
36 | @Suppress("DEPRECATION")
37 | private val shortcutConfigLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
38 | val intent = result.data?.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT)
39 |
40 | eventManager.sendEvent(Event.ShortcutConfigResult(
41 | success = intent != null,
42 | data = result.data?.getParcelableExtra(Drawer.EXTRA_SHORTCUT_DATA),
43 | intent = intent,
44 | name = result.data?.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
45 | iconRes = result.data?.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE),
46 | iconBmp = result.data?.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON)
47 | ))
48 | finish()
49 | }
50 |
51 | private val configLauncher = ConfigureLauncher()
52 |
53 | @SuppressLint("NewApi")
54 | override fun onCreate(savedInstanceState: Bundle?) {
55 | super.onCreate(savedInstanceState)
56 |
57 | when (intent.action) {
58 | Drawer.ACTION_PERM -> {
59 | val permIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
60 | permIntent.putExtras(intent.extras!!)
61 | widgetBindLauncher.launch(permIntent)
62 | }
63 | Drawer.ACTION_CONFIG -> {
64 | configLauncher.launch(intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1))
65 | }
66 | Intent.ACTION_CREATE_SHORTCUT -> {
67 | val info = intent.getParcelableExtra(Drawer.EXTRA_SHORTCUT_DATA)
68 | val outIntent = Intent(Intent.ACTION_CREATE_SHORTCUT)
69 |
70 | outIntent.`package` = info.activityInfo!!.packageName
71 | outIntent.component = ComponentName(info.activityInfo!!.packageName, info.activityInfo!!.name)
72 |
73 | shortcutConfigLauncher.launch(outIntent)
74 | }
75 | }
76 | }
77 |
78 | private inner class ConfigureLauncher {
79 | private val configLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
80 | onActivityResult(CONFIGURE_REQ, result.resultCode, result.data)
81 | }
82 |
83 | @SuppressLint("NewApi")
84 | fun launch(id: Int): Boolean {
85 | //Use the system API instead of ACTION_APPWIDGET_CONFIGURE to try to avoid some permissions issues
86 | try {
87 | val intentSender = IAppWidgetService.Stub.asInterface(ServiceManager.getService(Context.APPWIDGET_SERVICE))
88 | .createAppWidgetConfigIntentSender(opPackageName, id, 0)
89 |
90 | if (intentSender != null) {
91 | configLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
92 | return true
93 | }
94 | } catch (_: Exception) {}
95 |
96 | try {
97 | WidgetHostCompat.getInstance(this@PermConfigActivity, 1003).startAppWidgetConfigureActivityForResult(
98 | this@PermConfigActivity,
99 | id, 0, CONFIGURE_REQ, null
100 | )
101 | return true
102 | } catch (_: Exception) {}
103 |
104 | return false
105 | }
106 |
107 | fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
108 | if (requestCode == CONFIGURE_REQ) {
109 | val id = data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
110 |
111 | eventManager.sendEvent(Event.WidgetConfigResult(
112 | success = resultCode == Activity.RESULT_OK && id != -1,
113 | widgetId = id ?: -1
114 | ))
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/adapters/WidgetListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.adapters
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Parcelable
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.core.content.res.ResourcesCompat
10 | import androidx.core.view.isVisible
11 | import androidx.recyclerview.widget.RecyclerView
12 | import androidx.recyclerview.widget.SortedList
13 | import com.squareup.picasso.Callback
14 | import com.squareup.picasso.Picasso
15 | import com.squareup.picasso.Request
16 | import com.squareup.picasso.RequestHandler
17 | import tk.zwander.helperlib.toBitmap
18 | import tk.zwander.widgetdrawer.R
19 | import tk.zwander.widgetdrawer.databinding.WidgetItemBinding
20 | import tk.zwander.widgetdrawer.misc.WidgetInfo
21 |
22 |
23 | class WidgetListAdapter(private val picasso: Picasso, private val selectionCallback: (provider: Parcelable) -> Unit) :
24 | RecyclerView.Adapter() {
25 | private val widgets = SortedList(WidgetInfo::class.java, object : SortedList.Callback() {
26 | override fun areItemsTheSame(item1: WidgetInfo?, item2: WidgetInfo?): Boolean {
27 | return false
28 | }
29 |
30 | override fun areContentsTheSame(oldItem: WidgetInfo?, newItem: WidgetInfo?): Boolean {
31 | return false
32 | }
33 |
34 | override fun compare(o1: WidgetInfo, o2: WidgetInfo): Int {
35 | return o1.compareTo(o2)
36 | }
37 |
38 | override fun onMoved(fromPosition: Int, toPosition: Int) {
39 | notifyItemMoved(fromPosition, toPosition)
40 | }
41 |
42 | override fun onChanged(position: Int, count: Int) {
43 | notifyItemRangeChanged(position, count)
44 | }
45 |
46 | override fun onInserted(position: Int, count: Int) {
47 | notifyItemRangeInserted(position, count)
48 | }
49 |
50 | override fun onRemoved(position: Int, count: Int) {
51 | notifyItemRangeRemoved(position, count)
52 | }
53 | })
54 |
55 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
56 | WidgetVH(
57 | LayoutInflater.from(parent.context).inflate(
58 | R.layout.widget_item,
59 | parent,
60 | false
61 | )
62 | )
63 |
64 | override fun onBindViewHolder(holder: WidgetVH, position: Int) {
65 | holder.itemView
66 | .setOnClickListener { selectionCallback.invoke(widgets.get(holder.bindingAdapterPosition).component) }
67 | holder.parseInfo(widgets.get(holder.bindingAdapterPosition), picasso)
68 | }
69 |
70 | override fun getItemCount() = widgets.size()
71 |
72 | fun addItem(item: WidgetInfo) {
73 | widgets.add(item)
74 | }
75 |
76 | class WidgetVH(view: View) : RecyclerView.ViewHolder(view) {
77 | private val binding = WidgetItemBinding.bind(itemView)
78 |
79 | fun parseInfo(info: WidgetInfo, picasso: Picasso) {
80 | binding.widgetName.text = info.widgetName
81 |
82 | val img = binding.widgetImage
83 | binding.shortcutIndicator.isVisible = info.isShortcut
84 |
85 | picasso
86 | .load("${RemoteResourcesIconHandler.SCHEME}://${info.appInfo.packageName}/${info.previewImg}")
87 | .resize(img.maxWidth, img.maxHeight)
88 | .onlyScaleDown()
89 | .centerInside()
90 | .into(img, object : Callback {
91 | override fun onError(e: Exception?) {
92 | picasso
93 | .load(Uri.parse("${AppIconRequestHandler.SCHEME}:${info.appInfo.packageName}"))
94 | .resize(img.maxWidth, img.maxHeight)
95 | .onlyScaleDown()
96 | .into(img)
97 | }
98 |
99 | override fun onSuccess() {}
100 | })
101 | }
102 | }
103 |
104 | class AppIconRequestHandler(context: Context) : RequestHandler() {
105 | companion object {
106 | const val SCHEME = "package"
107 | }
108 |
109 | private val pm = context.packageManager
110 |
111 | override fun canHandleRequest(data: Request): Boolean {
112 | return (data.uri != null && data.uri.scheme == SCHEME)
113 | }
114 |
115 | override fun load(request: Request, networkPolicy: Int): Result? {
116 | val pName = request.uri.schemeSpecificPart
117 |
118 | val img = pm.getApplicationIcon(pName).toBitmap() ?: return null
119 |
120 | return Result(img, Picasso.LoadedFrom.DISK)
121 | }
122 | }
123 |
124 | class RemoteResourcesIconHandler(context: Context) : RequestHandler() {
125 | companion object {
126 | const val SCHEME = "remote_res_widget"
127 | }
128 |
129 | private val pm = context.packageManager
130 |
131 | override fun canHandleRequest(data: Request): Boolean {
132 | return (data.uri != null && data.uri.scheme == SCHEME)
133 | }
134 |
135 | override fun load(request: Request, networkPolicy: Int): Result? {
136 | val pathSegments = request.uri.pathSegments
137 |
138 | val pName = request.uri.host
139 | val id = pathSegments[0].toInt()
140 | val remRes = pm.getResourcesForApplication(pName)
141 |
142 | val img = ResourcesCompat.getDrawable(remRes, id, remRes.newTheme())?.toBitmap() ?: return null
143 |
144 | return Result(img, Picasso.LoadedFrom.DISK)
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/DrawerRecycler.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import android.view.GestureDetector
7 | import android.view.MotionEvent
8 | import android.view.View
9 | import androidx.core.view.NestedScrollingParent
10 | import androidx.recyclerview.widget.ItemTouchHelper
11 | import androidx.recyclerview.widget.RecyclerView
12 | import tk.zwander.widgetdrawer.adapters.DrawerAdapter
13 | import tk.zwander.widgetdrawer.services.DrawerService
14 | import tk.zwander.widgetdrawer.utils.Event
15 | import tk.zwander.widgetdrawer.utils.PrefsManager
16 | import tk.zwander.widgetdrawer.utils.eventManager
17 |
18 | //Nested scrolling implementation from https://medium.com/widgetlabs-engineering/scrollable-nestedscrollviews-inside-recyclerview-ca65050d828a
19 | class DrawerRecycler : RecyclerView, NestedScrollingParent {
20 | constructor(context: Context) : super(context)
21 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
22 |
23 | init {
24 | isNestedScrollingEnabled = true
25 | }
26 |
27 | private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
28 | override fun onSingleTapUp(e: MotionEvent?): Boolean {
29 | return if (PrefsManager.getInstance(context).closeOnEmptyTap && !allowReorder) {
30 | context.eventManager.sendEvent(Event.CloseDrawer)
31 | true
32 | } else false
33 | }
34 | })
35 |
36 | val touchCallback = object : ItemTouchHelper.SimpleCallback(
37 | ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END,
38 | ItemTouchHelper.START or ItemTouchHelper.END
39 | ) {
40 | override fun onMove(
41 | recyclerView: RecyclerView,
42 | viewHolder: ViewHolder,
43 | target: ViewHolder
44 | ): Boolean {
45 | return allowReorder && onMoveListener?.invoke(recyclerView, viewHolder, target) == true
46 | }
47 |
48 | override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
49 | if (allowReorder) {
50 | onSwipeListener?.invoke(viewHolder, direction)
51 | }
52 | }
53 |
54 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int {
55 | return if (viewHolder !is DrawerAdapter.HeaderVH) super.getMovementFlags(recyclerView, viewHolder)
56 | else 0
57 | }
58 |
59 | override fun isItemViewSwipeEnabled() = allowReorder
60 | override fun isLongPressDragEnabled() = allowReorder
61 | }
62 |
63 | private val touchHelper = ItemTouchHelper(touchCallback)
64 |
65 | var onMoveListener: ((
66 | recyclerView: RecyclerView,
67 | viewHolder: ViewHolder,
68 | target: ViewHolder
69 | ) -> Boolean)? = null
70 |
71 | var onSwipeListener: ((
72 | viewHolder: ViewHolder,
73 | direction: Int
74 | ) -> Unit)? = null
75 |
76 | var allowReorder = false
77 | set(value) {
78 | (adapter as DrawerAdapter).isEditing = value
79 | field = value
80 | }
81 |
82 | private var nestedScrollTarget: View? = null
83 | private var nestedScrollTargetIsBeingDragged = false
84 | private var nestedScrollTargetWasUnableToScroll = false
85 | private var skipsTouchInterception = false
86 |
87 | override fun onFinishInflate() {
88 | super.onFinishInflate()
89 |
90 | touchHelper.attachToRecyclerView(this)
91 | }
92 |
93 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
94 | val temporarilySkipsInterception = nestedScrollTarget != null
95 | if (temporarilySkipsInterception) {
96 | // If a descendant view is scrolling we set a flag to temporarily skip our onInterceptTouchEvent implementation
97 | skipsTouchInterception = true
98 | }
99 |
100 | // First dispatch, potentially skipping our onInterceptTouchEvent
101 | var handled = super.dispatchTouchEvent(ev)
102 |
103 | if (temporarilySkipsInterception) {
104 | skipsTouchInterception = false
105 |
106 | // If the first dispatch yielded no result or we noticed that the descendant view is unable to scroll in the
107 | // direction the user is scrolling, we dispatch once more but without skipping our onInterceptTouchEvent.
108 | // Note that RecyclerView automatically cancels active touches of all its descendants once it starts scrolling
109 | // so we don't have to do that.
110 | if (!handled || nestedScrollTargetWasUnableToScroll) {
111 | handled = super.dispatchTouchEvent(ev)
112 | }
113 | }
114 |
115 | return handled
116 | }
117 |
118 | override fun onInterceptTouchEvent(e: MotionEvent) =
119 | !skipsTouchInterception && super.onInterceptTouchEvent(e)
120 |
121 | @SuppressLint("ClickableViewAccessibility")
122 | override fun onTouchEvent(e: MotionEvent?): Boolean {
123 | return super.onTouchEvent(e) or gestureDetector.onTouchEvent(e)
124 | }
125 |
126 | override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
127 | if (target === nestedScrollTarget && !nestedScrollTargetIsBeingDragged) {
128 | if (dyConsumed != 0) {
129 | // The descendant was actually scrolled, so we won't bother it any longer.
130 | // It will receive all future events until it finished scrolling.
131 | nestedScrollTargetIsBeingDragged = true
132 | nestedScrollTargetWasUnableToScroll = false
133 | }
134 | else if (dyUnconsumed != 0) {
135 | // The descendant tried scrolling in response to touch movements but was not able to do so.
136 | // We remember that in order to allow RecyclerView to take over scrolling.
137 | nestedScrollTargetWasUnableToScroll = true
138 | target.parent?.requestDisallowInterceptTouchEvent(false)
139 | }
140 | }
141 | }
142 |
143 | override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
144 | if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
145 | // A descendant started scrolling, so we'll observe it.
146 | nestedScrollTarget = target
147 | nestedScrollTargetIsBeingDragged = false
148 | nestedScrollTargetWasUnableToScroll = false
149 | }
150 |
151 | super.onNestedScrollAccepted(child, target, axes)
152 | }
153 |
154 |
155 | // We only support vertical scrolling.
156 | override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int) =
157 | (nestedScrollAxes and View.SCROLL_AXIS_VERTICAL != 0)
158 |
159 |
160 | override fun onStopNestedScroll(child: View) {
161 | // The descendant finished scrolling. Clean up!
162 | nestedScrollTarget = null
163 | nestedScrollTargetIsBeingDragged = false
164 | nestedScrollTargetWasUnableToScroll = false
165 | }
166 | }
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "1052002379210",
4 | "firebase_url": "https://new-apps-fa48f.firebaseio.com",
5 | "project_id": "new-apps-fa48f",
6 | "storage_bucket": "new-apps-fa48f.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:1052002379210:android:cea0db2bf3a36dc0",
12 | "android_client_info": {
13 | "package_name": "com.zacharee1.calculatorwidget"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "1052002379210-7nc362gmbl3v7q44t5jcd8i7k1ghhmsb.apps.googleusercontent.com",
19 | "client_type": 1,
20 | "android_info": {
21 | "package_name": "com.zacharee1.calculatorwidget",
22 | "certificate_hash": "40e442e89aed48d57dcba50486a72b4badd1b597"
23 | }
24 | },
25 | {
26 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
27 | "client_type": 3
28 | }
29 | ],
30 | "api_key": [
31 | {
32 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
33 | }
34 | ],
35 | "services": {
36 | "appinvite_service": {
37 | "other_platform_oauth_client": [
38 | {
39 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
40 | "client_type": 3
41 | }
42 | ]
43 | }
44 | }
45 | },
46 | {
47 | "client_info": {
48 | "mobilesdk_app_id": "1:1052002379210:android:e0be962aa37894b4cadb33",
49 | "android_client_info": {
50 | "package_name": "com.zacharee1.insomnia"
51 | }
52 | },
53 | "oauth_client": [
54 | {
55 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
56 | "client_type": 3
57 | }
58 | ],
59 | "api_key": [
60 | {
61 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
62 | }
63 | ],
64 | "services": {
65 | "appinvite_service": {
66 | "other_platform_oauth_client": [
67 | {
68 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
69 | "client_type": 3
70 | }
71 | ]
72 | }
73 | }
74 | },
75 | {
76 | "client_info": {
77 | "mobilesdk_app_id": "1:1052002379210:android:5f1f1b6c34e78452cadb33",
78 | "android_client_info": {
79 | "package_name": "tk.zwander.lockscreenwidgets"
80 | }
81 | },
82 | "oauth_client": [
83 | {
84 | "client_id": "1052002379210-4pmsaldetschojl0jmd4co0mmnm8vm24.apps.googleusercontent.com",
85 | "client_type": 1,
86 | "android_info": {
87 | "package_name": "tk.zwander.lockscreenwidgets",
88 | "certificate_hash": "01229391ef3604cada4f43a54fe8ca512d9345c1"
89 | }
90 | },
91 | {
92 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
93 | "client_type": 3
94 | }
95 | ],
96 | "api_key": [
97 | {
98 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
99 | }
100 | ],
101 | "services": {
102 | "appinvite_service": {
103 | "other_platform_oauth_client": [
104 | {
105 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
106 | "client_type": 3
107 | }
108 | ]
109 | }
110 | },
111 | "admob_app_id": "ca-app-pub-1785189950169503~6931461702"
112 | },
113 | {
114 | "client_info": {
115 | "mobilesdk_app_id": "1:1052002379210:android:fdaeeb167e55117dcadb33",
116 | "android_client_info": {
117 | "package_name": "tk.zwander.rebooter"
118 | }
119 | },
120 | "oauth_client": [
121 | {
122 | "client_id": "1052002379210-124768lcg4s2bqjo015tgh4io3gpthkn.apps.googleusercontent.com",
123 | "client_type": 1,
124 | "android_info": {
125 | "package_name": "tk.zwander.rebooter",
126 | "certificate_hash": "668de23f73dfe100d638de7b368faae518988049"
127 | }
128 | },
129 | {
130 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
131 | "client_type": 3
132 | }
133 | ],
134 | "api_key": [
135 | {
136 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
137 | }
138 | ],
139 | "services": {
140 | "appinvite_service": {
141 | "other_platform_oauth_client": [
142 | {
143 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
144 | "client_type": 3
145 | }
146 | ]
147 | }
148 | }
149 | },
150 | {
151 | "client_info": {
152 | "mobilesdk_app_id": "1:1052002379210:android:dfdb8b8b4143ea39cadb33",
153 | "android_client_info": {
154 | "package_name": "tk.zwander.rootactivitylauncher"
155 | }
156 | },
157 | "oauth_client": [
158 | {
159 | "client_id": "1052002379210-g03hjiegmrrg8rcn8ocbb6lje2qon8ci.apps.googleusercontent.com",
160 | "client_type": 1,
161 | "android_info": {
162 | "package_name": "tk.zwander.rootactivitylauncher",
163 | "certificate_hash": "3accd44a4f8485bbd71fe7f39d303103d1a32464"
164 | }
165 | },
166 | {
167 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
168 | "client_type": 3
169 | }
170 | ],
171 | "api_key": [
172 | {
173 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
174 | }
175 | ],
176 | "services": {
177 | "appinvite_service": {
178 | "other_platform_oauth_client": [
179 | {
180 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
181 | "client_type": 3
182 | }
183 | ]
184 | }
185 | }
186 | },
187 | {
188 | "client_info": {
189 | "mobilesdk_app_id": "1:1052002379210:android:dbfcfbf1dde7e883",
190 | "android_client_info": {
191 | "package_name": "tk.zwander.widgetdrawer"
192 | }
193 | },
194 | "oauth_client": [
195 | {
196 | "client_id": "1052002379210-sseuef0psthf6v8liempcr5b8phqa4d9.apps.googleusercontent.com",
197 | "client_type": 1,
198 | "android_info": {
199 | "package_name": "tk.zwander.widgetdrawer",
200 | "certificate_hash": "a0454543a13e7f4b002fba62bd007626a9b07528"
201 | }
202 | },
203 | {
204 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
205 | "client_type": 3
206 | }
207 | ],
208 | "api_key": [
209 | {
210 | "current_key": "AIzaSyA8Uxh9vNxhVQbudvG_DZqUXampeebOwo4"
211 | }
212 | ],
213 | "services": {
214 | "appinvite_service": {
215 | "other_platform_oauth_client": [
216 | {
217 | "client_id": "1052002379210-iumvod4sktu2ojjg6eqgrdpufapsmss0.apps.googleusercontent.com",
218 | "client_type": 3
219 | }
220 | ]
221 | }
222 | }
223 | }
224 | ],
225 | "configuration_version": "1"
226 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/drawer_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
26 |
27 |
38 |
39 |
46 |
47 |
54 |
55 |
61 |
62 |
69 |
70 |
77 |
78 |
79 |
80 |
86 |
87 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
108 |
118 |
119 |
127 |
128 |
136 |
137 |
145 |
146 |
154 |
155 |
164 |
165 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/utils/PrefsManager.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.utils
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.SharedPreferences
7 | import android.graphics.Color
8 | import android.net.Uri
9 | import android.preference.PreferenceManager
10 | import android.view.Gravity
11 | import androidx.core.content.ContextCompat
12 | import com.google.gson.GsonBuilder
13 | import com.google.gson.reflect.TypeToken
14 | import tk.zwander.helperlib.dpAsPx
15 | import tk.zwander.widgetdrawer.R
16 | import tk.zwander.widgetdrawer.misc.BaseWidgetInfo
17 | import tk.zwander.widgetdrawer.misc.WidgetInfo
18 | import tk.zwander.widgetdrawer.misc.WidgetSizeInfo
19 |
20 | class PrefsManager private constructor(private val context: Context) {
21 | companion object {
22 | const val WIDGETS = "saved_widgets"
23 | const val ENABLED = "enabled"
24 | const val HANDLE_SIDE = "handle_side"
25 | const val HANDLE_Y = "handle_y"
26 | const val HANDLE_HEIGHT = "handle_height"
27 | const val HANDLE_WIDTH = "handle_width"
28 | const val HANDLE_COLOR = "handle_color"
29 | const val HANDLE_SHADOW = "handle_shadow"
30 | const val TRANSPARENT_WIDGETS = "transparent_widgets"
31 | const val CURRENT_SHORTCUT_IDS = "shortcut_ids"
32 | const val SHOW_HANDLE = "show_handle"
33 | const val CLOSE_ON_EMPTY_TAP = "close_on_empty_tap"
34 | const val COLUMN_COUNT = "column_count"
35 | const val WIDGET_SIZE_INFO = "widget_size_info"
36 |
37 | @SuppressLint("RtlHardcoded")
38 | const val HANDLE_LEFT = Gravity.LEFT
39 | @SuppressLint("RtlHardcoded")
40 | const val HANDLE_RIGHT = Gravity.RIGHT
41 | const val HANDLE_UNCHANGED = -1
42 | const val HANDLE_COLOR_DEF = Color.WHITE
43 |
44 | const val DRAWER_BACKGROUND_COLOR = "drawer_background_color"
45 |
46 | @SuppressLint("StaticFieldLeak")
47 | private var instance: PrefsManager? = null
48 |
49 | fun getInstance(context: Context): PrefsManager {
50 | if (instance == null) instance = PrefsManager(context.applicationContext)
51 | return instance!!
52 | }
53 | }
54 |
55 | private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
56 | val gson = GsonBuilder()
57 | .create()
58 |
59 | var currentWidgets: List
60 | get() {
61 | return GsonBuilder()
62 | .setExclusionStrategies(CrashFixExclusionStrategy())
63 | .registerTypeAdapter(Uri::class.java, GsonUriHandler())
64 | .registerTypeAdapter(Intent::class.java, GsonIntentHandler())
65 | .create()
66 | .fromJson>(
67 | getString(WIDGETS, null) ?: return ArrayList(),
68 | object : TypeToken>() {}.type
69 | ).apply { removeAll { it.id == -1 } }
70 | }
71 | set(value) {
72 | putString(
73 | WIDGETS, GsonBuilder()
74 | .setExclusionStrategies(CrashFixExclusionStrategy())
75 | .registerTypeAdapter(Uri::class.java, GsonUriHandler())
76 | .registerTypeAdapter(Intent::class.java, GsonIntentHandler())
77 | .create()
78 | .toJson(ArrayList(value)
79 | .apply { removeAll { it.id == -1 } })
80 | )
81 | }
82 | var enabled: Boolean
83 | get() = getBoolean(ENABLED, false)
84 | set(value) {
85 | putBoolean(ENABLED, value)
86 | }
87 | var handleSide: Int
88 | get() = getInt(HANDLE_SIDE, HANDLE_RIGHT)
89 | set(value) {
90 | putInt(HANDLE_SIDE, value)
91 | }
92 | var handleYPx: Float
93 | get() = getFloat(HANDLE_Y, context.dpAsPx(64).toFloat())
94 | set(value) {
95 | putFloat(HANDLE_Y, value)
96 | }
97 | var handleHeightDp: Int
98 | get() = getInt(HANDLE_HEIGHT, 140)
99 | set(value) {
100 | putInt(HANDLE_HEIGHT, value)
101 | }
102 | var handleWidthDp: Int
103 | get() = getInt(HANDLE_WIDTH, 6)
104 | set(value) {
105 | putInt(HANDLE_HEIGHT, value)
106 | }
107 | var handleColor: Int
108 | get() = getInt(HANDLE_COLOR, HANDLE_COLOR_DEF)
109 | set(value) {
110 | putInt(HANDLE_COLOR, value)
111 | }
112 | var handleShadow: Boolean
113 | get() = getBoolean(HANDLE_SHADOW, true)
114 | set(value) {
115 | putBoolean(HANDLE_SHADOW, value)
116 | }
117 | var transparentWidgets: Boolean
118 | get() = getBoolean(TRANSPARENT_WIDGETS, false)
119 | set(value) {
120 | putBoolean(TRANSPARENT_WIDGETS, value)
121 | }
122 | var shortcutIds: Set
123 | get() = HashSet(getStringSet(CURRENT_SHORTCUT_IDS, HashSet())!!)
124 | set(value) {
125 | putStringSet(CURRENT_SHORTCUT_IDS, value.toSet())
126 | }
127 | var showHandle: Boolean
128 | get() = getBoolean(SHOW_HANDLE, true)
129 | set(value) {
130 | putBoolean(SHOW_HANDLE, value)
131 | }
132 | var closeOnEmptyTap: Boolean
133 | get() = getBoolean(CLOSE_ON_EMPTY_TAP, false)
134 | set(value) {
135 | putBoolean(CLOSE_ON_EMPTY_TAP, value)
136 | }
137 | var drawerBg: Int
138 | get() = getInt(DRAWER_BACKGROUND_COLOR, ContextCompat.getColor(context, R.color.drawerBackgroundDefault))
139 | set(value) {
140 | putInt(DRAWER_BACKGROUND_COLOR, value)
141 | }
142 | var columnCount: Int
143 | get() = getInt(COLUMN_COUNT, 2)
144 | set(value) {
145 | putInt(COLUMN_COUNT, value)
146 | }
147 | var widgetSizes: HashMap
148 | get() = gson.fromJson(
149 | getString(WIDGET_SIZE_INFO, null),
150 | object : TypeToken>() {}.type
151 | ) ?: HashMap()
152 | set(value) {
153 | putString(
154 | WIDGET_SIZE_INFO,
155 | gson.toJson(value)
156 | )
157 | }
158 |
159 | fun updateWidgetSize(id: Int, newWidth: Int, newHeight: Int) {
160 | val sizeInfo = WidgetSizeInfo(newWidth, newHeight, id)
161 |
162 | updateWidgetSize(sizeInfo)
163 | }
164 |
165 | fun updateWidgetSize(widgetSizeInfo: WidgetSizeInfo) {
166 | widgetSizes = widgetSizes.apply { this[widgetSizeInfo.id] = widgetSizeInfo }
167 | }
168 |
169 | fun getString(key: String, def: String?) = prefs.getString(key, def)
170 | fun getFloat(key: String, def: Float) = prefs.getFloat(key, def)
171 | fun getInt(key: String, def: Int) = prefs.getInt(key, def)
172 | fun getLong(key: String, def: Long) = prefs.getLong(key, def)
173 | fun getBoolean(key: String, def: Boolean) = prefs.getBoolean(key, def)
174 | fun getStringSet(key: String, def: Set) = prefs.getStringSet(key, def)
175 |
176 | fun putString(key: String, value: String) = prefs.edit().putString(key, value).commit()
177 | fun putFloat(key: String, value: Float) = prefs.edit().putFloat(key, value).commit()
178 | fun putInt(key: String, value: Int) = prefs.edit().putInt(key, value).commit()
179 | fun putLong(key: String, value: Long) = prefs.edit().putLong(key, value).commit()
180 | fun putBoolean(key: String, value: Boolean) = prefs.edit().putBoolean(key, value).commit()
181 | fun putStringSet(key: String, value: Set) = prefs.edit().putStringSet(key, value).commit()
182 |
183 | fun remove(key: String) = prefs.edit().remove(key).commit()
184 |
185 | fun addPrefListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
186 | prefs.registerOnSharedPreferenceChangeListener(listener)
187 |
188 | fun removePrefListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
189 | prefs.unregisterOnSharedPreferenceChangeListener(listener)
190 |
191 | fun addShortcutId(id: String) {
192 | shortcutIds = ArrayList(shortcutIds).apply { add(id) }.toSet()
193 | }
194 |
195 | fun removeShortcutId(id: String) {
196 | shortcutIds = ArrayList(shortcutIds).apply { remove(id) }.toSet()
197 | }
198 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/Handle.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.graphics.Color
7 | import android.graphics.PixelFormat
8 | import android.os.Build
9 | import android.os.Handler
10 | import android.os.Looper
11 | import android.os.Message
12 | import android.util.AttributeSet
13 | import android.view.GestureDetector
14 | import android.view.Gravity
15 | import android.view.MotionEvent
16 | import android.view.WindowManager
17 | import android.widget.LinearLayout
18 | import androidx.appcompat.content.res.AppCompatResources
19 | import tk.zwander.helperlib.dpAsPx
20 | import tk.zwander.widgetdrawer.R
21 | import tk.zwander.widgetdrawer.utils.PrefsManager
22 | import tk.zwander.widgetdrawer.utils.screenSize
23 | import tk.zwander.widgetdrawer.utils.vibrate
24 | import kotlin.math.absoluteValue
25 |
26 | class Handle : LinearLayout, SharedPreferences.OnSharedPreferenceChangeListener {
27 | companion object {
28 | private const val MSG_LONG_PRESS = 0
29 |
30 | private const val LONG_PRESS_DELAY = 300
31 | private const val SWIPE_THRESHOLD = 50
32 | }
33 |
34 | constructor(context: Context) : super(context)
35 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
36 |
37 | var onOpenListener: (() -> Unit)? = null
38 |
39 | private var inMoveMode = false
40 | private var calledOpen = false
41 | private var screenWidth = -1
42 |
43 | private val gestureManager = GestureManager()
44 | private val wm = context.applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
45 | private val prefs = PrefsManager.getInstance(context)
46 |
47 | private val handleLeft = AppCompatResources.getDrawable(context, R.drawable.handle_left)
48 | private val handleRight = AppCompatResources.getDrawable(context, R.drawable.handle_right)
49 |
50 | private val longClickHandler = @SuppressLint("HandlerLeak")
51 | object : Handler(Looper.getMainLooper()) {
52 | override fun handleMessage(msg: Message?) {
53 | when (msg?.what) {
54 | MSG_LONG_PRESS -> gestureManager.onLongPress()
55 | }
56 | }
57 | }
58 |
59 | val params = WindowManager.LayoutParams().apply {
60 | type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PRIORITY_PHONE
61 | else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
62 | flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
63 | width = context.dpAsPx(prefs.handleWidthDp)
64 | height = context.dpAsPx(prefs.handleHeightDp)
65 | gravity = Gravity.TOP or prefs.handleSide
66 | y = prefs.handleYPx.toInt()
67 | format = PixelFormat.RGBA_8888
68 | }
69 |
70 | init {
71 | setSide()
72 | setTint(prefs.handleColor)
73 | isClickable = true
74 | prefs.addPrefListener(this)
75 | elevation = if (prefs.handleShadow) context.dpAsPx(8).toFloat() else 0f
76 | contentDescription = resources.getString(R.string.open_widget_drawer)
77 | }
78 |
79 | @SuppressLint("ClickableViewAccessibility")
80 | override fun onTouchEvent(event: MotionEvent): Boolean {
81 | when (event.action) {
82 | MotionEvent.ACTION_DOWN -> {
83 | screenWidth = context.screenSize().x
84 | longClickHandler.sendEmptyMessageAtTime(
85 | MSG_LONG_PRESS,
86 | event.downTime + LONG_PRESS_DELAY
87 | )
88 | }
89 | MotionEvent.ACTION_UP -> {
90 | longClickHandler.removeMessages(MSG_LONG_PRESS)
91 | setMoveMove(false)
92 | prefs.handleYPx = params.y.toFloat()
93 | }
94 | MotionEvent.ACTION_MOVE -> {
95 | if (inMoveMode) {
96 | val gravity = when {
97 | event.rawX <= 1 / 3f * screenWidth -> {
98 | PrefsManager.HANDLE_LEFT
99 | }
100 | event.rawX >= 2 / 3f * screenWidth -> {
101 | PrefsManager.HANDLE_RIGHT
102 | }
103 | else -> -1
104 | }
105 | params.y = (event.rawY - params.height / 2f).toInt()
106 | if (gravity != PrefsManager.HANDLE_UNCHANGED) {
107 | params.gravity = Gravity.TOP or gravity
108 | prefs.handleSide = gravity
109 | setSide(gravity)
110 | }
111 | updateLayout()
112 | }
113 | }
114 | }
115 |
116 | gestureManager.onTouchEvent(event)
117 |
118 | return true
119 | }
120 |
121 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
122 | when (key) {
123 | PrefsManager.HANDLE_HEIGHT -> {
124 | params.height = context.dpAsPx(prefs.handleHeightDp)
125 | updateLayout()
126 | }
127 |
128 | PrefsManager.HANDLE_WIDTH -> {
129 | params.width = context.dpAsPx(prefs.handleWidthDp)
130 | updateLayout()
131 | }
132 |
133 | PrefsManager.HANDLE_COLOR -> {
134 | setTint(prefs.handleColor)
135 | }
136 |
137 | PrefsManager.HANDLE_SHADOW -> {
138 | elevation = if (prefs.handleShadow) context.dpAsPx(8).toFloat() else 0f
139 | }
140 | }
141 | }
142 |
143 | override fun onDetachedFromWindow() {
144 | super.onDetachedFromWindow()
145 |
146 | longClickHandler.removeMessages(MSG_LONG_PRESS)
147 | setMoveMove(false)
148 | }
149 |
150 | fun onDestroy() {
151 | prefs.removePrefListener(this)
152 | }
153 |
154 | fun show(wm: WindowManager = this.wm, overrideType: Int = params.type) {
155 | try {
156 | wm.addView(this, params.apply { type = overrideType })
157 | } catch (e: Exception) {}
158 | }
159 |
160 | fun hide(wm: WindowManager = this.wm) {
161 | try {
162 | wm.removeView(this)
163 | } catch (e: Exception) {}
164 | }
165 |
166 | private fun updateLayout(params: WindowManager.LayoutParams = this.params) {
167 | try {
168 | wm.updateViewLayout(this, params)
169 | } catch (e: Exception) {}
170 | }
171 |
172 | private fun setSide(gravity: Int = prefs.handleSide) {
173 | background = if (gravity == PrefsManager.HANDLE_RIGHT) handleRight
174 | else handleLeft
175 | }
176 |
177 | private fun setMoveMove(inMoveMode: Boolean) {
178 | this.inMoveMode = inMoveMode
179 | val tint = if (inMoveMode)
180 | Color.argb(255, 120, 200, 255)
181 | else
182 | prefs.handleColor
183 |
184 | setTint(tint)
185 | }
186 |
187 | private fun setTint(tint: Int) {
188 | handleLeft?.setTint(tint)
189 | handleRight?.setTint(tint)
190 | }
191 |
192 | inner class GestureManager : GestureDetector.SimpleOnGestureListener() {
193 | private val gestureDetector = GestureDetector(context, this, handler)
194 |
195 | fun onTouchEvent(event: MotionEvent?): Boolean {
196 | when (event?.action) {
197 | MotionEvent.ACTION_UP -> calledOpen = false
198 | }
199 | return gestureDetector.onTouchEvent(event)
200 | }
201 |
202 | override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
203 | return if (distanceX.absoluteValue > distanceY.absoluteValue && !inMoveMode) {
204 | if ((distanceX > SWIPE_THRESHOLD && prefs.handleSide == PrefsManager.HANDLE_RIGHT)
205 | || distanceX < -SWIPE_THRESHOLD
206 | ) {
207 | if (!calledOpen) {
208 | calledOpen = true
209 | onOpenListener?.invoke()
210 | true
211 | } else false
212 | } else false
213 | } else false
214 | }
215 |
216 | override fun onLongPress(e: MotionEvent?) {
217 | onLongPress()
218 | }
219 |
220 | fun onLongPress() {
221 | context.vibrate(50)
222 | setMoveMove(true)
223 | }
224 |
225 | override fun onDoubleTap(e: MotionEvent?): Boolean {
226 | return if (!calledOpen) {
227 | calledOpen = true
228 | onOpenListener?.invoke()
229 | true
230 | } else false
231 | }
232 | }
233 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/services/DrawerService.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.services
2 |
3 | import android.annotation.SuppressLint
4 | import android.annotation.TargetApi
5 | import android.app.AppOpsManager
6 | import android.app.NotificationChannel
7 | import android.app.NotificationManager
8 | import android.app.Service
9 | import android.content.*
10 | import android.content.res.Configuration
11 | import android.net.Uri
12 | import android.os.Build
13 | import android.os.PowerManager
14 | import android.os.Process
15 | import android.provider.Settings
16 | import android.view.LayoutInflater
17 | import android.widget.Toast
18 | import androidx.core.app.NotificationCompat
19 | import androidx.core.content.ContextCompat
20 | import tk.zwander.widgetdrawer.R
21 | import tk.zwander.widgetdrawer.utils.*
22 | import tk.zwander.widgetdrawer.views.Drawer
23 | import tk.zwander.widgetdrawer.views.Handle
24 |
25 |
26 | @SuppressLint("InflateParams")
27 | class DrawerService : Service(), SharedPreferences.OnSharedPreferenceChangeListener, EventObserver {
28 | companion object {
29 | private const val CHANNEL = "widget_drawer_main"
30 |
31 | fun start(context: Context) {
32 | ContextCompat.startForegroundService(context, Intent(context, DrawerService::class.java))
33 | }
34 |
35 | fun stop(context: Context) {
36 | context.stopService(Intent(context, DrawerService::class.java))
37 | }
38 | }
39 |
40 | private val nm by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
41 | private val appOpsManager by lazy { getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager }
42 |
43 | private val drawer by lazy {
44 | LayoutInflater.from(this)
45 | .inflate(R.layout.drawer_layout, null, false) as Drawer
46 | }
47 | private val handle by lazy { Handle(this) }
48 | private val overlayListener = AppOpsManager.OnOpChangedListener { op, packageName ->
49 | if (packageName == this.packageName) {
50 | when (op) {
51 | AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW -> {
52 | val allowed = appOpsManager.checkOpNoThrow(
53 | op,
54 | Process.myUid(),
55 | this.packageName
56 | ) == AppOpsManager.MODE_ALLOWED
57 | if (allowed) addHandle()
58 | else {
59 | stopForeground(true)
60 | stopSelf()
61 | }
62 | }
63 | }
64 | }
65 | }
66 | private val screenStateReceiver = object : BroadcastReceiver() {
67 | override fun onReceive(context: Context?, intent: Intent?) {
68 | when (intent?.action) {
69 | Intent.ACTION_SCREEN_OFF -> {
70 | isScreenOn = false
71 | }
72 | Intent.ACTION_SCREEN_ON -> {
73 | isScreenOn = true
74 | }
75 | }
76 |
77 | if (canShowHandle()) {
78 | addHandle()
79 | } else {
80 | remHandle()
81 | }
82 | }
83 | }
84 |
85 | private var isScreenOn = false
86 |
87 | override fun onBind(intent: Intent) = null
88 |
89 | override fun onCreate() {
90 | drawer.onCreate()
91 | isScreenOn = (getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive
92 | prefs.addPrefListener(this)
93 | registerReceiver(screenStateReceiver, IntentFilter().apply {
94 | addAction(Intent.ACTION_SCREEN_ON)
95 | addAction(Intent.ACTION_SCREEN_OFF)
96 | })
97 |
98 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
99 | val channel =
100 | NotificationChannel(CHANNEL, resources.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW)
101 | channel.enableVibration(false)
102 | channel.enableLights(false)
103 | nm.createNotificationChannel(channel)
104 | }
105 |
106 | startForeground(
107 | 100, NotificationCompat.Builder(this, CHANNEL)
108 | .setContentTitle(resources.getString(R.string.app_name))
109 | .setSmallIcon(R.mipmap.ic_launcher)
110 | .setPriority(NotificationCompat.PRIORITY_LOW)
111 | .build()
112 | )
113 |
114 | eventManager.addObserver(this)
115 |
116 | handle.onOpenListener = {
117 | if (!drawer.isAttachedToWindow) {
118 | vibrate(10)
119 | openDrawer()
120 | }
121 | }
122 |
123 | drawer.hideListener = {
124 | addHandle()
125 | }
126 |
127 | drawer.showListener = {
128 | remHandle()
129 | }
130 |
131 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1)
132 | appOpsManager.startWatchingMode(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, packageName, overlayListener)
133 |
134 | if (canDrawOverlays) {
135 | addHandle()
136 | } else {
137 | requestPermission()
138 | }
139 | }
140 |
141 | override fun onConfigurationChanged(newConfig: Configuration?) {
142 | @Suppress("DEPRECATION")
143 | when (newConfig?.orientation) {
144 | Configuration.ORIENTATION_LANDSCAPE -> {
145 | remHandle()
146 | }
147 | Configuration.ORIENTATION_PORTRAIT -> {
148 | addHandle()
149 | }
150 | Configuration.ORIENTATION_SQUARE -> {
151 | addHandle()
152 | }
153 | Configuration.ORIENTATION_UNDEFINED -> {
154 | remHandle()
155 | }
156 | }
157 | }
158 |
159 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
160 | when (key) {
161 | PrefsManager.SHOW_HANDLE -> if (canShowHandle()) addHandle() else remHandle()
162 | }
163 | }
164 |
165 | override fun onEvent(event: Event) {
166 | when (event) {
167 | is Event.ShowDrawer -> {
168 | if (event.wm != null) {
169 | drawer.showDrawer(event.wm, event.type)
170 | } else {
171 | drawer.showDrawer(overrideType = event.type)
172 | }
173 | }
174 | is Event.ShowHandle -> {
175 | if (event.wm != null) {
176 | handle.show(event.wm, event.type)
177 | } else {
178 | handle.show(overrideType = event.type)
179 | }
180 | }
181 | Event.AccessibilityConnected, Event.AccessibilityDisconnected -> {
182 | if (drawer.isAttachedToWindow) {
183 | closeDrawer()
184 | openDrawer()
185 | }
186 |
187 | if (handle.isAttachedToWindow) {
188 | remHandle()
189 | addHandle()
190 | }
191 | }
192 | Event.CloseDrawer -> {
193 | closeDrawer()
194 | }
195 | else -> {}
196 | }
197 | }
198 |
199 | private fun addHandle() {
200 | if (canShowHandle()) {
201 | if (accessibilityConnected) eventManager.sendEvent(Event.AddHandleFromAccessibility)
202 | else handle.show(overrideType = getProperWLPType())
203 | }
204 | }
205 |
206 | private fun remHandle() {
207 | handle.hide()
208 | }
209 |
210 | private fun openDrawer() {
211 | remHandle()
212 | if (accessibilityConnected) eventManager.sendEvent(Event.OpenDrawerFromAccessibility)
213 | else drawer.showDrawer(overrideType = getProperWLPType())
214 | }
215 |
216 | private fun closeDrawer() {
217 | drawer.hideDrawer()
218 | }
219 |
220 | @TargetApi(Build.VERSION_CODES.M)
221 | private fun requestPermission() {
222 | prefs.enabled = false
223 | Toast.makeText(this, R.string.allow_overlay, Toast.LENGTH_LONG).show()
224 | val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
225 | myIntent.data = Uri.parse("package:$packageName")
226 | myIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
227 | startActivity(myIntent)
228 | }
229 |
230 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
231 | return START_STICKY
232 | }
233 |
234 | override fun onDestroy() {
235 | remHandle()
236 |
237 | drawer.onDestroy()
238 | handle.onDestroy()
239 | prefs.removePrefListener(this)
240 | unregisterReceiver(screenStateReceiver)
241 |
242 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1)
243 | appOpsManager.stopWatchingMode(overlayListener)
244 |
245 | eventManager.removeObserver(this)
246 | }
247 |
248 | private fun canShowHandle(): Boolean =
249 | isScreenOn
250 | && prefs.showHandle
251 | }
252 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/adapters/DrawerAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.adapters
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.annotation.SuppressLint
7 | import android.appwidget.AppWidgetManager
8 | import android.appwidget.AppWidgetProviderInfo
9 | import android.content.Intent
10 | import android.graphics.Color
11 | import android.graphics.drawable.Drawable
12 | import android.os.Build
13 | import android.os.Bundle
14 | import android.util.SizeF
15 | import android.view.*
16 | import android.view.animation.AccelerateInterpolator
17 | import android.view.animation.AnticipateInterpolator
18 | import android.view.animation.DecelerateInterpolator
19 | import android.view.animation.OvershootInterpolator
20 | import android.widget.ListView
21 | import android.widget.RadioButton
22 | import androidx.core.view.forEach
23 | import androidx.recyclerview.widget.RecyclerView
24 | import com.arasthel.spannedgridlayoutmanager.SpanSize
25 | import com.arasthel.spannedgridlayoutmanager.SpannedGridLayoutManager
26 | import kotlinx.coroutines.CoroutineScope
27 | import kotlinx.coroutines.MainScope
28 | import kotlinx.coroutines.launch
29 | import tk.zwander.widgetdrawer.R
30 | import tk.zwander.widgetdrawer.databinding.HeaderLayoutBinding
31 | import tk.zwander.widgetdrawer.databinding.ShortcutHolderBinding
32 | import tk.zwander.widgetdrawer.host.WidgetHostCompat
33 | import tk.zwander.widgetdrawer.misc.BaseWidgetInfo
34 | import tk.zwander.widgetdrawer.observables.EditingObservable
35 | import tk.zwander.widgetdrawer.observables.SelectionObservable
36 | import tk.zwander.widgetdrawer.observables.TransparentObservable
37 | import tk.zwander.widgetdrawer.utils.*
38 | import tk.zwander.widgetdrawer.views.CustomCard
39 | import tk.zwander.widgetdrawer.views.Drawer
40 | import java.util.*
41 |
42 | class DrawerAdapter(
43 | private val manager: AppWidgetManager,
44 | private val appWidgetHost: WidgetHostCompat,
45 | private val params: WindowManager.LayoutParams
46 | ) : RecyclerView.Adapter(), CoroutineScope by MainScope() {
47 | companion object {
48 | const val TYPE_HEADER = BaseWidgetInfo.TYPE_HEADER
49 | const val TYPE_WIDGET = BaseWidgetInfo.TYPE_WIDGET
50 | const val TYPE_SHORTCUT = BaseWidgetInfo.TYPE_SHORTCUT
51 | }
52 |
53 | var isEditing = false
54 | set(value) {
55 | field = value
56 | if (!value) selectedId = -1
57 | if (value) {
58 | if (!widgets.contains(headerItem)) {
59 | widgets.add(0, headerItem)
60 | notifyItemInserted(0)
61 | }
62 | } else {
63 | val index = widgets.indexOf(headerItem)
64 | widgets.remove(headerItem)
65 | if (index != -1) {
66 | notifyItemRemoved(index)
67 | }
68 | }
69 | editingObservable.setEditing(value)
70 | }
71 | var selectedId = -1
72 | set(value) {
73 | field = value
74 | selectedObservable.setSelection(value)
75 | }
76 | var transparentWidgets = false
77 | set(value) {
78 | field = value
79 | transparentObservable.setTransparent(value)
80 | }
81 |
82 | val spanSizeLookup = SpanSizeLookup()
83 |
84 | private val editingObservable = EditingObservable()
85 | private val selectedObservable = SelectionObservable()
86 | private val transparentObservable = TransparentObservable()
87 |
88 | private val headerItem = BaseWidgetInfo.header()
89 |
90 | val widgets = ArrayList()
91 |
92 | val selectedWidget: BaseWidgetInfo?
93 | get() = widgets.firstOrNull { it.id == selectedId }
94 |
95 | init {
96 | setHasStableIds(true)
97 | }
98 |
99 | override fun getItemCount() = widgets.size
100 |
101 | override fun getItemId(position: Int) = widgets[position].id.toLong()
102 |
103 | override fun getItemViewType(position: Int): Int {
104 | val widget = widgets[position]
105 | return when (widget.type) {
106 | BaseWidgetInfo.TYPE_WIDGET -> TYPE_WIDGET
107 | BaseWidgetInfo.TYPE_SHORTCUT -> TYPE_SHORTCUT
108 | BaseWidgetInfo.TYPE_HEADER -> TYPE_HEADER
109 | else -> throw IllegalArgumentException("Bad widget type")
110 | }
111 | }
112 |
113 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
114 | when (viewType) {
115 | TYPE_HEADER -> HeaderVH(LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent, false))
116 | TYPE_WIDGET -> {
117 | val vh = WidgetVH(LayoutInflater.from(parent.context).inflate(R.layout.widget_holder, parent, false))
118 | updateTransparency(vh, true)
119 | vh
120 | }
121 | TYPE_SHORTCUT -> {
122 | val vh = ShortcutVH(
123 | LayoutInflater.from(parent.context).inflate(
124 | R.layout.shortcut_holder,
125 | parent,
126 | false
127 | )
128 | ) { motionEvent ->
129 | parent.onTouchEvent(motionEvent)
130 | }
131 | updateTransparency(vh, true)
132 | vh
133 | }
134 | else -> throw IllegalArgumentException("Bad view type")
135 | }
136 |
137 | @SuppressLint("CheckResult")
138 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
139 | if (holder is BaseItemVH) {
140 | val widget = widgets[position]
141 | launch {
142 | holder.onBind(widget)
143 | }
144 | } else if (holder is HeaderVH) {
145 | holder.onBind()
146 | }
147 | }
148 |
149 | private fun updateTransparency(holder: BaseItemVH, forInit: Boolean) {
150 | val card = holder.widgetFrame
151 |
152 | val attr = intArrayOf(android.R.attr.colorBackground)
153 | val array = card.context.obtainStyledAttributes(attr)
154 | val background = try {
155 | array.getColor(0, 0)
156 | } finally {
157 | array.recycle()
158 | }
159 |
160 | val elevation = card.context.resources
161 | .getDimensionPixelSize(R.dimen.elevation).toFloat()
162 |
163 | if (forInit) {
164 | card.setCardBackgroundColor(
165 | if (transparentWidgets) Color.TRANSPARENT
166 | else background
167 | )
168 |
169 | card.elevation =
170 | if (transparentWidgets) 0f
171 | else elevation
172 |
173 | return
174 | }
175 |
176 | val alphaAnim = ValueAnimator.ofArgb(
177 | if (transparentWidgets) background else Color.TRANSPARENT,
178 | if (transparentWidgets) Color.TRANSPARENT else background
179 | )
180 | alphaAnim.interpolator = if (transparentWidgets) AccelerateInterpolator() else DecelerateInterpolator()
181 | alphaAnim.duration = Drawer.ANIM_DURATION
182 | alphaAnim.addUpdateListener {
183 | card.setCardBackgroundColor(it.animatedValue.toString().toInt())
184 | }
185 | alphaAnim.start()
186 |
187 | val elevAnim = ValueAnimator.ofFloat(
188 | if (transparentWidgets) elevation else 0f,
189 | if (transparentWidgets) 0f else elevation
190 | )
191 | elevAnim.interpolator = if (transparentWidgets) AnticipateInterpolator() else OvershootInterpolator()
192 | elevAnim.duration = Drawer.ANIM_DURATION
193 | elevAnim.addUpdateListener {
194 | card.elevation = it.animatedValue.toString().toFloat()
195 | }
196 | elevAnim.start()
197 | }
198 |
199 | private fun updateSelectionCheck(holder: BaseItemVH, widget: BaseWidgetInfo) {
200 | holder.selection.isChecked = widget.id == selectedId
201 | }
202 |
203 | private fun updateSelectionVisibility(holder: BaseItemVH) {
204 | holder.selection.apply {
205 | if (isEditing) {
206 | visibility = View.VISIBLE
207 | animate()
208 | .scaleX(1f)
209 | .scaleY(1f)
210 | .setDuration(Drawer.ANIM_DURATION)
211 | .setInterpolator(OvershootInterpolator())
212 | .setListener(object : AnimatorListenerAdapter() {
213 | override fun onAnimationEnd(animation: Animator?) {
214 | scaleX = 1f
215 | scaleY = 1f
216 | }
217 | })
218 | } else {
219 | animate()
220 | .scaleX(0f)
221 | .scaleY(0f)
222 | .setDuration(Drawer.ANIM_DURATION)
223 | .setInterpolator(AnticipateInterpolator())
224 | .setListener(object : AnimatorListenerAdapter() {
225 | override fun onAnimationEnd(animation: Animator?) {
226 | visibility = View.GONE
227 | scaleX = 0f
228 | scaleY = 0f
229 | }
230 | })
231 | }
232 | }
233 | }
234 |
235 | fun addItem(widget: BaseWidgetInfo) {
236 | widgets.add(widget)
237 | notifyItemInserted(widgets.lastIndex)
238 | }
239 |
240 | fun addAt(index: Int, widget: BaseWidgetInfo) {
241 | widgets.add(index, widget)
242 | notifyItemInserted(index)
243 | }
244 |
245 | fun setAll(widgets: List) {
246 | this.widgets.removeAll { it.type != BaseWidgetInfo.TYPE_HEADER }
247 | this.widgets.addAll(widgets)
248 | }
249 |
250 | fun removeAt(position: Int): BaseWidgetInfo {
251 | return widgets.removeAt(position).also {
252 | notifyItemRemoved(position)
253 | }
254 | }
255 |
256 | inner class WidgetVH(view: View) : BaseItemVH(view) {
257 | override fun onBind(widget: BaseWidgetInfo) {
258 | super.onBind(widget)
259 |
260 | widgetFrame.removeAllViews()
261 |
262 | val widgetInfo = getWidgetInfo(widget.id)
263 |
264 | val span = spanSizeLookup.getSpanSize(bindingAdapterPosition)
265 | val calculatedWidth = itemView.calculateWidgetWidth(params.width, span)
266 | val calculatedHeight = itemView.calculateWidgetHeight(span)
267 |
268 | itemView.apply {
269 | layoutParams = (layoutParams as ViewGroup.LayoutParams).apply {
270 | width = calculatedWidth
271 | height = calculatedHeight
272 | }
273 | }
274 |
275 | val view = appWidgetHost.createView(
276 | itemView.context,
277 | widget.id,
278 | widgetInfo
279 | ).apply {
280 | findListViewsInHierarchy(this).forEach { list ->
281 | list.isNestedScrollingEnabled = true
282 | }
283 |
284 | this.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
285 | override fun onChildViewAdded(parent: View, child: View?) {
286 | findListViewsInHierarchy(parent).forEach { list ->
287 | list.isNestedScrollingEnabled = true
288 | }
289 | }
290 |
291 | override fun onChildViewRemoved(parent: View?, child: View?) {}
292 | })
293 |
294 | val width = itemView.context.pxAsDp(calculatedWidth).toInt()
295 | val height = itemView.context.pxAsDp(calculatedHeight).toInt()
296 |
297 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
298 | updateAppWidgetSize(Bundle(), listOf(SizeF(width.toFloat(), height.toFloat())))
299 | } else {
300 | updateAppWidgetSize(Bundle(), width, height, width, height)
301 | }
302 |
303 | setOnClickListener {
304 | val newInfo = widgets[bindingAdapterPosition]
305 |
306 | if (isEditing) {
307 | selectedId = newInfo.id
308 | this@WidgetVH.selection.isChecked = true
309 | }
310 | }
311 | }
312 |
313 | widgetFrame.addView(view)
314 | }
315 |
316 | private fun findListViewsInHierarchy(root: View): List {
317 | val ret = arrayListOf()
318 |
319 | if (root is ViewGroup) {
320 | root.forEach { child ->
321 | if (child is ListView) {
322 | ret.add(child)
323 | } else if (child is ViewGroup) {
324 | ret.addAll(findListViewsInHierarchy(child))
325 | }
326 | }
327 | }
328 |
329 | return ret
330 | }
331 | }
332 |
333 | @SuppressLint("ClickableViewAccessibility")
334 | inner class ShortcutVH(view: View, private val scrollCallback: (MotionEvent) -> Unit) : BaseItemVH(view) {
335 | private val binding = ShortcutHolderBinding.bind(itemView)
336 |
337 | var name: String?
338 | get() = binding.shortcutLabel.text.toString()
339 | set(value) {
340 | binding.shortcutLabel.text = value
341 | }
342 | var icon: Drawable?
343 | get() = binding.shortcutIcon.drawable
344 | set(value) {
345 | binding.shortcutIcon.setImageDrawable(value)
346 | }
347 |
348 | private var wasScroll = false
349 | private val gestureDetector =
350 | GestureDetector(itemView.context, object : GestureDetector.SimpleOnGestureListener() {
351 | override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
352 | val newEvent = MotionEvent.obtain(e2)
353 |
354 | scaleMotionEvent(newEvent)
355 |
356 | scrollCallback.invoke(newEvent)
357 |
358 | newEvent.recycle()
359 | wasScroll = true
360 | return true
361 | }
362 | })
363 |
364 | init {
365 | itemView.setOnTouchListener { _, event ->
366 | val scrollCheck = (event.action == MotionEvent.ACTION_UP && wasScroll)
367 | if (scrollCheck) wasScroll = false
368 | gestureDetector.onTouchEvent(event) || scrollCheck
369 | }
370 |
371 | // sizeObservable.addObserver { _, arg ->
372 | // if (adapterPosition != -1) {
373 | // val currentShortcut = widgets[adapterPosition]
374 | //
375 | // updateDimens(this, currentShortcut)
376 | // }
377 | // }
378 | }
379 |
380 | override fun onBind(widget: BaseWidgetInfo) {
381 | super.onBind(widget)
382 |
383 | name = widget.label
384 | icon = widget.iconBmpEncoded.base64ToBitmap()?.toBitmapDrawable(itemView.resources) ?: widget.iconRes?.loadToDrawable(itemView.context)
385 | itemView.setOnClickListener {
386 | selection.performClick()
387 |
388 | if (!isEditing) {
389 | itemView.context.startActivity(
390 | widget.shortcutIntent
391 | ?.also { intent -> intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
392 | ?: return@setOnClickListener)
393 | }
394 | }
395 | }
396 |
397 | private fun scaleMotionEvent(event: MotionEvent) {
398 | event.setLocation(
399 | event.x + itemView.left + (itemView.parent as ViewGroup).left,
400 | event.y + itemView.top + (itemView.parent as ViewGroup).top
401 | )
402 | }
403 | }
404 |
405 | open inner class BaseItemVH(view: View) : RecyclerView.ViewHolder(view) {
406 | val selection: RadioButton
407 | get() = itemView.findViewById(R.id.selection)
408 | val widgetFrame: CustomCard
409 | get() = itemView.findViewById(R.id.widget_frame)
410 |
411 | init {
412 | editingObservable.addObserver { _, _ ->
413 | updateSelectionVisibility(this)
414 | }
415 |
416 | selectedObservable.addObserver { _, _ ->
417 | widgets.getOrNull(bindingAdapterPosition)?.let {
418 | updateSelectionCheck(
419 | this,
420 | it
421 | )
422 | }
423 | }
424 |
425 | transparentObservable.addObserver { _, _ ->
426 | updateTransparency(this, false)
427 | }
428 |
429 | selection.setOnClickListener { if (isEditing) selectedId = widgets[bindingAdapterPosition].id }
430 | }
431 |
432 | fun getWidgetInfo(id: Int): AppWidgetProviderInfo? = manager.getAppWidgetInfo(id)
433 |
434 | open fun onBind(widget: BaseWidgetInfo) {
435 | updateSelectionVisibility(this)
436 | updateSelectionCheck(this, widget)
437 | }
438 | }
439 |
440 | inner class HeaderVH(view: View) : RecyclerView.ViewHolder(view) {
441 | private val binding = HeaderLayoutBinding.bind(itemView)
442 |
443 | fun onBind() {
444 | editingObservable.addObserver { _, _ ->
445 | val height = binding.editInstructions
446 | .apply { measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) }
447 | .measuredHeight
448 |
449 | ValueAnimator.ofInt(itemView.height, if (isEditing) height else 0)
450 | .apply {
451 | interpolator = if (isEditing) DecelerateInterpolator()
452 | else AccelerateInterpolator()
453 |
454 | addUpdateListener {
455 | itemView.layoutParams.apply {
456 | this.height = it.animatedValue.toString().toInt()
457 |
458 | itemView.layoutParams = this
459 | }
460 | }
461 | }
462 | .start()
463 | }
464 | }
465 | }
466 |
467 | inner class SpanSizeLookup : SpannedGridLayoutManager.SpanSizeLookup({ position ->
468 | val columnCount = appWidgetHost.context.prefs.columnCount
469 | val widget = widgets.getOrNull(position)
470 |
471 | if (widget?.type == TYPE_HEADER) {
472 | SpanSize(columnCount, 1)
473 | } else {
474 | val id = widget?.id ?: -1
475 | val sizeInfo = appWidgetHost.context.prefs.widgetSizes[id]
476 |
477 | SpanSize(sizeInfo?.getSafeWidthSpanSize(appWidgetHost.context) ?: 1, sizeInfo?.safeHeightSpanSize ?: 1)
478 | }
479 | })
480 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/widgetdrawer/views/Drawer.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.widgetdrawer.views
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.appwidget.AppWidgetManager
7 | import android.appwidget.AppWidgetProviderInfo
8 | import android.content.*
9 | import android.content.pm.ActivityInfo
10 | import android.graphics.PixelFormat
11 | import android.os.*
12 | import android.util.AttributeSet
13 | import android.view.Gravity
14 | import android.view.KeyEvent
15 | import android.view.View.OnClickListener
16 | import android.view.WindowManager
17 | import android.view.animation.AccelerateInterpolator
18 | import android.view.animation.AlphaAnimation
19 | import android.view.animation.Animation
20 | import android.view.animation.DecelerateInterpolator
21 | import android.widget.FrameLayout
22 | import androidx.core.animation.doOnEnd
23 | import androidx.recyclerview.widget.RecyclerView
24 | import com.arasthel.spannedgridlayoutmanager.SpannedGridLayoutManager
25 | import com.tingyik90.snackprogressbar.SnackProgressBar
26 | import com.tingyik90.snackprogressbar.SnackProgressBarManager
27 | import tk.zwander.widgetdrawer.R
28 | import tk.zwander.widgetdrawer.activities.PermConfigActivity
29 | import tk.zwander.widgetdrawer.activities.WidgetSelectActivity
30 | import tk.zwander.widgetdrawer.adapters.DrawerAdapter
31 | import tk.zwander.widgetdrawer.databinding.DrawerLayoutBinding
32 | import tk.zwander.widgetdrawer.host.WidgetHostCompat
33 | import tk.zwander.widgetdrawer.misc.*
34 | import tk.zwander.widgetdrawer.utils.*
35 | import java.util.*
36 |
37 | class Drawer : FrameLayout, SharedPreferences.OnSharedPreferenceChangeListener, EventObserver {
38 | companion object {
39 | const val ACTION_PERM = "PERMISSION"
40 | const val ACTION_CONFIG = "CONFIGURATION"
41 |
42 | const val EXTRA_SHORTCUT_DATA = "shortcut_data"
43 | const val EXTRA_APPWIDGET_CONFIGURE = "configure"
44 |
45 | const val ANIM_DURATION = 200L
46 | const val UNDO_DURATION = 3000L
47 | }
48 |
49 | constructor(context: Context) : super(context)
50 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
51 |
52 | var hideListener: (() -> Unit)? = null
53 | var showListener: (() -> Unit)? = null
54 |
55 | val params: WindowManager.LayoutParams
56 | get() = WindowManager.LayoutParams().apply {
57 | val displaySize = context.screenSize()
58 | type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_PRIORITY_PHONE
59 | else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
60 | flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
61 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
62 | width = displaySize.x
63 | height = WindowManager.LayoutParams.MATCH_PARENT
64 | format = PixelFormat.RGBA_8888
65 | gravity = Gravity.TOP
66 | screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
67 | }
68 |
69 | private val snackbarManager = SnackProgressBarManager(this)
70 | private val wm by lazy { context.applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager }
71 | private val host by lazy { WidgetHostCompat.getInstance(context.applicationContext, 1003, this) }
72 | private val manager by lazy { AppWidgetManager.getInstance(context.applicationContext) }
73 | private val shortcutIdManager by lazy { ShortcutIdManager.getInstance(context, host) }
74 | private val prefs by lazy { PrefsManager.getInstance(context) }
75 | private val adapter by lazy { DrawerAdapter(manager, host, params) }
76 |
77 | private val gridLayoutManager = SpannedGridLayoutManager(context, RecyclerView.VERTICAL, 1, context.prefs.columnCount)
78 |
79 | @Suppress("DEPRECATION")
80 | private val globalReceiver = object : BroadcastReceiver() {
81 | override fun onReceive(context: Context?, intent: Intent) {
82 | when (intent.action) {
83 | Intent.ACTION_CLOSE_SYSTEM_DIALOGS -> {
84 | hideDrawer()
85 | }
86 | }
87 | }
88 | }
89 |
90 | private val binding by lazy { DrawerLayoutBinding.bind(this) }
91 |
92 | override fun onFinishInflate() {
93 | super.onFinishInflate()
94 |
95 | if (!isInEditMode) {
96 | binding.addWidget.setOnClickListener { pickWidget() }
97 | binding.closeDrawer.setOnClickListener { hideDrawer() }
98 | binding.toggleTransparent.setOnClickListener {
99 | prefs.transparentWidgets = !prefs.transparentWidgets
100 | adapter.transparentWidgets = prefs.transparentWidgets
101 | }
102 |
103 | binding.widgetGrid.layoutManager = gridLayoutManager
104 | gridLayoutManager.spanSizeLookup = adapter.spanSizeLookup
105 |
106 | binding.widgetGrid.onMoveListener = { _, viewHolder, target ->
107 | val fromPosition = viewHolder.bindingAdapterPosition
108 | val toPosition = target.bindingAdapterPosition
109 |
110 | if (toPosition == 0 || fromPosition == 0) false
111 | else {
112 | if (fromPosition < toPosition) {
113 | for (i in fromPosition until toPosition) {
114 | Collections.swap(adapter.widgets, i, i + 1)
115 | }
116 | } else {
117 | for (i in fromPosition downTo toPosition + 1) {
118 | Collections.swap(adapter.widgets, i, i - 1)
119 | }
120 | }
121 |
122 | adapter.notifyItemMoved(fromPosition, toPosition)
123 |
124 | prefs.currentWidgets = adapter.widgets
125 |
126 | true
127 | }
128 | }
129 |
130 | binding.widgetGrid.onSwipeListener = { viewHolder, _ ->
131 | removeWidget(viewHolder.bindingAdapterPosition)
132 | }
133 |
134 | val inAnim = AlphaAnimation(0f, 1f).apply {
135 | duration = ANIM_DURATION
136 | interpolator = DecelerateInterpolator()
137 | }
138 | val outAnim = AlphaAnimation(1f, 0f).apply {
139 | duration = ANIM_DURATION
140 | interpolator = AccelerateInterpolator()
141 | }
142 |
143 | val animListener = object : Animation.AnimationListener {
144 | override fun onAnimationRepeat(animation: Animation?) {}
145 | override fun onAnimationStart(animation: Animation?) {}
146 | override fun onAnimationEnd(animation: Animation?) {
147 | binding.widgetGrid.allowReorder = adapter.isEditing
148 | }
149 | }
150 |
151 | inAnim.setAnimationListener(animListener)
152 | outAnim.setAnimationListener(animListener)
153 |
154 | binding.actionBarWrapper.inAnimation = inAnim
155 | binding.actionBarWrapper.outAnimation = outAnim
156 |
157 | binding.edit.setOnClickListener {
158 | adapter.isEditing = true
159 | binding.actionBarWrapper.showNext()
160 | }
161 |
162 | binding.goBack.setOnClickListener {
163 | adapter.isEditing = false
164 | binding.actionBarWrapper.showPrevious()
165 | }
166 |
167 | val listener = OnClickListener { view ->
168 | adapter.selectedWidget?.let { widget ->
169 | var changed = false
170 |
171 | when (view.id) {
172 | R.id.expand_horiz -> {
173 | changed = true
174 | prefs.apply {
175 | updateWidgetSize((widgetSizes[widget.id] ?: WidgetSizeInfo(1, 1, widget.id)).apply {
176 | safeWidthSpanSize = getSafeWidthSpanSize(context) + 1
177 | })
178 | }
179 | }
180 | R.id.collapse_horiz -> {
181 | changed = true
182 | prefs.apply {
183 | updateWidgetSize((widgetSizes[widget.id] ?: WidgetSizeInfo(1, 1, widget.id)).apply {
184 | safeWidthSpanSize = getSafeWidthSpanSize(context) - 1
185 | })
186 | }
187 | }
188 | R.id.expand_vert -> {
189 | changed = true
190 | prefs.apply {
191 | updateWidgetSize((widgetSizes[widget.id] ?: WidgetSizeInfo(1, 1, widget.id)).apply {
192 | safeHeightSpanSize += 1
193 | })
194 | }
195 | }
196 | R.id.collapse_vert -> {
197 | changed = true
198 | prefs.apply {
199 | updateWidgetSize((widgetSizes[widget.id] ?: WidgetSizeInfo(1, 1, widget.id)).apply {
200 | safeHeightSpanSize -= 1
201 | })
202 | }
203 | }
204 | }
205 |
206 | if (changed) {
207 | prefs.currentWidgets = adapter.widgets
208 | adapter.notifyItemChanged(adapter.widgets.indexOf(widget))
209 | }
210 | }
211 | }
212 |
213 | binding.expandHoriz.setOnClickListener(listener)
214 | binding.expandVert.setOnClickListener(listener)
215 | binding.collapseHoriz.setOnClickListener(listener)
216 | binding.collapseVert.setOnClickListener(listener)
217 |
218 | adapter.transparentWidgets = prefs.transparentWidgets
219 | }
220 | }
221 |
222 | override fun onAttachedToWindow() {
223 | super.onAttachedToWindow()
224 |
225 | host.startListening()
226 | Handler(Looper.getMainLooper()).postDelayed({
227 | adapter.notifyItemRangeChanged(0, adapter.itemCount)
228 | }, 50)
229 |
230 | setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom)
231 |
232 | handler?.postDelayed({
233 | val anim = ValueAnimator.ofFloat(0f, 1f)
234 | anim.interpolator = DecelerateInterpolator()
235 | anim.duration = ANIM_DURATION
236 | anim.addUpdateListener {
237 | alpha = it.animatedValue.toString().toFloat()
238 | }
239 | anim.doOnEnd {
240 | showListener?.invoke()
241 | }
242 | anim.start()
243 | }, 10)
244 |
245 | setBackgroundColor(prefs.drawerBg)
246 | }
247 |
248 | override fun onDetachedFromWindow() {
249 | super.onDetachedFromWindow()
250 |
251 | try {
252 | host.stopListening()
253 | } catch (e: NullPointerException) {
254 | //AppWidgetServiceImpl$ProviderId NPE
255 | }
256 | }
257 |
258 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
259 | when (key) {
260 | PrefsManager.TRANSPARENT_WIDGETS -> adapter.transparentWidgets = prefs.transparentWidgets
261 | PrefsManager.COLUMN_COUNT -> updateSpanCount()
262 | }
263 | }
264 |
265 | override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
266 | if (event?.keyCode == KeyEvent.KEYCODE_BACK) {
267 | hideDrawer()
268 | return true
269 | }
270 |
271 | return super.dispatchKeyEvent(event)
272 | }
273 |
274 | override fun onEvent(event: Event) {
275 | when (event) {
276 | is Event.PermissionResult -> {
277 | if (event.success) {
278 | tryBindWidget(manager.getAppWidgetInfo(event.widgetId))
279 | }
280 | }
281 | is Event.WidgetConfigResult -> {
282 | if (event.success) {
283 | addNewWidget(event.widgetId)
284 | } else {
285 | showDrawer()
286 | }
287 | }
288 | is Event.ShortcutConfigResult -> {
289 | if (event.success) {
290 | addNewShortcut(
291 | BaseWidgetInfo.shortcut(
292 | event.name ?: event.data?.label,
293 | event.iconBmp.toBase64(),
294 | event.iconRes ?: event.data?.iconRes,
295 | shortcutIdManager.allocateShortcutId(),
296 | event.intent
297 | )
298 | )
299 | }
300 | }
301 | is Event.PickWidgetResult -> {
302 | if (event.success) {
303 | tryBindWidget(event.providerInfo)
304 | } else {
305 | showDrawer()
306 | }
307 | }
308 | is Event.PickShortcutResult -> {
309 | if (event.success) {
310 | tryBindShortcut(event.shortcutData)
311 | } else {
312 | showDrawer()
313 | }
314 | }
315 | Event.PickFailedResult -> {
316 | showDrawer()
317 | }
318 | else -> {}
319 | }
320 | }
321 |
322 | fun onCreate() {
323 | context.registerReceiver(globalReceiver, IntentFilter().apply {
324 | @Suppress("DEPRECATION")
325 | addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
326 | })
327 | prefs.addPrefListener(this)
328 |
329 | binding.widgetGrid.adapter = adapter
330 | binding.widgetGrid.isNestedScrollingEnabled = true
331 | binding.widgetGrid.setHasFixedSize(true)
332 | updateSpanCount()
333 | adapter.setAll(prefs.currentWidgets)
334 | context.eventManager.addObserver(this)
335 | }
336 |
337 | fun onDestroy() {
338 | hideDrawer(false)
339 | prefs.currentWidgets = adapter.widgets
340 |
341 | context.unregisterReceiver(globalReceiver)
342 | prefs.removePrefListener(this)
343 | context.eventManager.removeObserver(this)
344 | }
345 |
346 | fun showDrawer(wm: WindowManager = this.wm, overrideType: Int = params.type) {
347 | try {
348 | wm.addView(this, params.apply { type = overrideType })
349 | } catch (_: Exception) {}
350 | }
351 |
352 | fun hideDrawer(callListener: Boolean = true) {
353 | val anim = ValueAnimator.ofFloat(1f, 0f)
354 | anim.interpolator = AccelerateInterpolator()
355 | anim.duration = ANIM_DURATION
356 | anim.addUpdateListener {
357 | alpha = it.animatedValue.toString().toFloat()
358 | }
359 | anim.addListener(object : AnimatorListenerAdapter() {
360 | override fun onAnimationEnd(animation: Animator?) {
361 | if (callListener) hideListener?.invoke()
362 | handler?.postDelayed({
363 | try {
364 | wm.removeView(this@Drawer)
365 | } catch (_: Exception) {
366 | }
367 | }, 10)
368 | }
369 | })
370 | anim.start()
371 | }
372 |
373 | private fun updateSpanCount() {
374 | gridLayoutManager.columnCount = context.prefs.columnCount
375 | gridLayoutManager.customHeight = context.widgetHeightUnit
376 | }
377 |
378 | private fun getWidgetPermission(id: Int, componentName: ComponentName, options: Bundle? = null) {
379 | val intent = Intent(ACTION_PERM)
380 | intent.component = ComponentName(context, PermConfigActivity::class.java)
381 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
382 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, componentName)
383 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options)
384 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
385 |
386 | context.startActivity(intent)
387 | }
388 |
389 | private fun configureWidget(
390 | id: Int,
391 | configure: ComponentName
392 | ) {
393 | val intent = Intent(ACTION_CONFIG)
394 | intent.putExtra(EXTRA_APPWIDGET_CONFIGURE, configure)
395 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
396 | intent.component = ComponentName(context, PermConfigActivity::class.java)
397 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
398 |
399 | context.startActivity(intent)
400 | }
401 |
402 | private fun pickWidget() {
403 | hideDrawer()
404 | val intent = Intent(context, WidgetSelectActivity::class.java)
405 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, host.allocateAppWidgetId())
406 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
407 | context.startActivity(intent)
408 | }
409 |
410 | private fun tryBindWidget(info: AppWidgetProviderInfo, id: Int = host.allocateAppWidgetId()) {
411 | val canBind = manager.bindAppWidgetIdIfAllowed(id, info.provider)
412 |
413 | if (!canBind) getWidgetPermission(id, info.provider)
414 | else {
415 | if (info.configure != null && !adapter.widgets.map { it.id }.contains(id)) {
416 | configureWidget(id, info.configure)
417 | } else {
418 | addNewWidget(id)
419 | }
420 | }
421 | }
422 |
423 | private fun tryBindShortcut(info: ShortcutData) {
424 | val intent = Intent(context, PermConfigActivity::class.java)
425 | intent.action = Intent.ACTION_CREATE_SHORTCUT
426 | intent.putExtra(EXTRA_SHORTCUT_DATA, info)
427 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
428 |
429 | context.startActivity(intent)
430 | }
431 |
432 | private fun addNewWidget(id: Int) {
433 | showDrawer()
434 | val info = createSavedWidget(id)
435 | adapter.addItem(info)
436 | prefs.currentWidgets = adapter.widgets
437 | }
438 |
439 | private fun addNewShortcut(info: BaseWidgetInfo) {
440 | showDrawer()
441 | adapter.addItem(info)
442 | prefs.currentWidgets = adapter.widgets
443 | }
444 |
445 | private fun createSavedWidget(id: Int): BaseWidgetInfo {
446 | return BaseWidgetInfo.widget(id)
447 | }
448 |
449 | private fun removeWidget(position: Int) {
450 | val info = adapter.removeAt(position)
451 |
452 | val timer = object : CountDownTimer(UNDO_DURATION, 1) {
453 | override fun onFinish() {
454 | snackbarManager.dismiss()
455 |
456 | if (info.type == BaseWidgetInfo.TYPE_WIDGET) host.deleteAppWidgetId(info.id)
457 | else if (info.type == BaseWidgetInfo.TYPE_SHORTCUT) shortcutIdManager.removeShortcutId(info.id)
458 | prefs.currentWidgets = adapter.widgets
459 | }
460 |
461 | override fun onTick(millisUntilFinished: Long) {
462 | snackbarManager.setProgress((UNDO_DURATION - millisUntilFinished).toInt())
463 | }
464 | }
465 |
466 | val snackbar = SnackProgressBar(
467 | SnackProgressBar.TYPE_HORIZONTAL,
468 | resources.getString(R.string.widget_removed)
469 | )
470 |
471 | snackbar.setAllowUserInput(true)
472 | snackbar.setSwipeToDismiss(true)
473 | snackbar.setIsIndeterminate(false)
474 | snackbar.setProgressMax(UNDO_DURATION.toInt())
475 |
476 | snackbar.setAction(resources.getString(R.string.undo), object : SnackProgressBar.OnActionClickListener {
477 | override fun onActionClick() {
478 | adapter.addAt(position, info)
479 | timer.cancel()
480 | }
481 | })
482 |
483 | snackbarManager.show(snackbar, SnackProgressBarManager.LENGTH_INDEFINITE, 100)
484 | timer.start()
485 | }
486 | }
--------------------------------------------------------------------------------