test build
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.claude
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
@@ -13,3 +14,5 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
app/build/outputs/apk/*
|
||||
app/release/app-release.apk
|
||||
|
||||
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/gradle.xml
generated
@@ -1,11 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/ScreenshotAssistant" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
48
app/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.customassistant'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.customassistant"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
20
app/release/output-metadata.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.customassistant",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -10,7 +14,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.CustomAssistant">
|
||||
android:theme="@style/Theme.CustomAssistant"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -18,10 +24,47 @@
|
||||
android:theme="@style/Theme.CustomAssistant">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AssistantActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true">
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.VOICE_COMMAND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.speech.action.VOICE_SEARCH_HANDS_FREE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".CustomAssistantService"
|
||||
android:permission="android.permission.BIND_VOICE_INTERACTION"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.voice_interaction"
|
||||
android:resource="@xml/assistant_service_config" />
|
||||
<intent-filter>
|
||||
<action android:name="android.service.voice.VoiceInteractionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".CustomAssistantSessionService"
|
||||
android:permission="android.permission.BIND_VOICE_INTERACTION"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
9
app/src/main/java/com/customassistant/ActionItem.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.customassistant
|
||||
|
||||
data class ActionItem(
|
||||
val type: ActionType,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val packageName: String? = null,
|
||||
val isSelected: Boolean = false
|
||||
)
|
||||
6
app/src/main/java/com/customassistant/ActionType.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package com.customassistant
|
||||
|
||||
enum class ActionType {
|
||||
TOGGLE_FLASHLIGHT,
|
||||
LAUNCH_APP
|
||||
}
|
||||
68
app/src/main/java/com/customassistant/ActionsAdapter.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ActionsAdapter(
|
||||
private val actions: MutableList<ActionItem>,
|
||||
private val onActionSelected: (ActionItem) -> Unit
|
||||
) : RecyclerView.Adapter<ActionsAdapter.ActionViewHolder>() {
|
||||
|
||||
private var selectedPosition = -1
|
||||
|
||||
class ActionViewHolder(
|
||||
private val rbAction: RadioButton,
|
||||
private val tvTitle: TextView,
|
||||
private val tvDescription: TextView
|
||||
) : RecyclerView.ViewHolder(rbAction.parent as ViewGroup) {
|
||||
|
||||
fun bind(action: ActionItem, isSelected: Boolean, onSelected: () -> Unit) {
|
||||
rbAction.isChecked = isSelected
|
||||
tvTitle.text = action.title
|
||||
tvDescription.text = action.description
|
||||
|
||||
itemView.setOnClickListener {
|
||||
onSelected()
|
||||
}
|
||||
|
||||
rbAction.setOnClickListener {
|
||||
onSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_action, parent, false)
|
||||
return ActionViewHolder(
|
||||
view.findViewById(R.id.rbAction),
|
||||
view.findViewById(R.id.tvActionTitle),
|
||||
view.findViewById(R.id.tvActionDescription)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ActionViewHolder, position: Int) {
|
||||
val action = actions[position]
|
||||
val isSelected = position == selectedPosition
|
||||
|
||||
holder.bind(action, isSelected) {
|
||||
val previousSelected = selectedPosition
|
||||
selectedPosition = position
|
||||
|
||||
if (previousSelected != -1) {
|
||||
notifyItemChanged(previousSelected)
|
||||
}
|
||||
notifyItemChanged(selectedPosition)
|
||||
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = actions.size
|
||||
|
||||
fun getSelectedAction(): ActionItem? {
|
||||
return if (selectedPosition != -1) actions[selectedPosition] else null
|
||||
}
|
||||
}
|
||||
34
app/src/main/java/com/customassistant/AppLauncher.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
class AppLauncher(private val context: Context) {
|
||||
|
||||
fun launchApp(packageName: String): Boolean {
|
||||
return try {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppName(packageName: String): String? {
|
||||
return try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/customassistant/AssistantActivity.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class AssistantActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
private lateinit var flashlightManager: FlashlightManager
|
||||
private lateinit var appLauncher: AppLauncher
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// No UI - execute action immediately and close
|
||||
initManagers()
|
||||
executeStoredAction()
|
||||
}
|
||||
|
||||
private fun initManagers() {
|
||||
preferenceManager = PreferenceManager(this)
|
||||
flashlightManager = FlashlightManager(this)
|
||||
appLauncher = AppLauncher(this)
|
||||
}
|
||||
|
||||
private fun executeStoredAction() {
|
||||
val (actionType, packageName) = preferenceManager.getSelectedAction()
|
||||
|
||||
// Execute action silently
|
||||
when (actionType) {
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
flashlightManager.toggleFlashlight()
|
||||
finish() // Close immediately after flashlight toggle
|
||||
}
|
||||
|
||||
ActionType.LAUNCH_APP -> {
|
||||
if (packageName != null) {
|
||||
appLauncher.launchApp(packageName)
|
||||
}
|
||||
finish() // Close immediately after launching app
|
||||
}
|
||||
|
||||
null -> {
|
||||
// No action configured - just close
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.service.voice.VoiceInteractionService
|
||||
|
||||
class CustomAssistantService : VoiceInteractionService()
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.service.voice.VoiceInteractionSession
|
||||
|
||||
class CustomAssistantSession(context: Context) : VoiceInteractionSession(context) {
|
||||
|
||||
override fun onShow(args: Bundle?, flags: Int) {
|
||||
super.onShow(args, flags)
|
||||
|
||||
// Launch our assistant activity
|
||||
val intent = Intent(context, AssistantActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.startActivity(intent)
|
||||
|
||||
// Hide the session since we're launching our own UI
|
||||
hide()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.os.Bundle
|
||||
import android.service.voice.VoiceInteractionSessionService
|
||||
|
||||
class CustomAssistantSessionService : VoiceInteractionSessionService() {
|
||||
override fun onNewSession(bundle: Bundle?) = CustomAssistantSession(this)
|
||||
}
|
||||
74
app/src/main/java/com/customassistant/FlashlightManager.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Build
|
||||
|
||||
class FlashlightManager(private val context: Context) {
|
||||
|
||||
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("flashlight_prefs", Context.MODE_PRIVATE)
|
||||
private var cameraId: String? = null
|
||||
|
||||
companion object {
|
||||
private const val KEY_FLASHLIGHT_STATE = "flashlight_on"
|
||||
}
|
||||
|
||||
init {
|
||||
try {
|
||||
cameraId = cameraManager.cameraIdList[0]
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFlashlight(): Boolean {
|
||||
return if (isFlashlightOn()) {
|
||||
turnOffFlashlight()
|
||||
} else {
|
||||
turnOnFlashlight()
|
||||
}
|
||||
}
|
||||
|
||||
private fun turnOnFlashlight(): Boolean {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
cameraId?.let { id ->
|
||||
cameraManager.setTorchMode(id, true)
|
||||
saveFlashlightState(true)
|
||||
true
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun turnOffFlashlight(): Boolean {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
cameraId?.let { id ->
|
||||
cameraManager.setTorchMode(id, false)
|
||||
saveFlashlightState(false)
|
||||
true
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFlashlightState(isOn: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_FLASHLIGHT_STATE, isOn).apply()
|
||||
}
|
||||
|
||||
fun isFlashlightOn(): Boolean = prefs.getBoolean(KEY_FLASHLIGHT_STATE, false)
|
||||
}
|
||||
117
app/src/main/java/com/customassistant/MainActivity.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var rvActions: RecyclerView
|
||||
private lateinit var tvSelectedAction: TextView
|
||||
private lateinit var btnSaveAction: Button
|
||||
private lateinit var actionsAdapter: ActionsAdapter
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
|
||||
private val actions = mutableListOf<ActionItem>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
preferenceManager = PreferenceManager(this)
|
||||
initViews()
|
||||
setupActions()
|
||||
setupRecyclerView()
|
||||
setupSaveButton()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
rvActions = findViewById(R.id.rvActions)
|
||||
tvSelectedAction = findViewById(R.id.tvSelectedAction)
|
||||
btnSaveAction = findViewById(R.id.btnSaveAction)
|
||||
}
|
||||
|
||||
private fun setupActions() {
|
||||
actions.clear()
|
||||
actions.addAll(
|
||||
listOf(
|
||||
ActionItem(
|
||||
type = ActionType.TOGGLE_FLASHLIGHT,
|
||||
title = getString(R.string.action_toggle_flashlight),
|
||||
description = "Turn flashlight on/off"
|
||||
),
|
||||
ActionItem(
|
||||
type = ActionType.LAUNCH_APP,
|
||||
title = getString(R.string.action_launch_app),
|
||||
description = "Launch a selected app"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
actionsAdapter = ActionsAdapter(actions) { selectedAction ->
|
||||
when (selectedAction.type) {
|
||||
ActionType.LAUNCH_APP -> showAppSelectionDialog()
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
tvSelectedAction.text = selectedAction.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rvActions.layoutManager = LinearLayoutManager(this)
|
||||
rvActions.adapter = actionsAdapter
|
||||
}
|
||||
|
||||
private fun setupSaveButton() {
|
||||
btnSaveAction.setOnClickListener {
|
||||
val selectedAction = actionsAdapter.getSelectedAction()
|
||||
if (selectedAction != null) {
|
||||
when (selectedAction.type) {
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
preferenceManager.saveSelectedAction(ActionType.TOGGLE_FLASHLIGHT)
|
||||
Toast.makeText(this, "Flashlight toggle saved!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
ActionType.LAUNCH_APP -> {
|
||||
Toast.makeText(this, "Please select an app first", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Please select an action", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAppSelectionDialog() {
|
||||
val packageManager = packageManager
|
||||
val installedApps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
.filter { packageManager.getLaunchIntentForPackage(it.packageName) != null }
|
||||
.map {
|
||||
Pair(it.loadLabel(packageManager).toString(), it.packageName)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
|
||||
val appNames = installedApps.map { it.first }.toTypedArray()
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.select_app))
|
||||
.setItems(appNames) { _, which ->
|
||||
val selectedApp = installedApps[which]
|
||||
tvSelectedAction.text = "Launch: ${selectedApp.first}"
|
||||
|
||||
btnSaveAction.setOnClickListener {
|
||||
preferenceManager.saveSelectedAction(ActionType.LAUNCH_APP, selectedApp.second)
|
||||
Toast.makeText(this, "App launch saved: ${selectedApp.first}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
28
app/src/main/java/com/customassistant/PreferenceManager.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class PreferenceManager(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "custom_assistant_prefs"
|
||||
private const val KEY_SELECTED_ACTION = "selected_action"
|
||||
private const val KEY_SELECTED_PACKAGE = "selected_package"
|
||||
}
|
||||
|
||||
fun saveSelectedAction(actionType: ActionType, packageName: String? = null) {
|
||||
prefs.edit()
|
||||
.putString(KEY_SELECTED_ACTION, actionType.name)
|
||||
.putString(KEY_SELECTED_PACKAGE, packageName)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getSelectedAction(): Pair<ActionType?, String?> {
|
||||
val actionName = prefs.getString(KEY_SELECTED_ACTION, null)
|
||||
val packageName = prefs.getString(KEY_SELECTED_PACKAGE, null)
|
||||
val actionType = actionName?.let { ActionType.valueOf(it) }
|
||||
return Pair(actionType, packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package sh.sar.customass
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import sh.sar.customass.ui.theme.CustomAssistantTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
CustomAssistantTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
CustomAssistantTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package sh.sar.customass.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -1,58 +0,0 @@
|
||||
package sh.sar.customass.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CustomAssistantTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package sh.sar.customass.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
34
app/src/main/res/layout/activity_assistant.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_executed"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Close"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
45
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_action"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSelectedAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_action_selected"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:padding="12dp"
|
||||
android:background="@android:color/background_light" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSaveAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save Selected Action"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
41
app/src/main/res/layout/item_action.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,3 +1,11 @@
|
||||
<resources>
|
||||
<string name="app_name">Custom Assistant</string>
|
||||
<string name="action_toggle_flashlight">Toggle Flashlight</string>
|
||||
<string name="action_launch_app">Launch App</string>
|
||||
<string name="select_action">Select Action</string>
|
||||
<string name="no_action_selected">No action selected</string>
|
||||
<string name="action_executed">Action executed</string>
|
||||
<string name="select_app">Select App</string>
|
||||
<string name="flashlight_on">Flashlight ON</string>
|
||||
<string name="flashlight_off">Flashlight OFF</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.CustomAssistant" parent="Theme.Material3.DayNight">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.CustomAssistant" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.CustomAssistant.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
5
app/src/main/res/xml/assistant_service_config.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:recognitionService="com.customassistant.CustomAssistantService"
|
||||
android:sessionService="com.customassistant.CustomAssistantSessionService"
|
||||
android:supportsAssist="true" />
|
||||
5
app/src/main/res/xml/device_admin.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-policies>
|
||||
</uses-policies>
|
||||
</device-admin>
|
||||
47
app/src/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.CustomAssistant"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.CustomAssistant">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AssistantActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.CustomAssistant.NoActionBar">
|
||||
<intent-filter android:priority="1">
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter android:priority="1">
|
||||
<action android:name="android.intent.action.VOICE_COMMAND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
9
app/src/src/main/java/com/customassistant/ActionItem.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.customassistant
|
||||
|
||||
data class ActionItem(
|
||||
val type: ActionType,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val packageName: String? = null,
|
||||
val isSelected: Boolean = false
|
||||
)
|
||||
6
app/src/src/main/java/com/customassistant/ActionType.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package com.customassistant
|
||||
|
||||
enum class ActionType {
|
||||
TOGGLE_FLASHLIGHT,
|
||||
LAUNCH_APP
|
||||
}
|
||||
68
app/src/src/main/java/com/customassistant/ActionsAdapter.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ActionsAdapter(
|
||||
private val actions: MutableList<ActionItem>,
|
||||
private val onActionSelected: (ActionItem) -> Unit
|
||||
) : RecyclerView.Adapter<ActionsAdapter.ActionViewHolder>() {
|
||||
|
||||
private var selectedPosition = -1
|
||||
|
||||
class ActionViewHolder(
|
||||
private val rbAction: RadioButton,
|
||||
private val tvTitle: TextView,
|
||||
private val tvDescription: TextView
|
||||
) : RecyclerView.ViewHolder(rbAction.parent as ViewGroup) {
|
||||
|
||||
fun bind(action: ActionItem, isSelected: Boolean, onSelected: () -> Unit) {
|
||||
rbAction.isChecked = isSelected
|
||||
tvTitle.text = action.title
|
||||
tvDescription.text = action.description
|
||||
|
||||
itemView.setOnClickListener {
|
||||
onSelected()
|
||||
}
|
||||
|
||||
rbAction.setOnClickListener {
|
||||
onSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_action, parent, false)
|
||||
return ActionViewHolder(
|
||||
view.findViewById(R.id.rbAction),
|
||||
view.findViewById(R.id.tvActionTitle),
|
||||
view.findViewById(R.id.tvActionDescription)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ActionViewHolder, position: Int) {
|
||||
val action = actions[position]
|
||||
val isSelected = position == selectedPosition
|
||||
|
||||
holder.bind(action, isSelected) {
|
||||
val previousSelected = selectedPosition
|
||||
selectedPosition = position
|
||||
|
||||
if (previousSelected != -1) {
|
||||
notifyItemChanged(previousSelected)
|
||||
}
|
||||
notifyItemChanged(selectedPosition)
|
||||
|
||||
onActionSelected(action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = actions.size
|
||||
|
||||
fun getSelectedAction(): ActionItem? {
|
||||
return if (selectedPosition != -1) actions[selectedPosition] else null
|
||||
}
|
||||
}
|
||||
34
app/src/src/main/java/com/customassistant/AppLauncher.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
class AppLauncher(private val context: Context) {
|
||||
|
||||
fun launchApp(packageName: String): Boolean {
|
||||
return try {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppName(packageName: String): String? {
|
||||
return try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class AssistantActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tvActionStatus: TextView
|
||||
private lateinit var tvActionDetails: TextView
|
||||
private lateinit var btnClose: Button
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
private lateinit var flashlightManager: FlashlightManager
|
||||
private lateinit var appLauncher: AppLauncher
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_assistant)
|
||||
|
||||
initViews()
|
||||
initManagers()
|
||||
executeStoredAction()
|
||||
setupCloseButton()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
tvActionStatus = findViewById(R.id.tvActionStatus)
|
||||
tvActionDetails = findViewById(R.id.tvActionDetails)
|
||||
btnClose = findViewById(R.id.btnClose)
|
||||
}
|
||||
|
||||
private fun initManagers() {
|
||||
preferenceManager = PreferenceManager(this)
|
||||
flashlightManager = FlashlightManager(this)
|
||||
appLauncher = AppLauncher(this)
|
||||
}
|
||||
|
||||
private fun executeStoredAction() {
|
||||
val (actionType, packageName) = preferenceManager.getSelectedAction()
|
||||
|
||||
if (actionType == null) {
|
||||
tvActionStatus.text = "No Action Configured"
|
||||
tvActionDetails.text = "Please configure an action in the main app"
|
||||
return
|
||||
}
|
||||
|
||||
when (actionType) {
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
val success = flashlightManager.toggleFlashlight()
|
||||
if (success) {
|
||||
val status = if (flashlightManager.isFlashlightOn()) {
|
||||
getString(R.string.flashlight_on)
|
||||
} else {
|
||||
getString(R.string.flashlight_off)
|
||||
}
|
||||
tvActionStatus.text = getString(R.string.action_executed)
|
||||
tvActionDetails.text = status
|
||||
} else {
|
||||
tvActionStatus.text = "Action Failed"
|
||||
tvActionDetails.text = "Could not toggle flashlight"
|
||||
}
|
||||
}
|
||||
|
||||
ActionType.LAUNCH_APP -> {
|
||||
if (packageName != null) {
|
||||
val appName = appLauncher.getAppName(packageName)
|
||||
val success = appLauncher.launchApp(packageName)
|
||||
|
||||
if (success) {
|
||||
tvActionStatus.text = getString(R.string.action_executed)
|
||||
tvActionDetails.text = "Launching ${appName ?: packageName}"
|
||||
|
||||
finish()
|
||||
} else {
|
||||
tvActionStatus.text = "Action Failed"
|
||||
tvActionDetails.text = "Could not launch ${appName ?: packageName}"
|
||||
}
|
||||
} else {
|
||||
tvActionStatus.text = "Action Failed"
|
||||
tvActionDetails.text = "No app configured"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCloseButton() {
|
||||
btnClose.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Build
|
||||
|
||||
class FlashlightManager(private val context: Context) {
|
||||
|
||||
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
private var cameraId: String? = null
|
||||
private var isFlashlightOn = false
|
||||
|
||||
init {
|
||||
try {
|
||||
cameraId = cameraManager.cameraIdList[0]
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFlashlight(): Boolean {
|
||||
return if (isFlashlightOn) {
|
||||
turnOffFlashlight()
|
||||
} else {
|
||||
turnOnFlashlight()
|
||||
}
|
||||
}
|
||||
|
||||
private fun turnOnFlashlight(): Boolean {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
cameraId?.let { id ->
|
||||
cameraManager.setTorchMode(id, true)
|
||||
isFlashlightOn = true
|
||||
true
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun turnOffFlashlight(): Boolean {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
cameraId?.let { id ->
|
||||
cameraManager.setTorchMode(id, false)
|
||||
isFlashlightOn = false
|
||||
true
|
||||
} ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: CameraAccessException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isFlashlightOn(): Boolean = isFlashlightOn
|
||||
}
|
||||
117
app/src/src/main/java/com/customassistant/MainActivity.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var rvActions: RecyclerView
|
||||
private lateinit var tvSelectedAction: TextView
|
||||
private lateinit var btnSaveAction: Button
|
||||
private lateinit var actionsAdapter: ActionsAdapter
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
|
||||
private val actions = mutableListOf<ActionItem>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
preferenceManager = PreferenceManager(this)
|
||||
initViews()
|
||||
setupActions()
|
||||
setupRecyclerView()
|
||||
setupSaveButton()
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
rvActions = findViewById(R.id.rvActions)
|
||||
tvSelectedAction = findViewById(R.id.tvSelectedAction)
|
||||
btnSaveAction = findViewById(R.id.btnSaveAction)
|
||||
}
|
||||
|
||||
private fun setupActions() {
|
||||
actions.clear()
|
||||
actions.addAll(
|
||||
listOf(
|
||||
ActionItem(
|
||||
type = ActionType.TOGGLE_FLASHLIGHT,
|
||||
title = getString(R.string.action_toggle_flashlight),
|
||||
description = "Turn flashlight on/off"
|
||||
),
|
||||
ActionItem(
|
||||
type = ActionType.LAUNCH_APP,
|
||||
title = getString(R.string.action_launch_app),
|
||||
description = "Launch a selected app"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
actionsAdapter = ActionsAdapter(actions) { selectedAction ->
|
||||
when (selectedAction.type) {
|
||||
ActionType.LAUNCH_APP -> showAppSelectionDialog()
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
tvSelectedAction.text = selectedAction.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rvActions.layoutManager = LinearLayoutManager(this)
|
||||
rvActions.adapter = actionsAdapter
|
||||
}
|
||||
|
||||
private fun setupSaveButton() {
|
||||
btnSaveAction.setOnClickListener {
|
||||
val selectedAction = actionsAdapter.getSelectedAction()
|
||||
if (selectedAction != null) {
|
||||
when (selectedAction.type) {
|
||||
ActionType.TOGGLE_FLASHLIGHT -> {
|
||||
preferenceManager.saveSelectedAction(ActionType.TOGGLE_FLASHLIGHT)
|
||||
Toast.makeText(this, "Flashlight toggle saved!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
ActionType.LAUNCH_APP -> {
|
||||
Toast.makeText(this, "Please select an app first", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Please select an action", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAppSelectionDialog() {
|
||||
val packageManager = packageManager
|
||||
val installedApps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
.filter { packageManager.getLaunchIntentForPackage(it.packageName) != null }
|
||||
.map {
|
||||
Pair(it.loadLabel(packageManager).toString(), it.packageName)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
|
||||
val appNames = installedApps.map { it.first }.toTypedArray()
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.select_app))
|
||||
.setItems(appNames) { _, which ->
|
||||
val selectedApp = installedApps[which]
|
||||
tvSelectedAction.text = "Launch: ${selectedApp.first}"
|
||||
|
||||
btnSaveAction.setOnClickListener {
|
||||
preferenceManager.saveSelectedAction(ActionType.LAUNCH_APP, selectedApp.second)
|
||||
Toast.makeText(this, "App launch saved: ${selectedApp.first}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.customassistant
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class PreferenceManager(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "custom_assistant_prefs"
|
||||
private const val KEY_SELECTED_ACTION = "selected_action"
|
||||
private const val KEY_SELECTED_PACKAGE = "selected_package"
|
||||
}
|
||||
|
||||
fun saveSelectedAction(actionType: ActionType, packageName: String? = null) {
|
||||
prefs.edit()
|
||||
.putString(KEY_SELECTED_ACTION, actionType.name)
|
||||
.putString(KEY_SELECTED_PACKAGE, packageName)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getSelectedAction(): Pair<ActionType?, String?> {
|
||||
val actionName = prefs.getString(KEY_SELECTED_ACTION, null)
|
||||
val packageName = prefs.getString(KEY_SELECTED_PACKAGE, null)
|
||||
val actionType = actionName?.let { ActionType.valueOf(it) }
|
||||
return Pair(actionType, packageName)
|
||||
}
|
||||
}
|
||||
34
app/src/src/main/res/layout/activity_assistant.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_executed"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnClose"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Close"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
45
app/src/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_action"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSelectedAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_action_selected"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:padding="12dp"
|
||||
android:background="@android:color/background_light" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSaveAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save Selected Action"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
41
app/src/src/main/res/layout/item_action.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActionDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
10
app/src/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
11
app/src/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<resources>
|
||||
<string name="app_name">Custom Assistant</string>
|
||||
<string name="action_toggle_flashlight">Toggle Flashlight</string>
|
||||
<string name="action_launch_app">Launch App</string>
|
||||
<string name="select_action">Select Action</string>
|
||||
<string name="no_action_selected">No action selected</string>
|
||||
<string name="action_executed">Action executed</string>
|
||||
<string name="select_app">Select App</string>
|
||||
<string name="flashlight_on">Flashlight ON</string>
|
||||
<string name="flashlight_off">Flashlight OFF</string>
|
||||
</resources>
|
||||
16
app/src/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.CustomAssistant" parent="Theme.Material3.DayNight">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.CustomAssistant.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
3
app/src/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
</full-backup-content>
|
||||
11
app/src/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
4
build.gradle
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
}
|
||||
@@ -1,23 +1,5 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
android.enableJetifier=true
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
17
settings.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Custom Assistant"
|
||||
include ':app'
|
||||