1 /*
2  * Copyright (C) 2020 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 
17 package com.android.settings.notification;
18 
19 import static android.app.NotificationChannel.DEFAULT_ALLOW_BUBBLE;
20 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
21 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
22 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
23 
24 import android.app.NotificationChannel;
25 import android.app.NotificationChannelGroup;
26 import android.content.Context;
27 import android.content.pm.ShortcutInfo;
28 import android.graphics.drawable.Drawable;
29 import android.service.notification.ConversationChannelWrapper;
30 import android.view.View;
31 import android.widget.ImageView;
32 
33 import androidx.annotation.Nullable;
34 import androidx.preference.Preference;
35 import androidx.preference.PreferenceViewHolder;
36 
37 import com.android.settings.R;
38 import com.android.settings.notification.app.AppConversationListPreferenceController;
39 import com.android.settingslib.RestrictedLockUtils;
40 
41 import com.google.common.annotations.VisibleForTesting;
42 
43 import java.util.List;
44 import java.util.stream.Collectors;
45 
46 /**
47  * Displays a list of conversations that have either been selected or excluded from bubbling.
48  */
49 public class AppBubbleListPreferenceController extends AppConversationListPreferenceController {
50 
51     private static final String KEY = "bubble_conversations";
52 
AppBubbleListPreferenceController(Context context, NotificationBackend backend)53     public AppBubbleListPreferenceController(Context context, NotificationBackend backend) {
54         super(context, backend);
55     }
56 
57     @Override
updateState(Preference preference)58     public void updateState(Preference preference) {
59         // loading convos is async; hide header until we know we have conversations to show
60         preference.setVisible(false);
61         super.updateState(preference);
62     }
63 
64     @Override
onResume(NotificationBackend.AppRow appRow, @Nullable NotificationChannel channel, @Nullable NotificationChannelGroup group, Drawable conversationDrawable, ShortcutInfo conversationInfo, RestrictedLockUtils.EnforcedAdmin admin, List<String> preferenceFilter)65     public void onResume(NotificationBackend.AppRow appRow,
66             @Nullable NotificationChannel channel, @Nullable NotificationChannelGroup group,
67             Drawable conversationDrawable,
68             ShortcutInfo conversationInfo,
69             RestrictedLockUtils.EnforcedAdmin admin,
70             List<String> preferenceFilter) {
71         super.onResume(appRow, channel, group, conversationDrawable, conversationInfo, admin,
72                 preferenceFilter);
73         // In case something changed in the foreground (e.g. via bubble button on notification)
74         loadConversationsAndPopulate();
75     }
76 
77     @Override
getPreferenceKey()78     public String getPreferenceKey() {
79         return KEY;
80     }
81 
82     @Override
isAvailable()83     public boolean isAvailable() {
84         // copy rather than inherit super's isAvailable because apps can link to this page
85         // as part of onboarding, before they send a valid conversation notification
86         if (mAppRow == null) {
87             return false;
88         }
89         if (mAppRow.banned) {
90             return false;
91         }
92         if (mChannel != null) {
93             if (mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid)
94                     || NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId())) {
95                 return false;
96             }
97         }
98         if (mAppRow.bubblePreference == BUBBLE_PREFERENCE_NONE) {
99             return false;
100         }
101         return true;
102     }
103 
104     @VisibleForTesting
105     @Override
filterAndSortConversations( List<ConversationChannelWrapper> conversations)106     public List<ConversationChannelWrapper> filterAndSortConversations(
107             List<ConversationChannelWrapper> conversations) {
108         return conversations.stream()
109                 .sorted(mConversationComparator)
110                 .filter((c) -> {
111                     if (mAppRow.bubblePreference == BUBBLE_PREFERENCE_SELECTED) {
112                         return c.getNotificationChannel().canBubble();
113                     } else if (mAppRow.bubblePreference == BUBBLE_PREFERENCE_ALL) {
114                         return c.getNotificationChannel().getAllowBubbles() == 0;
115                     }
116                     return false;
117                 })
118                 .collect(Collectors.toList());
119     }
120 
121     @Override
122     protected int getTitleResId() {
123         // TODO: possible to left align like mocks?
124         return mAppRow.bubblePreference == BUBBLE_PREFERENCE_SELECTED
125                 ? R.string.bubble_app_setting_selected_conversation_title
126                 : R.string.bubble_app_setting_excluded_conversation_title;
127     }
128 
129     @VisibleForTesting
130     @Override
131     public Preference createConversationPref(final ConversationChannelWrapper conversation) {
132         final ConversationPreference pref = new ConversationPreference(mContext);
133         populateConversationPreference(conversation, pref);
134         pref.setOnClickBubblesConversation(mAppRow.bubblePreference == BUBBLE_PREFERENCE_ALL);
135         pref.setOnClickListener((v) -> {
136             conversation.getNotificationChannel().setAllowBubbles(DEFAULT_ALLOW_BUBBLE);
137             mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, conversation.getNotificationChannel());
138             mPreference.removePreference(pref);
139             if (mPreference.getPreferenceCount() == 0) {
140                 mPreference.setVisible(false);
141             }
142         });
143         return pref;
144     }
145 
146     @Override
147     protected void populateList() {
148         super.populateList();
149         if (mPreference == null) {
150             return;
151         }
152         mPreference.setVisible(mPreference.getPreferenceCount() > 0);
153     }
154 
155     /** Simple preference with a 'x' button at the end. */
156     @VisibleForTesting
157     public static class ConversationPreference extends Preference implements View.OnClickListener {
158 
159         View.OnClickListener mOnClickListener;
160         boolean mOnClickBubbles;
161 
162         ConversationPreference(Context context) {
163             super(context);
164             setWidgetLayoutResource(R.layout.bubble_conversation_remove_button);
165         }
166 
167         @Override
168         public void onBindViewHolder(final PreferenceViewHolder holder) {
169             super.onBindViewHolder(holder);
170             ImageView view =  holder.itemView.findViewById(R.id.button);
171             view.setContentDescription(mOnClickBubbles
172                     ? getContext().getString(R.string.bubble_app_setting_bubble_conversation)
173                     : getContext().getString(R.string.bubble_app_setting_unbubble_conversation));
174             view.setOnClickListener(mOnClickListener);
175         }
176 
177         public void setOnClickBubblesConversation(boolean enablesBubbles) {
178             mOnClickBubbles = enablesBubbles;
179         }
180 
181         public void setOnClickListener(View.OnClickListener listener) {
182             mOnClickListener = listener;
183         }
184 
185         @Override
186         public void onClick(View v) {
187             if (mOnClickListener != null) {
188                 mOnClickListener.onClick(v);
189             }
190         }
191     }
192 }
193