Allow hiding notification permission nag (#6730)

- Support showing most error messages as a snackbar
- Ask for notification permission when enabling episode notifications
- Clarify what we use notifications for
This commit is contained in:
ByteHamster 2023-10-29 16:10:38 +01:00 committed by GitHub
parent 8a011badd3
commit 4931734d94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 150 additions and 31 deletions

View File

@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -17,9 +18,11 @@ import androidx.preference.PreferenceFragmentCompat;
import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; import com.bytehamster.lib.preferencesearch.SearchPreferenceResult;
import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.ThemeSwitcher; import de.danoeh.antennapod.core.preferences.ThemeSwitcher;
import de.danoeh.antennapod.databinding.SettingsActivityBinding; import de.danoeh.antennapod.databinding.SettingsActivityBinding;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment;
@ -29,6 +32,9 @@ import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment; import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment; import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment; import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/** /**
* PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see
@ -162,4 +168,26 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
result.highlight(fragment); result.highlight(fragment);
} }
} }
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
protected void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(MessageEvent event) {
Log.d(FRAGMENT_TAG, "onEvent(" + event + ")");
Snackbar s = Snackbar.make(binding.getRoot(), event.message, Snackbar.LENGTH_LONG);
if (event.action != null) {
s.setAction(event.actionText, v -> event.action.accept(this));
}
s.show();
}
} }

View File

@ -32,9 +32,11 @@ import android.widget.SeekBar;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog; import de.danoeh.antennapod.dialog.MediaPlayerErrorDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.PlayerErrorEvent;
@ -85,7 +87,7 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
private boolean videoSurfaceCreated = false; private boolean videoSurfaceCreated = false;
private boolean destroyingDueToReload = false; private boolean destroyingDueToReload = false;
private long lastScreenTap = 0; private long lastScreenTap = 0;
private Handler videoControlsHider = new Handler(Looper.getMainLooper()); private final Handler videoControlsHider = new Handler(Looper.getMainLooper());
private VideoplayerActivityBinding viewBinding; private VideoplayerActivityBinding viewBinding;
private PlaybackController controller; private PlaybackController controller;
private boolean showTimeLeft = false; private boolean showTimeLeft = false;
@ -516,6 +518,17 @@ public class VideoplayerActivity extends CastEnabledActivity implements SeekBar.
MediaPlayerErrorDialog.show(this, event); MediaPlayerErrorDialog.show(this, event);
} }
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(MessageEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
final MaterialAlertDialogBuilder errorDialog = new MaterialAlertDialogBuilder(this);
errorDialog.setMessage(event.message);
if (event.action != null) {
errorDialog.setPositiveButton(event.actionText, (dialog, which) -> event.action.accept(this));
}
errorDialog.show();
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);

View File

@ -1,12 +1,22 @@
package de.danoeh.antennapod.fragment; package de.danoeh.antennapod.fragment;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.appbar.MaterialToolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -122,6 +132,23 @@ public class FeedSettingsFragment extends Fragment {
return fragment; return fragment;
} }
boolean notificationPermissionDenied = false;
private final ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
return;
}
if (notificationPermissionDenied) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getContext().getPackageName(), null);
intent.setData(uri);
startActivity(intent);
return;
}
Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show();
notificationPermissionDenied = true;
});
@Override @Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle state) { public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle state) {
final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state); final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state);
@ -466,6 +493,11 @@ public class FeedSettingsFragment extends Fragment {
pref.setChecked(feedPreferences.getShowEpisodeNotification()); pref.setChecked(feedPreferences.getShowEpisodeNotification());
pref.setOnPreferenceChangeListener((preference, newValue) -> { pref.setOnPreferenceChangeListener((preference, newValue) -> {
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
return false;
}
boolean checked = newValue == Boolean.TRUE; boolean checked = newValue == Boolean.TRUE;
feedPreferences.setShowEpisodeNotification(checked); feedPreferences.setShowEpisodeNotification(checked);
DBWriter.setFeedPreferences(feedPreferences); DBWriter.setFeedPreferences(feedPreferences);

View File

@ -59,6 +59,7 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
public static final String TAG = "HomeFragment"; public static final String TAG = "HomeFragment";
public static final String PREF_NAME = "PrefHomeFragment"; public static final String PREF_NAME = "PrefHomeFragment";
public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString"; public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString";
public static final String PREF_DISABLE_NOTIFICATION_PERMISSION_NAG = "DisableNotificationPermissionNag";
private static final String KEY_UP_ARROW = "up_arrow"; private static final String KEY_UP_ARROW = "up_arrow";
private boolean displayUpArrow; private boolean displayUpArrow;
@ -95,8 +96,11 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis
if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(), if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(getContext(),
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
SharedPreferences prefs = getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE);
if (!prefs.getBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, false)) {
addSection(new AllowNotificationsSection()); addSection(new AllowNotificationsSection());
} }
}
List<String> hiddenSections = getHiddenSections(getContext()); List<String> hiddenSections = getHiddenSections(getContext());
String[] sectionTags = getResources().getStringArray(R.array.home_section_tags); String[] sectionTags = getResources().getStringArray(R.array.home_section_tags);

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.ui.home.sections; package de.danoeh.antennapod.ui.home.sections;
import android.Manifest; import android.Manifest;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -15,6 +16,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.databinding.HomeSectionNotificationBinding; import de.danoeh.antennapod.databinding.HomeSectionNotificationBinding;
@ -29,6 +31,7 @@ public class AllowNotificationsSection extends Fragment {
((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null); ((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null);
} else { } else {
viewBinding.openSettingsButton.setVisibility(View.VISIBLE); viewBinding.openSettingsButton.setVisibility(View.VISIBLE);
viewBinding.allowButton.setVisibility(View.GONE);
Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show(); Toast.makeText(getContext(), R.string.notification_permission_denied, Toast.LENGTH_LONG).show();
} }
}); });
@ -49,6 +52,17 @@ public class AllowNotificationsSection extends Fragment {
intent.setData(uri); intent.setData(uri);
startActivity(intent); startActivity(intent);
}); });
viewBinding.denyButton.setOnClickListener(v -> {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());
builder.setMessage(R.string.notification_permission_deny_warning);
builder.setPositiveButton(R.string.deny_label, (dialog, which) -> {
getContext().getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE)
.edit().putBoolean(HomeFragment.PREF_DISABLE_NOTIFICATION_PERMISSION_NAG, true).apply();
((MainActivity) getActivity()).loadFragment(HomeFragment.TAG, null);
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.show();
});
return viewBinding.getRoot(); return viewBinding.getRoot();
} }
} }

View File

@ -24,24 +24,41 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_margin="16dp"
android:text="@string/notification_permission_text" /> android:text="@string/notification_permission_text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button <Button
android:id="@+id/allowButton" android:id="@+id/denyButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="right" android:text="@string/deny_label"
android:text="@android:string/ok" /> style="@style/Widget.MaterialComponents.Button.TextButton" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button <Button
android:id="@+id/openSettingsButton" android:id="@+id/openSettingsButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="right"
android:visibility="gone" android:visibility="gone"
android:text="@string/open_settings" /> android:text="@string/open_settings" />
<Button
android:id="@+id/allowButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@ -129,11 +129,7 @@ public class EpisodeDownloadWorker extends Worker {
downloader.call(); downloader.call();
} catch (Exception e) { } catch (Exception e) {
DBWriter.addDownloadStatus(downloader.getResult()); DBWriter.addDownloadStatus(downloader.getResult());
if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { sendErrorNotification(request.getTitle());
sendMessage(request.getTitle(), false);
} else {
sendErrorNotification();
}
FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
return Result.failure(); return Result.failure();
} }
@ -176,11 +172,7 @@ public class EpisodeDownloadWorker extends Worker {
|| status.getReason() == DownloadError.ERROR_UNAUTHORIZED || status.getReason() == DownloadError.ERROR_UNAUTHORIZED
|| status.getReason() == DownloadError.ERROR_IO_BLOCKED) { || status.getReason() == DownloadError.ERROR_IO_BLOCKED) {
// Fail fast, these are probably unrecoverable // Fail fast, these are probably unrecoverable
if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { sendErrorNotification(request.getTitle());
sendMessage(request.getTitle(), false);
} else {
sendErrorNotification();
}
FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
return Result.failure(); return Result.failure();
} }
@ -193,7 +185,7 @@ public class EpisodeDownloadWorker extends Worker {
private Result retry3times() { private Result retry3times() {
if (isLastRunAttempt()) { if (isLastRunAttempt()) {
sendErrorNotification(); sendErrorNotification(downloader.getDownloadRequest().getTitle());
return Result.failure(); return Result.failure();
} else { } else {
return Result.retry(); return Result.retry();
@ -227,7 +219,12 @@ public class EpisodeDownloadWorker extends Worker {
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
} }
private void sendErrorNotification() { private void sendErrorNotification(String title) {
if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) {
sendMessage(title, false);
return;
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(),
NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR);
builder.setTicker(getApplicationContext().getString(R.string.download_report_title)) builder.setTicker(getApplicationContext().getString(R.string.download_report_title))

View File

@ -562,6 +562,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@SuppressLint("LaunchActivityFromNotification") @SuppressLint("LaunchActivityFromNotification")
private void displayStreamingNotAllowedNotification(Intent originalIntent) { private void displayStreamingNotAllowedNotification(Intent originalIntent) {
if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) {
EventBus.getDefault().post(new MessageEvent(
getString(R.string.confirm_mobile_streaming_notification_message)));
return;
}
Intent intentAllowThisTime = new Intent(originalIntent); Intent intentAllowThisTime = new Intent(originalIntent);
intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME); intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME);
intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true); intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true);

View File

@ -22,6 +22,7 @@ import androidx.work.WorkerParameters;
import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.download.FeedUpdateManager;
import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.feed.SortOrder;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -298,13 +299,18 @@ public class SyncService extends Worker {
} }
private void updateErrorNotification(Exception exception) { private void updateErrorNotification(Exception exception) {
Log.d(TAG, "Posting sync error notification");
final String description = getApplicationContext().getString(R.string.gpodnetsync_error_descr)
+ exception.getMessage();
if (!UserPreferences.gpodnetNotificationsEnabled()) { if (!UserPreferences.gpodnetNotificationsEnabled()) {
Log.d(TAG, "Skipping sync error notification because of user setting"); Log.d(TAG, "Skipping sync error notification because of user setting");
return; return;
} }
Log.d(TAG, "Posting sync error notification"); if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) {
final String description = getApplicationContext().getString(R.string.gpodnetsync_error_descr) EventBus.getDefault().post(new MessageEvent(description));
+ exception.getMessage(); return;
}
Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage( Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage(
getApplicationContext().getPackageName()); getApplicationContext().getPackageName());

View File

@ -61,8 +61,10 @@
<string name="home_downloads_title">Manage downloads</string> <string name="home_downloads_title">Manage downloads</string>
<string name="home_welcome_title">Welcome to AntennaPod!</string> <string name="home_welcome_title">Welcome to AntennaPod!</string>
<string name="home_welcome_text">You are not subscribed to any podcasts yet. Open the side menu to add a podcast.</string> <string name="home_welcome_text">You are not subscribed to any podcasts yet. Open the side menu to add a podcast.</string>
<string name="notification_permission_text">AntennaPod needs your permission to show notifications while downloading episodes.</string> <string name="notification_permission_text">AntennaPod needs your permission to show notifications. By default, AntennaPod only shows notifications while something is being downloaded or when something goes wrong.</string>
<string name="notification_permission_denied">You denied the permission.</string> <string name="notification_permission_denied">You denied the permission.</string>
<string name="notification_permission_deny_warning">If you disable notifications and something goes wrong, you might be unable to find out why it went wrong.</string>
<string name="deny_label">Deny</string>
<string name="open_settings">Open settings</string> <string name="open_settings">Open settings</string>
<string name="configure_home">Configure Home Screen</string> <string name="configure_home">Configure Home Screen</string>