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