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.ButtonPreference; 44 import com.android.settingslib.widget.LayoutPreference; 45 46 import java.text.Collator; 47 import java.util.Collections; 48 import java.util.Comparator; 49 import java.util.List; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 import java.util.concurrent.atomic.AtomicInteger; 52 53 public class RecentConversationsPreferenceController extends AbstractPreferenceController { 54 55 private static final String TAG = "RecentConversationsPC"; 56 private static final String KEY = "recent_conversations"; 57 private static final String CLEAR_ALL_KEY_SUFFIX = "_clear_all"; 58 private final IPeopleManager mPs; 59 private final NotificationBackend mBackend; 60 private PreferenceGroup mPreferenceGroup; 61 RecentConversationsPreferenceController( Context context, NotificationBackend backend, IPeopleManager ps)62 public RecentConversationsPreferenceController( 63 Context context, NotificationBackend backend, IPeopleManager ps) { 64 super(context); 65 mBackend = backend; 66 mPs = ps; 67 } 68 69 @Override getPreferenceKey()70 public String getPreferenceKey() { 71 return KEY; 72 } 73 74 @Override isAvailable()75 public boolean isAvailable() { 76 return true; 77 } 78 getClearAll(PreferenceGroup parent)79 ButtonPreference getClearAll(PreferenceGroup parent) { 80 ButtonPreference pref = new ButtonPreference(mContext); 81 pref.setTitle(R.string.conversation_settings_clear_recents); 82 pref.setKey(getPreferenceKey() + CLEAR_ALL_KEY_SUFFIX); 83 pref.setOrder(1); 84 pref.setOnClickListener(v -> { 85 try { 86 mPs.removeAllRecentConversations(); 87 // Removing recents is asynchronous, so we can't immediately reload the list from 88 // the backend. Instead, proactively remove all of items that were marked as 89 // clearable, so long as we didn't get an error 90 91 for (int i = parent.getPreferenceCount() - 1; i >= 0; i--) { 92 Preference p = parent.getPreference(i); 93 if (p instanceof RecentConversationPreference) { 94 if (((RecentConversationPreference) p).hasClearListener()) { 95 parent.removePreference(p); 96 } 97 } 98 } 99 pref.getButton().announceForAccessibility( 100 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 instanceof RecentConversationPreference 164 && ((RecentConversationPreference) pref).hasClearListener()) { 165 hasClearable.set(true); 166 } 167 }); 168 return hasClearable.get(); 169 } 170 createConversationPref( final ConversationChannel conversation)171 protected Preference createConversationPref( 172 final ConversationChannel conversation) { 173 final String pkg = conversation.getShortcutInfo().getPackage(); 174 final int uid = conversation.getUid(); 175 final String conversationId = conversation.getShortcutInfo().getId(); 176 Preference pref = conversation.hasActiveNotifications() ? new Preference(mContext) 177 : new RecentConversationPreference(mContext); 178 179 if (!conversation.hasActiveNotifications()) { 180 ((RecentConversationPreference) pref).setOnClearClickListener(() -> { 181 try { 182 mPs.removeRecentConversation(pkg, UserHandle.getUserId(uid), conversationId); 183 ((RecentConversationPreference) pref).getClearView().announceForAccessibility( 184 mContext.getString(R.string.recent_convo_removed)); 185 mPreferenceGroup.removePreference(pref); 186 } catch (RemoteException e) { 187 Slog.w(TAG, "Could not clear recent", e); 188 } 189 }); 190 } 191 192 pref.setTitle(getTitle(conversation)); 193 pref.setSummary(getSummary(conversation)); 194 pref.setIcon(mBackend.getConversationDrawable(mContext, conversation.getShortcutInfo(), 195 pkg, uid, false)); 196 pref.setKey(conversation.getNotificationChannel().getId() 197 + ":" + conversationId); 198 pref.setOnPreferenceClickListener(preference -> { 199 mBackend.createConversationNotificationChannel( 200 pkg, uid, 201 conversation.getNotificationChannel(), 202 conversationId); 203 getSubSettingLauncher(conversation, pref.getTitle()).launch(); 204 return true; 205 }); 206 207 return pref; 208 } 209 getSummary(ConversationChannel conversation)210 CharSequence getSummary(ConversationChannel conversation) { 211 return conversation.getNotificationChannelGroup() == null 212 ? conversation.getNotificationChannel().getName() 213 : mContext.getString(R.string.notification_conversation_summary, 214 conversation.getNotificationChannel().getName(), 215 conversation.getNotificationChannelGroup().getName()); 216 } 217 getTitle(ConversationChannel conversation)218 CharSequence getTitle(ConversationChannel conversation) { 219 ShortcutInfo si = conversation.getShortcutInfo(); 220 return si.getLabel(); 221 } 222 getSubSettingLauncher(ConversationChannel conversation, CharSequence title)223 SubSettingLauncher getSubSettingLauncher(ConversationChannel conversation, 224 CharSequence title) { 225 Bundle channelArgs = new Bundle(); 226 channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, conversation.getUid()); 227 channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, 228 conversation.getShortcutInfo().getPackage()); 229 channelArgs.putString(Settings.EXTRA_CHANNEL_ID, 230 conversation.getNotificationChannel().getId()); 231 channelArgs.putString(Settings.EXTRA_CONVERSATION_ID, 232 conversation.getShortcutInfo().getId()); 233 234 return new SubSettingLauncher(mContext) 235 .setDestination(ChannelNotificationSettings.class.getName()) 236 .setArguments(channelArgs) 237 .setExtras(channelArgs) 238 .setUserHandle(UserHandle.getUserHandleForUid(conversation.getUid())) 239 .setTitleText(title) 240 .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_CONVERSATION_LIST_SETTINGS); 241 } 242 243 @VisibleForTesting 244 Comparator<ConversationChannel> mConversationComparator = 245 new Comparator<ConversationChannel>() { 246 private final Collator sCollator = Collator.getInstance(); 247 248 @Override 249 public int compare(ConversationChannel o1, ConversationChannel o2) { 250 int labelComparison = 0; 251 if (o1.getShortcutInfo().getLabel() != null 252 && o2.getShortcutInfo().getLabel() != null) { 253 labelComparison = sCollator.compare( 254 o1.getShortcutInfo().getLabel().toString(), 255 o2.getShortcutInfo().getLabel().toString()); 256 } 257 258 if (labelComparison == 0) { 259 return o1.getNotificationChannel().getId().compareTo( 260 o2.getNotificationChannel().getId()); 261 } 262 263 return labelComparison; 264 } 265 }; 266 } 267