blob: f08ba1936b406029a88cd9b7c0a51aa97f03816e [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.bubbles;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
/**
* Container for the expanded bubble view, handles rendering the caret and header of the view.
*/
public class BubbleExpandedViewContainer extends LinearLayout implements View.OnClickListener {
private static final String TAG = "BubbleExpandedView";
// The triangle pointing to the expanded view
private View mPointerView;
// Header
private View mHeaderView;
private TextView mHeaderTextView;
private ImageButton mDeepLinkIcon;
private ImageButton mSettingsIcon;
// Permission view
private View mPermissionView;
// The view that is being displayed for the expanded state
private View mExpandedView;
private NotificationEntry mEntry;
private PackageManager mPm;
private String mAppName;
private Drawable mAppIcon;
private INotificationManager mNotificationManagerService;
// Need reference to let it know to collapse when new task is launched
private BubbleStackView mStackView;
private OnBubbleBlockedListener mOnBubbleBlockedListener;
public BubbleExpandedViewContainer(Context context) {
this(context, null);
}
public BubbleExpandedViewContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleExpandedViewContainer(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public BubbleExpandedViewContainer(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mPm = context.getPackageManager();
try {
mNotificationManagerService = INotificationManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE));
} catch (ServiceManager.ServiceNotFoundException e) {
Log.w(TAG, e);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Resources res = getResources();
mPointerView = findViewById(R.id.pointer_view);
int width = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
int height = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
TypedArray ta = getContext().obtainStyledAttributes(
new int[] {android.R.attr.colorBackgroundFloating});
int bgColor = ta.getColor(0, Color.WHITE);
ta.recycle();
ShapeDrawable triangleDrawable = new ShapeDrawable(
TriangleShape.create(width, height, true /* pointUp */));
triangleDrawable.setTint(bgColor);
mPointerView.setBackground(triangleDrawable);
FrameLayout viewWrapper = findViewById(R.id.header_permission_wrapper);
LayoutTransition transition = new LayoutTransition();
transition.setDuration(200);
ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
transition.setAnimateParentHierarchy(false);
viewWrapper.setLayoutTransition(transition);
viewWrapper.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
mHeaderView = findViewById(R.id.header_layout);
mHeaderTextView = findViewById(R.id.header_text);
mDeepLinkIcon = findViewById(R.id.deep_link_button);
mSettingsIcon = findViewById(R.id.settings_button);
mDeepLinkIcon.setOnClickListener(this);
mSettingsIcon.setOnClickListener(this);
mPermissionView = findViewById(R.id.permission_layout);
findViewById(R.id.no_bubbles_button).setOnClickListener(this);
findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
}
/**
* Sets the listener to notify when a bubble has been blocked.
*/
public void setOnBlockedListener(OnBubbleBlockedListener listener) {
mOnBubbleBlockedListener = listener;
}
/**
* Sets the notification entry used to populate this view.
*/
public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
mStackView = stackView;
mEntry = entry;
ApplicationInfo info;
try {
info = mPm.getApplicationInfo(
entry.notification.getPackageName(),
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_DIRECT_BOOT_AWARE);
if (info != null) {
mAppName = String.valueOf(mPm.getApplicationLabel(info));
mAppIcon = mPm.getApplicationIcon(info);
}
} catch (PackageManager.NameNotFoundException e) {
// Ahh... just use package name
mAppName = entry.notification.getPackageName();
}
if (mAppIcon == null) {
mAppIcon = mPm.getDefaultActivityIcon();
}
updateHeaderView();
updatePermissionView();
}
private void updateHeaderView() {
mSettingsIcon.setContentDescription(getResources().getString(
R.string.bubbles_settings_button_description, mAppName));
mDeepLinkIcon.setContentDescription(getResources().getString(
R.string.bubbles_deep_link_button_description, mAppName));
if (mEntry != null && mEntry.getBubbleMetadata() != null) {
mHeaderTextView.setText(mEntry.getBubbleMetadata().getTitle());
} else {
// This should only happen if we're auto-bubbling notification content that isn't
// explicitly a bubble
mHeaderTextView.setText(mAppName);
}
}
private void updatePermissionView() {
boolean hasUserApprovedBubblesForPackage = false;
try {
hasUserApprovedBubblesForPackage =
mNotificationManagerService.hasUserApprovedBubblesForPackage(
mEntry.notification.getPackageName(), mEntry.notification.getUid());
} catch (RemoteException e) {
Log.w(TAG, e);
}
if (hasUserApprovedBubblesForPackage) {
mHeaderView.setVisibility(VISIBLE);
mPermissionView.setVisibility(GONE);
} else {
mHeaderView.setVisibility(GONE);
mPermissionView.setVisibility(VISIBLE);
((ImageView) mPermissionView.findViewById(R.id.pkgicon)).setImageDrawable(mAppIcon);
((TextView) mPermissionView.findViewById(R.id.pkgname)).setText(mAppName);
}
}
@Override
public void onClick(View view) {
if (mEntry == null) {
return;
}
Notification n = mEntry.notification.getNotification();
int id = view.getId();
if (id == R.id.deep_link_button) {
mStackView.collapseStack(() -> {
try {
n.contentIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Failed to send intent for bubble with key: "
+ (mEntry != null ? mEntry.key : " null entry"));
}
});
} else if (id == R.id.settings_button) {
Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
mEntry.notification.getUid());
mStackView.collapseStack(() -> mContext.startActivity(intent));
} else if (id == R.id.no_bubbles_button) {
setBubblesAllowed(false);
} else if (id == R.id.yes_bubbles_button) {
setBubblesAllowed(true);
}
}
private void setBubblesAllowed(boolean allowed) {
try {
mNotificationManagerService.setBubblesAllowed(
mEntry.notification.getPackageName(),
mEntry.notification.getUid(),
allowed);
if (allowed) {
mPermissionView.setVisibility(GONE);
mHeaderView.setVisibility(VISIBLE);
} else if (mOnBubbleBlockedListener != null) {
mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
}
} catch (RemoteException e) {
Log.w(TAG, e);
}
}
/**
* Set the x position that the tip of the triangle should point to.
*/
public void setPointerPosition(int x) {
// Adjust for the pointer size
x -= (mPointerView.getWidth() / 2);
mPointerView.setTranslationX(x);
}
/**
* Set the view to display for the expanded state. Passing null will clear the view.
*/
public void setExpandedView(View view) {
if (mExpandedView == view) {
return;
}
if (mExpandedView != null) {
removeView(mExpandedView);
}
mExpandedView = view;
if (mExpandedView != null) {
addView(mExpandedView);
}
}
/**
* @return the view containing the expanded content, can be null.
*/
@Nullable
public View getExpandedView() {
return mExpandedView;
}
private Intent getSettingsIntent(String packageName, final int appUid) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
intent.putExtra(Settings.EXTRA_APP_UID, appUid);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
/**
* Listener that is notified when a bubble is blocked.
*/
public interface OnBubbleBlockedListener {
/**
* Called when a bubble is blocked for the provided entry.
*/
void onBubbleBlocked(NotificationEntry entry);
}
}