From 0877c916408809ab335be3e09eff156435923db3 Mon Sep 17 00:00:00 2001
From: Yeshey <41551785+Yeshey@users.noreply.github.com>
Date: Tue, 29 Apr 2025 03:42:31 +0100
Subject: [PATCH 1/4] doesnt immidiatley crash on android 14
---
app/build.gradle | 60 +-
app/src/main/AndroidManifest.xml | 33 +-
.../activity/FavoritesListsActivity.java | 572 ++++++++----
.../fmradio/activity/MainActivity.java | 813 ++++++++++++------
.../fmradio/activity/SettingsActivity.java | 15 +-
.../fmradio/controller/RadioController.java | 98 ++-
.../vlad805/fmradio/service/FMService.java | 65 +-
build.gradle | 7 +-
gradle.properties | 3 +-
9 files changed, 1167 insertions(+), 499 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 25cb2ee..aaee379 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,7 +15,7 @@ android {
applicationId dev ? "com.vlad805.fmradio.dev" : "com.vlad805.fmradio"
minSdkVersion 21
//noinspection ExpiredTargetSdkVersion
- targetSdkVersion 22
+ targetSdkVersion 34
//noinspection GroovyAssignabilityCheck
versionCode versionMajor * 10000 + versionMinor * 100 + versionPatch
//noinspection GroovyAssignabilityCheck
@@ -40,6 +40,18 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
+ afterEvaluate {
+ tasks.withType(JavaCompile).configureEach {
+ // Emit basic deprecation notes
+ options.deprecation = true
+ // Emit full @Deprecated usage details
+ options.compilerArgs += ['-Xlint:deprecation']
+ // If you also care about unchecked warnings:
+ options.compilerArgs += ['-Xlint:unchecked']
+ }
+ }
+
+
productFlavors {
}
lint {
@@ -49,33 +61,43 @@ android {
//noinspection GroovyMissingReturnStatement
- applicationVariants.all { variant ->
- variant.outputs.all {
+ applicationVariants.configureEach { variant ->
+ variant.outputs.configureEach {
outputFileName = "${defaultConfig.applicationId}-${variant.versionName}.apk"
}
}
}
-configurations.all {
- resolutionStrategy.eachDependency { DependencyResolveDetails details ->
- def requested = details.requested
- if (requested.group == "com.android.support") {
- if (!requested.name.startsWith("multidex")) {
- details.useVersion "26.+"
- }
- }
+configurations.configureEach {
+ resolutionStrategy {
+ // Force all kotlin standard library artifacts to a consistent version
+ // Use the version compatible with your androidx libraries (1.8.10 seems likely based on error)
+ force "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10"
+
+ // You can likely remove the old com.android.support rule now
+ // eachDependency { DependencyResolveDetails details ->
+ // def requested = details.requested
+ // if (requested.group == "com.android.support") {
+ // if (!requested.name.startsWith("multidex")) {
+ // details.useVersion "26.+"
+ // }
+ // }
+ // }
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'androidx.core:core:1.6.0-rc01'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- implementation 'androidx.recyclerview:recyclerview:1.2.1'
- implementation 'androidx.annotation:annotation:1.2.0'
- implementation 'androidx.preference:preference:1.1.1'
- implementation 'androidx.media:media:1.3.1'
- implementation 'com.github.naman14:TAndroidLame:1.1'
- implementation 'com.github.atsushi-ageet:tray:0.14.0'
+ // Example Updates (check for latest stable versions!)
+ implementation 'androidx.core:core-ktx:1.10.1' // Use ktx version if converting to Kotlin later, or core:1.10.1
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.recyclerview:recyclerview:1.3.1'
+ implementation 'androidx.annotation:annotation:1.6.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1' // Use ktx version if converting to Kotlin later, or preference:1.2.1
+ implementation 'androidx.media:media:1.6.0'
+ implementation 'com.github.naman14:TAndroidLame:1.1' // Check if this still works/is maintained
+ implementation 'com.github.atsushi-ageet:tray:0.14.0' // Check for updates
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index eb70c9b..1f4883c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,9 +6,23 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -38,12 +53,15 @@
+ tools:ignore="ExportedService"
+ android:foregroundServiceType="mediaPlayback"/>
+
-
+
@@ -51,4 +69,7 @@
+
+
+
diff --git a/app/src/main/java/com/vlad805/fmradio/activity/FavoritesListsActivity.java b/app/src/main/java/com/vlad805/fmradio/activity/FavoritesListsActivity.java
index e4e0e5b..8b38267 100644
--- a/app/src/main/java/com/vlad805/fmradio/activity/FavoritesListsActivity.java
+++ b/app/src/main/java/com/vlad805/fmradio/activity/FavoritesListsActivity.java
@@ -1,5 +1,6 @@
package com.vlad805.fmradio.activity;
+import android.Manifest; // <-- Add
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
@@ -7,19 +8,25 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager; // <-- Add
+import android.os.Build; // <-- Add
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
+import androidx.annotation.NonNull; // <-- Add
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat; // <-- Add
+import androidx.core.content.ContextCompat; // <-- Add
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.vlad805.fmradio.C;
@@ -36,11 +43,33 @@
import com.vlad805.fmradio.view.OnDragListener;
import com.vlad805.fmradio.view.SimpleItemTouchHelperCallback;
import com.vlad805.fmradio.view.adapter.FavoriteAdapter;
+import androidx.core.content.ContextCompat; // <-- Add this import
+
import java.io.FileNotFoundException;
+import java.util.ArrayList; // <-- Add if missing
import java.util.List;
public class FavoritesListsActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener, OnDragListener {
+
+ // --- Permission Request Codes & Strings ---
+ private static final int REQUEST_CODE_PERMISSIONS_STORAGE = 201;
+ private static final String[] PERMISSIONS_STORAGE; // Initialized statically below
+
+ static {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
+ PERMISSIONS_STORAGE = new String[]{
+ Manifest.permission.READ_MEDIA_AUDIO // Placeholder, favorites need rework
+ };
+ } else {
+ PERMISSIONS_STORAGE = new String[]{
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ };
+ }
+ }
+
+
private ProgressDialog mProgress;
private Menu mMenu;
private String mCurrentNameList;
@@ -49,187 +78,180 @@ public class FavoritesListsActivity extends AppCompatActivity implements Adapter
private FavoriteController mController;
private Spinner mSpinner;
- private List mFavoriteListNames;
+ private List mFavoriteListNames = new ArrayList<>(); // Initialize to avoid nulls
private FavoriteAdapter mAdapter;
- private List mStationsList;
+ private List mStationsList = new ArrayList<>(); // Initialize to avoid nulls
private ItemTouchHelper mItemTouchHelper;
private Toast mToast;
+ private RecyclerView mRecycler; // Make RecyclerView a field for enabling/disabling
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_favorites_lists);
mRadioCtl = new RadioController(this);
-
mToast = Toast.create(this);
- mController = new FavoriteController(this);
+ mController = new FavoriteController(this); // OK to initialize
- initUI();
+ // --- Permission Check ---
+ // We need storage to read/write favorites JSON files
+ checkAndRequestStoragePermission();
}
- /**
- * Initialize UI
- */
- private void initUI() {
+ private void checkAndRequestStoragePermission() {
+ if (hasPermissions(PERMISSIONS_STORAGE)) {
+ // Permission granted, proceed with full UI initialization
+ initUI();
+ } else {
+ // Request permission
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE_PERMISSIONS_STORAGE);
+ // Initialize basic UI structure but disable/hide elements needing storage
+ initBaseUI(); // Separate method for non-storage dependent UI
+ mToast.text("Storage permission needed to manage favorites.").show();
+ }
+ }
+
+ private boolean hasPermissions(String... permissions) {
+ if (permissions != null) {
+ for (String permission : permissions) {
+ if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+
+ // Initialize only UI elements that DON'T immediately require storage access
+ private void initBaseUI() {
final Toolbar toolbar = findViewById(R.id.favorite_list_toolbar);
mSpinner = findViewById(R.id.favorite_list_lists);
+ mRecycler = findViewById(R.id.favorite_list_content); // Assign to field
setSupportActionBar(toolbar);
-
final ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
- mFavoriteListNames = mController.getFavoriteLists();
-
- final RecyclerView recycler = findViewById(R.id.favorite_list_content);
- recycler.addOnItemTouchListener(new RecyclerItemClickListener(this, recycler, new RecyclerItemClickListener.OnItemClickListener() {
- @Override
- public void onItemClick(final View view, final int position) {
- final FavoriteStation station = mStationsList.get(position);
-
- mRadioCtl.setFrequency(station.getFrequency());
- }
-
- @Override
- public void onLongItemClick(final View view, int position) {
- // TODO: open menu here
- }
- }));
-
- mAdapter = new FavoriteAdapter(mStationsList, this);
- recycler.setAdapter(mAdapter);
-
- reloadLists();
- reloadContent();
-
- final ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
- mItemTouchHelper = new ItemTouchHelper(callback);
- mItemTouchHelper.attachToRecyclerView(recycler);
-
- // Listen this only after setSelection
- mSpinner.setOnItemSelectedListener(this);
- }
-
- @Override
- public void onStartDrag(final RecyclerView.ViewHolder viewHolder) {
- mItemTouchHelper.startDrag(viewHolder);
- }
-
- @Override
- public void onEndDrag() {
- mController.save();
- }
-
- /**
- * Update list of favorite lists in spinner
- */
- private void reloadLists() {
- mFavoriteListNames = mController.getFavoriteLists();
-
- mCurrentNameList = mController.getCurrentFavoriteList();
- final int position = mFavoriteListNames.indexOf(mCurrentNameList);
-
- final ArrayAdapter> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, mFavoriteListNames.toArray(new String[0]));
+ // Disable elements until permission granted
+ if(mSpinner != null) mSpinner.setEnabled(false);
+ if(mRecycler != null) {
+ mRecycler.setVisibility(View.GONE); // Hide list view
+ }
- mSpinner.setAdapter(adapter);
- mSpinner.setSelection(position);
}
- /**
- * Update list of stations when favorite list selected
- */
- private void reloadContent() {
- if (mStationsList != null) {
- mStationsList.clear();
- }
-
- mStationsList = mController.getStationsInCurrentList();
+ // Initialize the rest of the UI, assuming permissions are granted
+ private void initUI() {
+ initBaseUI(); // Setup toolbar etc.
- mAdapter.setDataset(mStationsList);
- mAdapter.notifyDataSetChanged();
+ // Enable elements now
+ if(mSpinner != null) mSpinner.setEnabled(true);
+ if(mRecycler != null) mRecycler.setVisibility(View.VISIBLE); // Show list view
- if (mMenu != null) {
- mMenu.findItem(R.id.menu_favorite_remove).setVisible(!mCurrentNameList.equals(FavoriteController.DEFAULT_NAME));
- }
- }
+ // Now safe to access FavoriteController for reading lists
+ try {
+ mFavoriteListNames = mController.getFavoriteLists();
+
+ // Setup RecyclerView
+ if (mRecycler != null) {
+ mRecycler.addOnItemTouchListener(new RecyclerItemClickListener(this, mRecycler, new RecyclerItemClickListener.OnItemClickListener() {
+ @Override
+ public void onItemClick(final View view, final int position) {
+ if (mStationsList != null && position < mStationsList.size()) {
+ final FavoriteStation station = mStationsList.get(position);
+ mRadioCtl.setFrequency(station.getFrequency());
+ }
+ }
+
+ @Override
+ public void onLongItemClick(final View view, int position) {
+ // TODO: open menu here if needed
+ }
+ }));
+
+ // Initialize adapter with an empty list first, then load data
+ mAdapter = new FavoriteAdapter(mStationsList, this); // Use initialized empty list
+ mRecycler.setAdapter(mAdapter);
+
+ // Item Touch Helper for drag/drop
+ final ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
+ mItemTouchHelper = new ItemTouchHelper(callback);
+ mItemTouchHelper.attachToRecyclerView(mRecycler);
+ }
- /**
- * Listener for selected item in spinner (favorite list)
- */
- @Override
- public void onItemSelected(final AdapterView> parent, final View view, final int position, final long id) {
- String item = (String) parent.getItemAtPosition(position);
- try {
- mController.setCurrentFavoriteList(item);
- mCurrentNameList = item;
+ // Load data
+ reloadLists();
reloadContent();
- // need to remove and replace by broadcast?
- final Intent intent = new Intent().putExtra("changed", true);
- setResult(Activity.RESULT_OK, intent);
+ // Listen for spinner changes only after initial setup
+ if (mSpinner != null) mSpinner.setOnItemSelectedListener(this);
- sendBroadcast(new Intent(C.Event.FAVORITE_LIST_CHANGED));
- } catch (final FileNotFoundException e) {
- mToast.text("Not found this list").show();
+ } catch (Exception e) {
+ // This might happen if storage access fails unexpectedly
+ mToast.text("Error initializing favorites data: " + e.getMessage()).show();
+ if(mSpinner != null) mSpinner.setEnabled(false);
+ if(mRecycler != null) mRecycler.setVisibility(View.GONE);
}
}
+ // --- Permission Result Handler ---
@Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- super.onCreateOptionsMenu(menu);
- mMenu = menu;
- getMenuInflater().inflate(R.menu.favorite, menu);
- return true;
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == REQUEST_CODE_PERMISSIONS_STORAGE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Permission granted, initialize the full UI now
+ mToast.text("Storage permission granted.").show();
+ initUI(); // Call the full initialization method
+ } else {
+ // Permission denied
+ mToast.text("Storage permission denied. Cannot manage favorites. Exiting.").show();
+ // Keep UI disabled or finish the activity?
+ finish(); // Example: Exit if storage is essential
+ }
+ }
}
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- menu.findItem(R.id.menu_favorite_remove).setVisible(!mCurrentNameList.equals(FavoriteController.DEFAULT_NAME));
- return super.onPrepareOptionsMenu(menu);
- }
+ // --- Methods potentially needing permission check before action ---
- @SuppressLint("NonConstantResourceId")
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_favorite_add: {
- addDialog();
- break;
- }
-
- case R.id.menu_favorite_remove: {
- removeDialog();
- break;
- }
-
- case R.id.menu_favorite_search: {
- searchDialog();
- break;
- }
-
- case android.R.id.home: {
- finish();
- break;
- }
+ public void onEndDrag() {
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ mToast.text("Storage permission required to save changes.").show();
+ // Re-request or prevent save? For now, just show toast.
+ // ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE_PERMISSIONS_STORAGE);
+ return;
}
- return super.onOptionsItemSelected(item);
+ mController.save();
}
private void addDialog() {
+ // Check permission before showing dialog that leads to adding a list
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ mToast.text("Storage permission required to add lists.").show();
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE_PERMISSIONS_STORAGE);
+ return;
+ }
+
final EditTextDialog dialog = new EditTextDialog(this, "", title -> {
try {
mController.addList(title);
- mToast.text("Created list '" + title + "'");
+ mToast.text("Created list '" + title + "'").show(); // Use show()
reloadLists();
- } catch (Error e) {
+ } catch (Error e) { // Catch specific error if defined, or Exception
mToast.text(e.getMessage()).show();
+ } catch (Exception e) { // Catch potential file IO exceptions
+ mToast.text("Error creating list: " + e.getMessage()).show();
}
});
final EditText et = dialog.getView();
@@ -257,85 +279,327 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
@Override
public void afterTextChanged(Editable s) {}
});
-
dialog.open();
}
- private void removeDialog() {
-
+ private void removeDialog() {
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ mToast.text("Storage permission required to remove lists.").show();
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE_PERMISSIONS_STORAGE);
+ return;
+ }
+ // Check if a list is selected and it's not the default one
+ if (mCurrentNameList == null || mCurrentNameList.equals(FavoriteController.DEFAULT_NAME)) {
+ mToast.text("Cannot remove the default list.").show();
+ return;
+ }
final AlertDialog.Builder dialog = new AlertDialog.Builder(this)
.setTitle(R.string.favorite_list_remove_title)
.setMessage(getString(R.string.favorite_list_remove_message, mCurrentNameList))
.setCancelable(false)
.setPositiveButton(android.R.string.yes, (dlg, buttonId) -> {
- mController.removeList(mCurrentNameList);
- mController.reload();
- reloadLists();
- reloadContent();
+ try {
+ boolean removed = mController.removeList(mCurrentNameList);
+ if(removed) {
+ mController.reload(); // Reload internal state after removal
+ reloadLists(); // Update spinner
+ reloadContent(); // Update RecyclerView
+ mToast.text("List removed.").show();
+ } else {
+ mToast.text("Failed to remove list.").show();
+ }
+ } catch (Exception e) {
+ mToast.text("Error removing list: " + e.getMessage()).show();
+ }
})
.setNegativeButton(android.R.string.no, (dlg, buttonId) -> {})
.setIcon(android.R.drawable.ic_dialog_alert);
dialog.create().show();
}
+
+ // --- Other existing methods (initUI needs careful review) ---
+
+ // reloadLists and reloadContent now assume permission is granted,
+ // as they are called from initUI or after actions that should have checked.
+
+ private void reloadLists() {
+ if (!hasPermissions(PERMISSIONS_STORAGE)) return; // Safety check
+ try {
+ mFavoriteListNames = mController.getFavoriteLists();
+ mCurrentNameList = mController.getCurrentFavoriteList();
+ final int position = mFavoriteListNames.indexOf(mCurrentNameList);
+
+ final ArrayAdapter> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, mFavoriteListNames.toArray(new String[0]));
+
+ if (mSpinner != null) {
+ mSpinner.setAdapter(adapter);
+ // Ensure position is valid before setting selection
+ if (position >= 0 && position < mFavoriteListNames.size()) {
+ mSpinner.setSelection(position);
+ } else if (!mFavoriteListNames.isEmpty()) {
+ mSpinner.setSelection(0); // Select first if current not found
+ // Optionally try setting default
+ try { mController.setCurrentFavoriteList(mFavoriteListNames.get(0)); } catch (Exception ignored) {}
+ }
+ }
+ } catch(Exception e) {
+ mToast.text("Error loading favorite lists: " + e.getMessage()).show();
+ }
+ }
+
+
+ private void reloadContent() {
+ if (!hasPermissions(PERMISSIONS_STORAGE)) return; // Safety check
+
+ // Ensure adapter exists
+ if (mAdapter == null) {
+ mAdapter = new FavoriteAdapter(new ArrayList<>(), this); // Create with empty list
+ if (mRecycler != null) mRecycler.setAdapter(mAdapter);
+ }
+
+ try {
+ // Clear the existing list in the adapter before getting the new one
+ if (mStationsList != null) {
+ mStationsList.clear();
+ } else {
+ mStationsList = new ArrayList<>();
+ }
+
+ // Get the new list and add items to the existing list object
+ List newStations = mController.getStationsInCurrentList();
+ if (newStations != null) {
+ mStationsList.addAll(newStations);
+ }
+
+ // Notify adapter
+ mAdapter.setDataset(mStationsList); // Ensure adapter uses the correct list instance
+ mAdapter.notifyDataSetChanged();
+
+ // Update menu visibility based on the current list name
+ if (mMenu != null) {
+ MenuItem removeItem = mMenu.findItem(R.id.menu_favorite_remove);
+ if (removeItem != null) {
+ removeItem.setVisible(!FavoriteController.DEFAULT_NAME.equals(mCurrentNameList));
+ }
+ }
+ } catch (Exception e) {
+ mToast.text("Error loading stations: " + e.getMessage()).show();
+ }
+ }
+
+
+ @Override
+ public void onItemSelected(final AdapterView> parent, final View view, final int position, final long id) {
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ // If permission was revoked, prevent changing list
+ mToast.text("Storage permission needed to change list.").show();
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE_PERMISSIONS_STORAGE);
+ // Revert spinner selection?
+ int previousPosition = mFavoriteListNames.indexOf(mCurrentNameList);
+ if (previousPosition >= 0) parent.setSelection(previousPosition);
+ return;
+ }
+
+ String item = (String) parent.getItemAtPosition(position);
+ if (item == null || item.equals(mCurrentNameList)) {
+ return; // No change
+ }
+
+ try {
+ mController.setCurrentFavoriteList(item);
+ mCurrentNameList = item; // Update local cache of current name
+ reloadContent(); // Reload stations for the new list
+
+ // Notify MainActivity that favorites might have changed (for seek by favorite)
+ final Intent intent = new Intent().putExtra("changed", true);
+ setResult(Activity.RESULT_OK, intent);
+ sendBroadcast(new Intent(C.Event.FAVORITE_LIST_CHANGED));
+
+ } catch (final FileNotFoundException e) {
+ mToast.text("Not found this list").show();
+ // Revert selection?
+ int previousPosition = mFavoriteListNames.indexOf(mCurrentNameList); // Use previous name
+ if (previousPosition >= 0) parent.setSelection(previousPosition);
+ } catch (Exception e) {
+ mToast.text("Error changing list: " + e.getMessage()).show();
+ int previousPosition = mFavoriteListNames.indexOf(mCurrentNameList);
+ if (previousPosition >= 0) parent.setSelection(previousPosition);
+ }
+ }
+
+ // ... (rest of the methods: onCreateOptionsMenu, onPrepareOptionsMenu, onOptionsItemSelected - searchDialog check) ...
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ mMenu = menu;
+ getMenuInflater().inflate(R.menu.favorite, menu);
+ // Initial visibility depends on whether permission is granted and data loaded
+ if (hasPermissions(PERMISSIONS_STORAGE) && mCurrentNameList != null) {
+ MenuItem removeItem = menu.findItem(R.id.menu_favorite_remove);
+ if (removeItem != null) {
+ removeItem.setVisible(!mCurrentNameList.equals(FavoriteController.DEFAULT_NAME));
+ }
+ } else {
+ MenuItem removeItem = menu.findItem(R.id.menu_favorite_remove);
+ if (removeItem != null) removeItem.setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ // Check permission and list name again
+ if (hasPermissions(PERMISSIONS_STORAGE) && mCurrentNameList != null) {
+ MenuItem removeItem = menu.findItem(R.id.menu_favorite_remove);
+ if (removeItem != null) {
+ removeItem.setVisible(!mCurrentNameList.equals(FavoriteController.DEFAULT_NAME));
+ }
+ } else {
+ MenuItem removeItem = menu.findItem(R.id.menu_favorite_remove);
+ if (removeItem != null) removeItem.setVisible(false);
+ }
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+
+ @SuppressLint("NonConstantResourceId")
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId(); // Use final variable
+ if (itemId == R.id.menu_favorite_add) {
+ addDialog(); // Check inside addDialog
+ } else if (itemId == R.id.menu_favorite_remove) {
+ removeDialog(); // Check inside removeDialog
+ } else if (itemId == R.id.menu_favorite_search) {
+ searchDialog(); // Needs RECORD_AUDIO check
+ } else if (itemId == android.R.id.home) {
+ finish();
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+
private void searchDialog() {
+ // 1) RECORD_AUDIO check
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ != PackageManager.PERMISSION_GRANTED) {
+ mToast.text("Audio permission required to check tuner state for search.").show();
+ return; // skip if no audio permission
+ }
+
+ // 2) check tuner state asynchronously
mRadioCtl.getCurrentState(state -> {
if (state.getStatus() != TunerStatus.ENABLED) {
mToast.text(R.string.favorite_list_search_error_tuner_not_enabled).show();
return;
}
- final AlertDialog.Builder dialog = new AlertDialog.Builder(this)
+ // 3) STORAGE check
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ mToast.text("Storage permission required to save search results.").show();
+ ActivityCompat.requestPermissions(
+ this,
+ PERMISSIONS_STORAGE,
+ REQUEST_CODE_PERMISSIONS_STORAGE
+ );
+ return;
+ }
+
+ // 4) confirmation dialog
+ new AlertDialog.Builder(this)
.setTitle(R.string.favorite_list_search_confirm_title)
.setMessage(R.string.favorite_list_search_confirm_message)
+ .setIcon(android.R.drawable.ic_dialog_alert)
.setCancelable(false)
- .setPositiveButton(R.string.favorite_list_search_confirm_continue, (dlg, buttonId) -> searchStart())
- .setNegativeButton(R.string.favorite_list_search_confirm_discard, (dlg, buttonId) -> {
- })
- .setIcon(android.R.drawable.ic_dialog_alert);
- dialog.create().show();
+ .setPositiveButton(R.string.favorite_list_search_confirm_continue,
+ (dlg, which) -> searchStart())
+ .setNegativeButton(R.string.favorite_list_search_confirm_discard, null)
+ .show();
});
}
+ // ... (rest of methods: searchStart, mSearchDone, onNothingSelected) ...
private void searchStart() {
mRadioCtl.hwSearch();
-
mProgress = ProgressDialog.create(this).text(R.string.favorite_list_search_progress).show();
+ // Original line:
+ // registerReceiver(mSearchDone, new IntentFilter(C.Event.HW_SEARCH_DONE));
- registerReceiver(mSearchDone, new IntentFilter(C.Event.HW_SEARCH_DONE));
+ // New line using ContextCompat:
+ ContextCompat.registerReceiver(this, mSearchDone, new IntentFilter(C.Event.HW_SEARCH_DONE), ContextCompat.RECEIVER_NOT_EXPORTED);
}
private final BroadcastReceiver mSearchDone = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
- if (intent == null || intent.getAction() == null) {
+ if (intent == null || intent.getAction() == null || !C.Event.HW_SEARCH_DONE.equals(intent.getAction())) {
return;
}
unregisterReceiver(mSearchDone);
+ if (mProgress != null) {
+ mProgress.hide();
+ mProgress = null; // Clean up progress dialog
+ }
+
+
+ // Check permission again before writing results
+ if (!hasPermissions(PERMISSIONS_STORAGE)) {
+ mToast.text("Storage permission denied. Cannot save search results.").show();
+ return;
+ }
+
+
final int[] frequencies = intent.getIntArrayExtra(C.Key.STATION_LIST);
+ if (frequencies == null || frequencies.length == 0) {
+ mToast.text("No stations found.").show();
+ return; // Don't clear list if nothing found
+ }
- final List stations = mController.getStationsInCurrentList();
- stations.clear();
- for (final int kHz : frequencies) {
- stations.add(new FavoriteStation(kHz, Utils.getMHz(kHz)));
+ // Ensure mStationsList is initialized
+ if (mStationsList == null) {
+ mStationsList = new ArrayList<>();
}
- mController.save();
+ // Use the controller to get/clear the list, then save
+ try {
+ List stations = mController.getStationsInCurrentList(); // Get the list managed by controller
+ stations.clear(); // Clear it
- reloadLists();
- reloadContent();
+ for (final int kHz : frequencies) {
+ stations.add(new FavoriteStation(kHz, Utils.getMHz(kHz))); // Add new stations
+ }
- if (mProgress != null) {
- mProgress.hide();
+ mController.save(); // Save the modified list
+
+ // Update UI
+ reloadLists(); // Might not be needed unless list name changed
+ reloadContent(); // Update recycler view
+ mToast.text("Found " + frequencies.length + " stations.").show();
+
+ } catch (Exception e) {
+ mToast.text("Error saving search results: " + e.getMessage()).show();
}
}
};
@Override
public void onNothingSelected(AdapterView> parent) {}
-}
+
+ // Need onStartDrag implementation from OnDragListener
+ @Override
+ public void onStartDrag(final RecyclerView.ViewHolder viewHolder) {
+ if (mItemTouchHelper != null) {
+ mItemTouchHelper.startDrag(viewHolder);
+ }
+ }
+
+
+} // End of FavoritesListsActivity class
\ No newline at end of file
diff --git a/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java b/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
index 4ae6aad..a82b209 100644
--- a/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
+++ b/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
@@ -1,8 +1,12 @@
package com.vlad805.fmradio.activity;
+import android.Manifest; // <-- Add
+import androidx.core.content.ContextCompat; // <-- Add this import
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
+import android.content.pm.PackageManager; // <-- Add
+import android.os.Build; // <-- Add
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
@@ -11,7 +15,12 @@
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IdRes;
+import androidx.annotation.NonNull; // <-- Add
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat; // <-- Add
+import androidx.core.content.ContextCompat; // <-- Add
+import androidx.recyclerview.widget.RecyclerView;
+// ... other imports remain ...
import com.vlad805.fmradio.C;
import com.vlad805.fmradio.R;
import com.vlad805.fmradio.Storage;
@@ -37,6 +46,50 @@
@SuppressLint("NonConstantResourceId")
public class MainActivity extends AppCompatActivity implements View.OnClickListener, RadioStateUpdater.TunerStateListener {
+ // --- Permission Request Codes ---
+ private static final int REQUEST_CODE_PERMISSIONS_START_RADIO = 101;
+ private static final int REQUEST_CODE_PERMISSIONS_RECORD = 102;
+ private static final int REQUEST_CODE_PERMISSIONS_PHONE_STATE = 103;
+ private static final int REQUEST_CODE_PERMISSIONS_STORAGE_FAVORITES = 104;
+ private static final int REQUEST_CODE_FAVORITES_OPENED = 1048; // Keep existing codes
+ private static final int REQUEST_CODE_SETTINGS_CHANGED = 1050; // Keep existing codes
+
+ private static final int REQUEST_CODE_PERMISSIONS_NOTIFICATIONS = 105;
+ private static final String[] PERMISSIONS_NOTIFICATIONS = {Manifest.permission.POST_NOTIFICATIONS};
+
+ // --- Permission Strings ---
+ // Group needed to start radio playback (Audio capture)
+ private static final String[] PERMISSIONS_START_RADIO = {Manifest.permission.RECORD_AUDIO};
+ // Group needed for explicit recording (Audio capture + Storage write)
+ private static final String[] PERMISSIONS_RECORD; // Initialized statically below
+ // Group needed for call detection
+ private static final String[] PERMISSIONS_PHONE_STATE = {Manifest.permission.READ_PHONE_STATE};
+ // Group needed for favorites read/write (will need rework for scoped storage)
+ private static final String[] PERMISSIONS_STORAGE_FAVORITES; // Initialized statically below
+
+ // Static initializer for permissions that depend on SDK version
+ static {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
+ PERMISSIONS_RECORD = new String[]{
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.READ_MEDIA_AUDIO // Needed to potentially *read* recordings later, write is handled by MediaStore
+ };
+ PERMISSIONS_STORAGE_FAVORITES = new String[]{
+ Manifest.permission.READ_MEDIA_AUDIO // Placeholder, favorites need rework
+ };
+ } else {
+ PERMISSIONS_RECORD = new String[]{
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ };
+ PERMISSIONS_STORAGE_FAVORITES = new String[]{
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ };
+ }
+ }
+
+
private ProgressDialog mProgress;
private Toast mToast;
private RadioUIView mFrequencyInfo;
@@ -55,234 +108,441 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private TextView mRecordDuration;
- private RadioState mLastState;
+ private RadioState mLastState = new RadioState(); // Initialize to avoid nulls early
private Menu mMenu;
- private static final int REQUEST_CODE_FAVORITES_OPENED = 1048;
- private static final int REQUEST_CODE_SETTINGS_CHANGED = 1050;
-
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToast = Toast.create(this);
-
setSupportActionBar(findViewById(R.id.main_toolbar));
mPreferences = new AppPreferences(this);
mRadioController = new RadioController(this);
- mRadioController.requestForCurrentState(this);
- mRadioController.registerForUpdates(this);
- mRadioController.setPowerMode(PowerMode.NORMAL);
-
- initUserInterface();
- initLogic();
- }
-
- /**
- * Activity handle, UI setup
- */
- private void initUserInterface() {
- mFrequencyInfo = findViewById(R.id.frequency_info);
-
- mFavoriteList = findViewById(R.id.favorite_list);
-
- mSeek = findViewById(R.id.frequency_seek);
-
- mRecordDuration = findViewById(R.id.record_duration);
-
- final int kHz = Storage.getInstance(this).getInt(C.PrefKey.LAST_FREQUENCY, C.PrefDefaultValue.LAST_FREQUENCY);
- mFrequencyInfo.setFrequency(kHz);
- mSeek.setFrequency(kHz);
-
- initClickableButtons();
- }
- /**
- * Initialize connection and service
- */
- private void initLogic() {
- final boolean needStartup = Storage.getPrefBoolean(this, C.PrefKey.APP_AUTO_STARTUP, C.PrefDefaultValue.APP_AUTO_STARTUP);
+ initUserInterface(); // Initialize UI elements
- if (needStartup) {
- mRadioController.setup();
- } else {
+ // --- Register listeners and set initial state ---
+ mRadioController.registerForUpdates(this);
+ mRadioController.setPowerMode(PowerMode.NORMAL);
+ mRadioController.requestForCurrentState(this); // Request initial state AFTER registering listener
+
+ // --- Permission Checks ---
+ checkAndRequestPhoneStatePermission(); // Request for CallReceiver functionality
+ checkAndRequestStoragePermissionForFavorites(); // Request for Favorites list loading
+
+ // --- Initial UI state based on auto-startup pref ---
+ // NOTE: Auto-startup itself would need RECORD_AUDIO permission, which we can't
+ // guarantee is granted at this point. So, we don't call initLogic() here directly.
+ // The user will have to press play, which triggers the permission request if needed.
+ if (!Storage.getPrefBoolean(this, C.PrefKey.APP_AUTO_STARTUP, C.PrefDefaultValue.APP_AUTO_STARTUP)) {
+ // Only disable UI elements if auto-start is off. Play button will handle permission request.
setEnabledUi(false);
}
}
- private void showProgress(final String text) {
- if (mProgress != null) {
- hideProgress();
- }
-
- mProgress = ProgressDialog.create(this).text(text).show();
- }
+ // --- Helper methods for permission checks ---
- private void hideProgress() {
- if (mProgress != null) {
- mProgress.hide();
- mProgress = null;
+ private boolean hasPermissions(String... permissions) {
+ if (permissions != null) {
+ for (String permission : permissions) {
+ if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
}
+ return true;
}
- @Override
- protected void onResume() {
- super.onResume();
-
- reloadPreferences();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- mRadioController.unregisterForUpdates();
-
- if (Storage.getPrefBoolean(this, C.PrefKey.TUNER_POWER_MODE, false)) {
- mRadioController.setPowerMode(PowerMode.LOW);
+ private void checkAndRequestPhoneStatePermission() {
+ if (!hasPermissions(PERMISSIONS_PHONE_STATE)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_PHONE_STATE, REQUEST_CODE_PERMISSIONS_PHONE_STATE);
}
+ // Else: Already granted
}
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_CODE_FAVORITES_OPENED: {
- if (resultCode == Activity.RESULT_OK && data.getBooleanExtra("changed", false)) {
- mFavoriteList.reload(true);
- }
- break;
- }
-
- case REQUEST_CODE_SETTINGS_CHANGED: {
- if (resultCode == RESULT_OK && data.getBooleanExtra("changed", false)) {
- alert(
- this,
- R.string.main_warning_pref_changed_title,
- R.string.main_warning_pref_changed_content,
- android.R.string.ok
- );
- }
+ private void checkAndRequestStoragePermissionForFavorites() {
+ if (!hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE_FAVORITES, REQUEST_CODE_PERMISSIONS_STORAGE_FAVORITES);
+ } else {
+ // Permission granted, load favorites if the view is ready
+ if (mFavoriteList != null) {
+ mFavoriteList.load();
}
}
- super.onActivityResult(requestCode, resultCode, data);
}
- /**
- * Setup clickable buttons
- */
- private void initClickableButtons() {
- mCtlToggle = findViewById(R.id.ctl_toggle);
- mViewRssi = findViewById(R.id.rssi_value);
- mViewRssiIcon = findViewById(R.id.rssi_icon);
- mViewStereoMode = findViewById(R.id.stereo_mono);
+ // --- UI Initialization ---
+ private void initUserInterface() {
+ mFrequencyInfo = findViewById(R.id.frequency_info);
+ mFavoriteList = findViewById(R.id.favorite_list); // Init the view
+ mSeek = findViewById(R.id.frequency_seek);
+ mRecordDuration = findViewById(R.id.record_duration);
- final int[] ids = {
- R.id.ctl_toggle,
- R.id.ctl_go_down,
- R.id.ctl_go_up,
- R.id.ctl_seek_down,
- R.id.ctl_seek_up,
- R.id.favorite_button
- };
+ final int kHz = Storage.getInstance(this).getInt(C.PrefKey.LAST_FREQUENCY, C.PrefDefaultValue.LAST_FREQUENCY);
+ mFrequencyInfo.setFrequency(kHz);
+ mSeek.setFrequency(kHz);
- for (int id : ids) {
- findViewById(id).setOnClickListener(this);
- }
- }
+ initClickableButtons();
+ reloadPreferences(); // Reload prefs that affect UI like frequency bar limits
+ // Defer loading favorites until permission check in onCreate
+ // if (mFavoriteList != null && hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ // mFavoriteList.load();
+ // }
+ }
+ // --- Click Handler with Permission Checks (including Notifications) ---
@Override
public void onClick(final View view) {
switch (view.getId()) {
// Main play/stop button
case R.id.ctl_toggle: {
- switch (mRadioController.getState().getStatus()) {
- case IDLE: {
- mRadioController.setup();
- break;
+ TunerStatus currentStatus = (mLastState != null) ? mLastState.getStatus() : TunerStatus.IDLE;
+
+ // --- Actions when trying to START the radio ---
+ if (currentStatus == TunerStatus.IDLE ||
+ currentStatus == TunerStatus.INSTALLED ||
+ currentStatus == TunerStatus.LAUNCHED ||
+ currentStatus == TunerStatus.FATAL_ERROR ||
+ currentStatus == TunerStatus.LAUNCH_FAILED)
+ {
+ // 1. Check Notification Permission (Required BEFORE starting foreground service)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPermissions(PERMISSIONS_NOTIFICATIONS)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_NOTIFICATIONS, REQUEST_CODE_PERMISSIONS_NOTIFICATIONS);
+ mToast.text("Notification permission needed for status updates.").show();
+ // Don't proceed further until permission is granted (or denied)
+ return;
}
-
- case INSTALLED:
- case LAUNCHED: {
- mRadioController.enable();
- break;
+ // 2. Check Record Audio Permission (Required for radio stream)
+ else if (!hasPermissions(PERMISSIONS_START_RADIO)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_START_RADIO, REQUEST_CODE_PERMISSIONS_START_RADIO);
+ mToast.text("Audio recording permission needed to play FM radio.").show();
+ // Don't proceed further
+ return;
}
-
- case ENABLED: {
- mRadioController.kill();
- break;
+ // 3. All necessary permissions granted - Proceed with starting the radio
+ else {
+ if (currentStatus == TunerStatus.IDLE || currentStatus == TunerStatus.FATAL_ERROR || currentStatus == TunerStatus.LAUNCH_FAILED) {
+ mRadioController.setup(); // This eventually leads to enable()
+ } else { // INSTALLED or LAUNCHED
+ mRadioController.enable();
+ }
}
}
+ // --- Action when trying to STOP the radio ---
+ else if (currentStatus == TunerStatus.ENABLED) {
+ // Stopping the radio doesn't need special permissions
+ mRadioController.kill();
+ }
+ // Ignore clicks during transient states
break;
}
+ // Jump/Seek controls - these assume radio is already enabled (permission granted)
case R.id.ctl_go_down: {
- mRadioController.jump(Direction.DOWN);
+ if (mLastState != null && mLastState.getStatus() == TunerStatus.ENABLED) mRadioController.jump(Direction.DOWN);
break;
}
-
case R.id.ctl_go_up: {
- mRadioController.jump(Direction.UP);
+ if (mLastState != null && mLastState.getStatus() == TunerStatus.ENABLED) mRadioController.jump(Direction.UP);
break;
}
-
case R.id.ctl_seek_down: {
- mRadioController.hwSeek(Direction.DOWN);
- showProgress(getString(R.string.progress_searching));
+ if (mLastState != null && mLastState.getStatus() == TunerStatus.ENABLED) {
+ mRadioController.hwSeek(Direction.DOWN);
+ showProgress(getString(R.string.progress_searching));
+ }
break;
}
-
case R.id.ctl_seek_up: {
- mRadioController.hwSeek(Direction.UP);
- showProgress(getString(R.string.progress_searching));
+ if (mLastState != null && mLastState.getStatus() == TunerStatus.ENABLED) {
+ mRadioController.hwSeek(Direction.UP);
+ showProgress(getString(R.string.progress_searching));
+ }
break;
}
case R.id.favorite_button: {
- startActivityForResult(new Intent(this, FavoritesListsActivity.class), REQUEST_CODE_FAVORITES_OPENED);
+ // Check storage permission before opening activity that requires it
+ if (!hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE_FAVORITES, REQUEST_CODE_PERMISSIONS_STORAGE_FAVORITES);
+ mToast.text("Storage permission needed to manage favorites.").show();
+ } else {
+ startActivityForResult(new Intent(this, FavoritesListsActivity.class), REQUEST_CODE_FAVORITES_OPENED);
+ }
break;
}
}
}
+ // --- Options Menu Handler with Permission Checks (including Notifications) ---
@Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- getMenuInflater().inflate(R.menu.main, menu);
- mMenu = menu;
- if (mLastState != null) {
- updateMenuState();
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.menu_record) {
+ // --- Actions when trying to START recording ---
+ if (mLastState != null && !mLastState.isRecording()) {
+ // 1. Check Notification Permission (for recording status notification)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPermissions(PERMISSIONS_NOTIFICATIONS)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_NOTIFICATIONS, REQUEST_CODE_PERMISSIONS_NOTIFICATIONS);
+ mToast.text("Notification permission needed for recording status.").show();
+ // Don't proceed until permission result
+ return true; // Handled (by requesting permission)
+ }
+ // 2. Check Record Audio & Storage Permissions
+ else if (!hasPermissions(PERMISSIONS_RECORD)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_RECORD, REQUEST_CODE_PERMISSIONS_RECORD);
+ mToast.text("Audio and Storage permissions needed to record.").show();
+ // Don't proceed until permission result
+ return true; // Handled (by requesting permission)
+ }
+ // 3. All permissions granted - Proceed with starting recording
+ else {
+ mRadioController.record(true);
+ }
+ }
+ // --- Actions when trying to STOP recording ---
+ else if (mLastState != null && mLastState.isRecording()) {
+ // Stopping recording doesn't need special permissions
+ mRadioController.record(false);
+ }
+ return true; // Handled (either started, stopped, or requested permissions)
}
- return super.onCreateOptionsMenu(menu);
+ // --- Other Menu Items ---
+ else if (itemId == R.id.menu_speaker) {
+ startService(new Intent(this, FMService.class).setAction(C.Command.SPEAKER_STATE));
+ return true; // Handled
+ } else if (itemId == R.id.menu_about) {
+ startActivity(new Intent(this, AboutActivity.class));
+ return true; // Handled
+ } else if (itemId == R.id.menu_settings) {
+ startActivityForResult(new Intent(this, SettingsActivity.class), REQUEST_CODE_SETTINGS_CHANGED);
+ return true; // Handled
+ } else if (itemId == android.R.id.home) {
+ finish();
+ return true; // Handled
+ }
+ // --- Default ---
+ return super.onOptionsItemSelected(item);
}
+
+ // --- Permission Result Handler (including Notifications) ---
@Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_about:
- startActivity(new Intent(this, AboutActivity.class));
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ switch (requestCode) {
+ case REQUEST_CODE_PERMISSIONS_START_RADIO:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Audio granted. Now check Notifications (if needed) before trying to start.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPermissions(PERMISSIONS_NOTIFICATIONS)) {
+ // Need notifications too, request them now
+ ActivityCompat.requestPermissions(this, PERMISSIONS_NOTIFICATIONS, REQUEST_CODE_PERMISSIONS_NOTIFICATIONS);
+ mToast.text("Audio permission granted. Notification permission also needed.").show();
+ } else {
+ // All needed permissions (Audio + maybe Notifications) granted.
+ mToast.text("Audio permission granted. Press play again.").show();
+ // Optional: try starting radio automatically here
+ // TunerStatus currentStatus = ...; if (...) { mRadioController.setup(); } else { mRadioController.enable(); }
+ }
+ } else {
+ mToast.text("Audio permission denied. Cannot play FM radio.").show();
+ setEnabledUi(false);
+ setPlayingMainPlayButton(false);
+ }
+ break;
+
+ case REQUEST_CODE_PERMISSIONS_RECORD:
+ if (hasPermissions(PERMISSIONS_RECORD)) {
+ // Audio/Storage granted. Check Notifications (if needed) before trying to record.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPermissions(PERMISSIONS_NOTIFICATIONS)) {
+ ActivityCompat.requestPermissions(this, PERMISSIONS_NOTIFICATIONS, REQUEST_CODE_PERMISSIONS_NOTIFICATIONS);
+ mToast.text("Recording permissions granted. Notification permission also needed.").show();
+ } else {
+ // All needed permissions (Audio/Storage + maybe Notifications) granted.
+ mToast.text("Recording permissions granted. Press record again.").show();
+ // Optional: try starting record automatically here
+ // mRadioController.record(true);
+ }
+ } else {
+ mToast.text("Recording permissions denied. Cannot record audio.").show();
+ }
break;
- case R.id.menu_settings:
- startActivityForResult(new Intent(this, SettingsActivity.class), REQUEST_CODE_SETTINGS_CHANGED);
+ case REQUEST_CODE_PERMISSIONS_PHONE_STATE:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ mToast.text("Phone state permission granted.").show();
+ } else {
+ mToast.text("Phone state permission denied. Calls might not pause radio.").show();
+ }
break;
- case R.id.menu_record:
- mRadioController.record(!mRadioController.getState().isRecording());
+ case REQUEST_CODE_PERMISSIONS_STORAGE_FAVORITES:
+ if (hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ mToast.text("Storage permission granted.").show();
+ if (mFavoriteList != null) {
+ mFavoriteList.setEnabled(true); // Ensure enabled after grant
+ mFavoriteList.load();
+ }
+ } else {
+ mToast.text("Storage permission denied. Cannot load/save favorites.").show();
+ if(mFavoriteList != null) mFavoriteList.setEnabled(false);
+ }
break;
- case R.id.menu_speaker: {
- startService(new Intent(this, FMService.class).setAction(C.Command.SPEAKER_STATE));
+ case REQUEST_CODE_PERMISSIONS_NOTIFICATIONS: // Handle Notification result
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ mToast.text("Notification permission granted.").show();
+ // Now the user might need to press Play/Record again, as the initial
+ // action was likely blocked waiting for this permission.
+ } else {
+ mToast.text("Notification permission denied. Status updates might not appear.").show();
+ // App can continue, just without notifications.
+ }
break;
+ }
+ }
+
+ // --- Other methods remain largely the same ---
+ // showProgress, hideProgress, onResume, onDestroy, onActivityResult,
+ // initClickableButtons, onCreateOptionsMenu, setEnabledToggleButton,
+ // reloadPreferences, mOnFrequencyChanged, onStateUpdated,
+ // handleChangingState, setPlayingMainPlayButton, setEnabledUi,
+ // updateMenuState, setRssi
+
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mRadioController.unregisterForUpdates();
+ // Low power mode logic remains
+ if (Storage.getPrefBoolean(this, C.PrefKey.TUNER_POWER_MODE, false)) {
+ mRadioController.setPowerMode(PowerMode.LOW);
+ }
+ }
+
+
+ // Make sure setEnabledUi handles the favorite list enable state
+ private void setEnabledUi(final boolean state) {
+ final @IdRes int[] ids = {
+ R.id.ctl_go_down,
+ R.id.ctl_go_up,
+ R.id.ctl_seek_down,
+ R.id.ctl_seek_up,
+ R.id.frequency_mhz, // Keep frequency display always enabled? Maybe.
+ R.id.frequency_ps,
+ R.id.frequency_rt,
+ R.id.frequency_pty,
+ R.id.frequency_seek,
+ R.id.rssi_icon,
+ R.id.rssi_value,
+ R.id.record_duration,
+ R.id.stereo_mono,
+ R.id.favorite_list, // Include favorite list here
+ R.id.favorite_button // Also the button to open favorites
+ };
+
+ for (final int id : ids) {
+ final View v = findViewById(id);
+ if (v != null) { // Add null check
+ // Make frequency display always visible, but controls depend on state
+ boolean shouldEnable = (id == R.id.frequency_mhz) ? true : state;
+ // Ensure favorites list is only enabled if storage permission is granted AND radio is enabled
+ if (id == R.id.favorite_list || id == R.id.favorite_button) {
+ shouldEnable = state && hasPermissions(PERMISSIONS_STORAGE_FAVORITES);
+ }
+
+ v.setAlpha(shouldEnable ? 1f : .5f);
+ v.setEnabled(shouldEnable);
+ // Special case for RecyclerView based view
+ if (v instanceof RecyclerView) {
+ v.setClickable(shouldEnable);
+ v.setFocusable(shouldEnable);
+ }
}
}
- return super.onOptionsItemSelected(item);
+ updateMenuState(); // Update menu item states too
}
- private void setEnabledToggleButton(final boolean enabled) {
- mCtlToggle.setEnabled(enabled);
+ // Adjust updateMenuState if necessary, especially for record button
+ private void updateMenuState() {
+ if (mMenu == null || mLastState == null) { // Add null check for mLastState
+ return;
+ }
+
+ final boolean isRadioEnabled = TunerStatus.ENABLED.equals(mLastState.getStatus());
+ // Check storage/audio permissions for record button enable state
+ final boolean canRecord = isRadioEnabled && hasPermissions(PERMISSIONS_RECORD);
+
+ mMenu.findItem(R.id.menu_record).setEnabled(canRecord); // Enable based on permissions too
+ mMenu.findItem(R.id.menu_speaker).setEnabled(isRadioEnabled);
+ mMenu.findItem(R.id.menu_speaker).setChecked(mLastState.isForceSpeaker());
+ }
+
+ // Add other necessary methods like showProgress, hideProgress etc. back in if removed
+
+ // ... (rest of the original MainActivity methods like setRssi, signal arrays, etc.)
+ private void showProgress(final String text) {
+ if (mProgress != null) {
+ hideProgress();
+ }
+ mProgress = ProgressDialog.create(this).text(text).show();
+ }
+
+ private void hideProgress() {
+ if (mProgress != null) {
+ mProgress.hide();
+ mProgress = null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ reloadPreferences();
+ // Refresh UI state based on current permissions and radio state
+ if (mLastState != null) {
+ setEnabledUi(mLastState.getStatus() == TunerStatus.ENABLED);
+ setPlayingMainPlayButton(mLastState.getStatus() == TunerStatus.ENABLED);
+ } else {
+ setEnabledUi(false);
+ setPlayingMainPlayButton(false);
+ }
+ // Re-check favorite storage permission in case it was granted while paused
+ if (mFavoriteList != null && hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ mFavoriteList.setEnabled(true);
+ mFavoriteList.load(); // Reload in case it changed
+ } else if (mFavoriteList != null) {
+ mFavoriteList.setEnabled(false);
+ }
+
+ }
+
+
+ // ... rest of existing methods like initClickableButtons, reloadPreferences, etc.
+ private void initClickableButtons() {
+ mCtlToggle = findViewById(R.id.ctl_toggle);
+ mViewRssi = findViewById(R.id.rssi_value);
+ mViewRssiIcon = findViewById(R.id.rssi_icon);
+ mViewStereoMode = findViewById(R.id.stereo_mono);
+
+ final int[] ids = {
+ R.id.ctl_toggle,
+ R.id.ctl_go_down,
+ R.id.ctl_go_up,
+ R.id.ctl_seek_down,
+ R.id.ctl_seek_up,
+ R.id.favorite_button
+ };
+
+ for (int id : ids) {
+ findViewById(id).setOnClickListener(this);
+ }
}
private void reloadPreferences() {
@@ -292,217 +552,228 @@ private void reloadPreferences() {
final BandUtils.BandLimit bandLimit = BandUtils.getBandLimit(regionPref);
final int spacing = BandUtils.getSpacing(spacingPref);
- mSeek.setMinMaxValue(bandLimit.lower, bandLimit.upper, spacing);
- mSeek.setOnFrequencyChangeListener(mOnFrequencyChanged);
+ if (mSeek != null) { // Add null check
+ mSeek.setMinMaxValue(bandLimit.lower, bandLimit.upper, spacing);
+ mSeek.setOnFrequencyChangeListener(mOnFrequencyChanged);
+ }
}
private final FrequencyBarView.OnFrequencyChangedListener mOnFrequencyChanged = new FrequencyBarView.OnFrequencyChangedListener() {
@Override
public void onChanged(final int kHz) {
- if (mRadioController.getState().getFrequency() == kHz) {
+ if (mLastState == null || mLastState.getFrequency() == kHz) { // Add null check
return;
}
-
mRadioController.setFrequency(kHz);
}
};
- /**
- * Called when the state of the tuner changes
- * @param state State object
- * @param mode Bitmask what was changed
- */
+
+ // Ensure onStateUpdated initializes mLastState if it's null
@Override
public void onStateUpdated(final RadioState state, final int mode) {
- mLastState = state;
+ if (state == null) return; // Safety check
+
+ mLastState = state; // Update the cached state
if ((mode & RadioStateUpdater.SET_STATUS) > 0) {
handleChangingState(state.getStatus());
- return;
+ // After handling state change, update UI enable state based on new status
+ setEnabledUi(state.getStatus() == TunerStatus.ENABLED);
+ setPlayingMainPlayButton(state.getStatus() == TunerStatus.ENABLED);
+ return; // Return early as state change often implies other things will update
}
-
+ // Add null checks for UI elements before updating them
if ((mode & RadioStateUpdater.SET_FREQUENCY) > 0) {
hideProgress();
-
- mFrequencyInfo.setFrequency(state.getFrequency());
-
- mSeek.setFrequency(state.getFrequency());
-
- final String str = getString(R.string.player_event_frequency_changed, state.getFrequency() / 1000f);
+ if (mFrequencyInfo != null) mFrequencyInfo.setFrequency(state.getFrequency());
+ if (mSeek != null) mSeek.setFrequency(state.getFrequency());
+ final String str = getString(R.string.notification_mhz, state.getFrequency() / 1000f, state.getRssi()); // Use correct format string
mToast.text(str).show();
-
- mFrequencyInfo.setRadioState(state);
- mViewRssi.setText("");
+ if (mFrequencyInfo != null) mFrequencyInfo.setRadioState(state);
+ if (mViewRssi != null) mViewRssi.setText("");
}
if (
- (mode & RadioStateUpdater.SET_PS) > 0 ||
- (mode & RadioStateUpdater.SET_PI) > 0 ||
- (mode & RadioStateUpdater.SET_RT) > 0 ||
- (mode & RadioStateUpdater.SET_PTY) > 0
+ (mode & RadioStateUpdater.SET_PS) > 0 ||
+ (mode & RadioStateUpdater.SET_PI) > 0 ||
+ (mode & RadioStateUpdater.SET_RT) > 0 ||
+ (mode & RadioStateUpdater.SET_PTY) > 0
) {
- mFrequencyInfo.setRadioState(state);
+ if (mFrequencyInfo != null) mFrequencyInfo.setRadioState(state);
}
if ((mode & RadioStateUpdater.SET_RSSI) > 0) {
- setRssi(state.getRssi());
+ if (mViewRssi != null && mViewRssiIcon != null) setRssi(state.getRssi());
}
if ((mode & RadioStateUpdater.SET_STEREO) > 0) {
- mViewStereoMode.setImageResource(state.isStereo() ? R.drawable.ic_stereo : R.drawable.ic_mono);
+ if (mViewStereoMode != null) mViewStereoMode.setImageResource(state.isStereo() ? R.drawable.ic_stereo : R.drawable.ic_mono);
}
if ((mode & RadioStateUpdater.SET_RECORDING) > 0 || (mode & RadioStateUpdater.SET_INITIAL) > 0) {
final boolean isRecording = state.isRecording();
-
- mRecordDuration.setVisibility(isRecording ? View.VISIBLE : View.GONE);
+ if (mRecordDuration != null) mRecordDuration.setVisibility(isRecording ? View.VISIBLE : View.GONE);
if (mMenu != null) {
- mMenu.findItem(R.id.menu_record).setIcon(isRecording ? R.drawable.ic_record_press : R.drawable.ic_record);
+ MenuItem recordItem = mMenu.findItem(R.id.menu_record);
+ if(recordItem != null) {
+ recordItem.setIcon(isRecording ? R.drawable.ic_record_press : R.drawable.ic_record);
+ // Re-check enable state based on recording status and permissions
+ // recordItem.setEnabled(!isRecording && hasPermissions(PERMISSIONS_RECORD));
+ }
}
-
- if (isRecording) {
+ if (isRecording && mRecordDuration != null) {
mRecordDuration.setText(getTimeStringBySeconds(state.getRecordingDuration()));
}
}
if ((mode & RadioStateUpdater.SET_SPEAKER) > 0 || (mode & RadioStateUpdater.SET_INITIAL) > 0) {
final boolean isSpeaker = state.isForceSpeaker();
-
if (mMenu != null) {
- mMenu.findItem(R.id.menu_speaker).setChecked(isSpeaker);
+ MenuItem speakerItem = mMenu.findItem(R.id.menu_speaker);
+ if(speakerItem != null) speakerItem.setChecked(isSpeaker);
}
}
+ // After any update, ensure general UI enable state matches radio status
+ // setEnabledUi(state.getStatus() == TunerStatus.ENABLED);
+ // setPlayingMainPlayButton(state.getStatus() == TunerStatus.ENABLED);
}
+
private void handleChangingState(final TunerStatus status) {
switch (status) {
- case IDLE: {
+ case IDLE: // IDLE covers the "off" state
+ // case DISABLED: // <-- DELETE THIS LINE
+ case FATAL_ERROR: // Treat errors like IDLE for UI reset
+ case LAUNCH_FAILED:
hideProgress();
setEnabledUi(false);
setPlayingMainPlayButton(false);
setEnabledToggleButton(true);
+ if (status == TunerStatus.LAUNCH_FAILED) {
+ Utils.alert(
+ this,
+ R.string.main_error_launching_title,
+ R.string.main_error_launching,
+ android.R.string.ok
+ );
+ }
break;
- }
- case INSTALLING: {
+ case INSTALLING:
showProgress(getString(R.string.progress_installing));
setEnabledToggleButton(false);
+ setEnabledUi(false); // Disable UI during install
break;
- }
case INSTALLED:
- case LAUNCHED: {
- showProgress(null);
+ case LAUNCHED:
+ hideProgress(); // Hide progress, but UI still disabled until enabled state
+ setEnabledUi(false);
+ setEnabledToggleButton(true); // Button might be enabled to trigger 'enable'
+ setPlayingMainPlayButton(false);
break;
- }
- case LAUNCHING: {
+ case LAUNCHING:
showProgress(getString(R.string.progress_launching));
+ setEnabledToggleButton(false);
+ setEnabledUi(false);
break;
- }
- case LAUNCH_FAILED: {
- hideProgress();
- Utils.alert(
- this,
- R.string.main_error_launching_title,
- R.string.main_error_launching,
- android.R.string.ok
- );
+ case ENABLING:
+ showProgress(getString(R.string.progress_starting));
+ setEnabledToggleButton(false);
+ setEnabledUi(false);
break;
- }
- case ENABLING: {
- showProgress(getString(R.string.progress_starting));
+ case DISABLING: // Add DISABLING case
+ // Potentially show stopping progress? Or just disable UI
+ hideProgress(); // Hide any previous progress
+ setEnabledUi(false);
+ setEnabledToggleButton(false); // Disable toggle while stopping
break;
- }
- case ENABLED: {
+ case ENABLED:
hideProgress();
- setEnabledUi(true);
+ setEnabledUi(true); // Enable UI now
setEnabledToggleButton(true);
- setPlayingMainPlayButton(true);
+ setPlayingMainPlayButton(true); // Show stop icon
break;
- }
}
}
private void setPlayingMainPlayButton(final boolean isPlay) {
- mCtlToggle.setImageResource(isPlay ? R.drawable.ic_stop : R.drawable.ic_play);
+ if (mCtlToggle != null) mCtlToggle.setImageResource(isPlay ? R.drawable.ic_stop : R.drawable.ic_play);
}
- /**
- * Enables or disables the entire interface
- * @param state True if enabled, false otherwise
- */
- private void setEnabledUi(final boolean state) {
- final @IdRes int[] ids = {
- R.id.ctl_go_down,
- R.id.ctl_go_up,
- R.id.ctl_seek_down,
- R.id.ctl_seek_up,
- R.id.frequency_mhz,
- R.id.frequency_ps,
- R.id.frequency_rt,
- R.id.frequency_pty,
- R.id.frequency_seek,
- R.id.rssi_icon,
- R.id.rssi_value,
- R.id.record_duration,
- R.id.stereo_mono,
- R.id.favorite_list
- };
-
- for (final int id : ids) {
- final View v = findViewById(id);
- v.setAlpha(state ? 1f : .5f);
- v.setEnabled(state);
- }
-
- updateMenuState();
- }
-
- private void updateMenuState() {
- if (mMenu == null) {
- return;
- }
-
- final boolean state = TunerStatus.ENABLED.equals(mLastState.getStatus());
-
- mMenu.findItem(R.id.menu_record).setEnabled(state);
- mMenu.findItem(R.id.menu_speaker).setEnabled(state);
- mMenu.findItem(R.id.menu_speaker).setChecked(mLastState.isForceSpeaker());
+ private void setEnabledToggleButton(final boolean enabled) {
+ if (mCtlToggle != null) mCtlToggle.setEnabled(enabled);
}
+ // --- Signal Strength UI ---
private static final int[] SIGNAL_RES_ID = {
- R.drawable.ic_signal_0,
- R.drawable.ic_signal_1,
- R.drawable.ic_signal_2,
- R.drawable.ic_signal_3,
- R.drawable.ic_signal_4,
- R.drawable.ic_signal_5,
+ R.drawable.ic_signal_0, R.drawable.ic_signal_1, R.drawable.ic_signal_2,
+ R.drawable.ic_signal_3, R.drawable.ic_signal_4, R.drawable.ic_signal_5,
R.drawable.ic_signal_6,
};
-
- private static final int[] SIGNAL_THRESHOLD = {
- -110,
- -98,
- -86,
- -74,
- -62,
- -50,
- -10,
- };
+ private static final int[] SIGNAL_THRESHOLD = { -110, -98, -86, -74, -62, -50, -10, };
private void setRssi(final int rssi) {
- mViewRssi.setText(String.valueOf(rssi));
+ if (mViewRssi != null) mViewRssi.setText(String.valueOf(rssi));
+ if (mViewRssiIcon != null) {
+ int iconRes = R.drawable.ic_signal_0; // Default to lowest
+ for (int i = 0; i < SIGNAL_THRESHOLD.length; ++i) {
+ if (rssi >= SIGNAL_THRESHOLD[i]) { // Check if >= threshold
+ iconRes = SIGNAL_RES_ID[i];
+ } else {
+ break; // Since thresholds are increasing, stop when below
+ }
+ }
+ // Ensure we handle the case where rssi is below the first threshold
+ if (rssi < SIGNAL_THRESHOLD[0]) {
+ iconRes = SIGNAL_RES_ID[0];
+ }
+ mViewRssiIcon.setImageResource(iconRes);
+ }
+ }
- for (int i = 0; i < SIGNAL_RES_ID.length; ++i) {
- if (rssi < SIGNAL_THRESHOLD[i]) {
- mViewRssiIcon.setImageResource(SIGNAL_RES_ID[i]);
- break;
+ // Need to handle onActivityResult for settings/favorites
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data); // Call super
+ if (requestCode == REQUEST_CODE_FAVORITES_OPENED) {
+ if (resultCode == Activity.RESULT_OK && data != null && data.getBooleanExtra("changed", false)) {
+ if (mFavoriteList != null && hasPermissions(PERMISSIONS_STORAGE_FAVORITES)) {
+ mFavoriteList.reload(true); // Reload if changed and permission exists
+ }
+ }
+ } else if (requestCode == REQUEST_CODE_SETTINGS_CHANGED) {
+ if (resultCode == RESULT_OK && data != null && data.getBooleanExtra("changed", false)) {
+ alert(
+ this,
+ R.string.main_warning_pref_changed_title,
+ R.string.main_warning_pref_changed_content,
+ android.R.string.ok
+ );
}
}
}
-}
+
+ // Need onCreateOptionsMenu
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ mMenu = menu;
+ // Update menu state immediately based on initial state
+ if (mLastState != null) {
+ updateMenuState();
+ } else {
+ // Default disabled state if state not yet known
+ menu.findItem(R.id.menu_record).setEnabled(false);
+ menu.findItem(R.id.menu_speaker).setEnabled(false);
+ menu.findItem(R.id.menu_speaker).setChecked(false);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+} // End of MainActivity class
\ No newline at end of file
diff --git a/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java b/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
index 233a13a..8ae7e6d 100644
--- a/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
+++ b/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
@@ -1,5 +1,6 @@
package com.vlad805.fmradio.activity;
+import androidx.core.content.ContextCompat;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -48,14 +49,22 @@ protected void onResume() {
final IntentFilter filter = new IntentFilter();
filter.addAction(C.Event.ERROR_INVALID_ANTENNA);
- registerReceiver(mEventListener, filter);
+ // --- FIX ---
+ ContextCompat.registerReceiver(this, mEventListener, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
+ // -----------
}
@Override
protected void onPause() {
super.onPause();
-
- unregisterReceiver(mEventListener);
+ // --- FIX ---
+ // Add try-catch for robustness
+ try {
+ unregisterReceiver(mEventListener);
+ } catch (IllegalArgumentException e) {
+ // Ignore if already unregistered
+ }
+ // -----------
}
private final BroadcastReceiver mEventListener = new BroadcastReceiver() {
diff --git a/app/src/main/java/com/vlad805/fmradio/controller/RadioController.java b/app/src/main/java/com/vlad805/fmradio/controller/RadioController.java
index d413542..fbb208c 100644
--- a/app/src/main/java/com/vlad805/fmradio/controller/RadioController.java
+++ b/app/src/main/java/com/vlad805/fmradio/controller/RadioController.java
@@ -1,5 +1,8 @@
package com.vlad805.fmradio.controller;
+// Add this import at the top
+import androidx.core.content.ContextCompat;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -26,33 +29,64 @@ public RadioController(final Context context) {
public void requestForCurrentState(@Nullable final RadioStateUpdater.TunerStateListener callback) {
getCurrentState(state -> {
- mState.setStatus(state.getStatus());
- mState.setFrequency(state.getFrequency());
- mState.setStereo(state.isStereo());
- mState.setPs(state.getPs());
- mState.setRecording(state.isRecording());
- mState.setRecordingStarted(state.getRecordingStarted());
-
- if (callback != null) {
- final int mode =
- RadioStateUpdater.SET_STATUS |
- RadioStateUpdater.SET_FREQUENCY |
- RadioStateUpdater.SET_INITIAL |
- RadioStateUpdater.SET_RECORDING;
-
- callback.onStateUpdated(mState, mode);
+ if (state != null) { // Add null check for safety
+ mState.setStatus(state.getStatus());
+ mState.setFrequency(state.getFrequency());
+ mState.setStereo(state.isStereo());
+ mState.setPs(state.getPs());
+ mState.setRecording(state.isRecording());
+ mState.setRecordingStarted(state.getRecordingStarted());
+ mState.setForceSpeaker(state.isForceSpeaker()); // Make sure speaker state is included
+ mState.setPi(state.getPi());
+ mState.setPty(state.getPty());
+ mState.setRt(state.getRt());
+ mState.setRssi(state.getRssi());
+
+ if (callback != null) {
+ final int mode =
+ RadioStateUpdater.SET_STATUS |
+ RadioStateUpdater.SET_FREQUENCY |
+ RadioStateUpdater.SET_INITIAL | // Include initial flag
+ RadioStateUpdater.SET_RECORDING |
+ RadioStateUpdater.SET_SPEAKER | // Include speaker
+ RadioStateUpdater.SET_PS | // Include RDS fields
+ RadioStateUpdater.SET_RT |
+ RadioStateUpdater.SET_PI |
+ RadioStateUpdater.SET_PTY |
+ RadioStateUpdater.SET_RSSI |
+ RadioStateUpdater.SET_STEREO;
+
+ callback.onStateUpdated(mState, mode);
+ }
}
});
}
public void registerForUpdates(RadioStateUpdater.TunerStateListener callback) {
mTunerStateUpdater = new RadioStateUpdater(mState, callback);
- mContext.registerReceiver(mTunerStateUpdater, RadioStateUpdater.sFilter);
+ // Original line that caused the crash:
+ // mContext.registerReceiver(mTunerStateUpdater, RadioStateUpdater.sFilter);
+
+ // --- FIX ---
+ // Use ContextCompat version and specify NOT_EXPORTED
+ ContextCompat.registerReceiver(
+ mContext,
+ mTunerStateUpdater,
+ RadioStateUpdater.sFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED // Specify the receiver is not exported
+ );
+ // -----------
}
public void unregisterForUpdates() {
if (mTunerStateUpdater != null) {
- mContext.unregisterReceiver(mTunerStateUpdater);
+ try { // Add try-catch as unregistering an already unregistered receiver can crash
+ mContext.unregisterReceiver(mTunerStateUpdater);
+ mTunerStateUpdater = null; // Set to null after successful unregister
+ } catch (IllegalArgumentException e) {
+ // Receiver was likely already unregistered, ignore
+ mTunerStateUpdater = null; // Still set to null
+ }
}
}
@@ -125,14 +159,34 @@ public interface CurrentStateListener {
}
public void getCurrentState(final CurrentStateListener listener) {
- mContext.registerReceiver(new BroadcastReceiver() {
+ BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
- listener.onCurrentStateReady(intent.getParcelableExtra(C.Key.STATE));
- mContext.unregisterReceiver(this);
+ // Check if the intent and extra are valid
+ if (intent != null && C.Event.CURRENT_STATE.equals(intent.getAction())) {
+ RadioState receivedState = intent.getParcelableExtra(C.Key.STATE);
+ if (receivedState != null) {
+ listener.onCurrentStateReady(receivedState);
+ } else {
+ // Handle case where state is null (optional, maybe provide default?)
+ listener.onCurrentStateReady(new RadioState()); // Or handle error
+ }
+ }
+ // Always try to unregister to prevent leaks
+ try {
+ mContext.unregisterReceiver(this);
+ } catch (IllegalArgumentException e) {
+ // Ignore if already unregistered
+ }
}
- }, new IntentFilter(C.Event.CURRENT_STATE));
+ };
+
+ // --- FIX for registerReceiver ---
+ // Use ContextCompat and specify NOT_EXPORTED
+ IntentFilter filter = new IntentFilter(C.Event.CURRENT_STATE);
+ ContextCompat.registerReceiver(mContext, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
+ // --------------------------------
send(C.Command.REQUEST_CURRENT_STATE);
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/vlad805/fmradio/service/FMService.java b/app/src/main/java/com/vlad805/fmradio/service/FMService.java
index bcf08f9..192bad7 100644
--- a/app/src/main/java/com/vlad805/fmradio/service/FMService.java
+++ b/app/src/main/java/com/vlad805/fmradio/service/FMService.java
@@ -1,11 +1,14 @@
package com.vlad805.fmradio.service;
+// Add this import
+import androidx.core.content.ContextCompat;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@@ -97,7 +100,7 @@ public void onCreate() {
mAudioService = getPreferredAudioService();
mTunerDriver = getPreferredTunerDriver();
- // Broadcast receivers
+ // Broadcast receivers instances
mEventReaction = new EventReaction();
mTunerStateUpdater = new RadioStateUpdater(mState);
@@ -107,8 +110,15 @@ public void onCreate() {
reloadFavorite();
- registerReceiver(mTunerStateUpdater, RadioStateUpdater.sFilter);
- registerReceiver(mEventReaction, RadioStateUpdater.sFilter);
+ // --- FIX Register Receivers ---
+ IntentFilter stateUpdateFilter = RadioStateUpdater.sFilter; // Use the existing filter
+ ContextCompat.registerReceiver(this, mTunerStateUpdater, stateUpdateFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
+
+ IntentFilter eventReactionFilter = RadioStateUpdater.sFilter; // Can often reuse the same filter if needed
+ // If EventReaction needs different filters, create a new IntentFilter for it.
+ // Example: IntentFilter eventReactionFilter = new IntentFilter(); eventReactionFilter.addAction(...);
+ ContextCompat.registerReceiver(this, mEventReaction, eventReactionFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
+ // ------------------------------
}
/**
@@ -288,21 +298,22 @@ private void kill() {
if (mTunerDriver instanceof IFMEventPoller) {
if (mTimer != null) {
mTimer.cancel();
+ mTimer = null; // Clear timer
}
}
mAudioService.stopAudio();
mTunerDriver.kill();
- if (mEventReaction != null) {
- unregisterReceiver(mEventReaction);
- mEventReaction = null;
- }
-
- if (mTunerStateUpdater != null) {
- unregisterReceiver(mTunerStateUpdater);
- mTunerStateUpdater = null;
- }
+ // Receivers are now unregistered in onDestroy
+ // if (mEventReaction != null) {
+ // unregisterReceiver(mEventReaction);
+ // mEventReaction = null;
+ // }
+ // if (mTunerStateUpdater != null) {
+ // unregisterReceiver(mTunerStateUpdater);
+ // mTunerStateUpdater = null;
+ // }
mStorage.unregisterOnTrayPreferenceChangeListener(this);
@@ -315,8 +326,23 @@ private void kill() {
*/
@Override
public void onDestroy() {
- kill();
+ // --- FIX Unregister Receivers ---
+ try {
+ if (mEventReaction != null) {
+ unregisterReceiver(mEventReaction);
+ mEventReaction = null;
+ }
+ if (mTunerStateUpdater != null) {
+ unregisterReceiver(mTunerStateUpdater);
+ mTunerStateUpdater = null;
+ }
+ } catch (IllegalArgumentException e) {
+ // Ignore if already unregistered
+ Log.w(TAG, "Receiver already unregistered?", e);
+ }
+ // -------------------------------
+ kill(); // Ensure kill logic runs *after* unregistering if possible
super.onDestroy();
}
@@ -541,14 +567,14 @@ private NotificationCompat.Builder createNotificationBuilder() {
this,
0,
mainIntent,
- FLAG_UPDATE_CURRENT
+ FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE
);
final PendingIntent pendingStop = PendingIntent.getService(
this,
1,
new Intent(this, FMService.class).setAction(C.Command.DISABLE),
- 0
+ PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE (replace 0)
);
final PendingIntent pendingRec = PendingIntent.getService(
@@ -556,17 +582,16 @@ private NotificationCompat.Builder createNotificationBuilder() {
2,
new Intent(this, FMService.class)
.setAction(C.Command.RECORD_START),
- 0
+ PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE (replace 0)
);
-
final PendingIntent pendingSeekDown = PendingIntent.getService(
this,
3,
new Intent(this, FMService.class)
.setAction(C.Command.NOTIFICATION_SEEK)
.putExtra(C.Key.SEEK_HW_DIRECTION, -1),
- 0
+ PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE (replace 0)
);
final PendingIntent pendingSeekUp = PendingIntent.getService(
@@ -575,7 +600,7 @@ private NotificationCompat.Builder createNotificationBuilder() {
new Intent(this, FMService.class)
.setAction(C.Command.NOTIFICATION_SEEK)
.putExtra(C.Key.SEEK_HW_DIRECTION, 1),
- 0
+ PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE (replace 0)
);
//.setMediaSession(mediaSession.getSessionToken()))
@@ -669,7 +694,7 @@ private void updateRecordingNotification(final int duration, final int size) {
5,
new Intent(this, FMService.class)
.setAction(C.Command.RECORD_STOP),
- 0
+ PendingIntent.FLAG_IMMUTABLE // Add FLAG_IMMUTABLE (replace 0)
);
NotificationCompat.Builder n = new NotificationCompat.Builder(this, CHANNEL_RECORDING_ID)
.setSmallIcon(R.drawable.ic_record_on)
diff --git a/build.gradle b/build.gradle
index c490458..d927584 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.8.1'
+ classpath 'com.android.tools.build:gradle:8.7.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -21,6 +21,7 @@ allprojects {
}
}
-task clean(type: Delete) {
- delete rootProject.buildDir
+tasks.register('clean', Delete) {
+ delete(rootProject.layout.buildDirectory)
}
+
diff --git a/gradle.properties b/gradle.properties
index b71adc9..437bf3a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -13,4 +13,5 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
-android.nonFinalResIds=false
\ No newline at end of file
+android.nonFinalResIds=false
+org.gradle.warning.mode=all
From 96a74ea3500b5dc4baa3cb5a4143a9f44235b207 Mon Sep 17 00:00:00 2001
From: Yeshey <41551785+Yeshey@users.noreply.github.com>
Date: Tue, 29 Apr 2025 03:59:10 +0100
Subject: [PATCH 2/4] idrk
---
app/build.gradle | 4 +-
.../fmradio/activity/MainActivity.java | 19 ++++--
.../fmradio/activity/SettingsActivity.java | 9 +--
.../vlad805/fmradio/service/FMService.java | 66 +++++++++++++++++--
4 files changed, 80 insertions(+), 18 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index aaee379..ad00e7c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -36,8 +36,8 @@ android {
//noinspection GroovyMissingReturnStatement
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_11 // Or VERSION_17
+ targetCompatibility JavaVersion.VERSION_11 // Or VERSION_17
}
afterEvaluate {
diff --git a/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java b/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
index a82b209..f4de86f 100644
--- a/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
+++ b/app/src/main/java/com/vlad805/fmradio/activity/MainActivity.java
@@ -55,7 +55,17 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private static final int REQUEST_CODE_SETTINGS_CHANGED = 1050; // Keep existing codes
private static final int REQUEST_CODE_PERMISSIONS_NOTIFICATIONS = 105;
- private static final String[] PERMISSIONS_NOTIFICATIONS = {Manifest.permission.POST_NOTIFICATIONS};
+ private static final String POST_NOTIFICATIONS_PERMISSION;
+ static {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ POST_NOTIFICATIONS_PERMISSION = Manifest.permission.POST_NOTIFICATIONS;
+ } else {
+ POST_NOTIFICATIONS_PERMISSION = null; // Set to null on older versions
+ }
+ }
+ // Use the dynamically assigned string or an empty array if null
+ private static final String[] PERMISSIONS_NOTIFICATIONS =
+ (POST_NOTIFICATIONS_PERMISSION != null) ? new String[]{POST_NOTIFICATIONS_PERMISSION} : new String[0];
// --- Permission Strings ---
// Group needed to start radio playback (Audio capture)
@@ -69,13 +79,14 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
// Static initializer for permissions that depend on SDK version
static {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
+ // POST_NOTIFICATIONS init moved above
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
PERMISSIONS_RECORD = new String[]{
Manifest.permission.RECORD_AUDIO,
- Manifest.permission.READ_MEDIA_AUDIO // Needed to potentially *read* recordings later, write is handled by MediaStore
+ Manifest.permission.READ_MEDIA_AUDIO
};
PERMISSIONS_STORAGE_FAVORITES = new String[]{
- Manifest.permission.READ_MEDIA_AUDIO // Placeholder, favorites need rework
+ Manifest.permission.READ_MEDIA_AUDIO
};
} else {
PERMISSIONS_RECORD = new String[]{
diff --git a/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java b/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
index 8ae7e6d..d84730d 100644
--- a/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
+++ b/app/src/main/java/com/vlad805/fmradio/activity/SettingsActivity.java
@@ -76,12 +76,9 @@ public void onReceive(final Context context, final Intent intent) {
return;
}
- switch (action) {
- case C.Event.ERROR_INVALID_ANTENNA: {
- Toast.makeText(SettingsActivity.this, R.string.pref_tuner_antenna_error, Toast.LENGTH_SHORT).show();
- break;
- }
- }
+ if (action.equals(C.Event.ERROR_INVALID_ANTENNA)) {
+ Toast.makeText(SettingsActivity.this, R.string.pref_tuner_antenna_error, Toast.LENGTH_SHORT).show();
+ }
}
};
}
diff --git a/app/src/main/java/com/vlad805/fmradio/service/FMService.java b/app/src/main/java/com/vlad805/fmradio/service/FMService.java
index 192bad7..0ce1e3e 100644
--- a/app/src/main/java/com/vlad805/fmradio/service/FMService.java
+++ b/app/src/main/java/com/vlad805/fmradio/service/FMService.java
@@ -1,6 +1,7 @@
package com.vlad805.fmradio.service;
// Add this import
+import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.app.Notification;
import android.app.PendingIntent;
@@ -44,6 +45,11 @@
import net.grandcentrix.tray.core.OnTrayPreferenceChangeListener;
import net.grandcentrix.tray.core.TrayItem;
+// Add these imports if not already present
+import android.content.pm.PackageManager;
+import android.Manifest;
+import android.os.Build;
+
import java.io.File;
import java.util.*;
@@ -80,6 +86,16 @@ public class FMService extends Service implements FMEventCallback, OnTrayPrefere
private RadioState mState;
+ private boolean hasNotificationPermission() {
+ // POST_NOTIFICATIONS is only needed from Android 13 (API 33) onwards
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
+ } else {
+ // On older versions, no special permission is needed to post notifications
+ return true;
+ }
+ }
+
@Override
public void onCreate() {
super.onCreate();
@@ -689,6 +705,11 @@ private void updateNotification() {
}
private void updateRecordingNotification(final int duration, final int size) {
+ if (!hasNotificationPermission()) {
+ Log.w(TAG, "Skipping recording notification: POST_NOTIFICATIONS permission not granted.");
+ return; // Don't show notification if permission missing
+ }
+
final PendingIntent pendingRecordStop = PendingIntent.getService(
this,
5,
@@ -709,19 +730,36 @@ private void updateRecordingNotification(final int duration, final int size) {
.addAction(R.drawable.ic_record_off, getString(R.string.seek_down), pendingRecordStop)
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setShowActionsInCompactView(0));
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ // TODO: Consider calling
+ // ActivityCompat#requestPermissions
+ // here to request the missing permissions, and then overriding
+ // public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ // int[] grantResults)
+ // to handle the case where the user grants the permission. See the documentation
+ // for ActivityCompat#requestPermissions for more details.
+ return;
+ }
mNotificationManager.notify(NOTIFICATION_RECORD_ID, n.build());
}
private void showRecorded(final Bundle extras) {
final boolean needNotification = Storage.getPrefBoolean(this, C.PrefKey.RECORDING_SHOW_NOTIFY, C.PrefDefaultValue.RECORDING_SHOW_NOTIFY);
- final int size = extras.getInt(C.Key.SIZE);
- final int duration = extras.getInt(C.Key.DURATION);
- final String path = extras.getString(C.Key.PATH);
- final File file = new File(path);
- final String filename = file.getName();
+ // --- FIX: Add permission check before attempting to notify ---
+ if (needNotification && !hasNotificationPermission()) {
+ Log.w(TAG, "Skipping 'recording ended' notification: POST_NOTIFICATIONS permission not granted.");
+ // Proceed with the Toast message below, even if notification can't be shown
+ }
+ // ------------------------------------------------------------
+ else if (needNotification) {
+ // Only build and notify if permission IS granted
+ final int size = extras.getInt(C.Key.SIZE);
+ final int duration = extras.getInt(C.Key.DURATION);
+ final String path = extras.getString(C.Key.PATH);
+ final File file = new File(path);
+ final String filename = file.getName();
- if (needNotification) {
final NotificationCompat.Builder n = new NotificationCompat.Builder(this, CHANNEL_RECORD_ID)
.setSmallIcon(R.drawable.ic_radio)
.setContentTitle(getString(R.string.app_name))
@@ -729,9 +767,25 @@ private void showRecorded(final Bundle extras) {
.setPriority(NotificationCompat.PRIORITY_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ // TODO: Consider calling
+ // ActivityCompat#requestPermissions
+ // here to request the missing permissions, and then overriding
+ // public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ // int[] grantResults)
+ // to handle the case where the user grants the permission. See the documentation
+ // for ActivityCompat#requestPermissions for more details.
+ return;
+ }
mNotificationManager.notify(NOTIFICATION_RECORD_ID + size, n.build());
}
+ // Show the Toast regardless of notification permission
+ final int size = extras.getInt(C.Key.SIZE);
+ final int duration = extras.getInt(C.Key.DURATION);
+ final String path = extras.getString(C.Key.PATH);
+ final File file = new File(path);
+ final String filename = file.getName();
Toast.makeText(this, getString(
R.string.toast_record_ended,
filename,
From be1cdab50137a74b0f637cafef17650f27cf4b15 Mon Sep 17 00:00:00 2001
From: Yeshey <41551785+Yeshey@users.noreply.github.com>
Date: Wed, 30 Apr 2025 15:02:21 +0100
Subject: [PATCH 3/4] Update README.md
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 6d2fe31..8cd9a0c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Real FM Radio [](https://github.com/vladislav805/RFM-Radio/releases/latest)
+Updated SDK and requiered premissions to run on android 14 and 15
+
Real hardware radio for Android smartphones based on Qualcomm Snapdragon 2xx, 4xx and 6xx processors (up to 625 and 650).
[
](https://apt.izzysoft.de/fdroid/index/apk/com.vlad805.fmradio)
From c88763e8616f33baeb3ce5f10ac1b587951ca1d3 Mon Sep 17 00:00:00 2001
From: Yeshey <41551785+Yeshey@users.noreply.github.com>
Date: Fri, 2 May 2025 00:36:23 +0100
Subject: [PATCH 4/4] Update README.md
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 8cd9a0c..9d3a674 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Real FM Radio [](https://github.com/vladislav805/RFM-Radio/releases/latest)
+I think the right way to make radio work in recent android versions is with https://www.apkmirror.com/apk/lineageos/fm-radio-10/fm-radio-10-13-release/fm-radio-13-android-apk-download/ or https://github.com/iusmac/RevampedFMRadio
+
Updated SDK and requiered premissions to run on android 14 and 15
Real hardware radio for Android smartphones based on Qualcomm Snapdragon 2xx, 4xx and 6xx processors (up to 625 and 650).