I have been struggling for days now to get local notifications to display on an Android device. Notifications simply do not show up and I’m getting a developer warning:
package Failed to post notification on channel "channel_id_here" error.
I went through many tutorials and StackOverflow posts to see if there’s anything I missed, but I simply can’t find it. Can someone please take a look if I did something wrong here or if there’s something I’m missing. I would appreciate some assistance in sorting this out.
My app is targeting API 29 and I used code from this git
Here is the relevant code from my app:
AppCompatPreferenceActivity.java
public abstract class AppCompatPreferenceActivity extends PreferenceActivity { private AppCompatDelegate mDelegate; @Override protected void onCreate(Bundle savedInstanceState) { getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); super.onCreate(savedInstanceState); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); getDelegate().onPostCreate(savedInstanceState); } public ActionBar getSupportActionBar() { return getDelegate().getSupportActionBar(); } public void setSupportActionBar(@Nullable Toolbar toolbar) { getDelegate().setSupportActionBar(toolbar); } @Override public MenuInflater getMenuInflater() { return getDelegate().getMenuInflater(); } @Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } @Override public void setContentView(View view) { getDelegate().setContentView(view); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { getDelegate().setContentView(view, params); } @Override public void addContentView(View view, ViewGroup.LayoutParams params) { getDelegate().addContentView(view, params); } @Override protected void onPostResume() { super.onPostResume(); getDelegate().onPostResume(); } @Override protected void onTitleChanged(CharSequence title, int color) { super.onTitleChanged(title, color); getDelegate().setTitle(title); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); getDelegate().onConfigurationChanged(newConfig); } @Override protected void onStop() { super.onStop(); getDelegate().onStop(); } @Override protected void onDestroy() { super.onDestroy(); getDelegate().onDestroy(); } public void invalidateOptionsMenu() { getDelegate().invalidateOptionsMenu(); } private AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, null); } return mDelegate; } }
NotificationPublisher.java
public class NotificationPublisher extends BroadcastReceiver { public static String NOTIFICATION = "notification"; @Override public void onReceive(Context ctx, Intent intent) { NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); // The notification may come from the received intent (see SettingsActivity for how to build // it and add it to the intent). If not then we'll build a notification here. Notification notification; if (intent.hasExtra(NOTIFICATION)) { notification = intent.getParcelableExtra(NOTIFICATION); } else { notification = buildNotification(ctx); } // notificationId is a unique int for each notification. // TODO Is the current time good enough? int notificationId = (int) System.currentTimeMillis(); notificationManager.notify(notificationId, notification); } private Notification buildNotification(Context ctx) { Intent intent = new Intent(ctx, com.app.myapp.activity.MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, intent, 0); NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_notification) .setContentTitle("Hello I'm a notification!") .setContentText("Well look at that, it's content") .setContentIntent(pendingIntent) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true); return builder.build(); } }
NotificationSchedulerApplication.java
public class NotificationSchedulerApplication extends Application { public static final String CHANNEL_ID = "1998882"; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } private void createNotificationChannel() { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = getString(R.string.channel_name); String description = getString(R.string.channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.enableLights(true); channel.setLightColor(Color.RED); channel.enableVibration(true); channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } }
SettingsActivity.java
public class SettingsActivity extends AppCompatPreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String ENABLE_NOTIFICATIONS_PREF_KEY = "notifications_enable"; private static final String NOTIFICATION_TIME_PREF_KEY = "notifications_time"; // This request code is used for all PendingIntents so that PendingIntents previously submitted // to the AlarmManager can be looked up for cancellation or modification. private static final int REQUEST_CODE = 0; /** * A preference value change listener that updates the preference's summary * to reflect its new value. */ private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object value) { if (preference instanceof ListPreference) { // For list preferences, look up the correct display value in // the preference's 'entries' list. ListPreference listPreference = (ListPreference) preference; int index = listPreference.findIndexOfValue(value.toString()); preference.setSummary( index >= 0 ? listPreference.getEntries()[index] : null); } else if (preference instanceof TimePreference) { TimePreference timePreference = (TimePreference) preference; preference.setSummary(timePreference.valueToSummary((int) value)); } else { // For all other preferences, set the summary to the value's // simple string representation. preference.setSummary(value.toString()); } return true; } }; /** * Helper method to determine if the device has an extra-large screen. For * example, 10" tablets are extra-large. */ private static boolean isXLargeTablet(Context context) { return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; } /** * Binds a preference's summary to its value. More specifically, when the * preference's value is changed, its summary (line of text below the * preference title) is updated to reflect the value. The summary is also * immediately updated upon calling this method. The exact display format is * dependent on the type of preference. * * @see #sBindPreferenceSummaryToValueListener */ private static void bindPreferenceSummaryToValue(Preference preference) { preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); // Trigger the listener immediately with the preference's current value. SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences( preference.getContext()); Object value; if (preference instanceof TimePreference) { value = sharedPrefs.getInt(preference.getKey(), 0); } else { value = sharedPrefs.getString(preference.getKey(), ""); } sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, value); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupActionBar(); // Listen for changes to any preferences. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.registerOnSharedPreferenceChangeListener(this); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { // We only want to take action if these particular preferences change. if (!s.equals(ENABLE_NOTIFICATIONS_PREF_KEY) && !s.equals(NOTIFICATION_TIME_PREF_KEY)) { return; } // If the preference was changed to false then we should cancel any pending notifications. if (s.equals(ENABLE_NOTIFICATIONS_PREF_KEY) && !sharedPreferences.getBoolean(s, false)) { cancelIntents(); return; } // Get the time at which to schedule notifications. Calendar startTime = TimePreference.valueToCalendar( sharedPreferences.getInt(NOTIFICATION_TIME_PREF_KEY, 0)); // If the time has already passed today, start notifications tomorrow. Calendar now = Calendar.getInstance(); if (now.after(startTime)) { startTime.add(Calendar.DATE, 1); } // Build a notification, add it to the intent we'll schedule, and schedule it. // Notification notification = buildNotification(); // scheduleNotification(notification, startTime); // Schedule an intent, and build and publish the notification in the future whenever // the intent is received. scheduleNotification(startTime); } private PendingIntent getNotificationPublisherIntent(Notification notification) { Intent intent = new Intent(this, NotificationPublisher.class); if (notification != null) { intent.putExtra(NotificationPublisher.NOTIFICATION, notification); } return PendingIntent.getBroadcast( this, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private Notification buildNotification() { Intent intent = new Intent(this, com.app.myapp.activity.MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_notification) .setContentTitle("Hello I'm a notification!") .setContentText("Well look at that, it's content 111") .setContentIntent(pendingIntent) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true); return builder.build(); } private void scheduleNotification(Notification notification, Calendar startTime) { scheduleIntent(getNotificationPublisherIntent(notification), startTime); } private void scheduleNotification(Calendar startTime) { scheduleIntent(getNotificationPublisherIntent(null), startTime); } private void scheduleIntent(PendingIntent intent, Calendar startTime) { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, startTime.getTimeInMillis(), AlarmManager.INTERVAL_DAY, intent); } private void cancelIntents() { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(getNotificationPublisherIntent(null)); } /** * Set up the {@link android.app.ActionBar}, if the API is available. */ private void setupActionBar() { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { // Show the Up button in the action bar. actionBar.setDisplayHomeAsUpEnabled(true); } } /** * {@inheritDoc} */ @Override public boolean onIsMultiPane() { return isXLargeTablet(this); } /** * {@inheritDoc} */ @Override @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.xml.pref_headers, target); } /** * This method stops fragment injection in malicious applications. * Make sure to deny any unknown fragments here. */ protected boolean isValidFragment(String fragmentName) { return PreferenceFragment.class.getName().equals(fragmentName) || NotificationPreferenceFragment.class.getName().equals(fragmentName); } /** * This fragment shows notification preferences only. It is used when the * activity is showing a two-pane settings UI. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static class NotificationPreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.pref_notification); setHasOptionsMenu(true); // Bind the summary of the TimePreference to its value. When the value changes, the // summary is updated to reflect the new value, per the Android Design guidelines. bindPreferenceSummaryToValue(findPreference("notifications_time")); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { startActivity(new Intent(getActivity(), SettingsActivity.class)); return true; } return super.onOptionsItemSelected(item); } } }
TimePreference.java
public class TimePreference extends DialogPreference { // This is the same as the default value set in the XML. Keep them in sync. private static final int DEFAULT_TIME = 7 * 60; // 0700 // The currently chosen time stored as the number of minutes after midnight. private int time; private TimePicker picker; public TimePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public TimePreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public TimePreference(Context context, AttributeSet attrs) { super(context, attrs); } public TimePreference(Context context) { super(context); } /** * valueToSummary takes the raw value of the preference and converts it into a human-readable * string fit for use in e.g. the preference's summary. * * @param value The raw value of the preference. * @return The time formatted according to the current settings (locale, 12/24 hour clock) */ public String valueToSummary(int value) { return DateFormat.getTimeFormat(getContext()).format(valueToCalendar(value).getTime()); } public static Calendar valueToCalendar(int value) { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, getHours(value)); calendar.set(Calendar.MINUTE, getMinutes(value)); calendar.set(Calendar.SECOND, 0); return calendar; } private void setTime(int minAfterMidnight) { time = minAfterMidnight; persistInt(time); notifyChanged(); } private static int getHours(int minAfterMidnight) { return minAfterMidnight / 60; } private static int getMinutes(int minAfterMidnight) { return minAfterMidnight % 60; } @Override protected View onCreateDialogView() { picker = new TimePicker(getContext()); return picker; } @Override protected void onBindDialogView(View view) { super.onBindDialogView(view); picker.setIs24HourView(DateFormat.is24HourFormat(getContext())); if (Build.VERSION.SDK_INT >= 23) { picker.setHour(getHours(time)); picker.setMinute(getMinutes(time)); } else { picker.setCurrentHour(getHours(time)); picker.setCurrentMinute(getMinutes(time)); } } @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); if (positiveResult) { int hours, minutes; if (Build.VERSION.SDK_INT >= 23) { hours = picker.getHour(); minutes = picker.getMinute(); } else { hours = picker.getCurrentHour(); minutes = picker.getCurrentMinute(); } int newTime = hours * 60 + minutes; if (callChangeListener(newTime)) { setTime(newTime); } } } @Override protected Object onGetDefaultValue(TypedArray a, int index) { return a.getInt(index, DEFAULT_TIME); } @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { super.onSetInitialValue(restorePersistedValue, defaultValue); setTime(restorePersistedValue ? getPersistedInt(time) : (int) defaultValue); } }
The receiver in my AndroidManifest.xml:
<receiver android:name=".activity.NotificationPublisher" />
I can set the time in my app and notifications are enabled for the app on the device. They simply don’t display.
Note: I am developing with Android API 29.
Advertisement
Answer
In the sample app, in NotificationSchedulerApplication:
@Override public void onCreate() { super.onCreate(); createNotificationChannel(); } private void createNotificationChannel() { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = getString(R.string.channel_name); String description = getString(R.string.channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } }
The bit in the AndroidManifest.xml that causes onCreate to be called in the Application followed by createNotificationChannel is this:
<application android:name=".NotificationSchedulerApplication" .../> </application>
onCreate in the Application should be called every time you do: Run -> Debug
Just click on the left of createNotificationChannel() to set a breakpoint.