- PVSM.RU - https://www.pvsm.ru -

Notifications на основе билдера с кастомным лейаутом и картинкой

Приветствую!

Балуюсь разработками приложений под Android, но до сих пор не использовал Builder для создания уведомлений, а делал это старым добрым методом, как описано, например, в данной статье [1]. Однако данный метод не только уже устарел, но даже больше — он является deprecated. Кроме того, передо мной еще стояла задача выводить в каждом Notification-е свою картинку, которой при том нет в составе проекта и я не могу на нее сослаться через R.drawable, как, например, аватарка пользователя, которого я добавляю в процессе использования приложения и т.п. Если интересно — добро пожаловать под кат.

Builder для создания Notifications введен с АОС 3.0 и если минимальный уровень SDK для приложения ниже, как в моем случае, то необходимо использовать библиотеку совместимости v4 [2], т.к. я использую для разработки AndroidStudio, то включение библиотеки в состав проекта состоит в добавлении ее в build.gradle в раздел зависимостей:

dependencies {
    compile 'com.android.support:support-v4:20.0.0'
}

Тот, кто использует для разработки старый добрый Eclipse может найти соответствующую jar-ку в папке, где установлен Android SDK в папке /extras/android/support/v4/ и копирнуть ее в папку libs своего проекта.

Собственно для создания уведомлений я написал небольшой класс-хелпер — NotificationHelper. Обычно такого рода классы я наполняю public static методами, а если внутри требуется ссылка на Context, то инициализирую такой хелпер из класса Application, используя Context самого приложения, т.е. Application Context. Хранить данный контекст абсолютно безопасно даже в статик-поле, в отличии от того, который Activity — этот хранить в статиках нельзя, во избежание утечек, короче говоря, Activity Context (он же — Base Context) предпочитаю вообще нигде никогда не хранить. Итого NotificationHelper выглядит так:

public class NotificationsHelper {

    private static Context appContext; // контекст приложения
    private static int lastNotificationId = 0; //уин последнего уведомления

    private static NotificationManager manager; // менеджер уведомлений

    // метод инциализации данного хелпера
    public static void init(Context context){
        if(manager==null){
            appContext = context.getApplicationContext(); // на случай инициализации Base Context-ом
            manager = (NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
        }
    }

    /**
     * Создает и возвращает общий NotificationCompat.Builder
     * @return
     */
    private static NotificationCompat.Builder getNotificationBuilder(){
        final NotificationCompat.Builder nb = new NotificationCompat.Builder(appContext)
                .setAutoCancel(true) // чтобы уведомление закрылось после тапа по нему
                .setOnlyAlertOnce(true) // уведомить однократно
                .setWhen(System.currentTimeMillis()) // время создания уведомления, будет отображено в стандартном уведомлении справа
                .setContentTitle(appContext.getString(R.string.app_name)) //заголовок 
                .setDefaults(Notification.DEFAULT_ALL); // alarm при выводе уведомления: звук, вибратор и диод-индикатор - по умолчанию

        return nb;
    }

    // удаляет все уведомления, созданные приложением
    public static void cancelAllNotifications(){
        manager.cancelAll();
    }

   // тут следуют методы, которые рассмотрим далее
}

Т.к. в методе инициализации я прописал appContext = context.getApplicationContext(), то инициализировать этот хелпер можно откуда угодно, главное чтобы был доступ к контексту, можно даже в активити передав саму активити в качестве параметра.

Метод, используемый для создания обычного, стандартного уведомления я создал такой:

