Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93dbe0941e | |||
| 22d2cf9eda | |||
| d2c3041b0a | |||
| 77c417117a | |||
|
|
2c65578bdd | ||
|
|
6aef195f30 | ||
|
|
cc4696dec2 |
@@ -160,7 +160,13 @@ Para autorizar un dispositivo pendiente:
|
|||||||
3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente.
|
3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente.
|
||||||
4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras.
|
4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras.
|
||||||
|
|
||||||
Cada nuevo registro también dispara una notificación de Telegram para que puedas reaccionar en tiempo real.
|
También puedes gestionar todo desde Telegram:
|
||||||
|
|
||||||
|
- `/allow <deviceId> <token_cliente>` autoriza el dispositivo (verifica el token y lo desbloquea).
|
||||||
|
- `/deny <deviceId> <token_cliente> [motivo]` lo bloquea con un motivo opcional.
|
||||||
|
- `/pending` lista los registros que aún esperan un token válido.
|
||||||
|
|
||||||
|
Cada nuevo registro dispara una notificación de Telegram con la parte admin del token y recordatorios de esos comandos.
|
||||||
|
|
||||||
## 📱 Estructura del Proyecto
|
## 📱 Estructura del Proyecto
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ android {
|
|||||||
applicationId "com.streamplayer"
|
applicationId "com.streamplayer"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 93100
|
versionCode 94600
|
||||||
versionName "9.3.1"
|
versionName "9.4.6"
|
||||||
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
|
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ public class EventRepository {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void prefetchEvents(Context context) {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String json = downloadJson();
|
||||||
|
parseEvents(json);
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_JSON, json)
|
||||||
|
.putLong(KEY_TIMESTAMP, System.currentTimeMillis())
|
||||||
|
.apply();
|
||||||
|
} catch (IOException | JSONException ignored) {
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
private String downloadJson() throws IOException {
|
private String downloadJson() throws IOException {
|
||||||
URL url = new URL(EVENTS_URL);
|
URL url = new URL(EVENTS_URL);
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import android.content.ClipData;
|
|||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
@@ -24,14 +27,18 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final long EVENT_PREFETCH_INTERVAL_MS = TimeUnit.HOURS.toMillis(1);
|
||||||
|
|
||||||
private RecyclerView sectionList;
|
private RecyclerView sectionList;
|
||||||
private RecyclerView contentList;
|
private RecyclerView contentList;
|
||||||
private ProgressBar loadingIndicator;
|
private ProgressBar loadingIndicator;
|
||||||
private TextView messageView;
|
private TextView messageView;
|
||||||
private TextView contentTitle;
|
private TextView contentTitle;
|
||||||
|
private Button eventsRefreshButton;
|
||||||
|
|
||||||
private ChannelAdapter channelAdapter;
|
private ChannelAdapter channelAdapter;
|
||||||
private EventAdapter eventAdapter;
|
private EventAdapter eventAdapter;
|
||||||
@@ -46,6 +53,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private AlertDialog updateDialog;
|
private AlertDialog updateDialog;
|
||||||
private AlertDialog blockedDialog;
|
private AlertDialog blockedDialog;
|
||||||
private DeviceRegistry deviceRegistry;
|
private DeviceRegistry deviceRegistry;
|
||||||
|
private Handler eventPrefetchHandler;
|
||||||
|
private Runnable eventPrefetchRunnable;
|
||||||
|
private boolean isEventsRefreshing;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -57,6 +67,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
loadingIndicator = findViewById(R.id.loading_indicator);
|
loadingIndicator = findViewById(R.id.loading_indicator);
|
||||||
messageView = findViewById(R.id.message_view);
|
messageView = findViewById(R.id.message_view);
|
||||||
contentTitle = findViewById(R.id.content_title);
|
contentTitle = findViewById(R.id.content_title);
|
||||||
|
eventsRefreshButton = findViewById(R.id.events_refresh_button);
|
||||||
|
eventsRefreshButton.setOnClickListener(v -> manualRefreshEvents());
|
||||||
|
applyFocusHighlight(eventsRefreshButton);
|
||||||
|
|
||||||
channelAdapter = new ChannelAdapter(
|
channelAdapter = new ChannelAdapter(
|
||||||
channel -> openPlayer(channel.getName(), channel.getPageUrl()));
|
channel -> openPlayer(channel.getName(), channel.getPageUrl()));
|
||||||
@@ -113,6 +126,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
startEventPrefetchScheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -138,6 +153,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (deviceRegistry != null) {
|
if (deviceRegistry != null) {
|
||||||
deviceRegistry.release();
|
deviceRegistry.release();
|
||||||
}
|
}
|
||||||
|
stopEventPrefetchScheduler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
closeAppCompletely();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectSection(int index) {
|
private void selectSection(int index) {
|
||||||
@@ -157,6 +178,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showChannels(SectionEntry section) {
|
private void showChannels(SectionEntry section) {
|
||||||
|
updateEventsRefreshVisibility(false);
|
||||||
contentTitle.setText(section.title);
|
contentTitle.setText(section.title);
|
||||||
contentList.setLayoutManager(channelLayoutManager);
|
contentList.setLayoutManager(channelLayoutManager);
|
||||||
contentList.setAdapter(channelAdapter);
|
contentList.setAdapter(channelAdapter);
|
||||||
@@ -172,6 +194,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showEvents() {
|
private void showEvents() {
|
||||||
|
updateEventsRefreshVisibility(true);
|
||||||
contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events));
|
contentTitle.setText(currentSection != null ? currentSection.title : getString(R.string.section_events));
|
||||||
contentList.setLayoutManager(eventLayoutManager);
|
contentList.setLayoutManager(eventLayoutManager);
|
||||||
contentList.setAdapter(eventAdapter);
|
contentList.setAdapter(eventAdapter);
|
||||||
@@ -183,6 +206,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadEvents(boolean forceRefresh) {
|
private void loadEvents(boolean forceRefresh) {
|
||||||
|
if (currentSection != null && currentSection.type == SectionEntry.Type.EVENTS) {
|
||||||
|
setEventsRefreshing(true);
|
||||||
|
}
|
||||||
loadingIndicator.setVisibility(View.VISIBLE);
|
loadingIndicator.setVisibility(View.VISIBLE);
|
||||||
messageView.setVisibility(View.GONE);
|
messageView.setVisibility(View.GONE);
|
||||||
eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() {
|
eventRepository.loadEvents(this, forceRefresh, new EventRepository.Callback() {
|
||||||
@@ -196,6 +222,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
} else {
|
} else {
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
loadingIndicator.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
setEventsRefreshing(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,11 +232,48 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
loadingIndicator.setVisibility(View.GONE);
|
loadingIndicator.setVisibility(View.GONE);
|
||||||
messageView.setVisibility(View.VISIBLE);
|
messageView.setVisibility(View.VISIBLE);
|
||||||
messageView.setText(getString(R.string.message_events_error, message));
|
messageView.setText(getString(R.string.message_events_error, message));
|
||||||
|
setEventsRefreshing(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void manualRefreshEvents() {
|
||||||
|
if (isEventsRefreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSection == null || currentSection.type != SectionEntry.Type.EVENTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadEvents(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateEventsRefreshVisibility(boolean visible) {
|
||||||
|
if (eventsRefreshButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eventsRefreshButton.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
if (visible) {
|
||||||
|
eventsRefreshButton.setEnabled(!isEventsRefreshing);
|
||||||
|
eventsRefreshButton.setText(isEventsRefreshing
|
||||||
|
? getString(R.string.events_refreshing)
|
||||||
|
: getString(R.string.events_refresh_action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setEventsRefreshing(boolean refreshing) {
|
||||||
|
isEventsRefreshing = refreshing;
|
||||||
|
if (eventsRefreshButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (eventsRefreshButton.getVisibility() == View.VISIBLE) {
|
||||||
|
eventsRefreshButton.setEnabled(!refreshing);
|
||||||
|
eventsRefreshButton.setText(refreshing
|
||||||
|
? getString(R.string.events_refreshing)
|
||||||
|
: getString(R.string.events_refresh_action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void displayEvents() {
|
private void displayEvents() {
|
||||||
loadingIndicator.setVisibility(View.GONE);
|
loadingIndicator.setVisibility(View.GONE);
|
||||||
if (cachedEvents.isEmpty()) {
|
if (cachedEvents.isEmpty()) {
|
||||||
@@ -233,32 +297,41 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (info == null) {
|
if (info == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean forceUpdate = info.isMandatory(BuildConfig.VERSION_CODE);
|
showUpdateDialog(info);
|
||||||
showUpdateDialog(info, forceUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showUpdateDialog(UpdateManager.UpdateInfo info, boolean mandatory) {
|
private void showUpdateDialog(UpdateManager.UpdateInfo info) {
|
||||||
if (isFinishing()) {
|
if (isFinishing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (updateDialog != null && updateDialog.isShowing()) {
|
if (updateDialog != null && updateDialog.isShowing()) {
|
||||||
updateDialog.dismiss();
|
updateDialog.dismiss();
|
||||||
}
|
}
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
View dialogView = getLayoutInflater().inflate(R.layout.dialog_update, null);
|
||||||
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title)
|
TextView titleView = dialogView.findViewById(R.id.update_title);
|
||||||
.setMessage(buildUpdateMessage(info))
|
TextView messageView = dialogView.findViewById(R.id.update_message);
|
||||||
.setPositiveButton(R.string.update_action_download,
|
Button positiveButton = dialogView.findViewById(R.id.update_positive_button);
|
||||||
(dialog, which) -> updateManager.downloadUpdate(MainActivity.this, info))
|
Button negativeButton = dialogView.findViewById(R.id.update_negative_button);
|
||||||
.setNeutralButton(R.string.update_action_view_release,
|
titleView.setText(R.string.update_required_title);
|
||||||
(dialog, which) -> openReleasePage(info));
|
messageView.setText(buildUpdateMessage(info));
|
||||||
if (mandatory) {
|
AlertDialog dialog = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
||||||
builder.setCancelable(false);
|
.setView(dialogView)
|
||||||
builder.setNegativeButton(R.string.update_action_close_app,
|
.setCancelable(false)
|
||||||
(dialog, which) -> finish());
|
.create();
|
||||||
} else {
|
dialog.setCanceledOnTouchOutside(false);
|
||||||
builder.setNegativeButton(R.string.update_action_later, null);
|
dialog.show();
|
||||||
|
if (dialog.getWindow() != null) {
|
||||||
|
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||||
}
|
}
|
||||||
updateDialog = builder.show();
|
positiveButton.setOnClickListener(v -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
updateManager.downloadUpdate(MainActivity.this, info);
|
||||||
|
});
|
||||||
|
negativeButton.setOnClickListener(v -> closeAppCompletely());
|
||||||
|
applyFocusHighlight(positiveButton);
|
||||||
|
applyFocusHighlight(negativeButton);
|
||||||
|
positiveButton.requestFocus();
|
||||||
|
updateDialog = dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) {
|
private CharSequence buildUpdateMessage(UpdateManager.UpdateInfo info) {
|
||||||
@@ -286,30 +359,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
builder.append(info.getReleaseNotesPreview());
|
builder.append(info.getReleaseNotesPreview());
|
||||||
}
|
}
|
||||||
if (!info.isMandatory(BuildConfig.VERSION_CODE)) {
|
|
||||||
builder.append("\n\n");
|
|
||||||
builder.append(getString(R.string.update_optional_hint));
|
|
||||||
}
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openReleasePage(UpdateManager.UpdateInfo info) {
|
|
||||||
String url = info.releasePageUrl;
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
url = info.downloadUrl;
|
|
||||||
}
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.update_error_missing_url, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
||||||
try {
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(this, R.string.update_error_open_release, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showBlockedDialog(String reason, String tokenPart) {
|
private void showBlockedDialog(String reason, String tokenPart) {
|
||||||
if (isFinishing()) {
|
if (isFinishing()) {
|
||||||
return;
|
return;
|
||||||
@@ -320,24 +372,30 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (blockedDialog != null && blockedDialog.isShowing()) {
|
if (blockedDialog != null && blockedDialog.isShowing()) {
|
||||||
blockedDialog.dismiss();
|
blockedDialog.dismiss();
|
||||||
}
|
}
|
||||||
StringBuilder messageBuilder = new StringBuilder();
|
View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null);
|
||||||
messageBuilder.append(getString(R.string.device_blocked_message, finalReason));
|
TextView messageText = dialogView.findViewById(R.id.blocked_message_text);
|
||||||
if (!TextUtils.isEmpty(tokenPart)) {
|
View tokenContainer = dialogView.findViewById(R.id.blocked_token_container);
|
||||||
messageBuilder.append("\n\n")
|
TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value);
|
||||||
.append(getString(R.string.device_blocked_token_hint, tokenPart));
|
messageText.setText(getString(R.string.device_blocked_message, finalReason));
|
||||||
|
boolean hasToken = !TextUtils.isEmpty(tokenPart);
|
||||||
|
if (hasToken) {
|
||||||
|
tokenContainer.setVisibility(View.VISIBLE);
|
||||||
|
tokenValue.setText(tokenPart);
|
||||||
|
tokenValue.setOnClickListener(v -> copyTokenToClipboard(tokenPart));
|
||||||
|
} else {
|
||||||
|
tokenContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
blockedDialog = new AlertDialog.Builder(this)
|
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
||||||
.setTitle(R.string.device_blocked_title)
|
.setTitle(R.string.device_blocked_title)
|
||||||
.setMessage(messageBuilder.toString())
|
.setView(dialogView)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.device_blocked_close,
|
.setPositiveButton(R.string.device_blocked_close,
|
||||||
(dialog, which) -> finish())
|
(dialog, which) -> closeAppCompletely());
|
||||||
.create();
|
if (hasToken) {
|
||||||
if (!TextUtils.isEmpty(tokenPart)) {
|
builder.setNeutralButton(R.string.device_blocked_copy_token,
|
||||||
blockedDialog.setButton(AlertDialog.BUTTON_NEUTRAL,
|
|
||||||
getString(R.string.device_blocked_copy_token),
|
|
||||||
(dialog, which) -> copyTokenToClipboard(tokenPart));
|
(dialog, which) -> copyTokenToClipboard(tokenPart));
|
||||||
}
|
}
|
||||||
|
blockedDialog = builder.create();
|
||||||
blockedDialog.show();
|
blockedDialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +410,50 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyFocusHighlight(View view) {
|
||||||
|
if (view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
view.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
float scale = hasFocus ? 1.05f : 1f;
|
||||||
|
v.animate().scaleX(scale).scaleY(scale).setDuration(120).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startEventPrefetchScheduler() {
|
||||||
|
if (eventPrefetchHandler == null) {
|
||||||
|
eventPrefetchHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
if (eventPrefetchRunnable == null) {
|
||||||
|
eventPrefetchRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (eventRepository != null) {
|
||||||
|
eventRepository.prefetchEvents(getApplicationContext());
|
||||||
|
}
|
||||||
|
if (eventPrefetchHandler != null) {
|
||||||
|
eventPrefetchHandler.postDelayed(this, EVENT_PREFETCH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
|
||||||
|
}
|
||||||
|
eventPrefetchHandler.post(eventPrefetchRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopEventPrefetchScheduler() {
|
||||||
|
if (eventPrefetchHandler != null && eventPrefetchRunnable != null) {
|
||||||
|
eventPrefetchHandler.removeCallbacks(eventPrefetchRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeAppCompletely() {
|
||||||
|
finishAffinity();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
private int getSpanCount() {
|
private int getSpanCount() {
|
||||||
return getResources().getInteger(R.integer.channel_grid_span);
|
return getResources().getInteger(R.integer.channel_grid_span);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.StrictMode;
|
import android.os.StrictMode;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -58,6 +59,7 @@ public class PlayerActivity extends AppCompatActivity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setContentView(R.layout.activity_player);
|
setContentView(R.layout.activity_player);
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
@@ -254,6 +256,7 @@ public class PlayerActivity extends AppCompatActivity {
|
|||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleOverlay() {
|
private void toggleOverlay() {
|
||||||
|
|||||||
62
app/src/main/res/drawable/bg_dialog_button_primary.xml
Normal file
62
app/src/main/res/drawable/bg_dialog_button_primary.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_disabled" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_focused="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_hovered="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
77
app/src/main/res/drawable/bg_dialog_button_secondary.xml
Normal file
77
app/src/main/res/drawable/bg_dialog_button_secondary.xml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/accent_blue_disabled" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#26FFFFFF" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_focused="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#26FFFFFF" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_hovered="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#26FFFFFF" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
<corners android:radius="28dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
10
app/src/main/res/drawable/bg_dialog_dark.xml
Normal file
10
app/src/main/res/drawable/bg_dialog_dark.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/dialog_background" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="16dp"
|
||||||
|
android:left="16dp"
|
||||||
|
android:right="16dp"
|
||||||
|
android:top="16dp" />
|
||||||
|
</shape>
|
||||||
62
app/src/main/res/drawable/bg_events_refresh_button.xml
Normal file
62
app/src/main/res/drawable/bg_events_refresh_button.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_disabled" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="20dp"
|
||||||
|
android:right="20dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="20dp"
|
||||||
|
android:right="20dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_focused="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="20dp"
|
||||||
|
android:right="20dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_hovered="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue_light" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="20dp"
|
||||||
|
android:right="20dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/accent_blue" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:left="20dp"
|
||||||
|
android:right="20dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
@@ -72,14 +72,37 @@
|
|||||||
app:layout_constraintStart_toEndOf="@id/divider"
|
app:layout_constraintStart_toEndOf="@id/divider"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/content_title"
|
android:id="@+id/content_header"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:gravity="center_vertical"
|
||||||
android:textSize="18sp"
|
android:orientation="horizontal">
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="Canales" />
|
<TextView
|
||||||
|
android:id="@+id/content_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Canales" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/events_refresh_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="@drawable/bg_events_refresh_button"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:text="@string/events_refresh_action"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/loading_indicator"
|
||||||
|
|||||||
48
app/src/main/res/layout/dialog_blocked.xml
Normal file
48
app/src/main/res/layout/dialog_blocked.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/blocked_message_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/blocked_token_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/blocked_token_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/device_blocked_token_label"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/blocked_token_value"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
79
app/src/main/res/layout/dialog_update.xml
Normal file
79
app/src/main/res/layout/dialog_update.xml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?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:background="@drawable/bg_dialog_dark"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/update_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/update_required_title" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:maxHeight="320dp"
|
||||||
|
android:overScrollMode="never">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/update_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/update_button_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/update_positive_button"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_dialog_button_primary"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:text="@string/update_action_download"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/update_negative_button"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_dialog_button_secondary"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:text="@string/update_action_close_app"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -3,4 +3,8 @@
|
|||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
<color name="text_secondary">#B3FFFFFF</color>
|
<color name="text_secondary">#B3FFFFFF</color>
|
||||||
|
<color name="accent_blue">#FF1E88E5</color>
|
||||||
|
<color name="accent_blue_light">#FF42A5F5</color>
|
||||||
|
<color name="accent_blue_disabled">#661E88E5</color>
|
||||||
|
<color name="dialog_background">#FF121212</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -10,16 +10,13 @@
|
|||||||
<string name="update_required_title">Actualización obligatoria</string>
|
<string name="update_required_title">Actualización obligatoria</string>
|
||||||
<string name="update_available_title">Actualización disponible</string>
|
<string name="update_available_title">Actualización disponible</string>
|
||||||
<string name="update_action_download">Actualizar</string>
|
<string name="update_action_download">Actualizar</string>
|
||||||
<string name="update_action_view_release">Ver detalles</string>
|
|
||||||
<string name="update_action_close_app">Salir</string>
|
<string name="update_action_close_app">Salir</string>
|
||||||
<string name="update_action_later">Más tarde</string>
|
|
||||||
<string name="update_current_version">Versión instalada: %1$s (%2$d)</string>
|
<string name="update_current_version">Versión instalada: %1$s (%2$d)</string>
|
||||||
<string name="update_latest_version">Última versión publicada: %1$s (%2$d)</string>
|
<string name="update_latest_version">Última versión publicada: %1$s (%2$d)</string>
|
||||||
<string name="update_min_supported">Versiones anteriores a %1$d ya no están permitidas.</string>
|
<string name="update_min_supported">Versiones anteriores a %1$d ya no están permitidas.</string>
|
||||||
<string name="update_download_size">Tamaño aproximado: %1$s</string>
|
<string name="update_download_size">Tamaño aproximado: %1$s</string>
|
||||||
<string name="update_downloads">Descargas registradas: %1$d</string>
|
<string name="update_downloads">Descargas registradas: %1$d</string>
|
||||||
<string name="update_release_notes_title">Novedades</string>
|
<string name="update_release_notes_title">Novedades</string>
|
||||||
<string name="update_optional_hint">Puedes continuar usando la app, pero recomendamos instalar la actualización para obtener el mejor rendimiento.</string>
|
|
||||||
<string name="update_error_checking">No se pudo verificar actualizaciones (%1$s)</string>
|
<string name="update_error_checking">No se pudo verificar actualizaciones (%1$s)</string>
|
||||||
<string name="update_error_open_release">No se pudo abrir el detalle de la versión</string>
|
<string name="update_error_open_release">No se pudo abrir el detalle de la versión</string>
|
||||||
<string name="update_error_empty_response">Respuesta vacía del servidor de releases</string>
|
<string name="update_error_empty_response">Respuesta vacía del servidor de releases</string>
|
||||||
@@ -40,9 +37,12 @@
|
|||||||
<string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</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_default_reason">Sin motivo especificado.</string>
|
||||||
<string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string>
|
<string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string>
|
||||||
|
<string name="device_blocked_token_label">Código de verificación</string>
|
||||||
<string name="device_blocked_close">Salir</string>
|
<string name="device_blocked_close">Salir</string>
|
||||||
<string name="device_blocked_copy_token">Copiar código</string>
|
<string name="device_blocked_copy_token">Copiar código</string>
|
||||||
<string name="device_blocked_copy_success">Código copiado al portapapeles</string>
|
<string name="device_blocked_copy_success">Código copiado al portapapeles</string>
|
||||||
<string name="device_blocked_copy_error">No se pudo copiar el código</string>
|
<string name="device_blocked_copy_error">No se pudo copiar el código</string>
|
||||||
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
|
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
|
||||||
|
<string name="events_refresh_action">Actualizar ahora</string>
|
||||||
|
<string name="events_refreshing">Actualizando...</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -6,4 +6,18 @@
|
|||||||
<item name="android:statusBarColor">@color/black</item>
|
<item name="android:statusBarColor">@color/black</item>
|
||||||
<item name="android:navigationBarColor">@color/black</item>
|
<item name="android:navigationBarColor">@color/black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="ThemeOverlay.StreamPlayer.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||||
|
<item name="android:windowBackground">@drawable/bg_dialog_dark</item>
|
||||||
|
<item name="android:colorBackground">@color/dialog_background</item>
|
||||||
|
<item name="colorBackgroundFloating">@color/dialog_background</item>
|
||||||
|
<item name="android:textColorPrimary">@color/white</item>
|
||||||
|
<item name="android:textColorSecondary">@color/text_secondary</item>
|
||||||
|
<item name="colorAccent">@color/accent_blue</item>
|
||||||
|
<item name="android:buttonStyle">@style/StreamPlayer.DialogButton</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="StreamPlayer.DialogButton" parent="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog">
|
||||||
|
<item name="android:textColor">@color/accent_blue</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,21 +6,21 @@
|
|||||||
"model": "SM-S928B",
|
"model": "SM-S928B",
|
||||||
"manufacturer": "Samsung",
|
"manufacturer": "Samsung",
|
||||||
"osVersion": "16 (API 36)",
|
"osVersion": "16 (API 36)",
|
||||||
"appVersionName": "9.3.0",
|
"appVersionName": "9.4.6",
|
||||||
"appVersionCode": 93000,
|
"appVersionCode": 94600,
|
||||||
"firstSeen": "2025-11-23T20:53:43.615Z",
|
"firstSeen": "2025-11-23T22:31:13.359Z",
|
||||||
"lastSeen": "2025-11-23T21:12:30.345Z",
|
"lastSeen": "2025-11-23T23:11:07.215Z",
|
||||||
"blocked": false,
|
"blocked": false,
|
||||||
"notes": "no pagó",
|
"notes": "",
|
||||||
"installs": 9,
|
"installs": 7,
|
||||||
"blockedAt": "2025-11-23T20:54:05.413Z",
|
|
||||||
"ip": "181.23.253.20",
|
"ip": "181.23.253.20",
|
||||||
"country": "AR",
|
"country": "AR",
|
||||||
"verification": {
|
"verification": {
|
||||||
"clientPart": "6e05a220abe0ed05",
|
"clientPart": "1714c2bb93670c3f",
|
||||||
"adminPart": "19d6ee4c992ee1a0",
|
"adminPart": "9924c7049211c58c",
|
||||||
"status": "pending",
|
"status": "verified",
|
||||||
"createdAt": "2025-11-23T21:09:04.607Z"
|
"createdAt": "2025-11-23T22:31:13.359Z",
|
||||||
|
"verifiedAt": "2025-11-23T22:33:11.942Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -45,6 +45,7 @@ function renderTable(devices) {
|
|||||||
actions.push('<button data-action="verify" class="primary">Verificar token</button>');
|
actions.push('<button data-action="verify" class="primary">Verificar token</button>');
|
||||||
}
|
}
|
||||||
actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>');
|
actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>');
|
||||||
|
actions.push('<button data-action="delete" class="danger ghost">Borrar</button>');
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>
|
<td>
|
||||||
@@ -97,6 +98,19 @@ async function unblockDevice(deviceId) {
|
|||||||
await fetchDevices();
|
await fetchDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteDevice(deviceId) {
|
||||||
|
const confirmation = confirm('¿Seguro que quieres borrar este dispositivo? Generará un nuevo token cuando se registre de nuevo.');
|
||||||
|
if (!confirmation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetch(`/api/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' });
|
||||||
|
if (!response.ok) {
|
||||||
|
alert('No se pudo borrar el dispositivo');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchDevices();
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyDevice(deviceId) {
|
async function verifyDevice(deviceId) {
|
||||||
const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:');
|
const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:');
|
||||||
if (clientTokenPart === null) {
|
if (clientTokenPart === null) {
|
||||||
@@ -154,6 +168,8 @@ tableBody.addEventListener('click', async (event) => {
|
|||||||
await updateAlias(deviceId);
|
await updateAlias(deviceId);
|
||||||
} else if (action === 'verify') {
|
} else if (action === 'verify') {
|
||||||
await verifyDevice(deviceId);
|
await verifyDevice(deviceId);
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
await deleteDevice(deviceId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ const CONFIG_PATH = path.join(__dirname, 'config.json');
|
|||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || '';
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || '';
|
||||||
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '';
|
const TELEGRAM_CHAT_ID = (process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '').toString();
|
||||||
|
let telegramOffset = 0;
|
||||||
|
let telegramPollingStarted = false;
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -109,22 +111,30 @@ function generateTokenParts() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTelegramNotification(message) {
|
async function sendTelegramMessage(message, options = {}) {
|
||||||
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
|
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
|
||||||
try {
|
try {
|
||||||
await axios.post(url, {
|
const payload = {
|
||||||
chat_id: TELEGRAM_CHAT_ID,
|
chat_id: TELEGRAM_CHAT_ID,
|
||||||
text: message,
|
text: message,
|
||||||
parse_mode: 'Markdown'
|
disable_web_page_preview: true
|
||||||
});
|
};
|
||||||
|
if (options.parseMode) {
|
||||||
|
payload.parse_mode = options.parseMode;
|
||||||
|
}
|
||||||
|
await axios.post(url, payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to send Telegram notification', error.response ? error.response.data : error.message);
|
console.warn('Failed to send Telegram message', error.response ? error.response.data : error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendTelegramNotification(message) {
|
||||||
|
await sendTelegramMessage(message, { parseMode: 'Markdown' });
|
||||||
|
}
|
||||||
|
|
||||||
function formatTelegramMessage(device, verificationRequired) {
|
function formatTelegramMessage(device, verificationRequired) {
|
||||||
const lines = [
|
const lines = [
|
||||||
'*Nuevo registro de dispositivo*',
|
'*Nuevo registro de dispositivo*',
|
||||||
@@ -137,11 +147,143 @@ function formatTelegramMessage(device, verificationRequired) {
|
|||||||
];
|
];
|
||||||
if (verificationRequired && device.verification) {
|
if (verificationRequired && device.verification) {
|
||||||
lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`');
|
lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`');
|
||||||
lines.push('Comparte el token del cliente y este admin para autorizar.');
|
lines.push('Autorizar: `/allow ' + device.deviceId + ' TOKEN_CLIENTE`');
|
||||||
|
lines.push('Rechazar: `/deny ' + device.deviceId + ' TOKEN_CLIENTE [motivo]`');
|
||||||
}
|
}
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initializeTelegramOffset() {
|
||||||
|
if (!TELEGRAM_BOT_TOKEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(url, { params: { timeout: 1 } });
|
||||||
|
if (data.ok && Array.isArray(data.result) && data.result.length > 0) {
|
||||||
|
telegramOffset = data.result[data.result.length - 1].update_id + 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to initialize Telegram offset', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollTelegramUpdates() {
|
||||||
|
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`;
|
||||||
|
const params = {
|
||||||
|
timeout: 25
|
||||||
|
};
|
||||||
|
if (telegramOffset) {
|
||||||
|
params.offset = telegramOffset;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(url, { params });
|
||||||
|
if (data.ok && Array.isArray(data.result)) {
|
||||||
|
for (const update of data.result) {
|
||||||
|
telegramOffset = update.update_id + 1;
|
||||||
|
if (update.message) {
|
||||||
|
processTelegramMessage(update.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Telegram polling error', error.message);
|
||||||
|
} finally {
|
||||||
|
setTimeout(pollTelegramUpdates, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTelegramPolling() {
|
||||||
|
if (telegramPollingStarted || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
telegramPollingStarted = true;
|
||||||
|
await initializeTelegramOffset();
|
||||||
|
pollTelegramUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTelegramMessage(message) {
|
||||||
|
const chatId = message.chat && message.chat.id ? message.chat.id.toString() : '';
|
||||||
|
if (chatId && TELEGRAM_CHAT_ID && chatId !== TELEGRAM_CHAT_ID.toString()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = (message.text || '').trim();
|
||||||
|
if (!text.startsWith('/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = text.split(/\s+/);
|
||||||
|
const command = parts[0].toLowerCase();
|
||||||
|
if (command === '/allow' && parts.length >= 3) {
|
||||||
|
handleTelegramAllow(parts[1], parts[2]);
|
||||||
|
} else if (command === '/deny' && parts.length >= 3) {
|
||||||
|
const reason = parts.slice(3).join(' ') || 'Bloqueado desde Telegram';
|
||||||
|
handleTelegramDeny(parts[1], parts[2], reason);
|
||||||
|
} else if (command === '/pending') {
|
||||||
|
handleTelegramPending();
|
||||||
|
} else {
|
||||||
|
sendTelegramMessage('Comandos disponibles:\n/allow <deviceId> <token_cliente>\n/deny <deviceId> <token_cliente> [motivo]\n/pending');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTelegramAllow(deviceId, clientToken) {
|
||||||
|
const devices = readDevices();
|
||||||
|
const device = devices.find(d => d.deviceId === deviceId);
|
||||||
|
if (!device || !device.verification) {
|
||||||
|
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (device.verification.status === 'verified') {
|
||||||
|
await sendTelegramMessage(`ℹ️ El dispositivo ${deviceId} ya está verificado.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (device.verification.clientPart !== clientToken.trim()) {
|
||||||
|
await sendTelegramMessage('❌ Token del cliente inválido.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device.verification.status = 'verified';
|
||||||
|
device.verification.verifiedAt = new Date().toISOString();
|
||||||
|
device.blocked = false;
|
||||||
|
device.notes = '';
|
||||||
|
writeDevices(devices);
|
||||||
|
await sendTelegramMessage(`✅ Dispositivo ${deviceId} autorizado correctamente.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTelegramDeny(deviceId, clientToken, reason) {
|
||||||
|
const devices = readDevices();
|
||||||
|
const device = devices.find(d => d.deviceId === deviceId);
|
||||||
|
if (!device || !device.verification) {
|
||||||
|
await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (device.verification.clientPart !== clientToken.trim()) {
|
||||||
|
await sendTelegramMessage('❌ Token del cliente inválido.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device.verification.status = 'denied';
|
||||||
|
device.verification.deniedAt = new Date().toISOString();
|
||||||
|
device.blocked = true;
|
||||||
|
device.notes = reason;
|
||||||
|
writeDevices(devices);
|
||||||
|
await sendTelegramMessage(`🚫 Dispositivo ${deviceId} bloqueado. Motivo: ${reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTelegramPending() {
|
||||||
|
const devices = readDevices();
|
||||||
|
const pending = devices.filter(d => !d.verification || d.verification.status !== 'verified');
|
||||||
|
if (!pending.length) {
|
||||||
|
await sendTelegramMessage('No hay dispositivos pendientes.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = pending.slice(0, 10).map(device => {
|
||||||
|
const token = device.verification ? device.verification.clientPart : 'N/A';
|
||||||
|
return `${device.deviceId} - Token cliente: ${token}`;
|
||||||
|
});
|
||||||
|
await sendTelegramMessage('Pendientes:\n' + lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
@@ -318,4 +460,5 @@ app.delete('/api/devices/:deviceId', (req, res) => {
|
|||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
ensureDataFile();
|
ensureDataFile();
|
||||||
console.log(`StreamPlayer dashboard server listening on port ${PORT}`);
|
console.log(`StreamPlayer dashboard server listening on port ${PORT}`);
|
||||||
|
startTelegramPolling();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"versionCode": 93000,
|
"versionCode": 94600,
|
||||||
"versionName": "9.3.0",
|
"versionName": "9.4.6",
|
||||||
"minSupportedVersionCode": 91000,
|
"minSupportedVersionCode": 91000,
|
||||||
"forceUpdate": false,
|
"forceUpdate": true,
|
||||||
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.3.0/StreamPlayer-v9.3.0.apk",
|
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.4.6/StreamPlayer-v9.4.6.apk",
|
||||||
"fileName": "StreamPlayer-v9.3.0.apk",
|
"fileName": "StreamPlayer-v9.4.6.apk",
|
||||||
"sizeBytes": 5941443,
|
"sizeBytes": 5951142,
|
||||||
"notes": "StreamPlayer v9.3.0\n\nMejoras importantes en esta versión:\n\n- Sistema de seguridad mejorado con verificación avanzada\n- Nuevas herramientas de administración y control\n- Mejoras en la gestión de dispositivos\n- Interfaz de usuario optimizada\n- Mayor rendimiento y estabilidad\n- Correcciones de seguridad mejoradas\n- Sistema de notificaciones más eficiente\n\nEsta actualización fortalece la seguridad y mejora la experiencia general de uso manteniendo todas las funcionalidades existentes."
|
"notes": "StreamPlayer v9.4.6\n\nMejoras clave:\n\n- Diálogo de actualización totalmente personalizado con fondo oscuro, compatible con OneUI 8/Android 16 y sin cuadros blancos.\n- Botones \"Actualizar\" y \"Salir\" ahora tienen indicadores de foco/hover y animaciones para controles de TV.\n- El botón \"Actualizar ahora\" en Eventos también hereda resaltado al navegar con control remoto.\n- Se mantiene el flujo obligatorio de actualización y la descarga directa desde la app.\n\nRecomendamos instalar esta versión para garantizar la mejor experiencia en televisores y dispositivos Samsung recientes."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user