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.systemui.people.widget; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.LauncherApps; 23 import android.os.Bundle; 24 import android.os.RemoteException; 25 import android.os.ServiceManager; 26 import android.os.UserHandle; 27 import android.os.UserManager; 28 import android.service.notification.NotificationStats; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.app.UnlaunchableAppActivity; 34 import com.android.internal.logging.UiEventLogger; 35 import com.android.internal.logging.UiEventLoggerImpl; 36 import com.android.internal.statusbar.IStatusBarService; 37 import com.android.internal.statusbar.NotificationVisibility; 38 import com.android.systemui.dagger.qualifiers.Background; 39 import com.android.systemui.people.PeopleSpaceUtils; 40 import com.android.systemui.statusbar.CommandQueue; 41 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 42 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; 43 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; 44 import com.android.systemui.wmshell.BubblesManager; 45 import com.android.wm.shell.bubbles.Bubble; 46 47 import java.util.Optional; 48 import java.util.concurrent.Executor; 49 50 import javax.inject.Inject; 51 52 /** Proxy activity to launch ShortcutInfo's conversation. */ 53 public class LaunchConversationActivity extends Activity { 54 private static final String TAG = "PeopleSpaceLaunchConv"; 55 private static final boolean DEBUG = PeopleSpaceUtils.DEBUG; 56 private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); 57 private NotificationVisibilityProvider mVisibilityProvider; 58 private CommonNotifCollection mCommonNotifCollection; 59 private final Optional<BubblesManager> mBubblesManagerOptional; 60 private final UserManager mUserManager; 61 private boolean mIsForTesting; 62 private IStatusBarService mIStatusBarService; 63 private CommandQueue mCommandQueue; 64 private Executor mBgExecutor; 65 private Bubble mBubble; 66 private NotificationEntry mEntryToBubble; 67 68 @Inject LaunchConversationActivity( NotificationVisibilityProvider visibilityProvider, CommonNotifCollection commonNotifCollection, Optional<BubblesManager> bubblesManagerOptional, UserManager userManager, CommandQueue commandQueue, @Background Executor bgExecutor )69 public LaunchConversationActivity( 70 NotificationVisibilityProvider visibilityProvider, 71 CommonNotifCollection commonNotifCollection, 72 Optional<BubblesManager> bubblesManagerOptional, 73 UserManager userManager, 74 CommandQueue commandQueue, 75 @Background Executor bgExecutor 76 ) { 77 super(); 78 mVisibilityProvider = visibilityProvider; 79 mCommonNotifCollection = commonNotifCollection; 80 mBubblesManagerOptional = bubblesManagerOptional; 81 mUserManager = userManager; 82 mCommandQueue = commandQueue; 83 mCommandQueue.addCallback(new CommandQueue.Callbacks() { 84 // (b/190833924) Wait for the app transition to finish before showing the bubble, 85 // opening the bubble while the transition is happening can mess with the placement 86 // of the bubble's surface. 87 @Override 88 public void appTransitionFinished(int displayId) { 89 if (mBubblesManagerOptional.isPresent()) { 90 if (mBubble != null) { 91 mBubblesManagerOptional.get().expandStackAndSelectBubble(mBubble); 92 } else if (mEntryToBubble != null) { 93 mBubblesManagerOptional.get().expandStackAndSelectBubble(mEntryToBubble); 94 } 95 } 96 mCommandQueue.removeCallback(this); 97 } 98 }); 99 mBgExecutor = bgExecutor; 100 } 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 if (!mIsForTesting) { 105 super.onCreate(savedInstanceState); 106 } 107 if (DEBUG) Log.d(TAG, "onCreate called"); 108 109 Intent intent = getIntent(); 110 String tileId = intent.getStringExtra(PeopleSpaceWidgetProvider.EXTRA_TILE_ID); 111 String packageName = intent.getStringExtra(PeopleSpaceWidgetProvider.EXTRA_PACKAGE_NAME); 112 UserHandle userHandle = intent.getParcelableExtra( 113 PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE); 114 String notificationKey = 115 intent.getStringExtra(PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY); 116 117 if (!TextUtils.isEmpty(tileId)) { 118 if (DEBUG) { 119 Log.d(TAG, "Launching conversation with shortcutInfo id " + tileId); 120 } 121 mUiEventLogger.log(PeopleSpaceUtils.PeopleSpaceWidgetEvent.PEOPLE_SPACE_WIDGET_CLICKED); 122 try { 123 124 if (mUserManager.isQuietModeEnabled(userHandle)) { 125 if (DEBUG) Log.d(TAG, "Cannot launch app when quieted"); 126 final Intent dialogIntent = 127 UnlaunchableAppActivity.createInQuietModeDialogIntent( 128 userHandle.getIdentifier()); 129 this.getApplicationContext().startActivity(dialogIntent); 130 finish(); 131 return; 132 } 133 134 // We can potentially bubble without a notification, so rather than rely on 135 // notificationKey here (which could be null if there's no notification or if the 136 // bubble is suppressing the notification), so we'll use the shortcutId for lookups. 137 // This misses one specific case: a bubble that was never opened & still has a 138 // visible notification, but the bubble was dismissed & aged out of the overflow. 139 // So it wouldn't exist in the stack or overflow to be looked up BUT the notif entry 140 // would still exist & be bubbleable. So if we don't get a bubble from the 141 // shortcutId, fallback to notificationKey if it exists. 142 if (mBubblesManagerOptional.isPresent()) { 143 mBubble = mBubblesManagerOptional.get().getBubbleWithShortcutId(tileId); 144 NotificationEntry entry = mCommonNotifCollection.getEntry(notificationKey); 145 if (mBubble != null || (entry != null && entry.canBubble())) { 146 mEntryToBubble = entry; 147 if (DEBUG) { 148 Log.d(TAG, 149 "Opening bubble: " + mBubble + ", entry: " + mEntryToBubble); 150 } 151 // Just opt-out and don't cancel the notification for bubbles. 152 finish(); 153 return; 154 } 155 } 156 157 if (mIStatusBarService == null) { 158 mIStatusBarService = IStatusBarService.Stub.asInterface( 159 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 160 } 161 clearNotificationIfPresent(notificationKey, packageName, userHandle); 162 LauncherApps launcherApps = 163 getApplicationContext().getSystemService(LauncherApps.class); 164 launcherApps.startShortcut( 165 packageName, tileId, null, null, userHandle); 166 } catch (Exception e) { 167 Log.e(TAG, "Exception launching shortcut:" + e); 168 } 169 } else { 170 if (DEBUG) Log.d(TAG, "Trying to launch conversation with null shortcutInfo."); 171 } 172 finish(); 173 } 174 clearNotificationIfPresent(String notifKey, String packageName, UserHandle userHandle)175 void clearNotificationIfPresent(String notifKey, String packageName, UserHandle userHandle) { 176 if (TextUtils.isEmpty(notifKey)) { 177 if (DEBUG) Log.d(TAG, "Skipping clear notification: notification key is empty"); 178 return; 179 } 180 181 if (mIStatusBarService == null || mCommonNotifCollection == null) { 182 if (DEBUG) { 183 Log.d(TAG, "Skipping clear notification: null services, key: " + notifKey); 184 } 185 return; 186 } 187 188 NotificationEntry entry = mCommonNotifCollection.getEntry(notifKey); 189 if (entry == null || entry.getRanking() == null) { 190 if (DEBUG) { 191 Log.d(TAG, "Skipping clear notification: NotificationEntry or its Ranking" 192 + " is null, key: " + notifKey); 193 } 194 return; 195 } 196 197 NotificationVisibility notifVisibility = mVisibilityProvider.obtain(entry, true); 198 int rank = notifVisibility.rank; 199 200 if (DEBUG) Log.d(TAG, "Clearing notification, key: " + notifKey + ", rank: " + rank); 201 mBgExecutor.execute(() -> { 202 try { 203 mIStatusBarService.onNotificationClear( 204 packageName, userHandle.getIdentifier(), notifKey, 205 NotificationStats.DISMISSAL_OTHER, 206 NotificationStats.DISMISS_SENTIMENT_POSITIVE, notifVisibility); 207 } catch (RemoteException e) { 208 Log.e(TAG, "Exception cancelling notification:" + e); 209 } 210 }); 211 } 212 213 @VisibleForTesting setIsForTesting(boolean isForTesting, IStatusBarService statusBarService)214 void setIsForTesting(boolean isForTesting, IStatusBarService statusBarService) { 215 mIsForTesting = isForTesting; 216 mIStatusBarService = statusBarService; 217 } 218 } 219