    /**
     *
     * @param message  - текст уведомления
     * @param targetActivityClass - класс целевой активити
     * @param iconResId - R.drawable необходимой иконки
     * @return
     */
    public static int createNotification(final String message, final Class targetActivityClass, final int iconResId) {

        // некоторые проверки на null не помешают, зачем нам NPE?
        if (targetActivityClass==null){
            new Exception("createNotification() targetActivity is null!").printStackTrace();
            return -1;
        }
        if (manager==null){
            new Exception("createNotification() NotificationUtils not initialized!").printStackTrace();
            return -1;
        }

        final Intent notificationIntent = new Intent(appContext, targetActivityClass); // интент для запуска указанного Activity по тапу на уведомлении
        
        final NotificationCompat.Builder nb = getNotificationBuilder() // получаем из хелпера generic Builder, и далее донастраиваем его
                .setContentText(message) // сообщение, которое будет отображаться в самом уведомлении
                .setTicker(message) //сообщение, которое будет показано в статус-баре при создании уведомления, ставлю тот же
                .setSmallIcon(iconResId != 0 ? iconResId : R.drawable.ic_launcher) // иконка, если 0, то используется иконка самого аппа
                .setContentIntent(PendingIntent.getActivity(appContext, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT)); // создание PendingIntent-а


        final Notification notification = nb.build(); //генерируем уведомление, getNotification() - deprecated!
        manager.notify(lastNotificationId, notification); // "запускаем" уведомление

        return lastNotificationId++;
    }

Вызывается этот метод, например, из активити, так:

NotificationsHelper.createNotification("Achtung message!", MessagesActivity.class, 0);

Это обычное стандартное уведомление. В принципе так как у меня во всех приложениях иконка приложения всегда называется именно ic_launcher, а не как-то еще, то данный хелпер универсален для меня. Т.е. его без изменений можно включать в состав любого приложения, где это требуется.

А вот вариант с картинкой, которой нет в составе проекта, как я уже отмечал выше, уже не получится сделать столь же универсальным, т.к. к нему требуется layout в придачу. Именно этот layout и дает возможность вывести что угодно, с него и начну, пожалуй (notification_layout.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="10dp">

    <ImageView
        android:id="@+id/notification_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/notification_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />

</LinearLayout>

Думаю тут все ясно из самих наименований id-шек:
notification_image — место под аватарку
notification_message — место для отображения текста уведомления.

И собственно метод создания уведомления с картинкой, загруженной приложением во время работы:

    /**
     * 
     * @param message - сообщение
     * @param targetActivityClass - класс целевой Активити
     * @param icon - картинка (аватарка)
     * @return
     */
    public static int createNotification(final String message, final Class targetActivityClass, final Bitmap icon){

        // аналогичные же проверки на null
        if (targetActivityClass==null){
            new Exception("createNotification() targetActivity is null!").printStackTrace();
            return -1;
        }
        if (manager==null){
            new Exception("createNotification() NotificationUtils not initialized!").printStackTrace();
        return -1;
        }

        // именно класс RemoteViews предоставляет возможность использования своего лейаута для уведомлений
        final RemoteViews contentView = new RemoteViews(appContext.getPackageName(), R.layout.notification_layout);
        contentView.setTextViewText(R.id.notification_message, message); // сообщение уведомления
        contentView.setImageViewBitmap(R.id.notification_image, icon); // картинка для уведомления, та же аватарка, к примеру

        final Intent notificationIntent = new Intent(appContext, targetActivityClass); // интент для запуска указанного Activity по тапу на уведомлении
        final NotificationCompat.Builder nb = getNotificationBuilder() // получаем билдер-основу
                .setTicker(message) // сообщение, которое будет показано в статус-баре при создании уведомления
                .setContentIntent(PendingIntent.getActivity(appContext, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT))
                .setSmallIcon(R.drawable.ic_launcher); // использую иконку приложения, без этого уведомление может не выводиться вообще
                //.setContent(contentView); // не работает на 2.3.*, у гугла - все как обычно
                // см. http://stackoverflow.com/questions/12574386/custom-notification-layout-dont-work-on-android-2-3-or-lower 

        final Notification notification = nb.build(); //генерируем уведомление
        notification.contentView = contentView; // поскольку setContent() в билдере не всегда работает, ставим здесь

        manager.notify(lastNotificationId, notification); // "запускаем" уведомление

        return lastNotificationId++;
    }

Вызывается этот метод, например, из активити, так:

Bitmap avatar = getAvatarBitmap(); // предположим есть такой метод и возвращает он необходимый для уведомления Bitmap
NotificationsHelper.createNotification("Achtung message", MessagesActivity.class, avatar);

Собственно, вот и все. Ну почти…
Все же остаются ньюансы, например, если приложение состоит из нескольких активити, то тупо запускать определенную активити через уведомление — несовсем правильно. Ведь, во-первых, эта активити может как раз быть открыта в приложении, и таким образом через уведомление такая активити запустится еще раз. Это можно обойти, указав в манифесте для такой активити следующий флаг типа запуска:

android:launchMode="singleTop"

Но это для исключения повторного запуска активной (topmost) активити, т.е. если активити запущена (в стеке), но «накрыта» какой-то еще, то способ не поможет. Можно попробовать подобрать еще какой-то из возможных параметров типа запуска.
А что делать, если приложение не запущено? Ведь нельзя же взять и запустить отдельную активити, которая обычно в приложении запускается по цепочке через другие активити, и ее нельзя запускать отдельно от других, да и выход из нее повлечет выход из приложения, вместо ожидаемого пользователем возврата на предыдущий «экран». А если в приложении стартовая активити — онлайн авторизация, что является обязательным условием для продолжения работы приложения, что тогда? Получится что запустим какую-то активити, минуя авторизацию, а это может привести к непредсказуемым последствиям. Можно ограничиться вызовом основной, стартовой активити из уведомления, но опять же, это хорошо, когда приложение не запущено, но когда пользователь уже в приложении, то к чему ему повторная авторизация? Здесь поможет разве что полезная нагрузка уведомления, т.е. передача в нем Bundle с некими параметрами.

Лично решил это следующим образом: создал активити-заглушку NotificationActivity, которая и вызывается из уведомления, при этом в бандл уведомления закидываю ту активити, что мне реально надо запустить, в NotificationActivity проверяю условия и выполняю соответствующие необходимые действия:

public class NotificationActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Bundle extras = getIntent().getExtras();
        if (extras!=null && extras.containsKey(KEY_EXTRAS_TARGET_ACTIVITY)){
            if (isAppRunning()) {
                if (LoginActivity.isRunning() /*isActivityRunning(LoginActivity.class)*/) {
                    //
                } else if (MainActivity.isRunning() /*isActivityRunning(MainActivity.class)*/) {
                    startActivity(new Intent(this, (Class) extras.getSerializable(KEY_EXTRAS_TARGET_ACTIVITY)));
                } else {
                    final Intent intent = new Intent(getBaseContext(), LoginActivity.class);
                    intent.putExtras(extras);
                    startActivity(intent);
                }
            } else {
                final Intent intent = new Intent(this, LoginActivity.class);
                intent.putExtras(extras);
                startActivity(intent);
            }
        }

        finish();
        return;
    }



