() ?: run {
60 | sendAction(Action.Message("Clipboard isn't supported on your device"))
61 | return@Leaf
62 | }
63 |
64 | manager.setPrimaryClip(ClipData.newPlainText("note", note.content))
65 | }
66 |
67 | override val commands: CommandList = CommandList.Builder()
68 | .putCommand(ADD_OPTION)
69 | .putCommand(LIST_OPTION)
70 | .putCommand(REMOVE_OPTION)
71 | .putCommand(CLEAR_OPTION)
72 | .putCommand(COPY_OPTION)
73 | .build()
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/commands/impls/PwdCommand.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.commands.impls
2 |
3 | import com.saidooubella.shellui.commands.Command
4 | import com.saidooubella.shellui.commands.Metadata
5 | import com.saidooubella.shellui.shell.Action
6 |
7 | internal val PWD_COMMAND = Command.Leaf(Metadata.Builder("pwd").build()) {
8 | sendAction(Action.Message(workingDir.canonicalPath))
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/commands/impls/ReadCommand.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.commands.impls
2 |
3 | import android.Manifest
4 | import android.os.Build
5 | import android.os.Environment
6 | import com.saidooubella.shellui.commands.Command
7 | import com.saidooubella.shellui.commands.Metadata
8 | import com.saidooubella.shellui.shell.Action
9 | import com.saidooubella.shellui.suggestions.Suggestions
10 | import com.saidooubella.shellui.utils.MANAGE_FILES_SETTINGS
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.withContext
13 | import java.io.FileReader
14 |
15 | internal val READ_COMMAND = Command.Leaf(
16 | Metadata.Builder("read").addRequiredArg("file", Suggestions.Files).build()
17 | ) { arguments ->
18 |
19 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
20 | if (!Environment.isExternalStorageManager()) {
21 | sendAction(Action.StartIntentForResult(MANAGE_FILES_SETTINGS))
22 | if (!Environment.isExternalStorageManager()) {
23 | sendAction(Action.Message("Can't read from the external storage"))
24 | return@Leaf
25 | }
26 | }
27 | } else {
28 | if (!sendAction(Action.RequestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)))) {
29 | sendAction(Action.Message("Can't read from the external storage"))
30 | return@Leaf
31 | }
32 | }
33 |
34 | val fileName = arguments[0].value
35 |
36 | val file = normalizePath(fileName).takeIf { it.exists() } ?: run {
37 | sendAction(Action.Message("$fileName is not found"))
38 | return@Leaf
39 | }
40 |
41 | withContext(Dispatchers.IO) {
42 | FileReader(file).buffered().use { reader ->
43 | while (true) {
44 | val line = reader.readLine() ?: break
45 | sendAction(Action.Message(line))
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/commands/impls/RmCommand.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.commands.impls
2 |
3 | import android.Manifest
4 | import android.os.Build
5 | import android.os.Environment
6 | import com.saidooubella.shellui.commands.Command
7 | import com.saidooubella.shellui.commands.Metadata
8 | import com.saidooubella.shellui.shell.Action
9 | import com.saidooubella.shellui.suggestions.Suggestions
10 | import com.saidooubella.shellui.utils.MANAGE_FILES_SETTINGS
11 |
12 | internal val RM_COMMAND = Command.Leaf(
13 | Metadata.Builder("rm").addRequiredNArgs("files", Suggestions.Files).build()
14 | ) { arguments ->
15 |
16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
17 | if (!Environment.isExternalStorageManager()) {
18 | sendAction(Action.StartIntentForResult(MANAGE_FILES_SETTINGS))
19 | if (!Environment.isExternalStorageManager()) {
20 | sendAction(Action.Message("Can't write to the external storage"))
21 | return@Leaf
22 | }
23 | }
24 | } else {
25 | if (!sendAction(Action.RequestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)))) {
26 | sendAction(Action.Message("Can't write to the external storage"))
27 | return@Leaf
28 | }
29 | }
30 |
31 | arguments.forEach {
32 | if (!normalizePath(it.value).deleteRecursively()) {
33 | sendAction(Action.Message("Can't delete ${it.value}"))
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/commands/impls/RssCommand.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.commands.impls
2 |
3 | import com.saidooubella.shellui.commands.Command
4 | import com.saidooubella.shellui.commands.CommandList
5 | import com.saidooubella.shellui.commands.Metadata
6 | import com.saidooubella.shellui.data.rss.RssFeed
7 | import com.saidooubella.shellui.shell.Action
8 | import com.saidooubella.shellui.suggestions.Suggestions
9 | import com.prof.rssparser.Parser
10 |
11 | private val SPACE_REGEX = "[\t ]+".toRegex()
12 | private val TAG_REGEX = "<.*?>".toRegex()
13 |
14 | internal val RSS_COMMAND = object : Command.Group("rss") {
15 |
16 | private val ADD_OPTION = Leaf(
17 | Metadata.Builder("add")
18 | .addRequiredArg("name", Suggestions.Empty)
19 | .addRequiredArg("url", Suggestions.Empty)
20 | .build()
21 | ) {
22 | if (!repository.insertFeed(RssFeed(it[0].value, it[1].value))) {
23 | sendAction(Action.Message("Failed to add '${it[0].value}' feed"))
24 | }
25 | }
26 |
27 | private val LS_OPTION = Leaf(
28 | Metadata.Builder("ls")
29 | .addRequiredArg("name", Suggestions.Empty)
30 | .build()
31 | ) {
32 |
33 | val feed = repository.getFeed(it[0].value) ?: run {
34 | sendAction(Action.Message("'${it[0].value}' not found"))
35 | return@Leaf
36 | }
37 |
38 | val channel = Parser.Builder().build().getChannel(feed.url)
39 |
40 | sendAction(Action.Message(channel.title ?: "Untitled"))
41 | if (channel.description != null) {
42 | sendAction(Action.Message(channel.description!!))
43 | }
44 |
45 | channel.articles.take(5).forEach { article ->
46 | sendAction(Action.Message(article.title ?: "Untitled"))
47 | if (article.description != null) {
48 | val content = cleanFromHtmlTags(article.description!!)
49 | sendAction(Action.Message(content))
50 | }
51 | }
52 | }
53 |
54 | fun cleanFromHtmlTags(source: String): String {
55 | return source.replace(TAG_REGEX, "")
56 | .replace(SPACE_REGEX, " ").trim()
57 | }
58 |
59 | override val commands: CommandList = CommandList.Builder()
60 | .putCommand(ADD_OPTION)
61 | .putCommand(LS_OPTION)
62 | .build()
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/commands/impls/TouchCommand.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.commands.impls
2 |
3 | import android.Manifest
4 | import android.os.Build
5 | import android.os.Environment
6 | import com.saidooubella.shellui.commands.Command
7 | import com.saidooubella.shellui.commands.Metadata
8 | import com.saidooubella.shellui.shell.Action
9 | import com.saidooubella.shellui.suggestions.Suggestions
10 | import com.saidooubella.shellui.utils.MANAGE_FILES_SETTINGS
11 | import java.io.IOException
12 |
13 | internal val TOUCH_COMMAND = Command.Leaf(
14 | Metadata.Builder("touch").addRequiredNArgs("files", Suggestions.Empty).build()
15 | ) { arguments ->
16 |
17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
18 | if (!Environment.isExternalStorageManager()) {
19 | sendAction(Action.StartIntentForResult(MANAGE_FILES_SETTINGS))
20 | if (!Environment.isExternalStorageManager()) {
21 | sendAction(Action.Message("Can't write to the external storage"))
22 | return@Leaf
23 | }
24 | }
25 | } else {
26 | if (!sendAction(Action.RequestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)))) {
27 | sendAction(Action.Message("Can't write to the external storage"))
28 | return@Leaf
29 | }
30 | }
31 |
32 | arguments.forEach { argument ->
33 |
34 | val file = normalizePath(argument.value).takeIf { file -> !file.exists() } ?: run {
35 | sendAction(Action.Message("'${argument.value}' already exists"))
36 | return@forEach
37 | }
38 |
39 | try {
40 | if (!file.createNewFile()) {
41 | sendAction(Action.Message("Cannot create '${argument.value}'"))
42 | }
43 | } catch (e: IOException) {
44 | sendAction(Action.Message("${e.message}"))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/DataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data
2 |
3 | import android.content.ContentResolver
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.content.pm.ResolveInfo
7 | import android.os.Build
8 | import android.provider.ContactsContract.CommonDataKinds.Phone.*
9 | import androidx.room.*
10 | import com.saidooubella.shellui.commands.Arguments
11 | import com.saidooubella.shellui.commands.Command
12 | import com.saidooubella.shellui.commands.CommandList
13 | import com.saidooubella.shellui.commands.CountCheckResult
14 | import com.saidooubella.shellui.data.DataRepository.Companion.loadLaunchIntent
15 | import com.saidooubella.shellui.data.DataRepository.Companion.queryLauncherActivities
16 | import com.saidooubella.shellui.data.notes.Note
17 | import com.saidooubella.shellui.data.pinned.PinnedApp
18 | import com.saidooubella.shellui.data.pinned.PinnedAppsDao
19 | import com.saidooubella.shellui.data.rss.RssFeed
20 | import com.saidooubella.shellui.models.Contact
21 | import com.saidooubella.shellui.models.LauncherApp
22 | import com.saidooubella.shellui.models.parseArguments
23 | import com.saidooubella.shellui.shell.Action
24 | import com.saidooubella.shellui.suggestions.Suggestion
25 | import com.saidooubella.shellui.utils.OpenableApp
26 | import com.saidooubella.shellui.utils.catch
27 | import com.saidooubella.shellui.utils.loadFromPackage
28 | import kotlinx.coroutines.Dispatchers
29 | import kotlinx.coroutines.flow.Flow
30 | import kotlinx.coroutines.flow.map
31 | import kotlinx.coroutines.isActive
32 | import kotlinx.coroutines.withContext
33 | import java.io.File
34 | import kotlin.coroutines.coroutineContext
35 |
36 | internal interface UseCase {
37 | suspend operator fun invoke(param: P): R
38 | }
39 |
40 | internal class LoadAppsUseCase(
41 | private val packageManager: PackageManager
42 | ) : UseCase<(String) -> Boolean, List> {
43 |
44 | override suspend fun invoke(param: (String) -> Boolean): List =
45 | withContext(Dispatchers.IO) {
46 | packageManager.queryLauncherActivities()
47 | .filter { param(it.loadLabel(packageManager).toString()) }
48 | .map {
49 | LauncherApp(
50 | it.loadLabel(packageManager).toString(),
51 | it.activityInfo.packageName,
52 | it.loadLaunchIntent(packageManager)
53 | )
54 | }
55 | .sortedBy { it.name }
56 | }
57 |
58 | companion object {
59 |
60 | private fun ResolveInfo.loadLaunchIntent(packageManager: PackageManager): Intent {
61 | return Intent(packageManager.getLaunchIntentForPackage(activityInfo.packageName))
62 | }
63 |
64 | private val FILTER_INTENT = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
65 |
66 | @Suppress("DEPRECATION")
67 | private fun PackageManager.queryLauncherActivities(): MutableList {
68 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
69 | queryIntentActivities(FILTER_INTENT, 0)
70 | } else {
71 | queryIntentActivities(FILTER_INTENT, PackageManager.ResolveInfoFlags.of(0))
72 | }
73 | }
74 | }
75 | }
76 |
77 | internal class LoadContactsUseCase(
78 | private val resolver: ContentResolver,
79 | ) : UseCase> {
80 |
81 | override suspend fun invoke(param: String): List {
82 | return catch {
83 | resolver.query(CONTENT_URI, PROJECTION, SELECTION, arrayOf("%$param%"), SORT_ORDER)
84 | ?.use { cursor ->
85 |
86 | val contactsList = ArrayList(cursor.count)
87 |
88 | val nameColumn = cursor.getColumnIndexOrThrow(DISPLAY_NAME_PRIMARY)
89 | val phoneColumn = cursor.getColumnIndexOrThrow(NUMBER)
90 |
91 | while (coroutineContext.isActive && cursor.moveToNext()) {
92 | val phone: String = cursor.getString(phoneColumn)
93 | val name: String = cursor.getString(nameColumn)
94 | contactsList.add(Contact(name, phone))
95 | }
96 |
97 | contactsList
98 | }
99 | } ?: emptyList()
100 | }
101 |
102 | companion object {
103 |
104 | private const val IS_SDN_CONTACT = "is_sdn_contact"
105 |
106 | private val PROJECTION = arrayOf(DISPLAY_NAME_PRIMARY, NUMBER)
107 |
108 | private const val SELECTION =
109 | "$DISPLAY_NAME_PRIMARY LIKE ? AND $HAS_PHONE_NUMBER = 1 AND $IS_SDN_CONTACT = 0"
110 |
111 | private const val SORT_ORDER = "$DISPLAY_NAME_PRIMARY ASC"
112 | }
113 | }
114 |
115 | internal class LoadFilesUseCase : UseCase> {
116 |
117 | override suspend fun invoke(param: Params): List = withContext(Dispatchers.IO) {
118 | param.file.listFiles()?.filter {
119 | (if (param.dirsOnly) it.isDirectory else true) && it.name.contains(param.query, true)
120 | }?.sortedBy { it.name } ?: listOf()
121 | }
122 |
123 | internal class Params(
124 | internal val file: File,
125 | internal val query: String = "",
126 | internal val dirsOnly: Boolean = false
127 | )
128 | }
129 |
130 | internal class GetPinnedAppsSuggestion(
131 | private val pinnedAppsDao: PinnedAppsDao,
132 | private val packageManager: PackageManager,
133 | ) : UseCase>> {
134 | override suspend fun invoke(param: Unit): Flow> {
135 | return pinnedAppsDao.get().map { list ->
136 | list.mapNotNull { app ->
137 | catch {
138 | val packageName = packageManager
139 | .getLaunchIntentForPackage(app.packageName)
140 | ?.component?.packageName ?: return@mapNotNull null
141 | val label = packageManager.loadFromPackage(packageName)
142 | .loadLabel(packageManager).toString()
143 | OpenableApp(label, app.packageName)
144 | }
145 | }
146 | }
147 | }
148 | }
149 |
150 | internal class DataRepository(
151 | private val packageManager: PackageManager,
152 | private val appDatabase: ShellDatabase,
153 | private val resolver: ContentResolver,
154 | ) {
155 |
156 | suspend fun loadLauncherApps(
157 | predicate: (String) -> Boolean = { true }
158 | ) = withContext(Dispatchers.IO) {
159 | packageManager.queryLauncherActivities()
160 | .filter { predicate(it.loadLabel(packageManager).toString()) }
161 | .map {
162 | LauncherApp(
163 | it.loadLabel(packageManager).toString(),
164 | it.activityInfo.packageName,
165 | it.loadLaunchIntent(packageManager)
166 | )
167 | }
168 | .sortedBy { it.name }
169 | }
170 |
171 | suspend fun loadFiles(
172 | file: File, query: String = "", dirsOnly: Boolean = false
173 | ) = withContext(Dispatchers.IO) {
174 | file.listFiles()?.filter {
175 | (if (dirsOnly) it.isDirectory else true) && it.name.contains(query, true)
176 | }?.sortedBy { it.name } ?: listOf()
177 | }
178 |
179 | suspend fun loadContacts(query: String = ""): List {
180 | return catch {
181 | resolver.query(
182 | CONTENT_URI, PROJECTION, SELECTION, arrayOf("%$query%"), SORT_ORDER
183 | )?.use { cursor ->
184 |
185 | val contactsList = ArrayList(cursor.count)
186 |
187 | val nameColumn = cursor.getColumnIndexOrThrow(DISPLAY_NAME_PRIMARY)
188 | val phoneColumn = cursor.getColumnIndexOrThrow(NUMBER)
189 |
190 | while (coroutineContext.isActive && cursor.moveToNext()) {
191 | val phone: String = cursor.getString(phoneColumn)
192 | val name: String = cursor.getString(nameColumn)
193 | contactsList.add(Contact(name, phone))
194 | }
195 |
196 | contactsList
197 | }
198 | } ?: emptyList()
199 | }
200 |
201 | suspend fun addNote(note: Note) {
202 | appDatabase.noteDao.insert(note)
203 | }
204 |
205 | suspend fun notesList(): List {
206 | return appDatabase.noteDao.list()
207 | }
208 |
209 | suspend fun getNote(index: Long): Note? {
210 | return appDatabase.noteDao.get(index)
211 | }
212 |
213 | suspend fun removeNote(index: Long): Int {
214 | return appDatabase.noteDao.remove(index)
215 | }
216 |
217 | suspend fun clearNotes() {
218 | appDatabase.noteDao.clear()
219 | }
220 |
221 | suspend fun pinApp(appPackage: String) {
222 | appDatabase.pinnedAppsDao.insert(PinnedApp(packageName = appPackage))
223 | }
224 |
225 | suspend fun unpinApp(appPackage: String): Boolean {
226 | return appDatabase.pinnedAppsDao.remove(appPackage) > 0
227 | }
228 |
229 | fun getPinnedApps(): Flow> {
230 | return appDatabase.pinnedAppsDao.get()
231 | }
232 |
233 | suspend fun getPinnedAppsList(): List {
234 | return appDatabase.pinnedAppsDao.getList()
235 | }
236 |
237 | suspend fun insertFeed(feed: RssFeed): Boolean {
238 | return appDatabase.rssFeedDao.insert(feed) > 0
239 | }
240 |
241 | suspend fun removeFeed(feedName: String): Boolean {
242 | return appDatabase.rssFeedDao.remove(feedName) > 0
243 | }
244 |
245 | suspend fun getFeeds(): List {
246 | return appDatabase.rssFeedDao.getAll()
247 | }
248 |
249 | suspend fun getFeed(feedName: String): RssFeed? {
250 | return appDatabase.rssFeedDao.get(feedName)
251 | }
252 |
253 | companion object {
254 |
255 | private const val IS_SDN_CONTACT = "is_sdn_contact"
256 |
257 | private val PROJECTION = arrayOf(DISPLAY_NAME_PRIMARY, NUMBER)
258 |
259 | private const val SELECTION =
260 | "$DISPLAY_NAME_PRIMARY LIKE ? AND $HAS_PHONE_NUMBER = 1 AND $IS_SDN_CONTACT = 0"
261 |
262 | private const val SORT_ORDER = "$DISPLAY_NAME_PRIMARY ASC"
263 |
264 | private fun ResolveInfo.loadLaunchIntent(packageManager: PackageManager): Intent {
265 | return Intent(packageManager.getLaunchIntentForPackage(activityInfo.packageName))
266 | }
267 |
268 | private val FILTER_INTENT = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
269 |
270 | @Suppress("DEPRECATION")
271 | private fun PackageManager.queryLauncherActivities(): MutableList {
272 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
273 | queryIntentActivities(FILTER_INTENT, 0)
274 | } else {
275 | queryIntentActivities(FILTER_INTENT, PackageManager.ResolveInfoFlags.of(0))
276 | }
277 | }
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/ShellDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data
2 |
3 | import android.app.Application
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import com.saidooubella.shellui.data.notes.Note
8 | import com.saidooubella.shellui.data.notes.NoteDao
9 | import com.saidooubella.shellui.data.pinned.PinnedApp
10 | import com.saidooubella.shellui.data.pinned.PinnedAppsDao
11 | import com.saidooubella.shellui.data.rss.RssFeed
12 | import com.saidooubella.shellui.data.rss.RssFeedDao
13 |
14 | @Database(
15 | entities = [Note::class, PinnedApp::class, RssFeed::class],
16 | exportSchema = false,
17 | version = 1,
18 | )
19 | internal abstract class ShellDatabase : RoomDatabase() {
20 |
21 | internal abstract val pinnedAppsDao: PinnedAppsDao
22 | internal abstract val rssFeedDao: RssFeedDao
23 | internal abstract val noteDao: NoteDao
24 |
25 | companion object {
26 |
27 | @Volatile
28 | private var APP_DB: ShellDatabase? = null
29 |
30 | private val LOCK = Any()
31 |
32 | internal fun get(application: Application) = APP_DB ?: synchronized(LOCK) {
33 | APP_DB ?: Room.databaseBuilder(application, ShellDatabase::class.java, "shell")
34 | .build().also { APP_DB = it }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/notes/Note.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.notes
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "notes")
8 | internal data class Note(
9 | @PrimaryKey(autoGenerate = true)
10 | internal val id: Long = 0L,
11 | @ColumnInfo(name = "content")
12 | internal val content: String = "",
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/notes/NoteDao.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.notes
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 |
7 | @Dao
8 | internal interface NoteDao {
9 |
10 | @Insert
11 | suspend fun insert(note: Note)
12 |
13 | @Query("SELECT * FROM notes")
14 | suspend fun list(): List
15 |
16 | @Query("SELECT * FROM notes ORDER BY id LIMIT 1 OFFSET :index")
17 | suspend fun get(index: Long): Note?
18 |
19 | @Query("DELETE FROM notes WHERE id IN ( SELECT id FROM notes ORDER BY id LIMIT 1 OFFSET :index )")
20 | suspend fun remove(index: Long): Int
21 |
22 | @Query("DELETE FROM notes")
23 | suspend fun clear()
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/pinned/PinnedApp.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.pinned
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "pinned_apps")
8 | internal data class PinnedApp(
9 | @PrimaryKey(autoGenerate = true)
10 | internal val id: Long = 0L,
11 | @ColumnInfo(name = "package_name")
12 | internal val packageName: String = "",
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/pinned/PinnedAppsDao.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.pinned
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | @Dao
9 | internal interface PinnedAppsDao {
10 |
11 | @Query("SELECT * FROM pinned_apps")
12 | fun get(): Flow>
13 |
14 | @Query("SELECT * FROM pinned_apps")
15 | suspend fun getList(): List
16 |
17 | @Insert
18 | suspend fun insert(pinnedApp: PinnedApp)
19 |
20 | @Query("DELETE FROM pinned_apps WHERE package_name = :packageName")
21 | suspend fun remove(packageName: String): Int
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/rss/RssFeed.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.rss
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "rss_feeds")
7 | internal data class RssFeed(
8 | @PrimaryKey
9 | internal val name: String,
10 | internal val url: String,
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/data/rss/RssFeedDao.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.data.rss
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 |
8 | @Dao
9 | internal interface RssFeedDao {
10 |
11 | @Insert(onConflict = OnConflictStrategy.IGNORE)
12 | suspend fun insert(rssFeed: RssFeed): Long
13 |
14 | @Query("DELETE FROM rss_feeds WHERE name = :feedName")
15 | suspend fun remove(feedName: String): Int
16 |
17 | @Query("SELECT * FROM rss_feeds")
18 | suspend fun getAll(): List
19 |
20 | @Query("SELECT * FROM rss_feeds WHERE name = :feedName")
21 | suspend fun get(feedName: String): RssFeed?
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/managers/FlashManager.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION")
2 |
3 | package com.saidooubella.shellui.managers
4 |
5 | import android.content.Context
6 | import android.hardware.Camera
7 | import android.hardware.camera2.CameraCharacteristics
8 | import android.hardware.camera2.CameraManager
9 | import android.os.Build
10 | import androidx.annotation.RequiresApi
11 |
12 | internal abstract class FlashManager(
13 | internal val FacingFront: Int,
14 | internal val FacingBack: Int,
15 | internal val NeedsPermission: Boolean,
16 | ) {
17 |
18 | internal abstract fun setTorchMode(facing: Int, enabled: Boolean): Result
19 |
20 | internal abstract fun onCleared()
21 |
22 | @RequiresApi(Build.VERSION_CODES.M)
23 | private class FlashManagerMarshmallowImpl(
24 | private val manager: CameraManager,
25 | ) : FlashManager(
26 | CameraCharacteristics.LENS_FACING_FRONT,
27 | CameraCharacteristics.LENS_FACING_BACK,
28 | false
29 | ) {
30 |
31 | override fun setTorchMode(facing: Int, enabled: Boolean): Result {
32 |
33 | val cameraId = manager.cameraIdList.find { cameraId ->
34 | manager.getCameraCharacteristics(cameraId)[CameraCharacteristics.LENS_FACING] == facing
35 | } ?: return Result.CameraNotFound
36 |
37 | if (manager.getCameraCharacteristics(cameraId)[CameraCharacteristics.FLASH_INFO_AVAILABLE] == false) {
38 | return Result.FlashNotFound
39 | }
40 |
41 | manager.setTorchMode(cameraId, enabled)
42 | return Result.Success
43 | }
44 |
45 | override fun onCleared() = Unit
46 | }
47 |
48 | private class FlashManagerLollipopImpl : FlashManager(
49 | Camera.CameraInfo.CAMERA_FACING_FRONT,
50 | Camera.CameraInfo.CAMERA_FACING_BACK,
51 | true
52 | ) {
53 |
54 | private var camera: Camera? = null
55 | private var facing: Int = -1
56 |
57 | override fun setTorchMode(facing: Int, enabled: Boolean): Result {
58 |
59 | if (this.facing != facing || this.camera == null) {
60 | releaseCamera()
61 | camera = openCamera(facing)
62 | val camera = camera ?: return Result.CameraNotFound
63 | if (Camera.Parameters.FLASH_MODE_TORCH !in camera.parameters.supportedFlashModes) {
64 | releaseCamera()
65 | return Result.FlashNotFound
66 | }
67 | }
68 |
69 | camera?.applyParameters {
70 | flashMode = if (enabled) Camera.Parameters.FLASH_MODE_TORCH else Camera.Parameters.FLASH_MODE_OFF
71 | }
72 |
73 | return Result.Success
74 | }
75 |
76 | override fun onCleared() = releaseCamera()
77 |
78 | private fun releaseCamera() {
79 | camera?.release()
80 | camera = null
81 | facing = -1
82 | }
83 |
84 | private fun openCamera(facing: Int): Camera? {
85 | val cameraInfo = Camera.CameraInfo()
86 | repeat(Camera.getNumberOfCameras()) { cameraId ->
87 | Camera.getCameraInfo(cameraId, cameraInfo)
88 | if (cameraInfo.facing == facing) {
89 | return Camera.open(cameraId)
90 | }
91 | }
92 | return null
93 | }
94 |
95 | private fun Camera.applyParameters(block: Camera.Parameters.() -> Unit) {
96 | parameters = parameters.apply(block)
97 | }
98 | }
99 |
100 | enum class Result { CameraNotFound, FlashNotFound, Success }
101 |
102 | companion object {
103 | fun of(context: Context) = when {
104 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
105 | val cameraManager = context.getSystemService(Context.CAMERA_SERVICE)
106 | FlashManagerMarshmallowImpl(cameraManager as CameraManager)
107 | }
108 | else -> FlashManagerLollipopImpl()
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/models/Argument.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.models
2 |
3 | import com.saidooubella.shellui.commands.Arguments
4 |
5 | internal data class Argument(
6 | internal val value: String,
7 | internal val start: Int,
8 | internal val end: Int
9 | ) {
10 | override fun toString(): String = value
11 | }
12 |
13 | private fun String.isSpaceAt(i: Int): Boolean {
14 | return i < length && this[i].isWhitespace()
15 | }
16 |
17 | internal fun String.parseArguments(): Arguments {
18 |
19 | val arguments = mutableListOf()
20 | val builder = StringBuilder()
21 | var index = 0
22 |
23 | while (index < length) {
24 |
25 | while (isSpaceAt(index)) {
26 | index += 1
27 | }
28 |
29 | var escape = false
30 | var quote = false
31 | val start = index
32 |
33 | builder.delete(0, builder.length)
34 |
35 | while (index < length) {
36 |
37 | val c = this[index]
38 |
39 | if (escape) {
40 | escape = false
41 | builder.append(c)
42 | index++
43 | continue
44 | }
45 |
46 | if (!quote && c.isWhitespace()) {
47 | break
48 | }
49 |
50 | when (c) {
51 | '\\' -> escape = true
52 | '"' -> quote = !quote
53 | else -> builder.append(c)
54 | }
55 |
56 | index += 1
57 | }
58 |
59 | if (start >= index) {
60 | continue
61 | }
62 |
63 | arguments.add(Argument(builder.toString(), start, index))
64 | }
65 |
66 | return Arguments(arguments.toList())
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/models/Contact.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.models
2 |
3 | internal data class Contact(val name: String, val phone: String)
4 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/models/LauncherApp.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.models
2 |
3 | import android.content.Intent
4 | import android.provider.Settings
5 | import androidx.core.net.toUri
6 |
7 | internal data class LauncherApp(
8 | internal val name: String,
9 | internal val packageName: String,
10 | internal val launchIntent: Intent,
11 | )
12 |
13 | internal val LauncherApp.uninstallIntent: Intent
14 | get() = Intent(Intent.ACTION_DELETE, "package:$packageName".toUri())
15 |
16 | internal val LauncherApp.settingsIntent: Intent
17 | get() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:$packageName".toUri())
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/models/LogItem.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.models
2 |
3 | internal data class LogItem(
4 | internal val message: String,
5 | internal val action: (suspend () -> Unit)?,
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/preferences/ShellPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.preferences
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.datastore.preferences.preferencesDataStore
9 | import com.saidooubella.shellui.utils.toStateFlow
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.flow.*
12 | import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
13 | import kotlinx.coroutines.runBlocking
14 |
15 | private val Context.preferences by preferencesDataStore(name = "settings")
16 | private val DARK_THEME_KEY = booleanPreferencesKey("dark_theme")
17 |
18 | internal class ShellPreferences(private val context: Context) {
19 |
20 | internal suspend fun toggleDarkTheme() {
21 | context.preferences.edit { settings ->
22 | val darkTheme = settings[DARK_THEME_KEY] ?: false
23 | settings[DARK_THEME_KEY] = !darkTheme
24 | }
25 | }
26 |
27 | internal fun getDarkTheme(scope: CoroutineScope): StateFlow {
28 | return context.preferences.toStateFlow(scope, DARK_THEME_KEY, false)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/shell/Action.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.shell
2 |
3 | import android.content.Intent
4 | import androidx.activity.result.ActivityResult
5 | import com.saidooubella.shellui.IntentForResult
6 | import com.saidooubella.shellui.PermissionsHandler
7 | import com.saidooubella.shellui.ScreenState
8 | import com.saidooubella.shellui.ShellMode
9 | import com.saidooubella.shellui.models.LogItem
10 | import kotlinx.collections.immutable.persistentListOf
11 | import kotlinx.coroutines.CompletableDeferred
12 | import kotlinx.coroutines.job
13 | import kotlin.coroutines.coroutineContext
14 |
15 | internal sealed interface Action {
16 |
17 | suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit): R
18 |
19 | object Clear : Action {
20 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit) {
21 | update { it.copy(logs = persistentListOf()) }
22 | }
23 | }
24 |
25 | object Exit : Action {
26 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit) {
27 | update { it.copy(exit = true) }
28 | }
29 | }
30 |
31 | class StartIntentForResult(private val intent: Intent) : Action {
32 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit): ActivityResult {
33 | val deferred = CompletableDeferred(coroutineContext.job)
34 | update { it.copy(intentForResult = IntentForResult(intent, deferred)) }
35 | return deferred.await()
36 | }
37 | }
38 |
39 | class Message(
40 | private val content: String,
41 | private val action: (suspend () -> Unit)? = null
42 | ) : Action {
43 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit) {
44 | update { it.copy(logs = it.logs.add(0, LogItem(content, action))) }
45 | }
46 | }
47 |
48 | class RequestPermissions(private val permissions: Array) : Action {
49 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit): Boolean {
50 | val deferred = CompletableDeferred(coroutineContext.job)
51 | update { it.copy(permissions = PermissionsHandler(permissions, deferred)) }
52 | return deferred.await()
53 | }
54 | }
55 |
56 | class StartIntent(private val intent: Intent) : Action {
57 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit) {
58 | update { it.copy(intent = intent) }
59 | }
60 | }
61 |
62 | class Prompt(private val hint: String) : Action {
63 | override suspend fun execute(update: ((ScreenState) -> ScreenState) -> Unit): String {
64 | val deferred = CompletableDeferred(coroutineContext.job)
65 | update { it.copy(mode = ShellMode.PromptMode(hint, deferred)) }
66 | return deferred.await()
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/shell/ShellContext.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.shell
2 |
3 | import android.app.Application
4 | import android.os.Environment
5 | import com.saidooubella.shellui.commands.CommandList
6 | import com.saidooubella.shellui.commands.Commands
7 | import com.saidooubella.shellui.data.DataRepository
8 | import com.saidooubella.shellui.managers.FlashManager
9 | import java.io.File
10 |
11 | internal abstract class ShellContext(
12 | val appContext: Application,
13 | val repository: DataRepository,
14 | val commands: CommandList = Commands,
15 | val flashManager: FlashManager = FlashManager.of(appContext),
16 | ) {
17 |
18 | internal var workingDir: File = Environment.getExternalStorageDirectory()
19 |
20 | internal abstract suspend fun sendAction(action: Action): R
21 |
22 | internal fun removeWorkingDir(path: String): String {
23 | val root = workingDir.path
24 | return if (path.startsWith(root)) path.substring(root.length + 1, path.length) else path
25 | }
26 |
27 | internal inline fun normalizePath(path: String, transformer: (String) -> String = { it }): File {
28 | return when (path.startsWith(File.separator)) {
29 | false -> File(workingDir, transformer(path))
30 | else -> File(transformer(path))
31 | }
32 | }
33 |
34 | internal fun onCleared() {
35 | flashManager.onCleared()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/suggestions/MergeAction.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.suggestions
2 |
3 | internal sealed interface MergeAction {
4 | class Replace(val start: Int, val end: Int) : MergeAction
5 | object Append : MergeAction
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/suggestions/Suggestion.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.suggestions
2 |
3 | class Suggestion(
4 | internal val label: String,
5 | replacement: String = "$label ",
6 | internal val runnable: Boolean = false,
7 | ) {
8 | internal val replacement: String = if (runnable) replacement else wrap(replacement)
9 | }
10 |
11 | private fun wrap(value: String) = value.trim().run {
12 | if (none { it.isWhitespace() }) value else '"' + this + '"'
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/suggestions/Suggestions.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.suggestions
2 |
3 | import com.saidooubella.shellui.shell.ShellContext
4 | import com.saidooubella.shellui.utils.catch
5 | import com.saidooubella.shellui.utils.loadFromPackage
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import java.io.File
9 |
10 | internal interface Suggestions {
11 |
12 | suspend fun load(context: ShellContext, hint: String): List
13 |
14 | class Custom(val suggestions: List) : Suggestions {
15 | override suspend fun load(context: ShellContext, hint: String): List {
16 | return suggestions.filter { hint in it.replacement }
17 | }
18 | }
19 |
20 | object Empty : Suggestions {
21 | override suspend fun load(context: ShellContext, hint: String) = emptyList()
22 | }
23 |
24 | object Commands : Suggestions {
25 | override suspend fun load(context: ShellContext, hint: String): List {
26 | return context.commands.names()
27 | .filter { it.contains(hint, ignoreCase = true) }
28 | .map { Suggestion(it) }
29 | }
30 | }
31 |
32 | object PinnedApps : Suggestions {
33 | override suspend fun load(context: ShellContext, hint: String): List {
34 | return context.repository.getPinnedAppsList().mapNotNull { app ->
35 | catch {
36 | val manager = context.appContext.packageManager
37 | Suggestion(manager.loadFromPackage(app).loadLabel(manager).toString())
38 | }
39 | }
40 | }
41 | }
42 |
43 | object NotPinnedApps : Suggestions {
44 | override suspend fun load(context: ShellContext, hint: String): List {
45 | val pinnedApps = context.repository.getPinnedAppsList()
46 | return context.repository
47 | .loadLauncherApps { it.contains(hint, true) }
48 | .filter { app -> pinnedApps.none { app.packageName == it.packageName } }
49 | .map { Suggestion(it.name) }
50 | }
51 | }
52 |
53 | object Apps : Suggestions {
54 | override suspend fun load(context: ShellContext, hint: String): List {
55 | return context.repository
56 | .loadLauncherApps { it.contains(hint, true) }
57 | .map { Suggestion(it.name) }
58 | }
59 | }
60 |
61 | object AppsPackages : Suggestions {
62 | override suspend fun load(context: ShellContext, hint: String): List {
63 | return context.repository
64 | .loadLauncherApps { it.contains(hint, true) }
65 | .map { Suggestion("${it.name}(${it.packageName})", it.packageName) }
66 | }
67 | }
68 |
69 | object Files : Suggestions {
70 | override suspend fun load(context: ShellContext, hint: String): List {
71 | return context.loadFiles(hint, false)
72 | }
73 | }
74 |
75 | object Directories : Suggestions {
76 | override suspend fun load(context: ShellContext, hint: String): List {
77 | return context.loadFiles(hint, true)
78 | }
79 | }
80 | }
81 |
82 | private suspend fun ShellContext.loadFiles(
83 | hint: String,
84 | dirsOnly: Boolean
85 | ): List {
86 |
87 | val index = hint.lastIndexOf(File.separator)
88 | val root = normalizePath(hint) {
89 | if (index != -1) hint.substring(0, index + 1) else ""
90 | }
91 |
92 | val query = if (index != -1) hint.substring(index + 1) else hint
93 | val specialPaths = buildList(3) {
94 | if (!hint.endsWith(File.separator))
95 | this.add(Suggestion("/", "$hint/"))
96 | this.add(Suggestion(".", "$hint./"))
97 | this.add(Suggestion("..", "$hint../"))
98 | }
99 |
100 | return when (!root.exists()) {
101 | true -> emptyList()
102 | else -> withContext(Dispatchers.Default) {
103 | specialPaths + repository.loadFiles(root, query, dirsOnly).map {
104 | val ending = if (it.isDirectory) File.separator else " "
105 | Suggestion(it.name, removeWorkingDir(it.path) + ending)
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/suggestions/SuggestionsGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.suggestions
2 |
3 | import com.saidooubella.shellui.commands.Arguments
4 | import com.saidooubella.shellui.commands.Command
5 | import com.saidooubella.shellui.commands.CountCheckResult
6 | import com.saidooubella.shellui.commands.Parameter
7 | import com.saidooubella.shellui.models.Argument
8 | import com.saidooubella.shellui.shell.ShellContext
9 | import com.saidooubella.shellui.utils.OpenableApp
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.withContext
12 |
13 | internal suspend fun ShellContext.loadSuggestions(
14 | args: Arguments,
15 | lineLength: Int,
16 | ): SuggestionsResult = withContext(Dispatchers.Default) {
17 | val context = this@loadSuggestions
18 | if (args.isEmpty()) {
19 | SuggestionsResult(loadFirstSuggestions(context, ""), MergeAction.Append)
20 | } else {
21 | val first = args.first()
22 | val parent = commands[first.value]
23 | if (first.end == lineLength) {
24 | SuggestionsResult(
25 | loadFirstSuggestions(context, first.value),
26 | MergeAction.Replace(first.start, first.end)
27 | )
28 | } else if (parent == null) {
29 | SuggestionsResult.EMPTY
30 | } else {
31 | proceedCommand(context, args.dropFirst(), parent, lineLength)
32 | }
33 | }
34 | }
35 |
36 | private suspend fun loadFirstSuggestions(context: ShellContext, hint: String) = buildList {
37 |
38 | if (hint.isNotBlank()) {
39 | addAll(context.repository.loadLauncherApps { it.contains(hint, true) }
40 | .map { OpenableApp(it.name, it.packageName) })
41 | }
42 |
43 | addAll(Suggestions.Commands.load(context, hint))
44 | }
45 |
46 | private suspend fun proceedCommand(
47 | shell: ShellContext,
48 | args: Arguments,
49 | command: Command,
50 | lineLength: Int,
51 | ): SuggestionsResult {
52 |
53 | var current: Command = command
54 | var arguments: Arguments = args
55 |
56 | while (true) {
57 |
58 | if (current is Command.Leaf) {
59 | return handleLeafCommand(shell, current, arguments, lineLength)
60 | }
61 |
62 | if (current is Command.Group) {
63 |
64 | val option = if (arguments.isEmpty()) {
65 | return SuggestionsResult(
66 | current.commands.names().toSuggestions(""),
67 | MergeAction.Append
68 | )
69 | } else {
70 | arguments.first()
71 | }
72 |
73 | val next = current.commands[option.value]
74 |
75 | if (option.end == lineLength) {
76 | return SuggestionsResult(
77 | current.commands.names().toSuggestions(option.value),
78 | MergeAction.Replace(option.start, option.end)
79 | )
80 | } else if (next == null) {
81 | return SuggestionsResult.EMPTY
82 | }
83 |
84 | arguments = arguments.dropFirst()
85 | current = next
86 | }
87 | }
88 | }
89 |
90 | private suspend fun handleLeafCommand(
91 | shell: ShellContext,
92 | command: Command.Leaf,
93 | args: Arguments,
94 | lineLength: Int,
95 | ): SuggestionsResult {
96 |
97 | if (args.isEmpty()) {
98 | return when (command.metadata.params.isNotEmpty()) {
99 | true -> {
100 | val suggestions = command.metadata.params.first().suggestions
101 | SuggestionsResult(suggestions.load(shell, ""), MergeAction.Append)
102 | }
103 | else -> SuggestionsResult.EMPTY
104 | }
105 | }
106 |
107 | if (command.metadata.validateCount(args.count()) == CountCheckResult.TooManyArgs) {
108 | return SuggestionsResult.EMPTY
109 | }
110 |
111 | val last: Argument = args.last()
112 | val (hint, limit) = when (last.end == lineLength) {
113 | true -> last.value to args.count() - 1
114 | else -> "" to args.count()
115 | }
116 |
117 | val argsInfo: List = command.metadata.params
118 | .dropWhileIndexed { index, param -> index < limit && !param.variadic }
119 |
120 | return when (argsInfo.isNotEmpty()) {
121 | true -> {
122 | val mergeAction: MergeAction = when (last.end == lineLength) {
123 | true -> MergeAction.Replace(last.start, last.end)
124 | else -> MergeAction.Append
125 | }
126 | SuggestionsResult(argsInfo[0].suggestions.load(shell, hint), mergeAction)
127 | }
128 | else -> SuggestionsResult.EMPTY
129 | }
130 | }
131 |
132 | private fun Collection.toSuggestions(query: String) =
133 | filter { it.indexOf(query, ignoreCase = true) != -1 }.map { Suggestion(it) }
134 |
135 | private inline fun Collection.dropWhileIndexed(block: (Int, T) -> Boolean): List {
136 | var index = 0
137 | return dropWhile { block(index++, it) }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/suggestions/SuggestionsResult.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.suggestions
2 |
3 | internal data class SuggestionsResult(
4 | internal val suggestions: List,
5 | internal val mergeAction: MergeAction,
6 | ) {
7 | companion object {
8 | internal val EMPTY = SuggestionsResult(emptyList(), MergeAction.Append)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.animation.animateColorAsState
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.SideEffect
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.toArgb
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.platform.LocalView
14 | import androidx.core.graphics.luminance
15 | import androidx.core.view.WindowCompat
16 |
17 | private val DarkColorScheme = darkColorScheme(
18 | primary = Purple80,
19 | secondary = PurpleGrey80,
20 | tertiary = Pink80
21 | )
22 |
23 | private val LightColorScheme = lightColorScheme(
24 | primary = Purple40,
25 | secondary = PurpleGrey40,
26 | tertiary = Pink40,
27 | )
28 |
29 | private fun Color.isDark(): Boolean {
30 | return toArgb().luminance < .5f
31 | }
32 |
33 | @Composable
34 | internal fun ShellUITheme(
35 | darkTheme: Boolean,
36 | content: @Composable () -> Unit
37 | ) {
38 |
39 | val colorScheme = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
40 | true -> when (darkTheme) {
41 | true -> dynamicDarkColorScheme(LocalContext.current)
42 | else -> dynamicLightColorScheme(LocalContext.current)
43 | }
44 | else -> if (darkTheme) DarkColorScheme else LightColorScheme
45 | }
46 |
47 | val view = LocalView.current
48 | if (!view.isInEditMode) {
49 | SideEffect {
50 | val window = (view.context as Activity).window
51 | val controller = WindowCompat.getInsetsController(window, view)
52 | controller.isAppearanceLightNavigationBars = colorScheme.primary.isDark()
53 | controller.isAppearanceLightStatusBars = colorScheme.primary.isDark()
54 | }
55 | }
56 |
57 | MaterialTheme(
58 | colorScheme = colorScheme.animate(),
59 | typography = Typography,
60 | content = content
61 | )
62 | }
63 |
64 | @Composable
65 | private fun ColorScheme.animate(): ColorScheme {
66 | return ColorScheme(
67 | animateColorAsState(primary).value,
68 | animateColorAsState(onPrimary).value,
69 | animateColorAsState(primaryContainer).value,
70 | animateColorAsState(onPrimaryContainer).value,
71 | animateColorAsState(inversePrimary).value,
72 | animateColorAsState(secondary).value,
73 | animateColorAsState(onSecondary).value,
74 | animateColorAsState(secondaryContainer).value,
75 | animateColorAsState(onSecondaryContainer).value,
76 | animateColorAsState(tertiary).value,
77 | animateColorAsState(onTertiary).value,
78 | animateColorAsState(tertiaryContainer).value,
79 | animateColorAsState(onTertiaryContainer).value,
80 | animateColorAsState(background).value,
81 | animateColorAsState(onBackground).value,
82 | animateColorAsState(surface).value,
83 | animateColorAsState(onSurface).value,
84 | animateColorAsState(surfaceVariant).value,
85 | animateColorAsState(onSurfaceVariant).value,
86 | animateColorAsState(surfaceTint).value,
87 | animateColorAsState(inverseSurface).value,
88 | animateColorAsState(inverseOnSurface).value,
89 | animateColorAsState(error).value,
90 | animateColorAsState(onError).value,
91 | animateColorAsState(errorContainer).value,
92 | animateColorAsState(onErrorContainer).value,
93 | animateColorAsState(outline).value,
94 | animateColorAsState(outlineVariant).value,
95 | animateColorAsState(scrim).value,
96 | )
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/saidooubella/shellui/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.ApplicationInfo
6 | import android.content.pm.PackageManager
7 | import android.net.Uri
8 | import android.os.Build
9 | import android.provider.Settings
10 | import androidx.annotation.RequiresApi
11 | import androidx.datastore.core.DataStore
12 | import androidx.datastore.preferences.core.Preferences
13 | import com.saidooubella.shellui.data.pinned.PinnedApp
14 | import com.saidooubella.shellui.suggestions.Suggestion
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.flow.*
17 | import kotlinx.coroutines.runBlocking
18 |
19 | internal inline fun catch(block: () -> T): T? {
20 | return try {
21 | block()
22 | } catch (_: Exception) {
23 | null
24 | }
25 | }
26 |
27 | internal fun PackageManager.loadFromPackage(packageName: String): ApplicationInfo {
28 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
29 | getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
30 | } else {
31 | @Suppress("DEPRECATION")
32 | getApplicationInfo(packageName, 0)
33 | }
34 | }
35 |
36 | internal fun Context.hasNotPermission(permission: String): Boolean {
37 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
38 | checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED
39 | } else {
40 | false
41 | }
42 | }
43 |
44 | internal val MANAGE_FILES_SETTINGS: Intent
45 | @RequiresApi(Build.VERSION_CODES.R)
46 | get() = Intent(
47 | Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
48 | Uri.parse("package:" + com.saidooubella.shellui.BuildConfig.APPLICATION_ID)
49 | )
50 |
51 | @Suppress("FunctionName")
52 | internal fun OpenableApp(label: String, packageName: String): Suggestion {
53 | return Suggestion(label, "apps open-pkg $packageName", true)
54 | }
55 |
56 | internal fun DataStore.toStateFlow(
57 | scope: CoroutineScope, key: Preferences.Key, default: T
58 | ): StateFlow = this.data.map { settings -> settings[key] ?: default }
59 | .stateIn(scope, SharingStarted.WhileSubscribed(), this[key] ?: default)
60 |
61 | internal operator fun DataStore.get(key: Preferences.Key): T? {
62 | return runBlocking { data.first()[key] }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Shell-UI
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/saidooubella/shellui/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.saidooubella.shellui
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '7.4.0' apply false
4 | id 'com.android.library' version '7.4.0' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Nov 21 18:24:08 WEST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/imgs/IMAGE-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/imgs/IMAGE-1.png
--------------------------------------------------------------------------------
/imgs/IMAGE-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saidooubella/android-shell-ui/76e2efb6996561437f92a58b9db842040445852d/imgs/IMAGE-2.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Shell-UI"
16 | include ':app'
17 |
--------------------------------------------------------------------------------