blob: 976a766dcc095d0278fc59726b61146592d20b15 [file] [log] [blame]
Mady Mellordea7ecf2018-12-10 15:47:40 -08001/*
2 * Copyright (C) 2018 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
17package com.android.systemui.bubbles;
18
Mady Mellore8e07712019-01-23 12:45:33 -080019import android.animation.LayoutTransition;
20import android.animation.ObjectAnimator;
Mady Mellordea7ecf2018-12-10 15:47:40 -080021import android.annotation.Nullable;
Mady Mellore8e07712019-01-23 12:45:33 -080022import android.app.INotificationManager;
Mady Mellor9801e852019-01-22 14:50:28 -080023import android.app.Notification;
24import android.app.PendingIntent;
Mady Mellordea7ecf2018-12-10 15:47:40 -080025import android.content.Context;
Mady Mellor9801e852019-01-22 14:50:28 -080026import android.content.Intent;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
Mady Mellordea7ecf2018-12-10 15:47:40 -080029import android.content.res.Resources;
Mady Mellordd497052019-01-30 17:23:48 -080030import android.content.res.TypedArray;
Mady Mellordea7ecf2018-12-10 15:47:40 -080031import android.graphics.Color;
Mady Mellore8e07712019-01-23 12:45:33 -080032import android.graphics.drawable.Drawable;
Mady Mellordea7ecf2018-12-10 15:47:40 -080033import android.graphics.drawable.ShapeDrawable;
Mady Mellore8e07712019-01-23 12:45:33 -080034import android.os.RemoteException;
35import android.os.ServiceManager;
Mady Mellor9801e852019-01-22 14:50:28 -080036import android.provider.Settings;
Steven Wub00225b2019-02-08 14:27:42 -050037import android.service.notification.StatusBarNotification;
Mady Mellordea7ecf2018-12-10 15:47:40 -080038import android.util.AttributeSet;
Mady Mellor9801e852019-01-22 14:50:28 -080039import android.util.Log;
Steven Wub00225b2019-02-08 14:27:42 -050040import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080041import android.view.View;
Mady Mellore8e07712019-01-23 12:45:33 -080042import android.widget.FrameLayout;
Mady Mellor9801e852019-01-22 14:50:28 -080043import android.widget.ImageButton;
Mady Mellore8e07712019-01-23 12:45:33 -080044import android.widget.ImageView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080045import android.widget.LinearLayout;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050046import android.widget.TextView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080047
Mady Mellore8e07712019-01-23 12:45:33 -080048import com.android.systemui.Interpolators;
Mady Mellordea7ecf2018-12-10 15:47:40 -080049import com.android.systemui.R;
50import com.android.systemui.recents.TriangleShape;
Mady Mellor9801e852019-01-22 14:50:28 -080051import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellordea7ecf2018-12-10 15:47:40 -080052
53/**
54 * Container for the expanded bubble view, handles rendering the caret and header of the view.
55 */
Mady Mellor3d82e682019-02-05 13:34:48 -080056public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
Mady Mellor9801e852019-01-22 14:50:28 -080057 private static final String TAG = "BubbleExpandedView";
Mady Mellordea7ecf2018-12-10 15:47:40 -080058
59 // The triangle pointing to the expanded view
60 private View mPointerView;
Mady Mellore8e07712019-01-23 12:45:33 -080061
62 // Header
63 private View mHeaderView;
64 private TextView mHeaderTextView;
Mady Mellor9801e852019-01-22 14:50:28 -080065 private ImageButton mDeepLinkIcon;
Mady Mellor9801e852019-01-22 14:50:28 -080066 private ImageButton mSettingsIcon;
Mady Mellore8e07712019-01-23 12:45:33 -080067
68 // Permission view
69 private View mPermissionView;
70
Mady Mellordea7ecf2018-12-10 15:47:40 -080071 // The view that is being displayed for the expanded state
72 private View mExpandedView;
73
Mady Mellor9801e852019-01-22 14:50:28 -080074 private NotificationEntry mEntry;
75 private PackageManager mPm;
76 private String mAppName;
Mady Mellore8e07712019-01-23 12:45:33 -080077 private Drawable mAppIcon;
78
79 private INotificationManager mNotificationManagerService;
Mady Mellor9801e852019-01-22 14:50:28 -080080
81 // Need reference to let it know to collapse when new task is launched
82 private BubbleStackView mStackView;
83
Mady Mellore8e07712019-01-23 12:45:33 -080084 private OnBubbleBlockedListener mOnBubbleBlockedListener;
85
Mady Mellor3d82e682019-02-05 13:34:48 -080086 public BubbleExpandedView(Context context) {
Mady Mellordea7ecf2018-12-10 15:47:40 -080087 this(context, null);
88 }
89
Mady Mellor3d82e682019-02-05 13:34:48 -080090 public BubbleExpandedView(Context context, AttributeSet attrs) {
Mady Mellordea7ecf2018-12-10 15:47:40 -080091 this(context, attrs, 0);
92 }
93
Mady Mellor3d82e682019-02-05 13:34:48 -080094 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
Mady Mellordea7ecf2018-12-10 15:47:40 -080095 this(context, attrs, defStyleAttr, 0);
96 }
97
Mady Mellor3d82e682019-02-05 13:34:48 -080098 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
Mady Mellordea7ecf2018-12-10 15:47:40 -080099 int defStyleRes) {
100 super(context, attrs, defStyleAttr, defStyleRes);
Mady Mellor9801e852019-01-22 14:50:28 -0800101 mPm = context.getPackageManager();
Mady Mellore8e07712019-01-23 12:45:33 -0800102 try {
103 mNotificationManagerService = INotificationManager.Stub.asInterface(
104 ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE));
105 } catch (ServiceManager.ServiceNotFoundException e) {
106 Log.w(TAG, e);
107 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800108 }
109
110 @Override
111 protected void onFinishInflate() {
112 super.onFinishInflate();
113
114 Resources res = getResources();
115 mPointerView = findViewById(R.id.pointer_view);
116 int width = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
117 int height = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
Mady Mellordd497052019-01-30 17:23:48 -0800118
119 TypedArray ta = getContext().obtainStyledAttributes(
120 new int[] {android.R.attr.colorBackgroundFloating});
121 int bgColor = ta.getColor(0, Color.WHITE);
122 ta.recycle();
123
Mady Mellordea7ecf2018-12-10 15:47:40 -0800124 ShapeDrawable triangleDrawable = new ShapeDrawable(
125 TriangleShape.create(width, height, true /* pointUp */));
Mady Mellordd497052019-01-30 17:23:48 -0800126 triangleDrawable.setTint(bgColor);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800127 mPointerView.setBackground(triangleDrawable);
Mady Mellor9801e852019-01-22 14:50:28 -0800128
Mady Mellore8e07712019-01-23 12:45:33 -0800129 FrameLayout viewWrapper = findViewById(R.id.header_permission_wrapper);
130 LayoutTransition transition = new LayoutTransition();
131 transition.setDuration(200);
132
133 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
134 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
135 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
136
137 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
138 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
139 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
140
141 transition.setAnimateParentHierarchy(false);
142 viewWrapper.setLayoutTransition(transition);
143 viewWrapper.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
144
145 mHeaderView = findViewById(R.id.header_layout);
146 mHeaderTextView = findViewById(R.id.header_text);
Mady Mellor9801e852019-01-22 14:50:28 -0800147 mDeepLinkIcon = findViewById(R.id.deep_link_button);
148 mSettingsIcon = findViewById(R.id.settings_button);
149 mDeepLinkIcon.setOnClickListener(this);
150 mSettingsIcon.setOnClickListener(this);
Mady Mellore8e07712019-01-23 12:45:33 -0800151
152 mPermissionView = findViewById(R.id.permission_layout);
153 findViewById(R.id.no_bubbles_button).setOnClickListener(this);
154 findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
155 }
156
157 /**
158 * Sets the listener to notify when a bubble has been blocked.
159 */
160 public void setOnBlockedListener(OnBubbleBlockedListener listener) {
161 mOnBubbleBlockedListener = listener;
Mady Mellor9801e852019-01-22 14:50:28 -0800162 }
163
164 /**
165 * Sets the notification entry used to populate this view.
166 */
167 public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
168 mStackView = stackView;
169 mEntry = entry;
170
171 ApplicationInfo info;
172 try {
173 info = mPm.getApplicationInfo(
174 entry.notification.getPackageName(),
175 PackageManager.MATCH_UNINSTALLED_PACKAGES
176 | PackageManager.MATCH_DISABLED_COMPONENTS
177 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
178 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
179 if (info != null) {
180 mAppName = String.valueOf(mPm.getApplicationLabel(info));
Mady Mellore8e07712019-01-23 12:45:33 -0800181 mAppIcon = mPm.getApplicationIcon(info);
Mady Mellor9801e852019-01-22 14:50:28 -0800182 }
183 } catch (PackageManager.NameNotFoundException e) {
184 // Ahh... just use package name
185 mAppName = entry.notification.getPackageName();
186 }
Mady Mellore8e07712019-01-23 12:45:33 -0800187 if (mAppIcon == null) {
188 mAppIcon = mPm.getDefaultActivityIcon();
189 }
Mady Mellor9801e852019-01-22 14:50:28 -0800190 updateHeaderView();
Mady Mellore8e07712019-01-23 12:45:33 -0800191 updatePermissionView();
Mady Mellor9801e852019-01-22 14:50:28 -0800192 }
193
194 private void updateHeaderView() {
195 mSettingsIcon.setContentDescription(getResources().getString(
196 R.string.bubbles_settings_button_description, mAppName));
197 mDeepLinkIcon.setContentDescription(getResources().getString(
198 R.string.bubbles_deep_link_button_description, mAppName));
199 if (mEntry != null && mEntry.getBubbleMetadata() != null) {
Mady Mellore8e07712019-01-23 12:45:33 -0800200 mHeaderTextView.setText(mEntry.getBubbleMetadata().getTitle());
Mady Mellor9801e852019-01-22 14:50:28 -0800201 } else {
202 // This should only happen if we're auto-bubbling notification content that isn't
203 // explicitly a bubble
Mady Mellore8e07712019-01-23 12:45:33 -0800204 mHeaderTextView.setText(mAppName);
205 }
206 }
207
208 private void updatePermissionView() {
209 boolean hasUserApprovedBubblesForPackage = false;
210 try {
211 hasUserApprovedBubblesForPackage =
212 mNotificationManagerService.hasUserApprovedBubblesForPackage(
213 mEntry.notification.getPackageName(), mEntry.notification.getUid());
214 } catch (RemoteException e) {
215 Log.w(TAG, e);
216 }
217 if (hasUserApprovedBubblesForPackage) {
218 mHeaderView.setVisibility(VISIBLE);
219 mPermissionView.setVisibility(GONE);
220 } else {
221 mHeaderView.setVisibility(GONE);
222 mPermissionView.setVisibility(VISIBLE);
223 ((ImageView) mPermissionView.findViewById(R.id.pkgicon)).setImageDrawable(mAppIcon);
224 ((TextView) mPermissionView.findViewById(R.id.pkgname)).setText(mAppName);
Mady Mellor9801e852019-01-22 14:50:28 -0800225 }
226 }
227
228 @Override
229 public void onClick(View view) {
230 if (mEntry == null) {
231 return;
232 }
233 Notification n = mEntry.notification.getNotification();
234 int id = view.getId();
235 if (id == R.id.deep_link_button) {
236 mStackView.collapseStack(() -> {
237 try {
238 n.contentIntent.send();
Steven Wub00225b2019-02-08 14:27:42 -0500239 logBubbleClickEvent(mEntry.notification,
240 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_APP);
Mady Mellor9801e852019-01-22 14:50:28 -0800241 } catch (PendingIntent.CanceledException e) {
242 Log.w(TAG, "Failed to send intent for bubble with key: "
243 + (mEntry != null ? mEntry.key : " null entry"));
244 }
245 });
246 } else if (id == R.id.settings_button) {
247 Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
248 mEntry.notification.getUid());
Steven Wub00225b2019-02-08 14:27:42 -0500249 mStackView.collapseStack(() -> {
250 mContext.startActivity(intent);
251 logBubbleClickEvent(mEntry.notification,
252 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
253 });
Mady Mellore8e07712019-01-23 12:45:33 -0800254 } else if (id == R.id.no_bubbles_button) {
255 setBubblesAllowed(false);
256 } else if (id == R.id.yes_bubbles_button) {
257 setBubblesAllowed(true);
258 }
259 }
260
261 private void setBubblesAllowed(boolean allowed) {
262 try {
263 mNotificationManagerService.setBubblesAllowed(
264 mEntry.notification.getPackageName(),
265 mEntry.notification.getUid(),
266 allowed);
267 if (allowed) {
268 mPermissionView.setVisibility(GONE);
269 mHeaderView.setVisibility(VISIBLE);
270 } else if (mOnBubbleBlockedListener != null) {
271 mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
272 }
Steven Wub00225b2019-02-08 14:27:42 -0500273 logBubbleClickEvent(mEntry.notification,
274 allowed ? StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_IN :
275 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_OUT);
Mady Mellore8e07712019-01-23 12:45:33 -0800276 } catch (RemoteException e) {
277 Log.w(TAG, e);
Mady Mellor9801e852019-01-22 14:50:28 -0800278 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800279 }
280
281 /**
282 * Set the x position that the tip of the triangle should point to.
283 */
284 public void setPointerPosition(int x) {
285 // Adjust for the pointer size
286 x -= (mPointerView.getWidth() / 2);
287 mPointerView.setTranslationX(x);
288 }
289
290 /**
291 * Set the view to display for the expanded state. Passing null will clear the view.
292 */
293 public void setExpandedView(View view) {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500294 if (mExpandedView == view) {
295 return;
296 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800297 if (mExpandedView != null) {
298 removeView(mExpandedView);
299 }
300 mExpandedView = view;
301 if (mExpandedView != null) {
302 addView(mExpandedView);
303 }
304 }
305
306 /**
307 * @return the view containing the expanded content, can be null.
308 */
309 @Nullable
310 public View getExpandedView() {
311 return mExpandedView;
312 }
Mady Mellor9801e852019-01-22 14:50:28 -0800313
314 private Intent getSettingsIntent(String packageName, final int appUid) {
315 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
316 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
317 intent.putExtra(Settings.EXTRA_APP_UID, appUid);
318 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
319 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
320 return intent;
321 }
Mady Mellore8e07712019-01-23 12:45:33 -0800322
323 /**
324 * Listener that is notified when a bubble is blocked.
325 */
326 public interface OnBubbleBlockedListener {
327 /**
328 * Called when a bubble is blocked for the provided entry.
329 */
330 void onBubbleBlocked(NotificationEntry entry);
331 }
Steven Wub00225b2019-02-08 14:27:42 -0500332
333 /**
334 * Logs bubble UI click event.
335 *
336 * @param notification the bubble notification that user is interacting with.
337 * @param action the user interaction enum.
338 */
339 private void logBubbleClickEvent(StatusBarNotification notification, int action) {
340 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
341 notification.getPackageName(),
342 notification.getNotification().getChannelId(),
343 notification.getId(),
344 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
345 mStackView.getBubbleCount(),
346 action,
347 mStackView.getNormalizedXPosition(),
348 mStackView.getNormalizedYPosition());
349 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800350}