initial commit

This commit is contained in:
2026-08-10 19:44:09 +09:00
commit fb7e3cede7
937 changed files with 85773 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Bluetooth permissions for Android 10 (API 29) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Bluetooth permissions for Android 12+ (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Internet for voice recognition -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.StageControl"
android:screenOrientation="portrait">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:keepScreenOn="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DeviceListActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>
@@ -0,0 +1,112 @@
package com.stage.control;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DeepSeekHelper {
private static final String TAG = "DeepSeekHelper";
// Replace with your actual DeepSeek API Key
private static final String API_KEY = "sk-09319d4513b34ef78be61b8863f1c6a5";
private static final String API_URL = "https://api.deepseek.com/chat/completions";
private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();
private final Executor executor = Executors.newSingleThreadExecutor();
public interface DeepSeekCallback {
void onResponse(String command);
void onError(Throwable t);
}
public void processVoiceCommand(String voiceText, DeepSeekCallback callback) {
executor.execute(() -> {
try {
// Constructing the JSON body according to the official DeepSeek format
JsonObject jsonRequest = new JsonObject();
jsonRequest.addProperty("model", "deepseek-chat");
JsonArray messages = new JsonArray();
JsonObject systemMessage = new JsonObject();
systemMessage.addProperty("role", "system");
systemMessage.addProperty("content", "You are a stage control mapper. Map the user's input to ONE of these characters ONLY: '1', '2', '3', 'S', 'H'. " +
"'1': Action 1, '2': Action 2, '3': Action 3, 'S': Stop, 'H': Home. Return ONLY the character. If no match, return 'UNKNOWN'.");
messages.add(systemMessage);
JsonObject userMessage = new JsonObject();
userMessage.addProperty("role", "user");
userMessage.addProperty("content", voiceText);
messages.add(userMessage);
jsonRequest.add("messages", messages);
jsonRequest.addProperty("stream", false);
// Using official format for RequestBody and Headers
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(jsonRequest.toString(), mediaType);
Request request = new Request.Builder()
.url(API_URL)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + API_KEY)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
callback.onError(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "No error body";
Log.e(TAG, "DeepSeek Error " + response.code() + ": " + errorBody);
callback.onError(new IOException("Error " + response.code() + ": " + errorBody));
return;
}
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
// Parsing according to official response format
String content = jsonResponse.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message")
.get("content").getAsString().trim().toUpperCase();
Log.d(TAG, "DeepSeek Raw: " + content);
String command = "UNKNOWN";
if (content.contains("1")) command = "1";
else if (content.contains("2")) command = "2";
else if (content.contains("3")) command = "3";
else if (content.contains("S")) command = "S";
else if (content.contains("H")) command = "H";
callback.onResponse(command);
}
});
} catch (Exception e) {
callback.onError(e);
}
});
}
}
@@ -0,0 +1,101 @@
package com.stage.control;
import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class DeviceListActivity extends AppCompatActivity {
private ListView listView;
private List<BluetoothDevice> deviceList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_device_list);
listView = findViewById(R.id.lv_devices);
loadPairedDevices();
}
private void loadPairedDevices() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
Toast.makeText(this, "블루투스를 사용할 수 없습니다.", Toast.LENGTH_SHORT).show();
finish();
return;
}
// Check permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
!= PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "블루투스 권한이 필요합니다.", Toast.LENGTH_SHORT).show();
finish();
return;
}
}
Set<BluetoothDevice> pairedDevices = adapter.getBondedDevices();
List<String> displayNames = new ArrayList<>();
deviceList.clear();
if (pairedDevices == null || pairedDevices.isEmpty()) {
Toast.makeText(this, "페어링된 장치가 없습니다.\n설정에서 HC-05를 먼저 페어링하세요.", Toast.LENGTH_LONG).show();
finish();
return;
}
for (BluetoothDevice device : pairedDevices) {
deviceList.add(device);
String name = device.getName() != null ? device.getName() : "Unknown";
displayNames.add(name + "\n" + device.getAddress());
}
ArrayAdapter<String> listAdapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
displayNames
) {
@Override
public View getView(int position, View convertView, android.view.ViewGroup parent) {
View v = super.getView(position, convertView, parent);
TextView tv = (TextView) v;
tv.setTextSize(20f);
tv.setPadding(32, 32, 32, 32);
tv.setTextColor(getResources().getColor(R.color.text_primary, null));
tv.setBackgroundColor(getResources().getColor(R.color.bg_card, null));
return v;
}
};
listView.setAdapter(listAdapter);
listView.setOnItemClickListener((parent, view, position, id) -> {
BluetoothDevice selected = deviceList.get(position);
Intent result = new Intent();
result.putExtra("device_address", selected.getAddress());
result.putExtra("device_name", selected.getName() != null ? selected.getName() : selected.getAddress());
setResult(Activity.RESULT_OK, result);
finish();
});
}
}
@@ -0,0 +1,86 @@
package com.stage.control;
import android.util.Log;
import com.google.ai.client.generativeai.GenerativeModel;
import com.google.ai.client.generativeai.java.GenerativeModelFutures;
import com.google.ai.client.generativeai.type.Content;
import com.google.ai.client.generativeai.type.GenerateContentResponse;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class GeminiHelper {
private static final String TAG = "GeminiHelper";
// Ensure this API key is valid from Google AI Studio
private static final String API_KEY = "AIzaSyCDiEk5wdKM2qZb988k0DAs7E-kXkeE2Xw";
private final GenerativeModelFutures model;
private final Executor executor = Executors.newSingleThreadExecutor();
public interface GeminiResponseCallback {
void onResponse(String command);
void onError(Throwable t);
}
public GeminiHelper() {
// Using gemini-1.5-flash for stability and free tier
GenerativeModel gm = new GenerativeModel("gemini-1.5-flash", API_KEY);
model = GenerativeModelFutures.from(gm);
}
public void processVoiceCommand(String voiceText, GeminiResponseCallback callback) {
// Updated prompt to include Action 3 and clearer mapping
String prompt = "Map this voice input to exactly one character: '1', '2', '3', 'S', or 'H'.\n" +
"- '1' for Action 1, First, One\n" +
"- '2' for Action 2, Second, Two\n" +
"- '3' for Action 3, Third, Three\n" +
"- 'S' for Stop, Halt, Pause\n" +
"- 'H' for Home, Reset, Return\n" +
"If no match, reply 'UNKNOWN'.\n" +
"Input: \"" + voiceText + "\"\n" +
"Reply ONLY with the character.";
Content content = new Content.Builder()
.addText(prompt)
.build();
try {
ListenableFuture<GenerateContentResponse> response = model.generateContent(content);
Futures.addCallback(response, new FutureCallback<GenerateContentResponse>() {
@Override
public void onSuccess(GenerateContentResponse result) {
try {
String text = result.getText();
if (text != null) {
text = text.trim().toUpperCase();
String command = "UNKNOWN";
if (text.contains("1")) command = "1";
else if (text.contains("2")) command = "2";
else if (text.contains("3")) command = "3";
else if (text.contains("S")) command = "S";
else if (text.contains("H")) command = "H";
callback.onResponse(command);
} else {
callback.onResponse("UNKNOWN");
}
} catch (Exception e) {
callback.onError(e);
}
}
@Override
public void onFailure(Throwable t) {
Log.e(TAG, "Gemini Failure: " + t.toString());
callback.onError(t);
}
}, executor);
} catch (Exception e) {
callback.onError(e);
}
}
}
@@ -0,0 +1,248 @@
package com.stage.control;
import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.speech.RecognizerIntent;
import android.util.Log;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "StageControl";
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final int REQUEST_PERMISSIONS = 100;
private TextView tvStatus, tvVoiceResult, tvSentCommand;
private Button btnConnect, btnVoice;
private BluetoothAdapter bluetoothAdapter;
private BluetoothSocket bluetoothSocket;
private OutputStream outputStream;
private String connectedDeviceName = "";
private GeminiHelper geminiHelper;
private enum BtState { NOT_CONNECTED, CONNECTING, CONNECTED }
private BtState currentState = BtState.NOT_CONNECTED;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private ActivityResultLauncher<Intent> devicePickerLauncher, voiceLauncher, enableBtLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
tvStatus = findViewById(R.id.tv_status);
tvVoiceResult = findViewById(R.id.tv_voice_result);
tvSentCommand = findViewById(R.id.tv_sent_command);
btnConnect = findViewById(R.id.btn_connect);
btnVoice = findViewById(R.id.btn_voice);
btnConnect.setOnClickListener(v -> onConnectClicked());
btnVoice.setOnClickListener(v -> onVoiceClicked());
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
Toast.makeText(this, "Bluetooth not supported", Toast.LENGTH_LONG).show();
btnConnect.setEnabled(false);
}
geminiHelper = new GeminiHelper();
registerLaunchers();
checkPermissions();
updateUI(BtState.NOT_CONNECTED);
}
private void registerLaunchers() {
devicePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
connectToDevice(result.getData().getStringExtra("device_address"), result.getData().getStringExtra("device_name"));
}
});
voiceLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
ArrayList<String> results = result.getData().getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if (results != null && !results.isEmpty()) processVoiceWithAI(results.get(0));
} else {
tvVoiceResult.setText("Voice recognition failed.");
}
});
enableBtLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() != Activity.RESULT_OK) Toast.makeText(this, "Please turn on Bluetooth", Toast.LENGTH_SHORT).show();
});
}
private void processVoiceWithAI(String voiceText) {
tvVoiceResult.setText("Analyzing (Gemini): " + voiceText);
geminiHelper.processVoiceCommand(voiceText, new GeminiHelper.GeminiResponseCallback() {
@Override
public void onResponse(String command) {
mainHandler.post(() -> {
// tvVoiceResult.setText(voiceText + " -> " + command);
tvVoiceResult.setText(voiceText); // 인식한 텍스트 그대로 출력
if (!command.equals("UNKNOWN")) sendBluetoothCommand(command);
});
}
@Override
public void onError(Throwable t) {
mainHandler.post(() -> {
String fullError = t.getMessage();
// tvVoiceResult.setText("Gemini Error!\n" + fullError);
tvVoiceResult.setText(voiceText); // 에러 시에도 인식한 텍스트 그대로 출력
Log.e(TAG, "Gemini Error: " + fullError);
String fallback = mapManual(voiceText);
if (fallback != null) {
tvVoiceResult.append("\nFallback: " + fallback);
sendBluetoothCommand(fallback);
}
});
}
});
}
private String mapManual(String t) {
String s = t.toLowerCase();
if (s.contains("push")) return "1";
if (s.contains("spin") || s.contains("spend")) return "2";
if (s.contains("penalty")) return "3";
return null;
}
private void onConnectClicked() {
if (currentState == BtState.CONNECTED) { disconnect(); return; }
if (bluetoothAdapter == null) return;
if (!bluetoothAdapter.isEnabled()) { enableBtLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); return; }
devicePickerLauncher.launch(new Intent(this, DeviceListActivity.class));
}
private void onVoiceClicked() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-US");
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak command");
try {
voiceLauncher.launch(intent);
} catch (Exception e) {
Toast.makeText(this, "Speech recognition unavailable", Toast.LENGTH_SHORT).show();
}
}
private void connectToDevice(String addr, String name) {
updateUI(BtState.CONNECTING);
new Thread(() -> {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
mainHandler.post(() -> {
Toast.makeText(this, "No Bluetooth Permission", Toast.LENGTH_SHORT).show();
updateUI(BtState.NOT_CONNECTED);
});
return;
}
}
BluetoothDevice dev = bluetoothAdapter.getRemoteDevice(addr);
bluetoothSocket = dev.createRfcommSocketToServiceRecord(SPP_UUID);
bluetoothAdapter.cancelDiscovery();
bluetoothSocket.connect();
outputStream = bluetoothSocket.getOutputStream();
mainHandler.post(() -> { connectedDeviceName = name; updateUI(BtState.CONNECTED); });
} catch (Exception e) {
mainHandler.post(() -> {
updateUI(BtState.NOT_CONNECTED);
Toast.makeText(this, "Connection failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
}).start();
}
private void sendBluetoothCommand(String cmd) {
if (outputStream == null) return;
new Thread(() -> {
try {
outputStream.write(cmd.getBytes());
outputStream.flush();
mainHandler.post(() -> tvSentCommand.setText(cmd));
} catch (Exception e) {
mainHandler.post(() -> {
Toast.makeText(this, "Send failed", Toast.LENGTH_SHORT).show();
updateUI(BtState.NOT_CONNECTED);
});
}
}).start();
}
private void disconnect() {
try { if (outputStream != null) outputStream.close(); if (bluetoothSocket != null) bluetoothSocket.close(); } catch (Exception e) {}
updateUI(BtState.NOT_CONNECTED);
}
private void updateUI(BtState s) {
currentState = s;
switch (s) {
case NOT_CONNECTED:
tvStatus.setText("⚫ Not Connected");
tvStatus.setTextColor(getColor(R.color.status_disconnected));
btnConnect.setText("Connect Bluetooth");
btnVoice.setEnabled(false);
btnVoice.setAlpha(0.4f);
break;
case CONNECTING:
tvStatus.setText("🟡 Connecting...");
tvStatus.setTextColor(getColor(R.color.status_connecting));
btnConnect.setText("Connecting...");
btnConnect.setEnabled(false);
break;
case CONNECTED:
tvStatus.setText("🟢 Connected: " + connectedDeviceName);
tvStatus.setTextColor(getColor(R.color.status_connected));
btnConnect.setText("Disconnect");
btnConnect.setEnabled(true);
btnVoice.setEnabled(true);
btnVoice.setAlpha(1.0f);
break;
}
}
private void checkPermissions() {
List<String> needed = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_CONNECT);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_SCAN);
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.RECORD_AUDIO);
if (!needed.isEmpty()) ActivityCompat.requestPermissions(this, needed.toArray(new String[0]), REQUEST_PERMISSIONS);
}
}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/bg_card"/>
<corners android:radius="12dp"/>
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FFFFFF"
android:pathData="M31,63.92V20.33h19.51v43.59h-19.51z M51.44,63.92V20.33h19.51v43.59h-19.51z" />
</vector>
@@ -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:background="@color/bg_dark">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Select Paired Device"
android:textColor="@color/text_primary"
android:textSize="24sp"
android:textStyle="bold"
android:padding="24dp"
android:background="@color/bg_card"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Please select your HC-05 device"
android:textColor="@color/text_label"
android:textSize="16sp"
android:padding="16dp"/>
<ListView
android:id="@+id/lv_devices"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/divider"
android:dividerHeight="1dp"/>
</LinearLayout>
+132
View File
@@ -0,0 +1,132 @@
<?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:background="@color/bg_dark"
android:orientation="vertical"
android:padding="24dp">
<!-- ─── BLUETOOTH STATUS ─── -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/card_bg"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:letterSpacing="0.1"
android:text="📡 Bluetooth Status"
android:textAllCaps="true"
android:textColor="@color/text_label"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚫ Not Connected"
android:textColor="@color/status_disconnected"
android:textSize="26sp"
android:textStyle="bold" />
</LinearLayout>
<!-- ─── VOICE RESULT ─── -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:layout_weight="1.5"
android:background="@drawable/card_bg"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:letterSpacing="0.1"
android:text="🎙 Recognized commands"
android:textAllCaps="true"
android:textColor="@color/text_label"
android:textSize="14sp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_voice_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:text="—"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="normal" />
</ScrollView>
</LinearLayout>
<!-- ─── SENT COMMAND ─── -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:background="@drawable/card_bg"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:letterSpacing="0.1"
android:text="📤 Commands sent"
android:textAllCaps="true"
android:textColor="@color/text_label"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_sent_command"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="—"
android:textColor="@color/accent_green"
android:textSize="60sp"
android:textStyle="bold" />
</LinearLayout>
<!-- ─── CONNECT BUTTON ─── -->
<Button
android:id="@+id/btn_connect"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginBottom="16dp"
android:backgroundTint="@color/btn_connect"
android:stateListAnimator="@null"
android:text="Bluetooth connect"
android:textColor="@color/white"
android:textSize="22sp"
android:textStyle="bold" />
<!-- ─── VOICE COMMAND BUTTON ─── -->
<Button
android:id="@+id/btn_voice"
android:layout_width="match_parent"
android:layout_height="96dp"
android:alpha="0.4"
android:backgroundTint="@color/btn_voice"
android:enabled="false"
android:stateListAnimator="@null"
android:text="🎤 Voice commands"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0D0D0D</color>
<!-- Backgrounds -->
<color name="bg_dark">#0D0D0D</color>
<color name="bg_card">#1A1A2E</color>
<!-- Text -->
<color name="text_primary">#FFFFFF</color>
<color name="text_label">#888888</color>
<color name="white">#FFFFFF</color>
<!-- Status colors -->
<color name="status_connected">#00E676</color>
<color name="status_connecting">#FFD600</color>
<color name="status_disconnected">#FF5252</color>
<!-- Button colors -->
<color name="btn_connect">#1565C0</color>
<color name="btn_disconnect">#B71C1C</color>
<color name="btn_voice">#6A1B9A</color>
<!-- Accent -->
<color name="accent_green">#00E676</color>
<!-- Misc -->
<color name="divider">#333333</color>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Stage Control</string>
</resources>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.StageControl" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/btn_connect</item>
<item name="colorPrimaryVariant">@color/bg_card</item>
<item name="colorOnPrimary">@color/white</item>
<item name="android:windowBackground">@color/bg_dark</item>
<item name="android:statusBarColor">@color/bg_card</item>
<item name="android:navigationBarColor">@color/bg_dark</item>
</style>
</resources>