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.app; 18 19 import static android.app.NotificationManager.IMPORTANCE_NONE; 20 21 import android.app.people.ConversationChannel; 22 import android.app.people.IPeopleManager; 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.content.pm.ShortcutInfo; 26 import android.os.Bundle; 27 import android.os.RemoteException; 28 import android.os.UserHandle; 29 import android.provider.Settings; 30 import android.util.Slog; 31 import android.widget.Button; 32 33 import androidx.annotation.VisibleForTesting; 34 import androidx.preference.Preference; 35 import androidx.preference.PreferenceGroup; 36 import androidx.preference.PreferenceScreen; 37 38 import com.android.settings.R; 39 import com.android.settings.applications.AppInfoBase; 40 import com.android.settings.core.SubSettingLauncher; 41 import com.android.settings.notification.NotificationBackend; 42 import com.android.settingslib.core.AbstractPreferenceController; 43 import com.android.settingslib.widget.LayoutPreference; 44 45 import java.text.Collator; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.List; 49 import java.util.concurrent.atomic.AtomicBoolean; 50 import java.util.concurrent.atomic.AtomicInteger; 51 52 public class RecentConversationsPreferenceController extends AbstractPreferenceController { 53 54 private static final String TAG = "RecentConversationsPC"; 55 private static final String KEY = "recent_conversations"; 56 private static final String CLEAR_ALL_KEY_SUFFIX = "_clear_all"; 57 private final IPeopleManager mPs; 58 private final NotificationBackend mBackend; 59 private PreferenceGroup mPreferenceGroup; 60 RecentConversationsPreferenceController( Context context, NotificationBackend backend, IPeopleManager ps)61 public RecentConversationsPreferenceController( 62 Context context, NotificationBackend backend, IPeopleManager ps) { 63 super(context); 64 mBackend = backend; 65 mPs = ps; 66 } 67 68 @Override getPreferenceKey()69 public String getPreferenceKey() { 70 return KEY; 71 } 72 73 @Override isAvailable()74 public boolean isAvailable() { 75 return true; 76 } 77 78 //TODO(b/233325816): Use ButtonPreference instead. getClearAll(PreferenceGroup parent)79 LayoutPreference getClearAll(PreferenceGroup parent) { 80 LayoutPreference pref = new LayoutPreference( 81 mContext, R.layout.conversations_clear_recents); 82 pref.setKey(getPreferenceKey() + CLEAR_ALL_KEY_SUFFIX); 83 pref.setOrder(1); 84 Button button = pref.findViewById(R.id.conversation_settings_clear_recents); 85 button.setOnClickListener(v -> { 86 try { 87 mPs.removeAllRecentConversations(); 88 // Removing recents is asynchronous, so we can't immediately reload the list from 89 // the backend. Instead, proactively remove all of items that were marked as 90 // clearable, so long as we didn't get an error 91 92 for (int i = parent.getPreferenceCount() - 1; i >= 0; i--) { 93 Preference p = parent.getPreference(i); 94 if (p instanceof RecentConversationPreference) { 95 if (((RecentConversationPreference) p).hasClearListener()) { 96 parent.removePreference(p); 97 } 98 } 99 } 100 button.announceForAccessibility(mContext.getString(R.string.recent_convos_removed)); 101 } catch (RemoteException e) { 102 Slog.w(TAG, "Could not clear recents", e); 103 } 104 }); 105 return pref; 106 } 107 108 @Override displayPreference(PreferenceScreen screen)109 public void displayPreference(PreferenceScreen screen) { 110 super.displayPreference(screen); 111 mPreferenceGroup = screen.findPreference(getPreferenceKey()); 112 } 113 114 /** 115 * Updates the conversation list. 116 * 117 * @return true if this controller has content to display. 118 */ updateList()119 boolean updateList() { 120 // Load conversations 121 List<ConversationChannel> conversations = Collections.emptyList(); 122 try { 123 conversations = mPs.getRecentConversations().getList(); 124 } catch (RemoteException e) { 125 Slog.w(TAG, "Could not get recent conversations", e); 126 } 127 128 return populateList(conversations); 129 } 130 131 @VisibleForTesting populateList(List<ConversationChannel> conversations)132 boolean populateList(List<ConversationChannel> conversations) { 133 mPreferenceGroup.removeAll(); 134 boolean hasClearable = false; 135 if (conversations != null) { 136 hasClearable = populateConversations(conversations); 137 } 138 139 boolean hashContent = mPreferenceGroup.getPreferenceCount() != 0; 140 mPreferenceGroup.setVisible(hashContent); 141 if (hashContent && hasClearable) { 142 Preference clearAll = getClearAll(mPreferenceGroup); 143 if (clearAll != null) { 144 mPreferenceGroup.addPreference(clearAll); 145 } 146 } 147 return hashContent; 148 } 149 populateConversations(List<ConversationChannel> conversations)150 protected boolean populateConversations(List<ConversationChannel> conversations) { 151 AtomicInteger order = new AtomicInteger(100); 152 AtomicBoolean hasClearable = new AtomicBoolean(false); 153 conversations.stream() 154 .filter(conversation -> 155 conversation.getNotificationChannel().getImportance() != IMPORTANCE_NONE 156 && (conversation.getNotificationChannelGroup() == null 157 || !conversation.getNotificationChannelGroup().isBlocked())) 158 .sorted(mConversationComparator) 159 .map(this::createConversationPref) 160 .forEachOrdered(pref -> { 161 pref.setOrder(order.getAndIncrement()); 162 mPreferenceGroup.addPreference(pref); 163 if (pref.hasClearListener()) { 164 hasClearable.set(true); 165 } 166 }); 167 return hasClearable.get(); 168 } 169 createConversationPref( final ConversationChannel conversation)170 protected RecentConversationPreference createConversationPref( 171 final ConversationChannel conversation) { 172 final String pkg = conversation.getShortcutInfo().getPackage(); 173 final int uid = conversation.getUid(); 174 final String conversationId = conversation.getShortcutInfo().getId(); 175 RecentConversationPreference pref = new RecentConversationPreference(mContext); 176 177 if (!conversation.hasActiveNotifications()) { 178 pref.setOnClearClickListener(() -> { 179 try { 180 mPs.removeRecentConversation(pkg, UserHandle.getUserId(uid), conversationId); 181 pref.getClearView().announceForAccessibility( 182 mContext.getString(R.string.recent_convo_removed)); 183 mPreferenceGroup.removePreference(pref); 184 } catch (RemoteException e) { 185 Slog.w(TAG, "Could not clear recent", e); 186 } 187 }); 188 } 189 190 pref.setTitle(getTitle(conversation)); 191 pref.setSummary(getSummary(conversation)); 192 pref.setIcon(mBackend.getConversationDrawable(mContext, conversation.getShortcutInfo(), 193 pkg, uid, false)); 194 pref.setKey(conversation.getNotificationChannel().getId() 195 + ":" + conversationId); 196 pref.setOnPreferenceClickListener(preference -> { 197 mBackend.createConversationNotificationChannel( 198 pkg, uid, 199 conversation.getNotificationChannel(), 200 conversationId); 201 getSubSettingLauncher(conversation, pref.getTitle()).launch(); 202 return true; 203 }); 204 205 return pref; 206 } 207 getSummary(ConversationChannel conversation)208 CharSequence getSummary(ConversationChannel conversation) { 209 return conversation.getNotificationChannelGroup() == null 210 ? conversation.getNotificationChannel().getName() 211 : mContext.getString(R.string.notification_conversation_summary, 212 conversation.getNotificationChannel().getName(), 213 conversation.getNotificationChannelGroup().getName()); 214 } 215 getTitle(ConversationChannel conversation)216 CharSequence getTitle(ConversationChannel conversation) { 217 ShortcutInfo si = conversation.getShortcutInfo(); 218 return si.getLabel(); 219 } 220 getSubSettingLauncher(ConversationChannel conversation, CharSequence title)221 SubSettingLauncher getSubSettingLauncher(ConversationChannel conversation, 222 CharSequence title) { 223 Bundle channelArgs = new Bundle(); 224 channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, conversation.getUid()); 225 channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, 226 conversation.getShortcutInfo().getPackage()); 227 channelArgs.putString(Settings.EXTRA_CHANNEL_ID, 228 conversation.getNotificationChannel().getId()); 229 channelArgs.putString(Settings.EXTRA_CONVERSATION_ID, 230 conversation.getShortcutInfo().getId()); 231 232 return new SubSettingLauncher(mContext) 233 .setDestination(ChannelNotificationSettings.class.getName()) 234 .setArguments(channelArgs) 235 .setExtras(channelArgs) 236 .setUserHandle(UserHandle.getUserHandleForUid(conversation.getUid())) 237 .setTitleText(title) 238 .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_CONVERSATION_LIST_SETTINGS); 239 } 240 241 @VisibleForTesting 242 Comparator<ConversationChannel> mConversationComparator = 243 new Comparator<ConversationChannel>() { 244 private final Collator sCollator = Collator.getInstance(); 245 246 @Override 247 public int compare(ConversationChannel o1, ConversationChannel o2) { 248 int labelComparison = 0; 249 if (o1.getShortcutInfo().getLabel() != null 250 && o2.getShortcutInfo().getLabel() != null) { 251 labelComparison = sCollator.compare( 252 o1.getShortcutInfo().getLabel().toString(), 253 o2.getShortcutInfo().getLabel().toString()); 254 } 255 256 if (labelComparison == 0) { 257 return o1.getNotificationChannel().getId().compareTo( 258 o2.getNotificationChannel().getId()); 259 } 260 261 return labelComparison; 262 } 263 }; 264 } 265