Introduce Lawnchair Advance Search (#3785)

* Introduce Lawnchair Device Search

Initial Implementation device search in Lawnchair:
Enables search functionality across Files, Contacts, and Suggestions on the Startpage.

* Apply reviewed suggestions by @Gooler

- Use retrofit for fetching suggestions

* use kotlinxJson

* Separate suggestion

* Apply changes code refine @Goooler

* Fix styling

* Implement MIME Type Compat

* Apply suggestion changes by @suphon-t

* Apply suggestion changes by @SuperDragonXD

* Cleanup

* Remodel FileInfo

* Private retrofit

* Clean up SearchRequest

* Add FileExtensions

* Optimize file search method

* Fix style

* Rename

* Optimize file opening

* Add type checkers

* Reformat AVDs

* Fixed crash java.lang.ClassCastException

error logs : https://katb.in/abahivihaqu

* More fixes

* Surround try catch to give proper error logs

java.lang.NullPointerException
	at app.lawnchair.search.data.SearchRequestKt$findContactsByName$2.invokeSuspend(SearchRequest.kt:124)
	at app.lawnchair.search.data.SearchRequestKt$findContactsByName$2.invoke(Unknown Source:8)
	at app.lawnchair.search.data.SearchRequestKt$findContactsByName$2.invoke(Unknown Source:4)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:78)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:167)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
	at app.lawnchair.search.data.SearchRequestKt.findContactsByName(SearchRequest.kt:68)
	at app.lawnchair.search.LawnchairSearchAlgorithm$performDeviceWideSearch$2.invokeSuspend(LawnchairSearchAlgorithm.kt:152)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)

* Fixed opened file instead of just redirect to file manager

* Fixed crash opening folder

* Initial implementation callback

* Add max results option for files, suggestion and people

* Add initial settings entry

* Properly use folder mimetype

* Fixed target icon styling

* Cleanups

* Revert

* Fixes

* Apply suggestions by @Goooler

* Fixed crash on external storage directory

* Implement history keyword using built-in SearchRecentSuggestionsProvider

* misc fixes

* Add timeout 3 seconds

- As we don't how long gonna take to receive response in startpage

* Misc fixes

* Misc fixes

---------

