Add v9.1.1: Device registry and remote blocking system
- Implement DeviceRegistry for remote device management - Add dashboard for device tracking and blocking - Remote device blocking capability with admin control - Support for device aliasing and notes - Enhanced device management interface - Dashboard with real-time device status - Configurable registry URL in build config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
155
app/src/main/java/com/streamplayer/DeviceRegistry.java
Normal file
155
app/src/main/java/com/streamplayer/DeviceRegistry.java
Normal file
@@ -0,0 +1,155 @@
|
||||
package com.streamplayer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* Informa al dashboard qué dispositivos tienen instalada la app y permite bloquearlos remotamente.
|
||||
*/
|
||||
public class DeviceRegistry {
|
||||
|
||||
public interface Callback {
|
||||
void onAllowed();
|
||||
|
||||
void onBlocked(String reason);
|
||||
|
||||
void onError(String message);
|
||||
}
|
||||
|
||||
private static final String TAG = "DeviceRegistry";
|
||||
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
|
||||
private final Context appContext;
|
||||
private final OkHttpClient httpClient;
|
||||
private final ExecutorService executorService;
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
public DeviceRegistry(Context context) {
|
||||
this.appContext = context.getApplicationContext();
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.callTimeout(20, TimeUnit.SECONDS)
|
||||
.build();
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public void syncDevice(Callback callback) {
|
||||
if (TextUtils.isEmpty(BuildConfig.DEVICE_REGISTRY_URL)) {
|
||||
postAllowed(callback);
|
||||
return;
|
||||
}
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("deviceId", getDeviceId());
|
||||
payload.put("deviceName", Build.MODEL);
|
||||
payload.put("model", Build.MODEL);
|
||||
payload.put("manufacturer", capitalize(Build.MANUFACTURER));
|
||||
payload.put("osVersion", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")");
|
||||
payload.put("appVersionName", BuildConfig.VERSION_NAME);
|
||||
payload.put("appVersionCode", BuildConfig.VERSION_CODE);
|
||||
|
||||
String endpoint = sanitizeBaseUrl(BuildConfig.DEVICE_REGISTRY_URL) + "/api/devices/register";
|
||||
RequestBody body = RequestBody.create(payload.toString(), JSON);
|
||||
Request request = new Request.Builder()
|
||||
.url(endpoint)
|
||||
.post(body)
|
||||
.build();
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
throw new IOException("HTTP " + response.code());
|
||||
}
|
||||
String responseText = response.body().string();
|
||||
JSONObject json = new JSONObject(responseText);
|
||||
boolean blocked = json.optBoolean("blocked", false);
|
||||
JSONObject deviceJson = json.optJSONObject("device");
|
||||
String reason = json.optString("message");
|
||||
if (TextUtils.isEmpty(reason) && deviceJson != null) {
|
||||
reason = deviceJson.optString("notes", "");
|
||||
}
|
||||
if (blocked) {
|
||||
postBlocked(callback, reason);
|
||||
} else {
|
||||
postAllowed(callback);
|
||||
}
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
Log.w(TAG, "Device sync error", e);
|
||||
postError(callback, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String sanitizeBaseUrl(String base) {
|
||||
if (TextUtils.isEmpty(base)) {
|
||||
return "";
|
||||
}
|
||||
if (base.endsWith("/")) {
|
||||
return base.substring(0, base.length() - 1);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private String getDeviceId() {
|
||||
String id = Settings.Secure.getString(appContext.getContentResolver(),
|
||||
Settings.Secure.ANDROID_ID);
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
id = Build.MODEL + "-" + Build.BOARD + "-" + BuildConfig.VERSION_CODE;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private String capitalize(String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return "";
|
||||
}
|
||||
return value.substring(0, 1).toUpperCase(Locale.getDefault())
|
||||
+ value.substring(1);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
|
||||
private void postAllowed(Callback callback) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
mainHandler.post(callback::onAllowed);
|
||||
}
|
||||
|
||||
private void postBlocked(Callback callback, String reason) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
mainHandler.post(() -> callback.onBlocked(reason));
|
||||
}
|
||||
|
||||
private void postError(Callback callback, String message) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
mainHandler.post(() -> callback.onError(message));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
@@ -40,6 +41,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private SectionEntry currentSection;
|
||||
private UpdateManager updateManager;
|
||||
private AlertDialog updateDialog;
|
||||
private AlertDialog blockedDialog;
|
||||
private DeviceRegistry deviceRegistry;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -85,6 +88,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
deviceRegistry = new DeviceRegistry(this);
|
||||
deviceRegistry.syncDevice(new DeviceRegistry.Callback() {
|
||||
@Override
|
||||
public void onAllowed() {
|
||||
// Device authorized, continue normally.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlocked(String reason) {
|
||||
showBlockedDialog(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String message) {
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
Toast.makeText(MainActivity.this,
|
||||
getString(R.string.device_registry_error, message),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,9 +126,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (updateDialog != null && updateDialog.isShowing()) {
|
||||
updateDialog.dismiss();
|
||||
}
|
||||
if (blockedDialog != null && blockedDialog.isShowing()) {
|
||||
blockedDialog.dismiss();
|
||||
}
|
||||
if (updateManager != null) {
|
||||
updateManager.release();
|
||||
}
|
||||
if (deviceRegistry != null) {
|
||||
deviceRegistry.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectSection(int index) {
|
||||
@@ -276,6 +307,25 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void showBlockedDialog(String reason) {
|
||||
if (isFinishing()) {
|
||||
return;
|
||||
}
|
||||
String finalReason = TextUtils.isEmpty(reason)
|
||||
? getString(R.string.device_blocked_default_reason)
|
||||
: reason;
|
||||
if (blockedDialog != null && blockedDialog.isShowing()) {
|
||||
blockedDialog.dismiss();
|
||||
}
|
||||
blockedDialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.device_blocked_title)
|
||||
.setMessage(getString(R.string.device_blocked_message, finalReason))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.device_blocked_close,
|
||||
(dialog, which) -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
private int getSpanCount() {
|
||||
return getResources().getInteger(R.integer.channel_grid_span);
|
||||
}
|
||||
|
||||
@@ -36,4 +36,9 @@
|
||||
<string name="update_error_install_intent">No se pudo abrir el instalador de paquetes</string>
|
||||
<string name="update_notification_title">StreamPlayer %1$s</string>
|
||||
<string name="update_notification_description">Descargando nueva versión</string>
|
||||
<string name="device_blocked_title">Dispositivo bloqueado</string>
|
||||
<string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</string>
|
||||
<string name="device_blocked_default_reason">Sin motivo especificado.</string>
|
||||
<string name="device_blocked_close">Salir</string>
|
||||
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user