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