    private boolean isAppRunning() {
        final String process = getPackageName();
        final ActivityManager activityManager = (ActivityManager) getSystemService( ACTIVITY_SERVICE );
        List<ActivityManager.RunningAppProcessInfo> procInfos = activityManager.getRunningAppProcesses();
        for(int i = 0; i < procInfos.size(); i++){
            if(procInfos.get(i).processName.equals(process)) {
                return true;
            }
        }

        return false;
    }

    // для использования этого метода придется добавлять в манифест разрешение:
    // <uses-permission android:name="android.permission.GET_TASKS"/>
    private boolean isActivityRunning(Class activityClass) {
        ActivityManager activityManager = (ActivityManager) getBaseContext().getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> tasks = activityManager.getRunningTasks(Integer.MAX_VALUE);

        for (ActivityManager.RunningTaskInfo task : tasks) {
            if (activityClass.getCanonicalName().equalsIgnoreCase(task.baseActivity.getClassName()))
                return true;
        }

        return false;
    }

}

Здесь я комментировал вызовы метода isActivityRunning(), т.к. это требует еще одного разрешения в манифесте. Кому это некритично, тот спокойно может удалить вызов статического метода до комментария, и раскомментировать вызов этого метода. Я же решил, что лучше мансов с манифестом избежать и потому написал статические методы в обе активити:

    private static boolean isRunning;
    public static boolean isRunning(){return isRunning;}

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        isRunning = true;
        ...
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isRunning = false;
    }

Изменения, которые для этого потребовалось внести в хелпер уведомлений (в оба метода createNotification()):

...
final Intent notificationIntent = new Intent(appContext, NotificationActivity.class);
notificationIntent.putExtra(KEY_EXTRAS_TARGET_ACTIVITY, targetActivityClass);
...

Ну вот, кажется, и все…

Автор: StanKo

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android-development/75763

Ссылки в тексте:

[1] в данной статье: http://habrahabr.ru/blogs/android_development/111238

[2] библиотеку совместимости v4: http://developer.android.com/sdk/compatibility-library.html

[3] Источник: http://habrahabr.ru/post/244423/