1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settings.notification;
18
19import static android.app.NotificationManager.IMPORTANCE_LOW;
20import static android.app.NotificationManager.IMPORTANCE_NONE;
21import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
22
23import android.app.Activity;
24import android.app.NotificationChannel;
25import android.app.NotificationChannelGroup;
26import android.app.NotificationManager;
27import android.content.Intent;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.provider.Settings;
31import android.support.v7.preference.Preference;
32import android.support.v7.preference.PreferenceCategory;
33import android.text.TextUtils;
34import android.util.ArrayMap;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.widget.Switch;
39
40import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
41import com.android.settings.AppHeader;
42import com.android.settings.R;
43import com.android.settings.Utils;
44import com.android.settings.applications.AppHeaderController;
45import com.android.settings.applications.AppInfoBase;
46import com.android.settings.applications.LayoutPreference;
47import com.android.settings.notification.NotificationBackend.AppRow;
48import com.android.settings.overlay.FeatureFactory;
49import com.android.settings.widget.FooterPreference;
50import com.android.settings.widget.MasterSwitchPreference;
51import com.android.settings.widget.SwitchBar;
52import com.android.settingslib.RestrictedSwitchPreference;
53
54import java.util.ArrayList;
55import java.util.Collections;
56import java.util.Comparator;
57import java.util.List;
58
59/** These settings are per app, so should not be returned in global search results. */
60public class AppNotificationSettings extends NotificationSettingsBase {
61    private static final String TAG = "AppNotificationSettings";
62    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
63
64    private static String KEY_GENERAL_CATEGORY = "categories";
65    private static String KEY_DELETED = "deleted";
66
67    private List<NotificationChannelGroup> mChannelGroupList;
68    private List<PreferenceCategory> mChannelGroups = new ArrayList();
69    private FooterPreference mDeletedChannels;
70
71    @Override
72    public int getMetricsCategory() {
73        return MetricsEvent.NOTIFICATION_APP_NOTIFICATION;
74    }
75
76    @Override
77    public void onResume() {
78        super.onResume();
79
80        if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) {
81            Log.w(TAG, "Missing package or uid or packageinfo");
82            finish();
83            return;
84        }
85
86        if (getPreferenceScreen() != null) {
87            getPreferenceScreen().removeAll();
88            mChannelGroups.clear();
89            mDeletedChannels = null;
90            mShowLegacyChannelConfig = false;
91        }
92
93        addPreferencesFromResource(R.xml.notification_settings);
94        getPreferenceScreen().setOrderingAsAdded(true);
95        setupBlock();
96        addHeaderPref();
97
98        mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid);
99        if (mShowLegacyChannelConfig) {
100            mChannel = mBackend.getChannel(
101                    mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID);
102            populateDefaultChannelPrefs();
103        } else {
104            addPreferencesFromResource(R.xml.upgraded_app_notification_settings);
105            setupBadge();
106            // Load channel settings
107            new AsyncTask<Void, Void, Void>() {
108                @Override
109                protected Void doInBackground(Void... unused) {
110                    mChannelGroupList = mBackend.getChannelGroups(mPkg, mUid).getList();
111                    Collections.sort(mChannelGroupList, mChannelGroupComparator);
112                    return null;
113                }
114
115                @Override
116                protected void onPostExecute(Void unused) {
117                    if (getHost() == null) {
118                        return;
119                    }
120                    populateChannelList();
121                    addAppLinkPref();
122                }
123            }.execute();
124        }
125
126        updateDependents(mAppRow.banned);
127    }
128
129    private void addHeaderPref() {
130        ArrayMap<String, AppRow> rows = new ArrayMap<String, AppRow>();
131        rows.put(mAppRow.pkg, mAppRow);
132        collectConfigActivities(rows);
133        final Activity activity = getActivity();
134        final Preference pref = FeatureFactory.getFactory(activity)
135                .getApplicationFeatureProvider(activity)
136                .newAppHeaderController(this /* fragment */, null /* appHeader */)
137                .setIcon(mAppRow.icon)
138                .setLabel(mAppRow.label)
139                .setPackageName(mAppRow.pkg)
140                .setUid(mAppRow.uid)
141                .setButtonActions(AppHeaderController.ActionType.ACTION_NONE,
142                        AppHeaderController.ActionType.ACTION_NOTIF_PREFERENCE)
143                .done(activity, getPrefContext());
144        pref.setKey(KEY_HEADER);
145        getPreferenceScreen().addPreference(pref);
146    }
147
148    private void populateChannelList() {
149        if (!mChannelGroups.isEmpty()) {
150            // If there's anything in mChannelGroups, we've called populateChannelList twice.
151            // Clear out existing channels and log.
152            Log.w(TAG, "Notification channel group posted twice to settings - old size " +
153                    mChannelGroups.size() + ", new size " + mChannelGroupList.size());
154            for (Preference p : mChannelGroups) {
155                getPreferenceScreen().removePreference(p);
156            }
157        }
158        if (mChannelGroupList.isEmpty()) {
159            PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext());
160            groupCategory.setTitle(R.string.notification_channels);
161            groupCategory.setKey(KEY_GENERAL_CATEGORY);
162            getPreferenceScreen().addPreference(groupCategory);
163            mChannelGroups.add(groupCategory);
164
165            Preference empty = new Preference(getPrefContext());
166            empty.setTitle(R.string.no_channels);
167            empty.setEnabled(false);
168            groupCategory.addPreference(empty);
169        } else {
170            for (NotificationChannelGroup group : mChannelGroupList) {
171                PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext());
172                if (group.getId() == null) {
173                    groupCategory.setTitle(mChannelGroupList.size() > 1
174                            ? R.string.notification_channels_other
175                            : R.string.notification_channels);
176                    groupCategory.setKey(KEY_GENERAL_CATEGORY);
177                } else {
178                    groupCategory.setTitle(group.getName());
179                    groupCategory.setKey(group.getId());
180                }
181                groupCategory.setOrderingAsAdded(true);
182                getPreferenceScreen().addPreference(groupCategory);
183                mChannelGroups.add(groupCategory);
184
185                final List<NotificationChannel> channels = group.getChannels();
186                Collections.sort(channels, mChannelComparator);
187                int N = channels.size();
188                for (int i = 0; i < N; i++) {
189                    final NotificationChannel channel = channels.get(i);
190                    populateSingleChannelPrefs(groupCategory, channel);
191                }
192            }
193
194            int deletedChannelCount = mBackend.getDeletedChannelCount(mAppRow.pkg, mAppRow.uid);
195            if (deletedChannelCount > 0) {
196                mDeletedChannels = new FooterPreference(getPrefContext());
197                mDeletedChannels.setSelectable(false);
198                mDeletedChannels.setTitle(getResources().getQuantityString(
199                        R.plurals.deleted_channels, deletedChannelCount, deletedChannelCount));
200                mDeletedChannels.setEnabled(false);
201                mDeletedChannels.setKey(KEY_DELETED);
202                mDeletedChannels.setOrder(ORDER_LAST);
203                getPreferenceScreen().addPreference(mDeletedChannels);
204            }
205        }
206
207        updateDependents(mAppRow.banned);
208    }
209
210    private void populateSingleChannelPrefs(PreferenceCategory groupCategory,
211            final NotificationChannel channel) {
212        MasterSwitchPreference channelPref = new MasterSwitchPreference(
213                getPrefContext());
214        channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null
215                && isChannelBlockable(mAppRow.systemApp, channel)
216                && isChannelConfigurable(channel));
217        channelPref.setKey(channel.getId());
218        channelPref.setTitle(channel.getName());
219        channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE);
220        channelPref.setSummary(getImportanceSummary(channel));
221        Bundle channelArgs = new Bundle();
222        channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid);
223        channelArgs.putBoolean(AppHeader.EXTRA_HIDE_INFO_BUTTON, true);
224        channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg);
225        channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
226        Intent channelIntent = Utils.onBuildStartFragmentIntent(getActivity(),
227                ChannelNotificationSettings.class.getName(),
228                channelArgs, null, R.string.notification_channel_title, null, false,
229                getMetricsCategory());
230        channelPref.setIntent(channelIntent);
231
232        channelPref.setOnPreferenceChangeListener(
233                new Preference.OnPreferenceChangeListener() {
234                    @Override
235                    public boolean onPreferenceChange(Preference preference,
236                            Object o) {
237                        boolean value = (Boolean) o;
238                        int importance = value ?  IMPORTANCE_LOW : IMPORTANCE_NONE;
239                        channel.setImportance(importance);
240                        channel.lockFields(
241                                NotificationChannel.USER_LOCKED_IMPORTANCE);
242                        channelPref.setSummary(getImportanceSummary(channel));
243                        mBackend.updateChannel(mPkg, mUid, channel);
244
245                        return true;
246                    }
247                });
248        groupCategory.addPreference(channelPref);
249    }
250
251    void setupBadge() {
252        mBadge = (RestrictedSwitchPreference) getPreferenceScreen().findPreference(KEY_BADGE);
253        mBadge.setDisabledByAdmin(mSuspendedAppsAdmin);
254        if (mChannel == null) {
255            mBadge.setChecked(mAppRow.showBadge);
256        } else {
257            mBadge.setChecked(mChannel.canShowBadge());
258        }
259        mBadge.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
260            @Override
261            public boolean onPreferenceChange(Preference preference, Object newValue) {
262                final boolean value = (Boolean) newValue;
263                if (mChannel == null) {
264                    mBackend.setShowBadge(mPkg, mUid, value);
265                } else {
266                    mChannel.setShowBadge(value);
267                    mChannel.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE);
268                    mBackend.updateChannel(mPkg, mUid, mChannel);
269                }
270                return true;
271            }
272        });
273    }
274
275    protected void setupBlock() {
276        View switchBarContainer = LayoutInflater.from(
277                getPrefContext()).inflate(R.layout.styled_switch_bar, null);
278        mSwitchBar = switchBarContainer.findViewById(R.id.switch_bar);
279        mSwitchBar.show();
280        mSwitchBar.setDisabledByAdmin(mSuspendedAppsAdmin);
281        mSwitchBar.setChecked(!mAppRow.banned);
282        mSwitchBar.addOnSwitchChangeListener(new SwitchBar.OnSwitchChangeListener() {
283            @Override
284            public void onSwitchChanged(Switch switchView, boolean isChecked) {
285                if (mShowLegacyChannelConfig && mChannel != null) {
286                    final int importance = isChecked ? IMPORTANCE_UNSPECIFIED : IMPORTANCE_NONE;
287                    mImportanceToggle.setChecked(importance == IMPORTANCE_UNSPECIFIED);
288                    mChannel.setImportance(importance);
289                    mChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
290                    mBackend.updateChannel(mPkg, mUid, mChannel);
291                }
292                mBackend.setNotificationsEnabledForPackage(mPkgInfo.packageName, mUid, isChecked);
293                mAppRow.banned = true;
294                updateDependents(!isChecked);
295            }
296        });
297
298        mBlockBar = new LayoutPreference(getPrefContext(), switchBarContainer);
299        mBlockBar.setOrder(ORDER_FIRST);
300        mBlockBar.setKey(KEY_BLOCK);
301        getPreferenceScreen().addPreference(mBlockBar);
302
303        if (mAppRow.systemApp && !mAppRow.banned) {
304            setVisible(mBlockBar, false);
305        }
306
307        setupBlockDesc(R.string.app_notifications_off_desc);
308    }
309
310    protected void updateDependents(boolean banned) {
311        for (PreferenceCategory category : mChannelGroups) {
312            setVisible(category, !banned);
313        }
314        if (mDeletedChannels != null) {
315            setVisible(mDeletedChannels, !banned);
316        }
317        setVisible(mBlockedDesc, banned);
318        setVisible(mBadge, !banned);
319        if (mShowLegacyChannelConfig) {
320            setVisible(mImportanceToggle, !banned);
321            setVisible(mPriority, checkCanBeVisible(NotificationManager.IMPORTANCE_DEFAULT)
322                    || (checkCanBeVisible(NotificationManager.IMPORTANCE_LOW)
323                    && mDndVisualEffectsSuppressed));
324            setVisible(mVisibilityOverride, !banned &&
325                    checkCanBeVisible(NotificationManager.IMPORTANCE_LOW) && isLockScreenSecure());
326        }
327        if (mAppLink != null) {
328            setVisible(mAppLink, !banned);
329        }
330        if (mAppRow.systemApp && !mAppRow.banned) {
331            setVisible(mBlockBar, false);
332        }
333    }
334
335    private String getImportanceSummary(NotificationChannel channel) {
336        switch (channel.getImportance()) {
337            case NotificationManager.IMPORTANCE_UNSPECIFIED:
338                return getContext().getString(R.string.notification_importance_unspecified);
339            case NotificationManager.IMPORTANCE_NONE:
340                return getContext().getString(R.string.notification_toggle_off);
341            case NotificationManager.IMPORTANCE_MIN:
342                return getContext().getString(R.string.notification_importance_min);
343            case NotificationManager.IMPORTANCE_LOW:
344                return getContext().getString(R.string.notification_importance_low);
345            case NotificationManager.IMPORTANCE_DEFAULT:
346                return getContext().getString(R.string.notification_importance_default);
347            case NotificationManager.IMPORTANCE_HIGH:
348            case NotificationManager.IMPORTANCE_MAX:
349            default:
350                return getContext().getString(R.string.notification_importance_high);
351        }
352
353    }
354
355    private Comparator<NotificationChannel> mChannelComparator =
356            new Comparator<NotificationChannel>() {
357
358        @Override
359        public int compare(NotificationChannel left, NotificationChannel right) {
360            if (left.isDeleted() != right.isDeleted()) {
361                return Boolean.compare(left.isDeleted(), right.isDeleted());
362            }
363            return left.getId().compareTo(right.getId());
364        }
365    };
366
367    private Comparator<NotificationChannelGroup> mChannelGroupComparator =
368            new Comparator<NotificationChannelGroup>() {
369
370                @Override
371                public int compare(NotificationChannelGroup left, NotificationChannelGroup right) {
372                    // Non-grouped channels (in placeholder group with a null id) come last
373                    if (left.getId() == null && right.getId() != null) {
374                        return 1;
375                    } else if (right.getId() == null && left.getId() != null) {
376                        return -1;
377                    }
378                    return left.getId().compareTo(right.getId());
379                }
380            };
381}
382