├── 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 | 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 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 | 19 | 21 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 66 | 74 | 82 | 83 | 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 | 6 | 7 | 8 | 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 | 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 | } --------------------------------------------------------------------------------