Co-authored-by: Goooler <wangzongler@gmail.com>
This commit is contained in:
John Andrew Camu
2023-12-09 08:13:28 +08:00
committed by GitHub
parent bd461eb474
commit be8bb75d95
57 changed files with 2409 additions and 209 deletions
+11
View File
@@ -24,6 +24,11 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="com.google.android.apps.nexuslauncher.permission.QSB" />
<uses-permission android:name="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_HISTORY_BOOKMARKS" />
<uses-permission android:name="android.permission.WRITE_HISTORY_BOOKMARKS" />
<permission
android:name="${packageName}.permission.READ_SETTINGS"
@@ -99,6 +104,12 @@
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="app.lawnchair.search.LawnchairRecentSuggestionProvider"
android:authorities="app.lawnchair.search.LawnchairRecentSuggestionProvider"
android:syncable="true"
android:exported="false" />
<receiver
android:name="app.lawnchair.bugreport.BugReportReceiver"
android:exported="false">
+15
View File
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M9,17H15M9,13H15M9,9H10M13,3H8.2C7.08,3 6.52,3 6.092,3.218C5.716,3.41 5.41,3.716 5.218,4.092C5,4.52 5,5.08 5,6.2V17.8C5,18.92 5,19.48 5.218,19.908C5.41,20.284 5.716,20.59 6.092,20.782C6.52,21 7.08,21 8.2,21H15.8C16.92,21 17.48,21 17.908,20.782C18.284,20.59 18.59,20.284 18.782,19.908C19,19.48 19,18.92 19,17.8V9M13,3L19,9M13,3V7.4C13,7.96 13,8.24 13.109,8.454C13.205,8.642 13.358,8.795 13.546,8.891C13.76,9 14.04,9 14.6,9H19"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+15
View File
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M9,3V5.4C9,5.96 9,6.24 9.109,6.454C9.205,6.642 9.358,6.795 9.546,6.891C9.76,7 10.04,7 10.6,7H12.4C12.96,7 13.24,7 13.454,6.891C13.642,6.795 13.795,6.642 13.891,6.454C14,6.24 14,5.96 14,5.4V3.004M19,9.123V17.8C19,18.92 19,19.48 18.782,19.908C18.59,20.284 18.284,20.59 17.908,20.782C17.48,21 16.92,21 15.8,21H8.2C7.08,21 6.52,21 6.092,20.782C5.716,20.59 5.41,20.284 5.218,19.908C5,19.48 5,18.92 5,17.8V6.2C5,5.08 5,4.52 5.218,4.092C5.41,3.716 5.716,3.41 6.092,3.218C6.52,3 7.08,3 8.2,3H13.462C14.027,3 14.309,3 14.57,3.072C14.801,3.135 15.019,3.24 15.213,3.381C15.432,3.539 15.608,3.76 15.961,4.201L18.299,7.123C18.559,7.448 18.689,7.611 18.781,7.792C18.863,7.952 18.923,8.123 18.959,8.299C19,8.498 19,8.706 19,9.123ZM14,15.5C14,16.881 12.881,18 11.5,18C10.119,18 9,16.881 9,15.5C9,14.119 10.119,13 11.5,13C12.881,13 14,14.119 14,15.5Z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM11.26,14.793C10.87,15.183 10.87,15.817 11.26,16.207C11.651,16.598 12.284,16.598 12.674,16.207L11.26,14.793ZM9.062,12.251C8.924,12.786 9.246,13.331 9.781,13.468C10.316,13.606 10.861,13.284 10.999,12.749L9.062,12.251ZM11.967,17C11.415,17 10.967,17.448 10.967,18C10.967,18.552 11.415,19 11.967,19V17ZM11.977,19C12.529,19 12.977,18.552 12.977,18C12.977,17.448 12.529,17 11.977,17V19ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM12.967,13C12.967,13.173 12.922,13.305 12.648,13.57C12.494,13.718 12.313,13.865 12.062,14.074C11.824,14.272 11.544,14.509 11.26,14.793L12.674,16.207C12.89,15.991 13.111,15.803 13.341,15.612C13.559,15.431 13.815,15.222 14.037,15.009C14.513,14.549 14.967,13.931 14.967,13H12.967ZM11.967,12C12.519,12 12.967,12.448 12.967,13H14.967C14.967,11.343 13.624,10 11.967,10V12ZM10.999,12.749C11.11,12.318 11.503,12 11.967,12V10C10.568,10 9.395,10.957 9.062,12.251L10.999,12.749ZM11.967,19H11.977V17H11.967V19Z" />
</vector>
+15
View File
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M14.264,15.938L12.596,14.283C11.791,13.485 11.388,13.086 10.927,12.94C10.52,12.812 10.084,12.816 9.68,12.954C9.222,13.109 8.828,13.517 8.041,14.333L4.044,18.28M14.264,15.938L14.605,15.599C15.411,14.8 15.814,14.4 16.277,14.254C16.683,14.126 17.12,14.131 17.524,14.269C17.982,14.425 18.376,14.834 19.163,15.651L20,16.493M14.264,15.938L18.275,19.956M18.275,19.956C17.918,20 17.454,20 16.8,20H7.2C6.08,20 5.52,20 5.092,19.782C4.716,19.59 4.41,19.284 4.218,18.908C4.128,18.731 4.075,18.532 4.044,18.28M18.275,19.956C18.529,19.926 18.73,19.873 18.908,19.782C19.284,19.59 19.59,19.284 19.782,18.908C20,18.48 20,17.92 20,16.8V16.493M4.044,18.28C4,17.922 4,17.458 4,16.8V7.2C4,6.08 4,5.52 4.218,5.092C4.41,4.716 4.716,4.41 5.092,4.218C5.52,4 6.08,4 7.2,4H16.8C17.92,4 18.48,4 18.908,4.218C19.284,4.41 19.59,4.716 19.782,5.092C20,5.52 20,6.08 20,7.2V16.493M17,9C17,10.104 16.105,11 15,11C13.895,11 13,10.104 13,9C13,7.895 13.895,7 15,7C16.105,7 17,7.895 17,9Z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+15
View File
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M12.5,8.89V18.5M12.5,8.89V5.577C12.5,5.369 12.5,5.266 12.535,5.177C12.565,5.099 12.615,5.03 12.679,4.977C12.752,4.916 12.85,4.883 13.047,4.818L17.447,3.351C17.802,3.232 17.98,3.173 18.122,3.209C18.246,3.24 18.353,3.317 18.422,3.425C18.5,3.548 18.5,3.735 18.5,4.11V7.423C18.5,7.631 18.5,7.734 18.465,7.823C18.435,7.9 18.385,7.969 18.321,8.023C18.248,8.084 18.15,8.117 17.953,8.182L13.553,9.649C13.198,9.767 13.02,9.827 12.878,9.791C12.754,9.76 12.647,9.683 12.578,9.575C12.5,9.452 12.5,9.265 12.5,8.89ZM12.5,18.5C12.5,19.881 10.933,21 9,21C7.067,21 5.5,19.881 5.5,18.5C5.5,17.119 7.067,16 9,16C10.933,16 12.5,17.119 12.5,18.5Z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM11.26,14.793C10.87,15.183 10.87,15.817 11.26,16.207C11.651,16.598 12.284,16.598 12.674,16.207L11.26,14.793ZM9.062,12.251C8.924,12.786 9.246,13.331 9.781,13.468C10.316,13.606 10.861,13.284 10.999,12.749L9.062,12.251ZM11.967,17C11.415,17 10.967,17.448 10.967,18C10.967,18.552 11.415,19 11.967,19V17ZM11.977,19C12.529,19 12.977,18.552 12.977,18C12.977,17.448 12.529,17 11.977,17V19ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM12.967,13C12.967,13.173 12.922,13.305 12.648,13.57C12.494,13.718 12.313,13.865 12.062,14.074C11.824,14.272 11.544,14.509 11.26,14.793L12.674,16.207C12.89,15.991 13.111,15.803 13.341,15.612C13.559,15.431 13.815,15.222 14.037,15.009C14.513,14.549 14.967,13.931 14.967,13H12.967ZM11.967,12C12.519,12 12.967,12.448 12.967,13H14.967C14.967,11.343 13.624,10 11.967,10V12ZM10.999,12.749C11.11,12.318 11.503,12 11.967,12V10C10.568,10 9.395,10.957 9.062,12.251L10.999,12.749ZM11.967,19H11.977V17H11.967V19Z" />
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM11.26,14.793C10.87,15.183 10.87,15.817 11.26,16.207C11.651,16.598 12.284,16.598 12.674,16.207L11.26,14.793ZM9.062,12.251C8.924,12.786 9.246,13.331 9.781,13.468C10.316,13.606 10.861,13.284 10.999,12.749L9.062,12.251ZM11.967,17C11.415,17 10.967,17.448 10.967,18C10.967,18.552 11.415,19 11.967,19V17ZM11.977,19C12.529,19 12.977,18.552 12.977,18C12.977,17.448 12.529,17 11.977,17V19ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM12.967,13C12.967,13.173 12.922,13.305 12.648,13.57C12.494,13.718 12.313,13.865 12.062,14.074C11.824,14.272 11.544,14.509 11.26,14.793L12.674,16.207C12.89,15.991 13.111,15.803 13.341,15.612C13.559,15.431 13.815,15.222 14.037,15.009C14.513,14.549 14.967,13.931 14.967,13H12.967ZM11.967,12C12.519,12 12.967,12.448 12.967,13H14.967C14.967,11.343 13.624,10 11.967,10V12ZM10.999,12.749C11.11,12.318 11.503,12 11.967,12V10C10.568,10 9.395,10.957 9.062,12.251L10.999,12.749ZM11.967,19H11.977V17H11.967V19Z" />
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM11.26,14.793C10.87,15.183 10.87,15.817 11.26,16.207C11.651,16.598 12.284,16.598 12.674,16.207L11.26,14.793ZM9.062,12.251C8.924,12.786 9.246,13.331 9.781,13.468C10.316,13.606 10.861,13.284 10.999,12.749L9.062,12.251ZM11.967,17C11.415,17 10.967,17.448 10.967,18C10.967,18.552 11.415,19 11.967,19V17ZM11.977,19C12.529,19 12.977,18.552 12.977,18C12.977,17.448 12.529,17 11.977,17V19ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM12.967,13C12.967,13.173 12.922,13.305 12.648,13.57C12.494,13.718 12.313,13.865 12.062,14.074C11.824,14.272 11.544,14.509 11.26,14.793L12.674,16.207C12.89,15.991 13.111,15.803 13.341,15.612C13.559,15.431 13.815,15.222 14.037,15.009C14.513,14.549 14.967,13.931 14.967,13H12.967ZM11.967,12C12.519,12 12.967,12.448 12.967,13H14.967C14.967,11.343 13.624,10 11.967,10V12ZM10.999,12.749C11.11,12.318 11.503,12 11.967,12V10C10.568,10 9.395,10.957 9.062,12.251L10.999,12.749ZM11.967,19H11.977V17H11.967V19Z" />
</vector>
+15
View File
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M9,17H15M9,13H15M9,9H10M13,3H8.2C7.08,3 6.52,3 6.092,3.218C5.716,3.41 5.41,3.716 5.218,4.092C5,4.52 5,5.08 5,6.2V17.8C5,18.92 5,19.48 5.218,19.908C5.41,20.284 5.716,20.59 6.092,20.782C6.52,21 7.08,21 8.2,21H15.8C16.92,21 17.48,21 17.908,20.782C18.284,20.59 18.59,20.284 18.782,19.908C19,19.48 19,18.92 19,17.8V9M13,3L19,9M13,3V7.4C13,7.96 13,8.24 13.109,8.454C13.205,8.642 13.358,8.795 13.546,8.891C13.76,9 14.04,9 14.6,9H19"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M9,17H15M9,13H15M9,9H10M13,3H8.2C7.08,3 6.52,3 6.092,3.218C5.716,3.41 5.41,3.716 5.218,4.092C5,4.52 5,5.08 5,6.2V17.8C5,18.92 5,19.48 5.218,19.908C5.41,20.284 5.716,20.59 6.092,20.782C6.52,21 7.08,21 8.2,21H15.8C16.92,21 17.48,21 17.908,20.782C18.284,20.59 18.59,20.284 18.782,19.908C19,19.48 19,18.92 19,17.8V9M13,3L19,9M13,3V7.4C13,7.96 13,8.24 13.109,8.454C13.205,8.642 13.358,8.795 13.546,8.891C13.76,9 14.04,9 14.6,9H19"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
+14
View File
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M3,9H9.5M21,9H9.5M9.5,9L14.5,4M14.5,4H17.8C18.847,4 19.404,4 19.822,4.178M14.5,4H6.2C5.08,4 4.52,4 4.092,4.218C3.716,4.41 3.41,4.716 3.218,5.092C3,5.52 3,6.08 3,7.2V16.8C3,17.92 3,18.48 3.218,18.908C3.41,19.284 3.716,19.59 4.092,19.782C4.52,20 5.08,20 6.2,20H17.8C18.92,20 19.48,20 19.908,19.782C20.284,19.59 20.59,19.284 20.782,18.908C21,18.48 21,17.92 21,16.8V7.2C21,6.08 21,5.52 20.782,5.092C20.59,4.716 20.284,4.41 19.908,4.218C19.88,4.204 19.851,4.19 19.822,4.178M9,4L4,9M15,9L19.822,4.178M15,14.5L10,17.5V11.5L15,14.5Z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM11.26,14.793C10.87,15.183 10.87,15.817 11.26,16.207C11.651,16.598 12.284,16.598 12.674,16.207L11.26,14.793ZM9.062,12.251C8.924,12.786 9.246,13.331 9.781,13.468C10.316,13.606 10.861,13.284 10.999,12.749L9.062,12.251ZM11.967,17C11.415,17 10.967,17.448 10.967,18C10.967,18.552 11.415,19 11.967,19V17ZM11.977,19C12.529,19 12.977,18.552 12.977,18C12.977,17.448 12.529,17 11.977,17V19ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM12.967,13C12.967,13.173 12.922,13.305 12.648,13.57C12.494,13.718 12.313,13.865 12.062,14.074C11.824,14.272 11.544,14.509 11.26,14.793L12.674,16.207C12.89,15.991 13.111,15.803 13.341,15.612C13.559,15.431 13.815,15.222 14.037,15.009C14.513,14.549 14.967,13.931 14.967,13H12.967ZM11.967,12C12.519,12 12.967,12.448 12.967,13H14.967C14.967,11.343 13.624,10 11.967,10V12ZM10.999,12.749C11.11,12.318 11.503,12 11.967,12V10C10.568,10 9.395,10.957 9.062,12.251L10.999,12.749ZM11.967,19H11.977V17H11.967V19Z" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M13,3L13.707,2.293C13.52,2.105 13.265,2 13,2V3ZM19,9H20C20,8.735 19.895,8.48 19.707,8.293L19,9ZM13.109,8.454L14,8V8L13.109,8.454ZM13.546,8.891L14,8L13.546,8.891ZM10,13C10,12.448 9.552,12 9,12C8.448,12 8,12.448 8,13H10ZM8,16C8,16.552 8.448,17 9,17C9.552,17 10,16.552 10,16H8ZM8.5,9C7.948,9 7.5,9.448 7.5,10C7.5,10.552 7.948,11 8.5,11V9ZM9.5,11C10.052,11 10.5,10.552 10.5,10C10.5,9.448 10.052,9 9.5,9V11ZM8.5,6C7.948,6 7.5,6.448 7.5,7C7.5,7.552 7.948,8 8.5,8V6ZM9.5,8C10.052,8 10.5,7.552 10.5,7C10.5,6.448 10.052,6 9.5,6V8ZM17.908,20.782L17.454,19.891L17.454,19.891L17.908,20.782ZM18.782,19.908L19.673,20.362L18.782,19.908ZM5.218,19.908L4.327,20.362H4.327L5.218,19.908ZM6.092,20.782L6.546,19.891L6.546,19.891L6.092,20.782ZM6.092,3.218L5.638,2.327L5.638,2.327L6.092,3.218ZM5.218,4.092L4.327,3.638L4.327,3.638L5.218,4.092ZM12,3V7.4H14V3H12ZM14.6,10H19V8H14.6V10ZM12,7.4C12,7.664 11.999,7.921 12.017,8.138C12.036,8.367 12.08,8.637 12.218,8.908L14,8C14.029,8.058 14.019,8.08 14.01,7.975C14.001,7.859 14,7.697 14,7.4H12ZM14.6,8C14.304,8 14.141,7.999 14.025,7.99C13.92,7.981 13.943,7.971 14,8L13.092,9.782C13.363,9.92 13.633,9.964 13.862,9.983C14.079,10.001 14.337,10 14.6,10V8ZM12.218,8.908C12.41,9.284 12.716,9.59 13.092,9.782L14,8V8L12.218,8.908ZM8,13V16H10V13H8ZM8.5,11H9.5V9H8.5V11ZM8.5,8H9.5V6H8.5V8ZM13,2H8.2V4H13V2ZM4,6.2V17.8H6V6.2H4ZM8.2,22H15.8V20H8.2V22ZM20,17.8V9H18V17.8H20ZM19.707,8.293L13.707,2.293L12.293,3.707L18.293,9.707L19.707,8.293ZM15.8,22C16.344,22 16.811,22.001 17.195,21.969C17.59,21.937 17.984,21.866 18.362,21.673L17.454,19.891C17.404,19.916 17.304,19.954 17.032,19.976C16.749,19.999 16.377,20 15.8,20V22ZM18,17.8C18,18.377 17.999,18.749 17.976,19.032C17.954,19.304 17.916,19.404 17.891,19.454L19.673,20.362C19.866,19.984 19.937,19.59 19.969,19.195C20.001,18.811 20,18.344 20,17.8H18ZM18.362,21.673C18.927,21.385 19.385,20.927 19.673,20.362L17.891,19.454C17.795,19.642 17.642,19.795 17.454,19.891L18.362,21.673ZM4,17.8C4,18.344 3.999,18.811 4.031,19.195C4.063,19.59 4.134,19.984 4.327,20.362L6.109,19.454C6.084,19.404 6.046,19.304 6.024,19.032C6.001,18.749 6,18.377 6,17.8H4ZM8.2,20C7.623,20 7.251,19.999 6.968,19.976C6.696,19.954 6.595,19.916 6.546,19.891L5.638,21.673C6.016,21.866 6.41,21.937 6.805,21.969C7.189,22.001 7.656,22 8.2,22V20ZM4.327,20.362C4.615,20.927 5.074,21.385 5.638,21.673L6.546,19.891C6.358,19.795 6.205,19.642 6.109,19.454L4.327,20.362ZM8.2,2C7.656,2 7.189,1.999 6.805,2.031C6.41,2.063 6.016,2.134 5.638,2.327L6.546,4.109C6.595,4.084 6.696,4.046 6.968,4.024C7.251,4.001 7.623,4 8.2,4V2ZM6,6.2C6,5.623 6.001,5.251 6.024,4.968C6.046,4.696 6.084,4.595 6.109,4.546L4.327,3.638C4.134,4.016 4.063,4.41 4.031,4.805C3.999,5.189 4,5.656 4,6.2H6ZM5.638,2.327C5.074,2.615 4.615,3.074 4.327,3.638L6.109,4.546C6.205,4.358 6.358,4.205 6.546,4.109L5.638,2.327Z" />
</vector>
+1
View File
@@ -2,6 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:tint="?android:attr/textColorPrimary"
android:viewportHeight="24">
<path
android:pathData="M22,17C22,18.6569 20.6569,20 19,20H5C3.3431,20 2,18.6569 2,17V7C2,5.3432 3.3431,4 5,4H10L12,6H19C20.6569,6 22,7.3432 22,9V17ZM4,7V17C4,17.5523 4.4477,18 5,18H19C19.5523,18 20,17.5523 20,17V9C20,8.4477 19.5523,8 19,8H11L9,6H5C4.4477,6 4,6.4477 4,7Z"
+9
View File
@@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:tint="?android:attr/textColorPrimary"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M7,9H17M7,13H17M21,20L17.676,18.338C17.424,18.212 17.298,18.149 17.166,18.104C17.048,18.065 16.928,18.037 16.805,18.019C16.667,18 16.526,18 16.245,18H6.2C5.08,18 4.52,18 4.092,17.782C3.716,17.59 3.41,17.284 3.218,16.908C3,16.48 3,15.92 3,14.8V7.2C3,6.08 3,5.52 3.218,5.092C3.41,4.716 3.716,4.41 4.092,4.218C4.52,4 5.08,4 6.2,4H17.8C18.92,4 19.48,4 19.908,4.218C20.284,4.41 20.59,4.716 20.782,5.092C21,5.52 21,6.08 21,7.2V20Z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:tint="?android:attr/textColorPrimary"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M18.604,12.727L15.565,9.087C15.257,8.691 15.103,8.493 15.023,8.271C14.951,8.074 14.922,7.865 14.936,7.656C14.952,7.421 15.045,7.188 15.231,6.722L15.942,4.945C16.218,4.256 16.356,3.911 16.593,3.685C16.803,3.486 17.066,3.352 17.35,3.299C17.672,3.238 18.032,3.328 18.752,3.508L20.72,4C20.72,14 13.72,21 3.72,21L3.228,19.032C3.048,18.312 2.958,17.952 3.019,17.63C3.072,17.346 3.206,17.083 3.405,16.873C3.631,16.636 3.976,16.498 4.665,16.222L6.251,15.588C6.782,15.375 7.048,15.269 7.313,15.261C7.547,15.254 7.779,15.301 7.992,15.4C8.232,15.512 8.434,15.714 8.839,16.119L11.925,19.157"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>
+8
View File
@@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M4,12C4,16.418 7.582,20 12,20C16.418,20 20,16.418 20,12C20,7.582 16.418,4 12,4C9.611,4 7.466,5.048 6,6.708C5.916,6.804 5.834,6.901 5.755,7M12,8V12L14.5,14.5M5.754,4.004V7.004H8.754"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<app.lawnchair.allapps.SearchResultRightLeftIcon xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_result_icon_row_left_right"
android:layout_width="match_parent"
android:layout_height="@dimen/search_result_files_row_height"
android:orientation="horizontal"
android:paddingStart="8dp">
<app.lawnchair.util.ImageViewWrapper
android:id="@+id/files_preview"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_files_preview_width"
android:layout_height="@dimen/search_row_files_preview_height"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<app.lawnchair.allapps.SearchResultIcon
android:id="@+id/avatar"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<LinearLayout
android:id="@+id/text_rows"
style="@style/AllAppsSearchResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="horizontal"
android:paddingStart="@dimen/dynamic_grid_edge_margin"
android:paddingTop="@dimen/dynamic_grid_edge_margin"
android:paddingEnd="@dimen/dynamic_grid_edge_margin"
android:paddingBottom="@dimen/dynamic_grid_edge_margin"
android:singleLine="true">
<TextView
android:id="@+id/title"
style="@style/AllAppsSearchResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:paddingEnd="4dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_title_size" />
<TextView
android:id="@+id/delimiter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center"
android:maxLines="1"
android:text="•"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_subtitle_size"
android:visibility="gone" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="4dp"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/search_result_hero_subtitle_size" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingTop="@dimen/dynamic_grid_edge_margin"
android:paddingEnd="@dimen/dynamic_grid_edge_margin"
android:paddingBottom="@dimen/dynamic_grid_edge_margin"
android:orientation="horizontal"
android:singleLine="true">
<ImageView
android:id="@+id/icon1"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<ImageView
android:id="@+id/icon2"
style="@style/BaseIcon"
android:layout_marginLeft="@dimen/search_result_padding_medium"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
</LinearLayout>
</app.lawnchair.allapps.SearchResultRightLeftIcon>
@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<app.lawnchair.allapps.SearchResultIconRow xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
android:layout_height="@dimen/search_result_row_height"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="@dimen/search_result_margin"
android:paddingTop="@dimen/search_result_padding"
android:paddingBottom="@dimen/search_result_padding">
<app.lawnchair.allapps.SearchResultIcon
android:id="@id/icon"
android:layout_width="@dimen/search_row_icon_size"
android:layout_height="wrap_content"
launcher:iconDisplay="search_result_tall"
launcher:layoutHorizontal="true" />
<LinearLayout
android:id="@+id/text_rows"
android:layout_width="0.0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="@dimen/search_result_padding">
<TextView
android:id="@+id/title"
style="@style/AllAppsSearchResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_title_size" />
<TextView
android:id="@+id/subtitle"
style="@style/AllAppsSearchResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/search_result_hero_subtitle_size" />
</LinearLayout>
<app.lawnchair.allapps.SearchResultIcon
android:id="@+id/shortcut_0"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_icon_size"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:layout_marginStart="@dimen/search_result_margin"
android:textSize="@dimen/search_hero_inline_button_text_size"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true" />
<app.lawnchair.allapps.SearchResultIcon
android:id="@+id/shortcut_1"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_icon_size"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/search_result_margin"
android:textSize="@dimen/search_hero_inline_button_text_size"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true" />
<app.lawnchair.allapps.SearchResultIcon
android:id="@+id/shortcut_2"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_icon_size"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/search_result_margin"
android:textSize="@dimen/search_hero_inline_button_text_size"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true" />
</app.lawnchair.allapps.SearchResultIconRow>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<app.lawnchair.allapps.SearchResultRightLeftIcon xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_result_small_icon_row_left_right"
android:layout_width="match_parent"
android:layout_height="@dimen/search_result_small_row_height"
android:orientation="horizontal"
android:paddingStart="8dp">
<app.lawnchair.util.ImageViewWrapper
android:id="@+id/files_preview"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_files_preview_width"
android:layout_height="@dimen/search_row_files_preview_height"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<app.lawnchair.allapps.SearchResultIcon
android:id="@+id/avatar"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<LinearLayout
android:id="@+id/text_rows"
style="@style/AllAppsSearchResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="horizontal"
android:paddingStart="@dimen/dynamic_grid_edge_margin"
android:paddingTop="@dimen/dynamic_grid_edge_margin"
android:paddingEnd="@dimen/dynamic_grid_edge_margin"
android:paddingBottom="@dimen/dynamic_grid_edge_margin"
android:singleLine="true">
<TextView
android:id="@+id/title"
style="@style/AllAppsSearchResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:paddingEnd="4dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_title_size" />
<TextView
android:id="@+id/delimiter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center"
android:maxLines="1"
android:text="•"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_subtitle_size"
android:visibility="gone" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="4dp"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/search_result_hero_subtitle_size" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingTop="@dimen/dynamic_grid_edge_margin"
android:paddingEnd="@dimen/dynamic_grid_edge_margin"
android:paddingBottom="@dimen/dynamic_grid_edge_margin"
android:orientation="horizontal"
android:singleLine="true">
<ImageView
android:id="@+id/icon1"
style="@style/BaseIcon"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
<ImageView
android:id="@+id/icon2"
style="@style/BaseIcon"
android:layout_marginLeft="@dimen/search_result_padding_medium"
android:layout_width="@dimen/search_row_medium_icon_size"
android:layout_height="@dimen/search_row_medium_icon_size"
android:drawableTint="?android:textColorPrimary"
android:ellipsize="end"
launcher:iconDisplay="search_result_small"
launcher:layoutHorizontal="true"
launcher:matchTextInsetWithQuery="true" />
</LinearLayout>
</app.lawnchair.allapps.SearchResultRightLeftIcon>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<app.lawnchair.allapps.SearchResultText xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/search_result_text"
android:layout_width="match_parent"
android:layout_height="@dimen/search_result_text_height"
android:orientation="horizontal"
android:paddingStart="8dp">
<LinearLayout
android:id="@+id/text_rows"
style="@style/AllAppsSearchResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="horizontal"
android:paddingStart="@dimen/dynamic_grid_edge_margin"
android:paddingTop="@dimen/dynamic_grid_edge_margin"
android:paddingEnd="@dimen/dynamic_grid_edge_margin"
android:paddingBottom="@dimen/dynamic_grid_edge_margin"
android:singleLine="true">
<TextView
android:id="@+id/title"
style="@style/AllAppsSearchResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:paddingEnd="4dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/search_result_hero_subtitle_size" />
</LinearLayout>
<Button
style="@android:style/Widget.DeviceDefault.Button.Borderless"
android:id="@+id/clear_history"
android:layout_width="wrap_content"
android:textAllCaps="false"
android:layout_height="match_parent"
android:text="@string/clear_history"
android:visibility="gone"
android:textStyle="normal"
android:contentDescription="@string/recents_clear_all" />
</app.lawnchair.allapps.SearchResultText>
+7
View File
@@ -110,6 +110,7 @@
<bool name="config_default_show_component_names">false</bool>
<bool name="config_default_smartspace_show_date">true</bool>
<bool name="config_default_smartspace_show_time">false</bool>
<bool name="config_default_perform_wide_search">true</bool>
<item name="config_default_home_icon_size_factor" type="dimen" format="float">1.0</item>
<item name="config_default_folder_preview_background_opacity" type="dimen" format="float">1.0</item>
@@ -118,6 +119,12 @@
<item name="config_default_drawer_icon_label_size_factor" type="dimen" format="float">1.0</item>
<item name="config_default_drawer_cell_height_factor" type="dimen" format="float">1.0</item>
<item name="config_default_search_max_result_count" type="dimen" format="integer">5</item>
<item name="config_default_files_max_result_count" type="dimen" format="integer">3</item>
<item name="config_default_people_max_result_count" type="dimen" format="integer">10</item>
<item name="config_default_suggestion_max_result_count" type="dimen" format="integer">3</item>
<item name="config_default_settings_entry_max_result_count" type="dimen" format="integer">5</item>
<item name="config_default_recent_max_result_count" type="dimen" format="integer">2</item>
<!-- The max scale for the wallpaper when it's zoomed in -->
<item name="config_wallpaperMaxScale" format="float" type="dimen">1.10</item>
+12
View File
@@ -52,10 +52,13 @@
<dimen name="search_result_hero_subtitle_size">14sp</dimen>
<dimen name="search_result_margin">8dp</dimen>
<dimen name="search_result_padding">16dp</dimen>
<dimen name="search_result_padding_medium">12dp</dimen>
<dimen name="search_result_radius">4dp</dimen>
<dimen name="search_result_row_height">88dp</dimen>
<dimen name="search_result_row_medium_height">72dp</dimen>
<dimen name="search_result_small_row_height">56dp</dimen>
<dimen name="search_result_files_row_height">96dp</dimen>
<dimen name="search_result_text_height">52dp</dimen>
<dimen name="search_result_subtitle_padding_start">4dp</dimen>
<!-- smartspace -->
@@ -91,4 +94,13 @@
<dimen name="status_bar_header_height_keyguard">40dp</dimen>
<dimen name="theme_icon_size">24dp</dimen>
<dimen name="search_row_medium_icon_size">24dp</dimen>
<dimen name="search_row_mini_icon_size">22dp</dimen>
<dimen name="space_layout_height">14dp</dimen>
<dimen name="space_layout_mini_height">8dp</dimen>
<dimen name="search_row_files_preview_width">80dp</dimen>
<dimen name="search_row_files_preview_height">64dp</dimen>
<dimen name="search_row_preview_radius">18dp</dimen>
</resources>
+28
View File
@@ -332,11 +332,25 @@
<string name="fuzzy_search_title">Fuzzy Search</string>
<string name="fuzzy_search_desc">Approximate matching for app searches.</string>
<string name="max_search_result_count_title">Max Search Result Count</string>
<string name="max_people_result_count_title">Max People Result Count</string>
<string name="max_file_result_count_title">Max Files Result Count</string>
<string name="max_suggestion_result_count_title">Max Suggestion Result Count</string>
<string name="max_settings_entry_result_count_title">Max Settings Entry Result Count</string>
<string name="max_recent_result_count_title">Max History Result Count</string>
<string name="clear_history">Clear</string>
<!-- <string name="grid" /> -->
<string name="app_drawer_columns">App Drawer Columns</string>
<string name="row_height_label">Row Height</string>
<string name="perform_wide_search_title">Device Search</string>
<string name="perform_wide_search_file">Files</string>
<string name="pref_suggestion_title">Web suggestions (via <xliff:g id="startpage">Startpage</xliff:g>)</string>
<string name="pref_advance_search_category">Advanced Search</string>
<string name="perform_wide_search_description">Search your contacts, files, and settings in app drawer search.</string>
<string name="pref_recent_suggestion_title">History</string>
<!--
<string name="icons" />
<string name="icon_size" />
@@ -481,9 +495,23 @@
<string name="label_lens">Google Lens</string>
<string name="label_voice_search">Voice Search</string>
<string name="search_pref_result_settings_entry_title">Settings Entry</string>
<string name="all_apps_search_market_message">Search for More Apps</string>
<string name="all_apps_search_startpage_message">Search on <xliff:g id="startpage">Startpage</xliff:g></string>
<string name="error_no_market_or_browser_installed">No app store or browser installed.</string>
<string name="all_apps_search_result_suggestions">From the web</string>
<string name="all_apps_search_result_contacts_from_device">From your contacts</string>
<string name="all_apps_search_result_files">From your files</string>
<string name="all_apps_search_result_settings_entry_from_device">From your settings</string>
<string name="warn_contact_permission_content">Requires contact permission</string>
<string name="warn_files_permission_content">Requires storage permission</string>
<string name="all_apps_search_result_files_description">Media, documents, folders, and more.</string>
<string name="all_apps_search_result_contacts_description">Contacts and more</string>
<string name="quickstep_incompatible">Incompatible System Integration</string>
<string name="quickstep_incompatible_description">Your device is configured to have system gestures (known as Quickstep) provided by %1$s, but this version of %1$s is not compatible with your Android version. To continue using your device, please uninstall %1$s updates or disable %1$s as a system-gesture provider.</string>
+2
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="log_files" path="logs"/>
<external-path name="external_files" path="." />
<external-path name="external_storage" path="/"/>
</paths>
@@ -4,6 +4,7 @@ import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.provider.SearchRecentSuggestions
import android.text.Selection
import android.text.SpannableStringBuilder
import android.text.Spanned.SPAN_POINT_MARK
@@ -23,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
import app.lawnchair.launcher
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.search.LawnchairRecentSuggestionProvider
import app.lawnchair.search.LawnchairSearchAlgorithm
import app.lawnchair.theme.drawable.DrawableTokens
import com.android.launcher3.Insettable
@@ -72,6 +74,8 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
private val bgAlphaAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 300 }
private var bgVisible = true
private var bgAlpha = 1f
private val suggestionsRecent = SearchRecentSuggestions(launcher, LawnchairRecentSuggestionProvider.AUTHORITY, LawnchairRecentSuggestionProvider.MODE)
private val pref = PreferenceManager.getInstance(launcher)
override fun onFinishInflate() {
super.onFinishInflate()
@@ -84,7 +88,7 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
input = ViewCompat.requireViewById(this, R.id.input)
with(input) {
if (LawnchairSearchAlgorithm.isDeviceSearchEnabled(context)) {
if (pref.performWideSearchExperimental.get()) {
setHint(R.string.all_apps_device_search_hint)
} else {
setHint(R.string.all_apps_search_bar_hint)
@@ -102,6 +106,15 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
}
}
if (pref.searchResulRecentSuggestion.get()) {
input.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val query = editText.text.toString()
suggestionsRecent.saveRecentQuery(query, null)
}
}
}
input.addTextChangedListener(
beforeTextChanged = { _, _, _, _ ->
hint.isInvisible = true
@@ -113,6 +126,13 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
enableDebugMenu.set(!enableDebugMenu.get())
launcher.stateManager.goToState(LauncherState.NORMAL)
}
// Make sure to empty
// if user used backspace instead of clear action btn
if (input.text.isEmpty() || input.text.isBlank()) {
input.reset()
resetSearch()
clearSearchResult()
}
},
)
@@ -128,6 +148,10 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
updateHint()
}
override fun refreshResults() {
onAppsUpdated()
}
private fun updateHint() {
val inputString = input.text.toString()
val inputLowerCase = inputString.lowercase(Locale.getDefault())
@@ -207,16 +231,6 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) :
}
}
fun onAppendSearchResult(
query: String,
items: ArrayList<AdapterItem>?,
) {
if (items != null) {
apps.setSearchResults(items)
notifyResultChanged()
}
}
override fun clearSearchResult() {
if (apps.setSearchResults(null)) {
notifyResultChanged()
@@ -18,13 +18,13 @@ class SearchItemBackground(
private val resources = context.resources
private val searchDecorationPadding = resources.getDimensionPixelSize(R.dimen.search_decoration_padding)
private val focusHighlight = ColorTokens.FocusHighlight.resolveColor(context)
private val groupHighlight = if (showBackground) ColorTokens.GroupHighlight.resolveColor(context) else 0
private val cornerRadii: FloatArray
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val tmpPath = Path()
private val tmpRect = RectF()
val focusHighlight = ColorTokens.FocusHighlight.resolveColor(context)
val groupHighlight = if (showBackground) ColorTokens.GroupHighlight.resolveColor(context) else 0
val cornerRadii: FloatArray
init {
val searchGroupRadius = resources.getDimensionPixelSize(R.dimen.search_group_radius).toFloat()
@@ -11,7 +11,9 @@ import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import app.lawnchair.launcher
import app.lawnchair.search.SHORTCUT
import app.lawnchair.search.SearchTargetCompat
import app.lawnchair.search.data.SearchResultActionCallBack
import app.lawnchair.util.runOnMainThread
import com.android.launcher3.BubbleTextView
import com.android.launcher3.LauncherAppState
@@ -68,7 +70,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) :
return true
}
override fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>) {
override fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>, callBack: SearchResultActionCallBack?) {
if (boundId == target.id) return
boundId = target.id
flags = getFlags(target.extras)
@@ -101,7 +103,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) :
fun bind(target: SearchTargetCompat, callback: (info: ItemInfoWithIcon) -> Unit) {
this.callback = callback
bind(target, emptyList())
bind(target, emptyList(), null)
if (!hasFlag(flags, SearchResultView.FLAG_HIDE_ICON)) {
isVisible = true
val lp = layoutParams as ViewGroup.MarginLayoutParams
@@ -184,7 +186,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) :
notifyApplied(si)
val cache = LauncherAppState.getInstance(launcher).iconCache
MODEL_EXECUTOR.handler.postAtFrontOfQueue {
cache.getShortcutInfoBadge(shortcutInfo)
cache.getShortcutIcon(si, shortcutInfo)
runOnMainThread { applyFromWorkspaceItem(si) }
}
}
@@ -210,9 +212,9 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) :
info.bitmap = when {
info.hasFlags(FLAG_PRIMARY_ICON_FROM_TITLE) ->
li.createIconBitmap("${info.title?.get(0)}", packageIcon.color)
li.createIconBitmap("${info.title}", packageIcon.color)
icon == null -> packageIcon
else -> li.createBadgedIconBitmap(icon.loadDrawable(context), info.user, false)
else -> li.createBadgedIconBitmap(icon.loadDrawable(context), info.user, target.packageName != SHORTCUT)
}
if (info.hasFlags(FLAG_BADGE_WITH_COMPONENT_NAME) && target.extras.containsKey("class")) {
try {
@@ -235,7 +237,7 @@ class SearchResultIcon(context: Context, attrs: AttributeSet?) :
val las = LauncherAppState.getInstance(context)
val info = PackageItemInfo(packageName, user)
info.user = user
las.iconCache.getTitleAndIconForApp(info, false)
las.iconCache.getTitleAndIcon(info, false)
return info.bitmap
}
@@ -8,7 +8,12 @@ import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import app.lawnchair.allapps.SearchResultView.Companion.FLAG_HIDE_SUBTITLE
import app.lawnchair.font.FontManager
import app.lawnchair.search.RECENT_KEYWORD
import app.lawnchair.search.SETTING
import app.lawnchair.search.SUGGESTION
import app.lawnchair.search.SearchTargetCompat
import app.lawnchair.search.data.SearchResultActionCallBack
import com.android.app.search.LayoutType
import com.android.launcher3.R
import com.android.launcher3.touch.ItemClickHandler
@@ -41,6 +46,8 @@ class SearchResultIconRow(context: Context, attrs: AttributeSet?) :
title = ViewCompat.requireViewById(this, R.id.title)
subtitle = ViewCompat.requireViewById(this, R.id.subtitle)
subtitle.isVisible = false
FontManager.INSTANCE.get(context).setCustomFont(title, R.id.font_heading)
FontManager.INSTANCE.get(context).setCustomFont(subtitle, R.id.font_body)
delimiter = findViewById(R.id.delimiter)
setOnClickListener(icon)
@@ -68,7 +75,7 @@ class SearchResultIconRow(context: Context, attrs: AttributeSet?) :
return true
}
override fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>) {
override fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>, callBack: SearchResultActionCallBack?) {
if (boundId == target.id) return
boundId = target.id
flags = getFlags(target.extras)
@@ -77,6 +84,14 @@ class SearchResultIconRow(context: Context, attrs: AttributeSet?) :
title.text = it.title
tag = it
}
val isSuggestion = (target.layoutType == LayoutType.HORIZONTAL_MEDIUM_TEXT || target.layoutType == LayoutType.WIDGET_LIVE) &&
target.resultType == SearchTargetCompat.RESULT_TYPE_SUGGESTIONS &&
(target.packageName == SUGGESTION || target.packageName == RECENT_KEYWORD)
val isSetting = target.layoutType == LayoutType.ICON_SLICE &&
target.resultType == SearchTargetCompat.RESULT_TYPE_SETTING_TILE &&
target.packageName == SETTING
bindShortcuts(shortcuts)
var showDelimiter = true
if (isSmall) {
@@ -94,6 +109,17 @@ class SearchResultIconRow(context: Context, attrs: AttributeSet?) :
}
}
setSubtitleText(target.searchAction?.subtitle, showDelimiter)
if (shouldHandleClick(target) && !isSmall) {
setOnClickListener {
target.searchAction?.intent?.let { intent -> handleSearchTargetClick(context, intent) }
}
}
if (isSuggestion || isSetting) {
layoutParams.height = resources.getDimensionPixelSize(R.dimen.search_result_small_row_height)
setOnClickListener {
target.searchAction?.intent?.let { intent -> handleSearchTargetClick(context, intent) }
}
}
}
private fun setSubtitleText(subtitleText: CharSequence?, showDelimiter: Boolean) {
@@ -111,7 +137,7 @@ class SearchResultIconRow(context: Context, attrs: AttributeSet?) :
shortcutIcons.forEachIndexed { index, icon ->
if (index < shortcuts.size) {
icon.isVisible = true
icon.bind(shortcuts[index], emptyList())
icon.bind(shortcuts[index], emptyList(), null)
} else {
icon.isVisible = false
}
@@ -0,0 +1,139 @@
package app.lawnchair.allapps
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.ViewCompat
import app.lawnchair.font.FontManager
import app.lawnchair.launcher
import app.lawnchair.search.SearchTargetCompat
import app.lawnchair.search.data.SearchResultActionCallBack
import app.lawnchair.util.AppInfo
import app.lawnchair.util.AppInfoHelper
import app.lawnchair.util.ImageViewWrapper
import com.android.app.search.LayoutType
import com.android.launcher3.R
class SearchResultRightLeftIcon(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs), SearchResultView {
private val launcher = context.launcher
private lateinit var title: TextView
private lateinit var avatar: SearchResultIcon
private lateinit var call: ImageView
private lateinit var message: ImageView
private lateinit var preview: ImageViewWrapper
private val appInfoHelper = AppInfoHelper(context)
private var defPhoneAppInfo: AppInfo? = null
private var defSmsAppInfo: AppInfo? = null
private var isSmall = false
override fun onFinishInflate() {
super.onFinishInflate()
isSmall = id == R.id.search_result_small_icon_row_left_right
defPhoneAppInfo = appInfoHelper.getDefaultPhoneAppInfo()
defSmsAppInfo = appInfoHelper.getDefaultMessageAppInfo()
onFocusChangeListener = launcher.focusHandler
title = ViewCompat.requireViewById(this, R.id.title)
avatar = ViewCompat.requireViewById(this, R.id.avatar)
call = ViewCompat.requireViewById(this, R.id.icon2)
message = ViewCompat.requireViewById(this, R.id.icon1)
preview = ViewCompat.requireViewById(this, R.id.files_preview)
FontManager.INSTANCE.get(context).setCustomFont(title, R.id.font_body)
setUpdateResources()
}
private fun setUpdateResources() {
if (isSmall) {
message.setImageDrawable(defSmsAppInfo?.appIcon)
call.setImageDrawable(defPhoneAppInfo?.appIcon)
call.visibility = VISIBLE
message.visibility = VISIBLE
avatar.visibility = VISIBLE
preview.visibility = GONE
} else {
call.visibility = GONE
message.visibility = GONE
avatar.visibility = GONE
preview.visibility = VISIBLE
}
val heightRes = if (isSmall) resources.getDimensionPixelSize(R.dimen.search_result_small_row_height) else resources.getDimensionPixelSize(R.dimen.search_result_files_row_height)
val layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
heightRes,
)
this.layoutParams = layoutParams
}
override val isQuickLaunch: Boolean get() = false
override val titleText: CharSequence? get() = title.text
override fun launch(): Boolean = false
override fun bind(
target: SearchTargetCompat,
shortcuts: List<SearchTargetCompat>,
callBack: SearchResultActionCallBack?,
) {
title.text = target.searchAction?.title
val isNewFile = target.resultType == SearchTargetCompat.RESULT_TYPE_FILE_TILE &&
target.layoutType == LayoutType.THUMBNAIL
val isFile = !isSmall && isNewFile
if (!isFile) {
avatar.bind(target) {
title.text = it.title
tag = it
}
val number = target.searchAction?.subtitle.toString()
message.setOnClickListener {
defSmsAppInfo?.let { appInfo ->
launchApp(appInfo.packageName, number)
}
}
call.setOnClickListener {
defPhoneAppInfo?.let { appInfo ->
launchApp(appInfo.packageName, number)
}
}
}
if (!isFile) {
isSmall = true
setUpdateResources()
}
if (isFile) {
preview.setImageIcon(target.searchAction?.icon)
title.isSingleLine = false
}
if (shouldHandleClick(target)) {
setOnClickListener {
target.searchAction?.intent?.let { intent -> handleSearchTargetClick(context, intent) }
}
}
}
private fun launchApp(packageName: String, phoneNumber: String? = null) {
val packageManager = context.packageManager
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
launchIntent ?: return
if (packageName == defSmsAppInfo?.packageName) {
val smsIntent = Intent(Intent.ACTION_VIEW)
smsIntent.data = Uri.parse("smsto:$phoneNumber")
smsIntent.putExtra("address", phoneNumber)
handleSearchTargetClick(context, smsIntent)
} else if (packageName == defPhoneAppInfo?.packageName && phoneNumber != null) {
val phoneIntent = Intent(Intent.ACTION_DIAL)
phoneIntent.data = Uri.parse("tel:$phoneNumber")
handleSearchTargetClick(context, phoneIntent)
}
}
}
@@ -0,0 +1,62 @@
package app.lawnchair.allapps
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.util.AttributeSet
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.ViewCompat
import app.lawnchair.font.FontManager
import app.lawnchair.launcher
import app.lawnchair.search.HEADER_JUSTIFY
import app.lawnchair.search.LawnchairRecentSuggestionProvider
import app.lawnchair.search.SPACE
import app.lawnchair.search.SPACE_MINI
import app.lawnchair.search.SearchTargetCompat
import app.lawnchair.search.data.SearchResultActionCallBack
import app.lawnchair.theme.color.ColorTokens
import com.android.launcher3.R
class SearchResultText(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs), SearchResultView {
private val launcher = context.launcher
private lateinit var title: TextView
private lateinit var clearHistory: Button
override fun onFinishInflate() {
super.onFinishInflate()
onFocusChangeListener = launcher.focusHandler
title = ViewCompat.requireViewById(this, R.id.title)
title.setTextColor(ColorTokens.ColorAccent.resolveColor(context))
clearHistory = ViewCompat.requireViewById(this, R.id.clear_history)
clearHistory.setTextColor(ColorTokens.ColorAccent.resolveColor(context))
clearHistory.visibility = GONE
FontManager.INSTANCE.get(context).setCustomFont(title, R.id.font_heading)
FontManager.INSTANCE.get(context).setCustomFont(clearHistory, R.id.font_heading)
}
override val isQuickLaunch: Boolean get() = false
override val titleText: CharSequence? get() = title.text
override fun launch(): Boolean = false
override fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>, callBack: SearchResultActionCallBack?) {
title.text = target.searchAction?.title
val res = when (title.text) {
SPACE -> resources.getDimensionPixelSize(R.dimen.space_layout_height)
SPACE_MINI -> resources.getDimensionPixelSize(R.dimen.space_layout_mini_height)
else -> resources.getDimensionPixelSize(R.dimen.search_result_text_height)
}
clearHistory.visibility = if (target.packageName == HEADER_JUSTIFY) VISIBLE else INVISIBLE
if (target.packageName == HEADER_JUSTIFY) {
clearHistory.setOnClickListener {
val suggestionsRecent = SearchRecentSuggestions(launcher, LawnchairRecentSuggestionProvider.AUTHORITY, LawnchairRecentSuggestionProvider.MODE)
suggestionsRecent.clearHistory()
callBack?.action()
}
}
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, res)
this.layoutParams = layoutParams
}
}
@@ -1,7 +1,17 @@
package app.lawnchair.allapps
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import app.lawnchair.search.CONTACT
import app.lawnchair.search.FILES
import app.lawnchair.search.MARKET_STORE
import app.lawnchair.search.START_PAGE
import app.lawnchair.search.SUGGESTION
import app.lawnchair.search.SearchTargetCompat
import app.lawnchair.search.data.SearchResultActionCallBack
import com.android.app.search.LayoutType
sealed interface SearchResultView {
@@ -10,7 +20,7 @@ sealed interface SearchResultView {
fun launch(): Boolean
fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>)
fun bind(target: SearchTargetCompat, shortcuts: List<SearchTargetCompat>, callBack: SearchResultActionCallBack?)
fun getFlags(extras: Bundle): Int {
var flags = 0
@@ -30,6 +40,21 @@ sealed interface SearchResultView {
return (flags and flag) != 0
}
fun shouldHandleClick(targetCompat: SearchTargetCompat): Boolean {
val packageName = targetCompat.packageName
return (packageName in listOf(START_PAGE, MARKET_STORE, SUGGESTION, CONTACT, FILES)) &&
targetCompat.layoutType != LayoutType.SMALL_ICON_HORIZONTAL_TEXT &&
targetCompat.resultType != SearchTargetCompat.RESULT_TYPE_SHORTCUT
}
fun handleSearchTargetClick(context: Context, searchTargetIntent: Intent) {
searchTargetIntent.resolveActivity(context.packageManager)?.let {
context.startActivity(searchTargetIntent)
} ?: run {
Toast.makeText(context, "No app found to handle this action", Toast.LENGTH_SHORT).show()
}
}
companion object {
const val FLAG_HIDE_SUBTITLE = 1 shl 0
const val FLAG_HIDE_ICON = 1 shl 1
@@ -74,10 +74,16 @@ class PreferenceManager private constructor(private val context: Context) : Base
val deviceSearch = BoolPref("device_search", true, recreate)
val searchResultShortcuts = BoolPref("pref_searchResultShortcuts", true)
val searchResultPeople = BoolPref("pref_searchResultPeople", true)
val searchResultPeople = BoolPref("pref_searchResultPeople", false, recreate)
val searchResultPixelTips = BoolPref("pref_searchResultPixelTips", false)
val searchResultSettings = BoolPref("pref_searchResultSettings", false)
val searchResultFiles = BoolPref("pref_searchResultFiles", false, recreate)
val searchResultStartPageSuggestion = BoolPref("pref_searchResultStartPageSuggestion", true, recreate)
val performWideSearchExperimental = BoolPref("pref_searchWideExperimental", false, recreate)
val searchResultSettingsEntry = BoolPref("pref_searchResultSettingsEntry", false, recreate)
val searchResulRecentSuggestion = BoolPref("pref_searchResultRecentSuggestion", false, recreate)
val themedIcons = BoolPref("themed_icons", true, recreate)
val drawerThemedIcons = BoolPref("drawer_themed_icons", false, recreate)
val hotseatQsbCornerRadius = FloatPref("pref_hotseatQsbCornerRadius", 1F, recreate)
@@ -254,6 +254,11 @@ class PreferenceManager2 private constructor(private val context: Context) : Pre
defaultValue = context.resources.getBoolean(R.bool.config_default_auto_show_keyboard_in_drawer),
)
val performWideSearch = preference(
key = booleanPreferencesKey(name = "performWideSearch"),
defaultValue = context.resources.getBoolean(R.bool.config_default_perform_wide_search),
)
val workspaceTextColor = preference(
key = stringPreferencesKey(name = "workspace_text_color"),
defaultValue = ColorMode.AUTO,
@@ -320,6 +325,31 @@ class PreferenceManager2 private constructor(private val context: Context) : Pre
defaultValue = resourceProvider.getInt(R.dimen.config_default_search_max_result_count),
)
val maxSuggestionResultCount = preference(
key = intPreferencesKey(name = "max_suggestion_result_count"),
defaultValue = resourceProvider.getInt(R.dimen.config_default_suggestion_max_result_count),
)
val maxFileResultCount = preference(
key = intPreferencesKey(name = "max_files_result_count"),
defaultValue = resourceProvider.getInt(R.dimen.config_default_files_max_result_count),
)
val maxPeopleResultCount = preference(
key = intPreferencesKey(name = "max_people_result_count"),
defaultValue = resourceProvider.getInt(R.dimen.config_default_people_max_result_count),
)
val maxSettingsEntryResultCount = preference(
key = intPreferencesKey(name = "max_settings_entry_result_count"),
defaultValue = resourceProvider.getInt(R.dimen.config_default_settings_entry_max_result_count),
)
val maxRecentResultCount = preference(
key = intPreferencesKey(name = "max_recent_result_count"),
defaultValue = resourceProvider.getInt(R.dimen.config_default_recent_max_result_count),
)
val enableSmartspace = preference(
key = booleanPreferencesKey(name = "enable_smartspace"),
defaultValue = context.resources.getBoolean(R.bool.config_default_enable_smartspace),
@@ -0,0 +1,277 @@
package app.lawnchair.search
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.provider.ContactsContract
import android.provider.MediaStore
import android.util.Log
import androidx.core.os.bundleOf
import app.lawnchair.allapps.SearchResultView
import app.lawnchair.search.data.ContactInfo
import app.lawnchair.search.data.FileInfo
import app.lawnchair.search.data.FileInfo.Companion.isImageType
import app.lawnchair.search.data.FolderInfo
import app.lawnchair.search.data.IFileInfo
import app.lawnchair.search.data.RecentKeyword
import app.lawnchair.search.data.SettingInfo
import app.lawnchair.theme.color.ColorTokens
import app.lawnchair.util.createTextBitmap
import app.lawnchair.util.file2Uri
import app.lawnchair.util.mimeCompat
import com.android.app.search.LayoutType
import com.android.launcher3.R
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.PackageManagerHelper
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.Locale
class GenerateSearchTarget(private val context: Context) {
private val marketSearchComponent = resolveMarketSearchActivity()
internal fun getSuggestionTarget(suggestion: String): SearchTargetCompat {
val url = getStartPageUrl(suggestion)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val id = suggestion + url
val action = SearchActionCompat.Builder(id, suggestion)
.setIcon(
Icon.createWithResource(context, R.drawable.ic_allapps_search)
.setTint(ColorTokens.Accent1_600.resolveColor(context)),
)
.setIntent(browserIntent)
.build()
return createSearchTarget(
id,
action,
LayoutType.HORIZONTAL_MEDIUM_TEXT,
SearchTargetCompat.RESULT_TYPE_SUGGESTIONS,
SUGGESTION,
)
}
internal fun getRecentKeywordTarget(recentKeyword: RecentKeyword): SearchTargetCompat {
val value = recentKeyword.getValueByKey("display1") ?: ""
val url = getStartPageUrl(value)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val id = recentKeyword.data.toString() + url
val action = SearchActionCompat.Builder(id, value)
.setIcon(
Icon.createWithResource(context, R.drawable.ic_recent)
.setTint(ColorTokens.TextColorSecondary.resolveColor(context)),
)
.setIntent(browserIntent)
.build()
return createSearchTarget(
id,
action,
LayoutType.WIDGET_LIVE,
SearchTargetCompat.RESULT_TYPE_SUGGESTIONS,
RECENT_KEYWORD,
)
}
internal fun getHeaderTarget(header: String, pkg: String = HEADER): SearchTargetCompat {
val id = "header_$header"
val action = SearchActionCompat.Builder(id, header)
.setIcon(
Icon.createWithResource(context, R.drawable.ic_allapps_search)
.setTint(ColorTokens.TextColorPrimary.resolveColor(context)),
)
.setIntent(Intent())
.build()
return createSearchTarget(
id,
action,
LayoutType.TEXT_HEADER,
SearchTargetCompat.RESULT_TYPE_SECTION_HEADER,
pkg,
)
}
internal fun getSettingSearchItem(info: SettingInfo): SearchTargetCompat? {
val id = "_${info.id}"
val intent = Intent(info.action)
if (info.requiresUri) {
intent.data = Uri.fromParts("package", context.packageName, null)
}
if (intent.resolveActivity(context.packageManager) == null) {
Log.w("SettingSearch", "No activity found to handle intent: $intent")
return null
}
if (!hasRequiredPermissions(intent)) {
Log.w("SettingSearch", "App does not have required permissions for intent: $intent")
return null
}
val actionBuilder = SearchActionCompat.Builder(id, formatSettingTitle(info.name))
.setIcon(
Icon.createWithResource(context, R.drawable.ic_setting)
.setTint(ColorTokens.Accent1_600.resolveColor(context)),
)
.setIntent(intent)
.build()
return createSearchTarget(
id,
actionBuilder,
LayoutType.ICON_SLICE,
SearchTargetCompat.RESULT_TYPE_SETTING_TILE,
SETTING,
)
}
private fun hasRequiredPermissions(intent: Intent): Boolean {
val packageManager = context.packageManager
val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
return resolveInfo?.activityInfo?.exported == true
}
private fun formatSettingTitle(rawTitle: String?): String {
return rawTitle?.replace('_', ' ')
?.replace("ACTION", "")
?.lowercase()
?.split(' ')
?.joinToString(" ") {
it.replaceFirstChar { char -> char.uppercase(Locale.ROOT) }
}.orEmpty()
}
internal fun getMarketSearchItem(query: String): SearchTargetCompat? {
if (marketSearchComponent == null) return null
val id = "marketSearch:$query"
val action = SearchActionCompat.Builder(id, context.getString(R.string.all_apps_search_market_message))
.setIcon(Icon.createWithResource(context, R.drawable.ic_launcher_home))
.setIntent(PackageManagerHelper.getMarketSearchIntent(context, query))
.build()
val extras = bundleOf(
SearchResultView.EXTRA_ICON_COMPONENT_KEY to marketSearchComponent.toString(),
SearchResultView.EXTRA_HIDE_SUBTITLE to true,
)
return createSearchTarget(id, action, MARKET_STORE, extras)
}
internal fun getStartPageSearchItem(query: String): SearchTargetCompat {
val url = getStartPageUrl(query)
val id = "browser:$query"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val action = SearchActionCompat.Builder(id, context.getString(R.string.all_apps_search_startpage_message))
.setIcon(Icon.createWithResource(context, R.drawable.ic_startpage))
.setIntent(browserIntent)
.build()
val extras = bundleOf(
SearchResultView.EXTRA_HIDE_SUBTITLE to true,
)
return createSearchTarget(id, action, START_PAGE, extras)
}
internal fun getContactSearchItem(info: ContactInfo): SearchTargetCompat {
val id = "contact:${info.contactId}${info.number}"
val contactUri = Uri.withAppendedPath(
ContactsContract.Contacts.CONTENT_URI,
info.contactId,
)
val contactIntent = Intent(Intent.ACTION_VIEW, contactUri)
val action = SearchActionCompat.Builder(id, info.name)
.setIcon(displayContactPhoto(context, info.name, Uri.parse(info.uri)))
.setContentDescription(info.contactId)
.setSubtitle(info.number)
.setIntent(contactIntent)
.build()
return createSearchTarget(
id,
action,
LayoutType.PEOPLE_TILE,
SearchTargetCompat.RESULT_TYPE_CONTACT_TILE,
CONTACT,
Bundle(),
)
}
internal fun getFileInfoSearchItem(info: IFileInfo): SearchTargetCompat {
val fileUri = when (info) {
is FileInfo -> Uri.withAppendedPath(
MediaStore.Files.getContentUri("external"),
info.fileId,
)
is FolderInfo -> File(info.path).file2Uri()
}
val mimeType = when (info) {
is FileInfo -> info.mimeType.mimeCompat
is FolderInfo -> "resource/folder"
}
val fileIntent = Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(fileUri, mimeType)
val action = SearchActionCompat.Builder(info.path, info.name)
.setIcon(getPreviewIcon(info))
.setIntent(fileIntent)
.build()
return createSearchTarget(
info.path,
action,
LayoutType.THUMBNAIL,
SearchTargetCompat.RESULT_TYPE_FILE_TILE,
FILES,
Bundle(),
)
}
private fun displayContactPhoto(context: Context, name: String, contactUri: Uri): Icon {
var inputStream: InputStream? = null
try {
inputStream = context.contentResolver.openInputStream(contactUri)
inputStream?.use { input ->
val bitmap = BitmapFactory.decodeStream(input)
return Icon.createWithAdaptiveBitmap(bitmap)
}
} catch (e: IOException) {
// ignore
} finally {
inputStream?.close()
}
// If contact photo not available, create an icon with the first letter of the name
val initial = if (name.isNotEmpty()) name[0].uppercaseChar().toString() else "U"
val textBitmap = createTextBitmap(context, initial)
return Icon.createWithBitmap(textBitmap)
}
private fun getStartPageUrl(query: String): String {
return "https://www.startpage.com/do/search?segment=startpage.lawnchair&query=$query&cat=web"
}
private fun resolveMarketSearchActivity(): ComponentKey? {
val intent = PackageManagerHelper.getMarketSearchIntent(context, "")
val resolveInfo = context.packageManager.resolveActivity(intent, 0) ?: return null
val packageName = resolveInfo.activityInfo.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) ?: return null
return ComponentKey(launchIntent.component, Process.myUserHandle())
}
private fun getPreviewIcon(info: IFileInfo): Icon {
val fileInfo = info as? FileInfo
return if (fileInfo?.isImageType == true) {
Icon.createWithFilePath(fileInfo.path)
} else {
Icon.createWithResource(context, fileInfo?.iconRes ?: R.drawable.ic_folder)
}
}
}
@@ -2,17 +2,18 @@ package app.lawnchair.search
import android.content.Context
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.os.Handler
import android.os.Process
import androidx.core.os.bundleOf
import app.lawnchair.allapps.SearchResultView
import app.lawnchair.launcher
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.search.data.ContactInfo
import app.lawnchair.search.data.IFileInfo
import app.lawnchair.search.data.RecentKeyword
import app.lawnchair.search.data.SearchResult
import app.lawnchair.search.data.SettingInfo
import app.lawnchair.util.isDefaultLauncher
import com.android.launcher3.LauncherAppState
import com.android.launcher3.R
import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem
import com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_EMPTY_SEARCH
import com.android.launcher3.model.AllAppsList
import com.android.launcher3.model.BaseModelUpdateTask
import com.android.launcher3.model.BgDataModel
@@ -21,13 +22,11 @@ import com.android.launcher3.popup.PopupPopulator
import com.android.launcher3.search.SearchCallback
import com.android.launcher3.search.StringMatcherUtility
import com.android.launcher3.shortcuts.ShortcutRequest
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.Executors
import com.android.launcher3.util.PackageManagerHelper
import com.patrykmichalik.opto.core.onEach
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch
import me.xdrop.fuzzywuzzy.algorithms.WeightedRatio
@@ -40,9 +39,10 @@ class LawnchairAppSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm(c
private lateinit var hiddenApps: Set<String>
private var showHiddenAppsInSearch = false
private var enableSmartHide = false
private val marketSearchComponent = resolveMarketSearchActivity()
private val coroutineScope = CoroutineScope(context = Dispatchers.IO)
private val pref2 = PreferenceManager2.getInstance(context)
private val generateSearchTarget = GenerateSearchTarget(context)
private var enableWideSearch = false
private val prefs: PreferenceManager = PreferenceManager.getInstance(context)
init {
pref2.enableFuzzySearch.onEach(launchIn = coroutineScope) {
@@ -60,13 +60,18 @@ class LawnchairAppSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm(c
pref2.enableSmartHide.onEach(launchIn = coroutineScope) {
enableSmartHide = it
}
pref2.performWideSearch.onEach(launchIn = coroutineScope) {
enableWideSearch = it
}
}
override fun doSearch(query: String, callback: SearchCallback<AdapterItem>) {
appState.model.enqueueModelUpdateTask(object : BaseModelUpdateTask() {
override fun execute(app: LauncherAppState, dataModel: BgDataModel, apps: AllAppsList) {
val result = getResult(apps.data, query)
resultHandler.post { callback.onSearchResult(query, result) }
coroutineScope.launch(Dispatchers.Main) {
val results = getResult(apps.data, query)
callback.onSearchResult(query, results)
}
}
})
}
@@ -77,35 +82,82 @@ class LawnchairAppSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm(c
}
}
private fun getResult(
private suspend fun getResult(
apps: MutableList<AppInfo>,
query: String,
): java.util.ArrayList<AdapterItem> {
): ArrayList<AdapterItem> {
val appResults = if (enableFuzzySearch) {
fuzzySearch(apps, query)
} else {
normalSearch(apps, query)
}
val results = mutableListOf<SearchTargetCompat>()
if (appResults.size == 1) {
val app = appResults.first()
val shortcuts = getShortcuts(app)
results.add(createSearchTarget(app, true))
shortcuts.mapTo(results, ::createSearchTarget)
} else {
appResults.mapTo(results, ::createSearchTarget)
val wideSearchResults = if (enableWideSearch) performDeviceWideSearch(query, prefs) else emptyList()
val searchTargets = mutableListOf<SearchTargetCompat>()
if (appResults.isNotEmpty()) {
appResults.mapTo(searchTargets, ::createSearchTarget)
}
if (results.isEmpty()) {
if (marketSearchComponent == null) {
return arrayListOf(AdapterItem(VIEW_TYPE_EMPTY_SEARCH))
if (appResults.size == 1 && context.isDefaultLauncher()) {
val singleAppResult = appResults.first()
val shortcuts = getShortcuts(singleAppResult)
if (shortcuts.isNotEmpty()) {
searchTargets.add(generateSearchTarget.getHeaderTarget(SPACE))
searchTargets.add(createSearchTarget(singleAppResult, true))
searchTargets.addAll(shortcuts.map(::createSearchTarget))
}
results.add(getEmptySearchItem(query))
}
val adapterItems = transformSearchResults(results)
val contacts = filterByType(wideSearchResults, CONTACT)
if (contacts.isNotEmpty()) {
val contactsHeader = generateSearchTarget.getHeaderTarget(context.getString(R.string.all_apps_search_result_contacts_from_device))
searchTargets.add(contactsHeader)
searchTargets.addAll(contacts.map { generateSearchTarget.getContactSearchItem(it.resultData as ContactInfo) })
}
val settings = filterByType(wideSearchResults, SETTING)
if (settings.isNotEmpty()) {
val settingsHeader = generateSearchTarget.getHeaderTarget(context.getString(R.string.all_apps_search_result_settings_entry_from_device))
searchTargets.add(settingsHeader)
searchTargets.addAll(settings.mapNotNull { generateSearchTarget.getSettingSearchItem(it.resultData as SettingInfo) })
}
val recentKeyword = filterByType(wideSearchResults, RECENT_KEYWORD)
if (recentKeyword.isNotEmpty()) {
val recentKeywordHeader = generateSearchTarget.getHeaderTarget(context.getString(R.string.pref_recent_suggestion_title), HEADER_JUSTIFY)
searchTargets.add(recentKeywordHeader)
searchTargets.addAll(recentKeyword.map { generateSearchTarget.getRecentKeywordTarget(it.resultData as RecentKeyword) })
}
val suggestions = filterByType(wideSearchResults, SUGGESTION)
if (suggestions.isNotEmpty()) {
val suggestionsHeader = generateSearchTarget.getHeaderTarget(context.getString(R.string.all_apps_search_result_suggestions))
searchTargets.add(suggestionsHeader)
searchTargets.addAll(suggestions.map { generateSearchTarget.getSuggestionTarget(it.resultData as String) })
}
val files = filterByType(wideSearchResults, FILES)
if (files.isNotEmpty()) {
val filesHeader = generateSearchTarget.getHeaderTarget(context.getString(R.string.all_apps_search_result_files))
searchTargets.add(filesHeader)
searchTargets.addAll(files.map { generateSearchTarget.getFileInfoSearchItem(it.resultData as IFileInfo) })
}
searchTargets.add(generateSearchTarget.getHeaderTarget(SPACE))
searchTargets.add(generateSearchTarget.getStartPageSearchItem(query))
generateSearchTarget.getMarketSearchItem(query)?.let { searchTargets.add(it) }
val adapterItems = transformSearchResults(searchTargets)
LawnchairSearchAdapterProvider.setFirstItemQuickLaunch(adapterItems)
return ArrayList(adapterItems)
}
private fun filterByType(results: List<SearchResult>, type: String): List<SearchResult> {
return results.filter { it.resultType == type }
}
private fun getShortcuts(app: AppInfo): List<ShortcutInfo> {
val shortcuts = ShortcutRequest(context.launcher, app.user)
.withContainer(app.targetComponent)
@@ -142,34 +194,6 @@ class LawnchairAppSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm(c
.map { it.referent }
}
private fun resolveMarketSearchActivity(): ComponentKey? {
val intent = PackageManagerHelper.getMarketSearchIntent(context, "")
val resolveInfo = context.packageManager.resolveActivity(intent, 0) ?: return null
val packageName = resolveInfo.activityInfo.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) ?: return null
return ComponentKey(launchIntent.component, Process.myUserHandle())
}
private fun getEmptySearchItem(query: String): SearchTargetCompat {
val id = "marketSearch:$query"
val action = SearchActionCompat.Builder(
id,
context.getString(R.string.all_apps_search_market_message),
)
.setIcon(Icon.createWithResource(context, R.drawable.ic_launcher_home))
.setIntent(PackageManagerHelper.getMarketSearchIntent(context, query))
.build()
val extras = bundleOf(
if (marketSearchComponent != null) {
SearchResultView.EXTRA_ICON_COMPONENT_KEY to marketSearchComponent.toString()
} else {
SearchResultView.EXTRA_HIDE_ICON to true
},
SearchResultView.EXTRA_HIDE_SUBTITLE to true,
)
return createSearchTarget(id, action, extras)
}
private fun Sequence<AppInfo>.filterHiddenApps(query: String): Sequence<AppInfo> {
return if (showHiddenAppsInSearch) {
this
@@ -0,0 +1,14 @@
package app.lawnchair.search
import android.content.SearchRecentSuggestionsProvider
class LawnchairRecentSuggestionProvider : SearchRecentSuggestionsProvider() {
companion object {
const val AUTHORITY = "app.lawnchair.search.LawnchairRecentSuggestionProvider"
const val MODE = DATABASE_MODE_QUERIES
}
init {
setupSuggestions(AUTHORITY, MODE)
}
}
@@ -9,13 +9,13 @@ import app.lawnchair.LawnchairLauncher
import app.lawnchair.allapps.SearchItemDecorator
import app.lawnchair.allapps.SearchResultView
import app.lawnchair.allapps.SearchResultView.Companion.EXTRA_QUICK_LAUNCH
import app.lawnchair.search.data.SearchResultActionCallBack
import com.android.app.search.LayoutType
import com.android.launcher3.R
import com.android.launcher3.allapps.ActivityAllAppsContainerView
import com.android.launcher3.allapps.AllAppsGridAdapter
import com.android.launcher3.allapps.BaseAllAppsAdapter
import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider
import com.android.launcher3.util.PackageManagerHelper
class LawnchairSearchAdapterProvider(
launcher: LawnchairLauncher,
@@ -28,7 +28,12 @@ class LawnchairSearchAdapterProvider(
append(SEARCH_RESULT_ICON_ROW, R.layout.search_result_tall_icon_row)
append(SEARCH_RESULT_SMALL_ICON_ROW, R.layout.search_result_small_icon_row)
append(SEARCH_RESULT_DIVIDER, R.layout.search_result_divider)
append(SEARCH_RESULT_MARKET, R.layout.search_result_market)
append(SEARCH_TEXT_HEADER, R.layout.search_result_text_header)
append(SEARCH_PEOPLE_TILE, R.layout.search_result_icon_right_left)
append(SEARCH_RESULT_FILE_TILE, R.layout.search_result_icon_right_left)
append(SEARCH_RESULT_SUGGESTION_TILE, R.layout.search_result_small_icon_row)
append(SEARCH_RESULT_SETTINGS_TILE, R.layout.search_result_small_icon_row)
append(SEARCH_RESULT_RECENT_TILE, R.layout.search_result_small_icon_row)
}
private var quickLaunchItem: SearchResultView? = null
set(value) {
@@ -40,8 +45,17 @@ class LawnchairSearchAdapterProvider(
override fun onBindView(holder: BaseAllAppsAdapter.ViewHolder, position: Int) {
val adapterItem = appsView.mSearchRecyclerView.mApps.adapterItems[position] as SearchAdapterItem
adapterItem.setRippleEffect(holder.itemView)
val itemView = holder.itemView as SearchResultView
itemView.bind(adapterItem.searchTarget, emptyList())
itemView.bind(
adapterItem.searchTarget,
emptyList(),
object : SearchResultActionCallBack {
override fun action() {
appsView.searchUiManager.refreshResults()
}
},
)
if (itemView.isQuickLaunch) {
quickLaunchItem = itemView
}
@@ -53,18 +67,6 @@ class LawnchairSearchAdapterProvider(
viewType: Int,
): BaseAllAppsAdapter.ViewHolder {
val view = layoutInflater.inflate(layoutIdMap[viewType], parent, false)
if (viewType == SEARCH_RESULT_MARKET) {
val marketSearchIntent = PackageManagerHelper
.getMarketSearchIntent(
view.context,
appsView.searchUiManager.editText?.text.toString(),
)
val marketView = layoutInflater.inflate(R.layout.search_result_market, parent, false)
marketView.setOnClickListener {
mLauncher.startActivitySafely(it, marketSearchIntent, null)
}
return BaseAllAppsAdapter.ViewHolder(marketView)
}
return BaseAllAppsAdapter.ViewHolder(view)
}
@@ -82,15 +84,24 @@ class LawnchairSearchAdapterProvider(
private const val SEARCH_RESULT_ICON_ROW = 1 shl 9
private const val SEARCH_RESULT_SMALL_ICON_ROW = 1 shl 10
private const val SEARCH_RESULT_DIVIDER = 1 shl 11
private const val SEARCH_RESULT_MARKET = 1 shl 12
private const val SEARCH_TEXT_HEADER = 1 shl 12
private const val SEARCH_PEOPLE_TILE = 1 shl 13
private const val SEARCH_RESULT_FILE_TILE = 1 shl 14
private const val SEARCH_RESULT_SUGGESTION_TILE = 1 shl 15
private const val SEARCH_RESULT_SETTINGS_TILE = 1 shl 16
private const val SEARCH_RESULT_RECENT_TILE = 1 shl 17
val viewTypeMap = mapOf(
LayoutType.ICON_SINGLE_VERTICAL_TEXT to SEARCH_RESULT_ICON,
LayoutType.ICON_HORIZONTAL_TEXT to SEARCH_RESULT_ICON_ROW,
LayoutType.SMALL_ICON_HORIZONTAL_TEXT to SEARCH_RESULT_SMALL_ICON_ROW,
LayoutType.HORIZONTAL_MEDIUM_TEXT to SEARCH_RESULT_SMALL_ICON_ROW,
LayoutType.HORIZONTAL_MEDIUM_TEXT to SEARCH_RESULT_SUGGESTION_TILE,
LayoutType.EMPTY_DIVIDER to SEARCH_RESULT_DIVIDER,
LayoutType.ICON_HORIZONTAL_TEXT to SEARCH_RESULT_MARKET,
LayoutType.TEXT_HEADER to SEARCH_TEXT_HEADER,
LayoutType.PEOPLE_TILE to SEARCH_PEOPLE_TILE,
LayoutType.THUMBNAIL to SEARCH_RESULT_FILE_TILE,
LayoutType.ICON_SLICE to SEARCH_RESULT_SETTINGS_TILE,
LayoutType.WIDGET_LIVE to SEARCH_RESULT_RECENT_TILE,
)
fun setFirstItemQuickLaunch(items: List<SearchAdapterItem>) {
@@ -4,14 +4,39 @@ import android.content.Context
import app.lawnchair.LawnchairApp
import app.lawnchair.allapps.SearchItemBackground
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.search.SearchTargetCompat.Companion.RESULT_TYPE_APPLICATION
import app.lawnchair.search.SearchTargetCompat.Companion.RESULT_TYPE_SHORTCUT
import app.lawnchair.search.data.SearchCallback
import app.lawnchair.search.data.SearchResult
import app.lawnchair.search.data.findContactsByName
import app.lawnchair.search.data.findSettingsByNameAndAction
import app.lawnchair.search.data.getRecentKeyword
import app.lawnchair.search.data.getStartPageSuggestions
import app.lawnchair.search.data.queryFilesInMediaStore
import app.lawnchair.util.checkAndRequestFilesPermission
import app.lawnchair.util.requestContactPermissionGranted
import com.android.app.search.LayoutType.EMPTY_DIVIDER
import com.android.app.search.LayoutType.HORIZONTAL_MEDIUM_TEXT
import com.android.app.search.LayoutType.ICON_HORIZONTAL_TEXT
import com.android.app.search.LayoutType.ICON_SINGLE_VERTICAL_TEXT
import com.android.app.search.LayoutType.ICON_SLICE
import com.android.app.search.LayoutType.PEOPLE_TILE
import com.android.app.search.LayoutType.SMALL_ICON_HORIZONTAL_TEXT
import com.android.app.search.LayoutType.TEXT_HEADER
import com.android.app.search.LayoutType.THUMBNAIL
import com.android.app.search.LayoutType.WIDGET_LIVE
import com.android.launcher3.BuildConfig
import com.android.launcher3.Utilities
import com.android.launcher3.allapps.BaseAllAppsAdapter
import com.android.launcher3.search.SearchAlgorithm
import com.patrykmichalik.opto.core.onEach
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
sealed class LawnchairSearchAlgorithm(
protected val context: Context,
@@ -55,20 +80,64 @@ sealed class LawnchairSearchAlgorithm(
.filter { LawnchairSearchAdapterProvider.viewTypeMap[it.layoutType] != null }
.removeDuplicateDividers()
.toList()
return filtered
.mapIndexedNotNull { index, target ->
val isFirst = index == 0 || filtered[index - 1].isDivider
val isLast = index == filtered.lastIndex || filtered[index + 1].isDivider
val background = when {
target.layoutType == ICON_SINGLE_VERTICAL_TEXT -> iconBackground
target.layoutType == EMPTY_DIVIDER -> iconBackground
isFirst && isLast -> normalBackground
isFirst -> topBackground
isLast -> bottomBackground
else -> centerBackground
}
SearchAdapterItem.createAdapterItem(target, background)
}
val smallIconIndices = findIndices(filtered, SMALL_ICON_HORIZONTAL_TEXT)
val iconRowIndices = findIndices(filtered, ICON_HORIZONTAL_TEXT)
val peopleTileIndices = findIndices(filtered, PEOPLE_TILE)
val suggestionIndices = findIndices(filtered, HORIZONTAL_MEDIUM_TEXT)
val fileIndices = findIndices(filtered, THUMBNAIL)
val settingIndices = findIndices(filtered, ICON_SLICE)
val recentIndices = findIndices(filtered, WIDGET_LIVE)
return filtered.mapIndexedNotNull { index, target ->
val isFirst = index == 0 || filtered[index - 1].isDivider
val isLast = index == filtered.lastIndex || filtered[index + 1].isDivider
val background = getBackground(
target.layoutType, index, isFirst, isLast,
smallIconIndices, iconRowIndices, peopleTileIndices, suggestionIndices, fileIndices, settingIndices, recentIndices,
)
SearchAdapterItem.createAdapterItem(target, background)
}
}
private fun findIndices(filtered: List<SearchTargetCompat>, layoutType: String): List<Int> {
return filtered.indices.filter {
filtered[it].layoutType == layoutType
}
}
private fun getBackground(
layoutType: String,
index: Int,
isFirst: Boolean,
isLast: Boolean,
smallIconIndices: List<Int>,
iconRowIndices: List<Int>,
peopleTileIndices: List<Int>,
suggestionIndices: List<Int>,
fileIndices: List<Int>,
settingIndices: List<Int>,
recentIndices: List<Int>,
): SearchItemBackground = when {
layoutType == TEXT_HEADER || layoutType == ICON_SINGLE_VERTICAL_TEXT || layoutType == EMPTY_DIVIDER -> iconBackground
layoutType == SMALL_ICON_HORIZONTAL_TEXT -> getGroupedBackground(index, smallIconIndices)
layoutType == ICON_HORIZONTAL_TEXT -> getGroupedBackground(index, iconRowIndices)
layoutType == PEOPLE_TILE -> getGroupedBackground(index, peopleTileIndices)
layoutType == HORIZONTAL_MEDIUM_TEXT -> getGroupedBackground(index, suggestionIndices)
layoutType == THUMBNAIL -> getGroupedBackground(index, fileIndices)
layoutType == ICON_SLICE -> getGroupedBackground(index, settingIndices)
layoutType == WIDGET_LIVE -> getGroupedBackground(index, recentIndices)
isFirst && isLast -> normalBackground
isFirst -> topBackground
isLast -> bottomBackground
else -> centerBackground
}
private fun getGroupedBackground(index: Int, indices: List<Int>): SearchItemBackground = when {
indices.size == 1 -> normalBackground
index == indices.first() -> topBackground
index == indices.last() -> bottomBackground
else -> centerBackground
}
companion object {
@@ -92,6 +161,91 @@ sealed class LawnchairSearchAlgorithm(
else -> LawnchairAppSearchAlgorithm(context)
}
}
private var maxPeopleCount = 10
private var maxSuggestionCount = 3
private var maxFilesCount = 3
private var maxSettingsEntryCount = 5
private var maxRecentResultCount = 2
val coroutineScope = CoroutineScope(context = Dispatchers.IO)
val pref2 = PreferenceManager2.getInstance(context)
init {
pref2.maxFileResultCount.onEach(launchIn = coroutineScope) {
maxFilesCount = it
}
pref2.maxPeopleResultCount.onEach(launchIn = coroutineScope) {
maxPeopleCount = it
}
pref2.maxSuggestionResultCount.onEach(launchIn = coroutineScope) {
maxSuggestionCount = it
}
pref2.maxSettingsEntryResultCount.onEach(launchIn = coroutineScope) {
maxSettingsEntryCount = it
}
pref2.maxRecentResultCount.onEach(launchIn = coroutineScope) {
maxRecentResultCount = it
}
}
protected suspend fun performDeviceWideSearch(query: String, prefs: PreferenceManager): MutableList<SearchResult> = withContext(Dispatchers.IO) {
val results = ArrayList<SearchResult>()
if (prefs.searchResultPeople.get()) {
if (requestContactPermissionGranted(context, prefs)) {
val contactResults = findContactsByName(context, query, maxPeopleCount)
results.addAll(contactResults.map { SearchResult(CONTACT, it) })
}
}
if (prefs.searchResultFiles.get()) {
if (checkAndRequestFilesPermission(context, prefs)) {
val fileResults = queryFilesInMediaStore(context, keyword = query, maxResult = maxFilesCount).toList()
results.addAll(fileResults.map { SearchResult(FILES, it) })
}
}
if (prefs.searchResultSettingsEntry.get()) {
val settingResult = findSettingsByNameAndAction(query, maxSettingsEntryCount)
results.addAll(settingResult.map { SearchResult(SETTING, it) })
}
if (prefs.searchResultStartPageSuggestion.get()) {
val startPageSuggestions = async {
try {
withTimeout(3000) {
getStartPageSuggestions(query, maxSuggestionCount)
}
} catch (e: TimeoutCancellationException) {
emptyList()
}
}
results.addAll(startPageSuggestions.await().map { SearchResult(SUGGESTION, it) })
}
if (prefs.searchResulRecentSuggestion.get()) {
getRecentKeyword(
context,
query,
maxRecentResultCount,
object : SearchCallback {
override fun onSearchLoaded(items: List<Any>) {
results.addAll(items.map { SearchResult(RECENT_KEYWORD, it) })
}
override fun onSearchFailed(error: String) {
results.add(SearchResult(ERROR, error))
}
override fun onLoading() {
results.add(SearchResult(LOADING, "Loading"))
}
},
)
}
results
}
}
private fun Sequence<SearchTargetCompat>.removeDuplicateDividers(): Sequence<SearchTargetCompat> {
@@ -1,5 +1,11 @@
package app.lawnchair.search
import android.content.res.ColorStateList
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.view.View
import app.lawnchair.allapps.SearchItemBackground
import com.android.launcher3.allapps.BaseAllAppsAdapter
@@ -9,6 +15,30 @@ data class SearchAdapterItem(
val viewType: Int,
) : BaseAllAppsAdapter.AdapterItem(viewType) {
fun setRippleEffect(child: View) {
val shape = RoundRectShape(background?.cornerRadii, null, null)
val shapeDrawable = ShapeDrawable(shape)
val colorDefault = background?.groupHighlight ?: 0
val colorPressed = background?.focusHighlight ?: 0
val colorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_pressed),
intArrayOf(-android.R.attr.state_pressed),
),
intArrayOf(
colorPressed,
colorDefault,
),
)
shapeDrawable.paint.color = colorDefault
val inset = 3
val insetDrawable = InsetDrawable(shapeDrawable, 0, inset, 0, inset)
val rippleDrawable = RippleDrawable(colorStateList, insetDrawable, null)
child.background = rippleDrawable
}
companion object {
fun createAdapterItem(
@@ -262,7 +262,20 @@ data class SearchTargetCompat(
/**
* @hide
*/
@IntDef(value = [RESULT_TYPE_APPLICATION, RESULT_TYPE_SHORTCUT, RESULT_TYPE_SLICE, RESULT_TYPE_WIDGETS])
@IntDef(
value = [
RESULT_TYPE_APPLICATION,
RESULT_TYPE_SHORTCUT,
RESULT_TYPE_SLICE,
RESULT_TYPE_WIDGETS,
RESULT_TYPE_SUGGESTIONS,
RESULT_TYPE_REDIRECTION,
RESULT_TYPE_SECTION_HEADER,
RESULT_TYPE_CONTACT_TILE,
RESULT_TYPE_FILE_TILE,
RESULT_TYPE_SETTING_TILE,
],
)
@Retention(AnnotationRetention.SOURCE)
annotation class SearchResultType
@@ -271,6 +284,12 @@ data class SearchTargetCompat(
const val RESULT_TYPE_SHORTCUT = 1 shl 1
const val RESULT_TYPE_SLICE = 1 shl 2
const val RESULT_TYPE_WIDGETS = 1 shl 3
const val RESULT_TYPE_SUGGESTIONS = 1 shl 4
const val RESULT_TYPE_REDIRECTION = 1 shl 5
const val RESULT_TYPE_SECTION_HEADER = 1 shl 6
const val RESULT_TYPE_CONTACT_TILE = 1 shl 7
const val RESULT_TYPE_FILE_TILE = 1 shl 8
const val RESULT_TYPE_SETTING_TILE = 1 shl 9
fun wrap(target: SearchTarget): SearchTargetCompat = SearchTargetCompat(target)
}
@@ -7,14 +7,22 @@ import androidx.core.os.bundleOf
import com.android.app.search.LayoutType
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.util.ComponentKey
import java.security.MessageDigest
import okio.ByteString
// We're generate hash key as alt in id, so we can avoid
// the wrong action when click,
// TODO remove when we properly manage creating search target
private fun generateHashKey(input: String): String =
ByteString.of(*MessageDigest.getInstance("SHA-256").digest(input.toByteArray())).hex()
fun createSearchTarget(appInfo: AppInfo, asRow: Boolean = false): SearchTargetCompat {
val componentName = appInfo.componentName
val user = appInfo.user
return SearchTargetCompat.Builder(
SearchTargetCompat.RESULT_TYPE_APPLICATION,
if (asRow) LayoutType.ICON_HORIZONTAL_TEXT else LayoutType.ICON_SINGLE_VERTICAL_TEXT,
ComponentKey(componentName, user).toString(),
if (asRow) LayoutType.SMALL_ICON_HORIZONTAL_TEXT else LayoutType.ICON_SINGLE_VERTICAL_TEXT,
generateHashKey(ComponentKey(componentName, user).toString()),
)
.setPackageName(componentName.packageName)
.setUserHandle(user)
@@ -26,7 +34,7 @@ fun createSearchTarget(shortcutInfo: ShortcutInfo): SearchTargetCompat {
return SearchTargetCompat.Builder(
SearchTargetCompat.RESULT_TYPE_SHORTCUT,
LayoutType.SMALL_ICON_HORIZONTAL_TEXT,
"${shortcutInfo.`package`}|${shortcutInfo.userHandle}|${shortcutInfo.id}",
"shortcut_" + generateHashKey("${shortcutInfo.`package`}|${shortcutInfo.userHandle}|${shortcutInfo.id}"),
)
.setShortcutInfo(shortcutInfo)
.setUserHandle(shortcutInfo.userHandle)
@@ -34,13 +42,38 @@ fun createSearchTarget(shortcutInfo: ShortcutInfo): SearchTargetCompat {
.build()
}
fun createSearchTarget(id: String, action: SearchActionCompat, extras: Bundle = Bundle()): SearchTargetCompat {
fun createSearchTarget(
id: String,
action: SearchActionCompat,
pkg: String,
extras: Bundle = Bundle(),
): SearchTargetCompat {
return SearchTargetCompat.Builder(
SearchTargetCompat.RESULT_TYPE_SHORTCUT,
SearchTargetCompat.RESULT_TYPE_REDIRECTION,
LayoutType.ICON_HORIZONTAL_TEXT,
id,
generateHashKey(id),
)
.setPackageName("")
.setPackageName(pkg)
.setUserHandle(Process.myUserHandle())
.setSearchAction(action)
.setExtras(extras)
.build()
}
fun createSearchTarget(
id: String,
action: SearchActionCompat,
layoutType: String,
targetCompat: Int,
pkg: String,
extras: Bundle = Bundle(),
): SearchTargetCompat {
return SearchTargetCompat.Builder(
targetCompat,
layoutType,
generateHashKey(id),
)
.setPackageName(pkg)
.setUserHandle(Process.myUserHandle())
.setSearchAction(action)
.setExtras(extras)
@@ -57,3 +90,18 @@ fun createDividerTarget(): SearchTargetCompat {
.setUserHandle(Process.myUserHandle())
.build()
}
const val START_PAGE = "startpage"
const val MARKET_STORE = "marketstore"
const val SUGGESTION = "suggestion"
const val HEADER = "header"
const val CONTACT = "contact"
const val FILES = "files"
const val SPACE = "space"
const val SPACE_MINI = "space_mini"
const val LOADING = "loading"
const val ERROR = "error"
const val SETTING = "setting"
const val SHORTCUT = "shortcut"
const val RECENT_KEYWORD = "recent_keyword"
const val HEADER_JUSTIFY = "header_justify"
@@ -0,0 +1,7 @@
package app.lawnchair.search.data
interface SearchCallback {
fun onSearchLoaded(items: List<Any>)
fun onSearchFailed(error: String)
fun onLoading()
}
@@ -0,0 +1,114 @@
package app.lawnchair.search.data
import android.annotation.DrawableRes
import app.lawnchair.util.androidPkgTypes
import app.lawnchair.util.archiveFileTypes
import app.lawnchair.util.audioFileTypes
import app.lawnchair.util.documentFileTypes
import app.lawnchair.util.imageFileTypes
import app.lawnchair.util.videoFileTypes
import com.android.launcher3.R
data class ContactInfo(
val contactId: String,
val name: String,
var number: String,
val phoneBookLabel: String,
val uri: String,
var packages: String,
)
const val CONTACT_ACCOUNT_ID = "contact.id"
const val CONTACT_ACCOUNT_MIME = "contact.mime"
const val CONTACT_ACCOUNT_NAME = "contact.account.name"
const val CONTACT_ACCOUNT_TITLE = "contact.title"
const val CONTACT_ACCOUNT_TYPE = "contact.account.type"
val EXCLUDED_MIME_TYPES = arrayOf(
"vnd.android.cursor.item/name",
"vnd.android.cursor.item/nickname",
"vnd.android.cursor.item/note",
"vnd.android.cursor.item/photo",
"vnd.com.google.cursor.item/contact_misc",
"vnd.android.cursor.item/identity",
"vnd.android.cursor.item/website",
)
data class SettingInfo(
val id: String,
val name: String,
val action: String,
val requiresUri: Boolean = false,
)
data class RecentKeyword(
val data: Map<String, String>,
) {
fun getValueByKey(key: String): String? {
return data[key]
}
}
sealed interface IFileInfo {
val path: String
val name: String
val size: Long
val dateModified: Long
}
data class FolderInfo(
override val path: String,
override val name: String,
override val size: Long,
override val dateModified: Long,
) : IFileInfo
data class FileInfo(
val fileId: String,
override val path: String,
override val name: String,
override val size: Long,
override val dateModified: Long,
val mimeType: String?,
) : IFileInfo {
@get:DrawableRes
val iconRes = when (val mime = mimeType.orEmpty()) {
in imageFileTypes.values -> R.drawable.ic_file_image
in videoFileTypes.values -> R.drawable.ic_file_video
in audioFileTypes.values -> R.drawable.ic_file_music
in androidPkgTypes.values -> R.drawable.ic_file_app
in archiveFileTypes.values -> R.drawable.ic_file_zip
in documentFileTypes.values -> when {
mime.contains("excel") || mime.contains("csv") -> R.drawable.ic_file_excel
mime.contains("word") -> R.drawable.ic_file_word
mime.contains("powerpoint") -> R.drawable.ic_file_powerpoint
mime.contains("pdf") -> R.drawable.ic_file_pdf
mime.contains("srt") -> R.drawable.ic_file_subtitle
else -> R.drawable.ic_file_text
}
else -> R.drawable.ic_file_unknown
}
companion object {
val FileInfo.isMediaType: Boolean get() {
return mimeType in videoFileTypes.values ||
mimeType in audioFileTypes.values
}
val FileInfo.isImageType: Boolean get() = mimeType in imageFileTypes.values
val FileInfo.isUnknownType: Boolean get() {
return mimeType !in imageFileTypes.values &&
mimeType !in videoFileTypes.values &&
mimeType !in audioFileTypes.values &&
mimeType !in androidPkgTypes.values &&
mimeType !in archiveFileTypes.values &&
mimeType !in documentFileTypes.values
}
}
}
data class SearchResult(
val resultType: String,
val resultData: Any,
)
@@ -0,0 +1,332 @@
package app.lawnchair.search.data
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.ContactsContract
import android.provider.MediaStore
import android.provider.Settings
import android.util.Log
import app.lawnchair.search.LawnchairRecentSuggestionProvider
import app.lawnchair.search.data.suggestion.StartPageService
import app.lawnchair.util.exists
import app.lawnchair.util.isDirectory
import app.lawnchair.util.isHidden
import app.lawnchair.util.isRegularFile
import app.lawnchair.util.kotlinxJson
import app.lawnchair.util.mimeType2Extension
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import java.lang.reflect.Modifier
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody
import okio.Path.Companion.toPath
import org.json.JSONArray
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.create
private val retrofit = Retrofit.Builder()
.baseUrl("https://www.startpage.com/")
.addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType()))
.build()
val startPageService: StartPageService = retrofit.create()
suspend fun getStartPageSuggestions(query: String, max: Int): List<String> = withContext(Dispatchers.IO) {
if (query.isEmpty() || query.isBlank() || max <= 0) {
return@withContext emptyList()
}
try {
val response: Response<ResponseBody> = startPageService.getStartPageSuggestions(
query = query,
segment = "startpage.lawnchair",
partner = "lawnchair",
format = "opensearch",
)
if (response.isSuccessful) {
val responseBody = response.body()?.string()
return@withContext JSONArray(responseBody).optJSONArray(1)?.let { array ->
(0 until array.length()).take(max).map { array.getString(it) }
} ?: emptyList()
} else {
Log.d("Failed to retrieve suggestions", ": ${response.code()}")
return@withContext emptyList()
}
} catch (e: Exception) {
Log.e("Exception", "Error during suggestion retrieval: ${e.message}")
return@withContext emptyList()
}
}
suspend fun getRecentKeyword(context: Context, query: String, max: Int, callback: SearchCallback) {
try {
if (query.isEmpty() || query.isBlank() || max <= 0) {
callback.onSearchLoaded(emptyList())
return
}
callback.onLoading()
withContext(Dispatchers.IO) {
val contentResolver: ContentResolver = context.contentResolver
val uri: Uri = Uri.parse("content://${LawnchairRecentSuggestionProvider.AUTHORITY}/suggestions")
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
val recentKeywords = mutableListOf<RecentKeyword>()
cursor?.use {
val columnCount = it.columnCount
while (it.moveToNext()) {
val recentKeywordData = mutableMapOf<String, String>()
for (i in 0 until columnCount) {
val columnName = it.getColumnName(i)
val columnValue = it.getString(i) ?: ""
recentKeywordData[columnName] = columnValue
}
recentKeywords.add(RecentKeyword(recentKeywordData))
}
}
callback.onSearchLoaded(recentKeywords.asReversed().take(max))
}
} catch (e: Exception) {
Log.e("Exception", "Error during recent keyword retrieval: ${e.message}")
callback.onSearchFailed("Error during recent keyword retrieval: ${e.message}")
}
}
suspend fun findSettingsByNameAndAction(query: String, max: Int): List<SettingInfo> = try {
if (query.isBlank() || max <= 0) {
emptyList()
} else {
withContext(
Dispatchers.IO + CoroutineExceptionHandler { _, e ->
Log.e("SettingSearch", "Something went wrong ", e)
},
) {
Settings::class.java.fields
.asSequence()
.filter { it.type == String::class.java && Modifier.isStatic(it.modifiers) && it.name.startsWith("ACTION_") }
.map { it.name to it.get(null) as String }
.filter { (name, action) ->
name.contains(query, ignoreCase = true) &&
!action.contains("REQUEST", ignoreCase = true) &&
!name.contains("REQUEST", ignoreCase = true) &&
!action.contains("PERMISSION", ignoreCase = true) &&
!name.contains("DETAIL", ignoreCase = true) &&
!name.contains("REMOTE", ignoreCase = true)
}
.map { (name, action) ->
val id = name + action
val requiresUri = action.contains("URI")
SettingInfo(id, name, action, requiresUri)
}
.toList().take(max)
}
}
} catch (e: Exception) {
Log.e("SettingSearch", "Something went wrong ", e)
emptyList()
}
suspend fun findContactsByName(context: Context, query: String, max: Int): List<ContactInfo> {
try {
if (query.isEmpty() || query.isBlank() || max <= 0) return emptyList()
val exceptionHandler = CoroutineExceptionHandler { _, e ->
Log.e("ContactSearch", "Something went wrong ", e)
}
return withContext(Dispatchers.IO + exceptionHandler) {
val contactMap = HashMap<String, ContactInfo>()
val projection = arrayOf(
ContactsContract.Data._ID,
ContactsContract.Data.CONTACT_ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.DATA1,
ContactsContract.Data.DATA3,
ContactsContract.Data.DATA5,
"phonebook_label",
"account_type",
"account_name",
ContactsContract.Data.MIMETYPE,
ContactsContract.Data.PHOTO_URI,
)
val selection = ContactsContract.Data.DISPLAY_NAME + " LIKE ?"
val selectionArgs = arrayOf("%$query%")
context.contentResolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
selection,
selectionArgs,
null,
)?.use {
while (it.moveToNext() && contactMap.size < max) {
val contactIdIndex = it.getColumnIndex(ContactsContract.Data.CONTACT_ID)
val displayNameIndex = it.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)
val data1Index = it.getColumnIndex(ContactsContract.Data.DATA1)
val data3Index = it.getColumnIndex(ContactsContract.Data.DATA3)
val data5Index = it.getColumnIndex(ContactsContract.Data.DATA5)
val phonebookLabelIndex = it.getColumnIndex("phonebook_label")
val accountTypeIndex = it.getColumnIndex("account_type")
val accountNameIndex = it.getColumnIndex("account_name")
val mimeTypeIndex = it.getColumnIndex(ContactsContract.Data.MIMETYPE)
val photoUriIndex = it.getColumnIndex(ContactsContract.Data.PHOTO_URI)
val contactId = it.getString(contactIdIndex)
val displayName = it.getString(displayNameIndex)
val data1 = it.getString(data1Index)
val data3 = it.getString(data3Index)
val data5 = it.getString(data5Index)
val phonebookLabel = it.getString(phonebookLabelIndex)
val accountType = it.getString(accountTypeIndex)
val accountName = it.getString(accountNameIndex)
val mimeType = it.getString(mimeTypeIndex)
val photoUri = it.getString(photoUriIndex)
val phoneNumber = data3 ?: data5 ?: data1
val key = contactId ?: phoneNumber
val imageUri = photoUri ?: ""
val phonebookLabel2 = phonebookLabel ?: ""
val pkg = contactId + displayName + phoneNumber
if (key != null && !EXCLUDED_MIME_TYPES.contains(mimeType)) {
contactMap[key] = ContactInfo(
contactId,
displayName,
phoneNumber,
phonebookLabel2,
imageUri,
pkg,
)
} else {
if (contactMap.containsKey(contactId)) {
val existingContact = contactMap[contactId]
val jsonArray = buildJsonArray {
add(
buildJsonObject {
put(CONTACT_ACCOUNT_ID, key)
put(CONTACT_ACCOUNT_TITLE, data5)
put(CONTACT_ACCOUNT_NAME, accountName)
put(CONTACT_ACCOUNT_TYPE, accountType)
put(CONTACT_ACCOUNT_MIME, mimeType)
},
)
}
existingContact?.packages = jsonArray.toString()
}
}
}
}
contactMap.values.toList()
}
} catch (e: Exception) {
Log.e("ContactSearch", "Something went wrong ", e)
return emptyList()
}
}
suspend fun queryFilesInMediaStore(
context: Context,
uri: Uri = MediaStore.Files.getContentUri("external"),
path: String = "",
keyword: String,
maxResult: Int,
mimes: Array<String>? = null,
): Sequence<IFileInfo> = withContext(Dispatchers.IO) {
val selection = "${commonProjection[0]} like ? AND ${commonProjection[0]} like ? ".let {
if (mimes == null) it else it + "AND ${commonProjection[4]} IN (${mimes.selectionArgsPlaceHolder})"
}
val selectionArgs = arrayOf("%$path%", "%$keyword%").let {
if (mimes == null) it else it + mimes
}
getFileListFromMediaStore(
context,
uri,
commonProjection,
selection,
selectionArgs,
maxResult = maxResult,
) { cursor ->
val filePath = cursor.getString(cursor.getColumnIndexOrThrow(commonProjection[0])).toPath()
if (filePath.isDirectory()) getFolderBean(cursor) else getFileBean(cursor)
}
}
private val Array<String>.selectionArgsPlaceHolder: String get() = Array(size) { "?" }.joinToString()
private val commonProjection = arrayOf(
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns._ID,
)
private suspend inline fun <T : Any> getFileListFromMediaStore(
context: Context,
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String = "${commonProjection[3]} DESC",
maxResult: Int,
crossinline body: (Cursor) -> T?,
): Sequence<T> = withContext(Dispatchers.IO) {
context.contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)
?.use { cursor ->
var count = 0
buildList {
while (cursor.moveToNext()) {
val bean = body(cursor) ?: continue
add(bean)
count++
if (count >= maxResult) break
}
}.asSequence()
} ?: emptySequence()
}
private fun getFileBean(cursor: Cursor): IFileInfo? = cursor.run {
val mimeType = getString(getColumnIndexOrThrow(commonProjection[4]))
val title = getString(getColumnIndexOrThrow(commonProjection[1]))
?: getString(getColumnIndexOrThrow(commonProjection[5]))?.let {
if (mimeType == null) it else "$it.${mimeType.mimeType2Extension()}"
} ?: return null
val path = getString(getColumnIndexOrThrow(commonProjection[0])).toPath()
val fileId = getString(getColumnIndexOrThrow(commonProjection[6]))
if (!path.isRegularFile() || path.isHidden) return null
val dateModified = getLong(getColumnIndexOrThrow(commonProjection[3])) * 1000
return FileInfo(
fileId,
path.toString(),
title,
getLong(getColumnIndexOrThrow(commonProjection[2])),
dateModified,
mimeType,
)
}
private fun getFolderBean(cursor: Cursor): FolderInfo? = cursor.run {
val title = getString(getColumnIndexOrThrow(commonProjection[1]))
?: getString(getColumnIndexOrThrow(commonProjection[5])) ?: return null
val path = getString(getColumnIndexOrThrow(commonProjection[0])).toPath()
if (!path.exists || path.isHidden) return null
val dateModified = getLong(getColumnIndexOrThrow(commonProjection[3])) * 1000
return FolderInfo(
path.toString(),
title,
getLong(getColumnIndexOrThrow(commonProjection[2])),
dateModified,
)
}
@@ -0,0 +1,5 @@
package app.lawnchair.search.data
interface SearchResultActionCallBack {
fun action()
}
@@ -0,0 +1,16 @@
package app.lawnchair.search.data.suggestion
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface StartPageService {
@GET("suggestions")
suspend fun getStartPageSuggestions(
@Query("q") query: String,
@Query("segment") segment: String,
@Query("partner") partner: String,
@Query("format") format: String,
): Response<ResponseBody>
}
@@ -0,0 +1,22 @@
package app.lawnchair.theme.color
import android.content.Context
fun generateColor(context: Context): Int {
val accentColors = listOf(
ColorTokens.Accent1_50,
ColorTokens.Accent1_100,
ColorTokens.Accent1_200,
ColorTokens.Accent1_300,
ColorTokens.Accent1_500,
ColorTokens.Accent1_600,
ColorTokens.Accent2_50,
ColorTokens.Accent2_100,
ColorTokens.Accent2_500,
ColorTokens.Accent2_600,
ColorTokens.Accent3_50,
ColorTokens.Accent3_100,
)
val randomAccent = accentColors.random()
return randomAccent.resolveColor(context)
}
@@ -32,6 +32,10 @@ import app.lawnchair.ui.preferences.components.PreferenceLayout
import app.lawnchair.ui.preferences.components.SliderPreference
import app.lawnchair.ui.preferences.components.SuggestionsPreference
import app.lawnchair.ui.preferences.components.SwitchPreference
import app.lawnchair.util.checkAndRequestFilesPermission
import app.lawnchair.util.contactPermissionGranted
import app.lawnchair.util.filesAndStorageGranted
import app.lawnchair.util.requestContactPermissionGranted
import com.android.launcher3.R
object AppDrawerRoutes {
@@ -48,7 +52,9 @@ fun NavGraphBuilder.appDrawerGraph(route: String) {
fun AppDrawerPreferences() {
val prefs = preferenceManager()
val prefs2 = preferenceManager2()
val resources = LocalContext.current.resources
val context = LocalContext.current
val resources = context.resources
PreferenceLayout(label = stringResource(id = R.string.app_drawer_label)) {
PreferenceGroup(heading = stringResource(id = R.string.general_label)) {
SliderPreference(
@@ -115,6 +121,108 @@ fun AppDrawerPreferences() {
}
}
}
val isDeviceSearch = prefs.performWideSearchExperimental.get() && showDrawerSearchBar.state.value
if (isDeviceSearch) {
PreferenceGroup(heading = stringResource(id = R.string.pref_advance_search_category)) {
SwitchPreference(
adapter = prefs2.performWideSearch.getAdapter(),
label = stringResource(id = R.string.perform_wide_search_title),
)
ExpandAndShrink(visible = prefs2.performWideSearch.getAdapter().state.value) {
DividerColumn {
SwitchPreference(
adapter = prefs.searchResultFiles.getAdapter(),
label = stringResource(id = R.string.perform_wide_search_file),
description = stringResource(
id = if (
filesAndStorageGranted(context)
) {
R.string.all_apps_search_result_files_description
} else {
R.string.warn_files_permission_content
},
),
onClick = { checkAndRequestFilesPermission(context, prefs) },
)
ExpandAndShrink(visible = prefs.searchResultFiles.getAdapter().state.value) {
SliderPreference(
label = stringResource(id = R.string.max_file_result_count_title),
adapter = prefs2.maxFileResultCount.getAdapter(),
step = 1,
valueRange = 3..10,
)
}
SwitchPreference(
adapter = prefs.searchResultPeople.getAdapter(),
label = stringResource(id = R.string.search_pref_result_people_title),
description = stringResource(
id = if (
contactPermissionGranted(context)
) {
R.string.all_apps_search_result_contacts_description
} else {
R.string.warn_contact_permission_content
},
),
onClick = { requestContactPermissionGranted(context, prefs) },
)
ExpandAndShrink(visible = prefs.searchResultPeople.getAdapter().state.value) {
SliderPreference(
label = stringResource(id = R.string.max_people_result_count_title),
adapter = prefs2.maxPeopleResultCount.getAdapter(),
step = 1,
valueRange = 3..15,
)
}
SwitchPreference(
adapter = prefs.searchResultSettingsEntry.getAdapter(),
label = stringResource(id = R.string.search_pref_result_settings_entry_title),
)
ExpandAndShrink(visible = prefs.searchResultSettingsEntry.getAdapter().state.value) {
SliderPreference(
label = stringResource(id = R.string.max_settings_entry_result_count_title),
adapter = prefs2.maxSettingsEntryResultCount.getAdapter(),
step = 1,
valueRange = 2..10,
)
}
}
}
}
}
ExpandAndShrink(visible = showDrawerSearchBar.state.value) {
PreferenceGroup {
DividerColumn {
SwitchPreference(
adapter = prefs.searchResultStartPageSuggestion.getAdapter(),
label = stringResource(id = R.string.pref_suggestion_title),
)
ExpandAndShrink(visible = prefs.searchResultStartPageSuggestion.getAdapter().state.value) {
SliderPreference(
label = stringResource(id = R.string.max_suggestion_result_count_title),
adapter = prefs2.maxSuggestionResultCount.getAdapter(),
step = 1,
valueRange = 3..10,
)
}
SwitchPreference(
adapter = prefs.searchResulRecentSuggestion.getAdapter(),
label = stringResource(id = R.string.pref_recent_suggestion_title),
)
ExpandAndShrink(visible = prefs.searchResulRecentSuggestion.getAdapter().state.value) {
SliderPreference(
label = stringResource(id = R.string.max_recent_result_count_title),
adapter = prefs2.maxRecentResultCount.getAdapter(),
step = 1,
valueRange = 1..10,
)
}
}
}
}
if (deviceSearchEnabled) {
ExpandAndShrink(visible = showDrawerSearchBar.state.value) {
PreferenceGroup(heading = stringResource(id = R.string.show_search_result_types)) {
@@ -45,6 +45,11 @@ fun ExperimentalFeaturesPreferences() {
adapter = prefs2.smartspaceModeSelection.getAdapter(),
label = stringResource(id = R.string.smartspace_mode_selection),
)
SwitchPreference(
adapter = prefs.performWideSearchExperimental.getAdapter(),
label = stringResource(id = R.string.perform_wide_search_title),
description = stringResource(id = R.string.perform_wide_search_description),
)
}
}
}
@@ -0,0 +1,39 @@
package app.lawnchair.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.util.Log
class AppInfoHelper(private val context: Context) {
fun getDefaultPhoneAppInfo(): AppInfo? {
val phoneIntent = Intent(Intent.ACTION_DIAL)
return getDefaultAppInfo(phoneIntent)
}
fun getDefaultMessageAppInfo(): AppInfo? {
val messageIntent = Intent(Intent.ACTION_SENDTO)
messageIntent.data = android.net.Uri.parse("smsto:")
return getDefaultAppInfo(messageIntent)
}
private fun getDefaultAppInfo(intent: Intent): AppInfo? {
val packageManager: PackageManager = context.packageManager
val resolveInfo: ResolveInfo? = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
return if (resolveInfo != null) {
val appName: String = resolveInfo.loadLabel(packageManager).toString()
val appIcon: Drawable = resolveInfo.loadIcon(packageManager)
val packageName: String = resolveInfo.activityInfo.packageName
AppInfo(appName, appIcon, packageName)
} else {
Log.e("AppInfoHelper", "No default app found for the given intent.")
null
}
}
}
data class AppInfo(val appName: String, val appIcon: Drawable, val packageName: String)
@@ -1,9 +1,34 @@
package app.lawnchair.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.GradientDrawable
import app.lawnchair.theme.color.generateColor
fun GradientDrawable.getCornerRadiiCompat(): FloatArray? = try {
cornerRadii
} catch (_: NullPointerException) {
null
}
fun createTextBitmap(context: Context, text: String): Bitmap {
val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
val bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// Set up paint for drawing text
val paint = Paint().apply {
color = generateColor(context)
textSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size).toFloat()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
// Draw text in the center of the bitmap
val x = (bitmap.width / 2).toFloat()
val y = (bitmap.height / 2 - (paint.descent() + paint.ascent()) / 2)
canvas.drawText(text, x, y, paint)
return bitmap
}
@@ -0,0 +1,65 @@
package app.lawnchair.util
import android.net.Uri
import androidx.core.content.FileProvider
import app.lawnchair.LawnchairApp
import java.io.File
import okio.FileMetadata
import okio.FileSystem
import okio.Path
internal fun Path.list(
fileSystem: FileSystem = FileSystem.SYSTEM,
isShowHidden: Boolean = false,
isRecursively: Boolean = false,
): Sequence<Path> {
return runCatching {
if (isRecursively) {
fileSystem.listRecursively(this)
} else {
fileSystem.list(this).asSequence()
}
}.getOrDefault(emptySequence()).filter {
if (isShowHidden) true else !it.isHidden
}
}
internal fun Path.getMetadata(fileSystem: FileSystem = FileSystem.SYSTEM): FileMetadata? =
fileSystem.metadataOrNull(this)
internal fun Path.isDirectory(fileSystem: FileSystem = FileSystem.SYSTEM): Boolean =
getMetadata(fileSystem)?.isDirectory == true
internal fun Path.isFile(fileSystem: FileSystem = FileSystem.SYSTEM): Boolean =
!isDirectory(fileSystem)
internal fun Path.sizeOrEmpty(fileSystem: FileSystem = FileSystem.SYSTEM): Long =
getMetadata(fileSystem)?.size ?: 0
internal fun Path.isRegularFile(fileSystem: FileSystem = FileSystem.SYSTEM): Boolean =
getMetadata(fileSystem)?.isRegularFile == true
internal val Path.isHidden: Boolean get() = toString().contains("/.")
internal val Path.exists: Boolean get() = toFile().exists()
internal val Path.extension: String?
get() {
val dotIndex = name.lastIndexOf(".")
return if (dotIndex == -1) null else name.substring(dotIndex + 1)
}
internal val Path.nameWithoutExtension: String get() = name.substringBeforeLast(".")
internal val Path.mimeType: String? get() = extension?.extension2MimeType()
val fileProviderAuthority: String = "${LawnchairApp.instance.packageName}.fileprovider"
fun String.path2Uri(): Uri? = File(this).file2Uri()
fun File.file2Uri(): Uri? = try {
FileProvider.getUriForFile(LawnchairApp.instance, fileProviderAuthority, this)
} catch (e: Exception) {
e.printStackTrace()
null
}
@@ -0,0 +1,43 @@
package app.lawnchair.util
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.widget.ImageView
import app.lawnchair.theme.color.ColorTokens
import com.android.launcher3.R
class ImageViewWrapper(context: Context, attrs: AttributeSet?) : ImageView(context, attrs) {
private val mRadius = resources.getDimensionPixelSize(R.dimen.search_row_preview_radius).toFloat()
private val path = Path()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = RectF()
init {
scaleType = ScaleType.CENTER_CROP
}
override fun onDraw(canvas: Canvas) {
rect.set(0f, 0f, width.toFloat(), height.toFloat())
val corners = floatArrayOf(
mRadius,
mRadius,
mRadius,
mRadius,
mRadius,
mRadius,
mRadius,
mRadius,
)
path.addRoundRect(rect, corners, Path.Direction.CW)
paint.color = ColorTokens.GroupHighlight.resolveColor(context)
canvas.drawPath(path, paint)
canvas.clipPath(path)
super.onDraw(canvas)
}
}
@@ -1,4 +1,4 @@
package com.snebula.android.file.util
package app.lawnchair.util
import android.webkit.MimeTypeMap
@@ -0,0 +1,85 @@
package app.lawnchair.util
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import app.lawnchair.preferences.PreferenceManager
private const val TAG: String = "PackagePermissionManager"
fun requestContactPermissionGranted(context: Context, prefs: PreferenceManager): Boolean {
val isGranted = contactPermissionGranted(context)
if (!isGranted) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri: Uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Log.e(TAG, "No activity found to handle application details settings intent")
}
prefs.searchResultPeople.set(false)
}
return isGranted
}
fun contactPermissionGranted(context: Context): Boolean {
return context.checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
}
fun checkAndRequestFilesPermission(context: Context, prefs: PreferenceManager): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
requestManageAllFilesAccessPermission(context)
prefs.searchResultFiles.set(false)
return false
}
} else {
if (!hasReadExternalStoragePermission(context)) {
requestReadExternalStoragePermission(context)
return false
}
}
return true
}
fun filesAndStorageGranted(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Environment.isExternalStorageManager() else hasReadExternalStoragePermission(context)
}
@RequiresApi(Build.VERSION_CODES.R)
private fun requestManageAllFilesAccessPermission(context: Context) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intent)
}
private fun hasReadExternalStoragePermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE,
) == PackageManager.PERMISSION_GRANTED
}
private fun requestReadExternalStoragePermission(context: Context) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
123,
)
}