blob: f451fda6235e60d76898d63a05deef521c923952 [file] [log] [blame]
/*
* Copyright (C) 2017 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.statusbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Interpolators;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Handles various NotificationGuts related tasks, such as binding guts to a row, opening and
* closing guts, and keeping track of the currently exposed notification guts.
*/
public class NotificationGutsManager implements Dumpable {
private static final String TAG = "NotificationGutsManager";
// Must match constant in Settings. Used to highlight preferences when linking to Settings.
private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final Set<String> mNonBlockablePkgs;
private final NotificationPresenter mPresenter;
// TODO: Create NotificationListContainer interface and use it instead of
// NotificationStackScrollLayout here
private final NotificationStackScrollLayout mStackScroller;
private final Context mContext;
private final AccessibilityManager mAccessibilityManager;
// which notification is currently being longpress-examined by the user
private NotificationGuts mNotificationGutsExposed;
private NotificationMenuRowPlugin.MenuItem mGutsMenuItem;
private final NotificationInfo.CheckSaveListener mCheckSaveListener;
private final OnSettingsClickListener mOnSettingsClickListener;
private String mKeyToRemoveOnGutsClosed;
public NotificationGutsManager(
NotificationPresenter presenter,
NotificationStackScrollLayout stackScroller,
NotificationInfo.CheckSaveListener checkSaveListener,
Context context,
OnSettingsClickListener onSettingsClickListener) {
mPresenter = presenter;
mStackScroller = stackScroller;
mCheckSaveListener = checkSaveListener;
mContext = context;
mOnSettingsClickListener = onSettingsClickListener;
Resources res = context.getResources();
mNonBlockablePkgs = new HashSet<>();
Collections.addAll(mNonBlockablePkgs, res.getStringArray(
com.android.internal.R.array.config_nonBlockableNotificationPackages));
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
public String getKeyToRemoveOnGutsClosed() {
return mKeyToRemoveOnGutsClosed;
}
public void setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed) {
mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed;
}
private void saveAndCloseNotificationMenu(
ExpandableNotificationRow row, NotificationGuts guts, View done) {
guts.resetFalsingCheck();
int[] rowLocation = new int[2];
int[] doneLocation = new int[2];
row.getLocationOnScreen(rowLocation);
done.getLocationOnScreen(doneLocation);
final int centerX = done.getWidth() / 2;
final int centerY = done.getHeight() / 2;
final int x = doneLocation[0] - rowLocation[0] + centerX;
final int y = doneLocation[1] - rowLocation[1] + centerY;
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, x, y, true /* resetMenu */);
}
/**
* Sends an intent to open the notification settings for a particular package and optional
* channel.
*/
private void startAppNotificationSettingsActivity(String packageName, final int appUid,
final NotificationChannel channel) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
intent.putExtra(Settings.EXTRA_APP_UID, appUid);
if (channel != null) {
intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId());
}
mPresenter.startNotificationGutsIntent(intent, appUid);
}
public void bindGuts(final ExpandableNotificationRow row) {
bindGuts(row, mGutsMenuItem);
}
private void bindGuts(final ExpandableNotificationRow row,
NotificationMenuRowPlugin.MenuItem item) {
row.inflateGuts();
row.setGutsView(item);
final StatusBarNotification sbn = row.getStatusBarNotification();
row.setTag(sbn.getPackageName());
final NotificationGuts guts = row.getGuts();
guts.setClosedListener((NotificationGuts g) -> {
if (!g.willBeRemoved() && !row.isRemoved()) {
mStackScroller.onHeightChanged(
row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */);
}
if (mNotificationGutsExposed == g) {
mNotificationGutsExposed = null;
mGutsMenuItem = null;
}
String key = sbn.getKey();
if (key.equals(mKeyToRemoveOnGutsClosed)) {
mKeyToRemoveOnGutsClosed = null;
mPresenter.removeNotification(key, mPresenter.getLatestRankingMap());
}
});
View gutsView = item.getGutsView();
if (gutsView instanceof NotificationSnooze) {
NotificationSnooze snoozeGuts = (NotificationSnooze) gutsView;
snoozeGuts.setSnoozeListener(mStackScroller.getSwipeActionHelper());
snoozeGuts.setStatusBarNotification(sbn);
snoozeGuts.setSnoozeOptions(row.getEntry().snoozeCriteria);
guts.setHeightChangedListener((NotificationGuts g) -> {
mStackScroller.onHeightChanged(row, row.isShown() /* needsAnimation */);
});
}
if (gutsView instanceof NotificationInfo) {
final UserHandle userHandle = sbn.getUser();
PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
userHandle.getIdentifier());
final INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
final String pkg = sbn.getPackageName();
NotificationInfo info = (NotificationInfo) gutsView;
// Settings link is only valid for notifications that specify a user, unless this is the
// system user.
NotificationInfo.OnSettingsClickListener onSettingsClick = null;
if (!userHandle.equals(UserHandle.ALL)
|| mPresenter.getCurrentUserId() == UserHandle.USER_SYSTEM) {
onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
guts.resetFalsingCheck();
mOnSettingsClickListener.onClick(sbn.getKey());
startAppNotificationSettingsActivity(pkg, appUid, channel);
};
}
final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = (View v,
Intent intent) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS);
guts.resetFalsingCheck();
mPresenter.startNotificationGutsIntent(intent, sbn.getUid());
};
final View.OnClickListener onDoneClick = (View v) -> {
saveAndCloseNotificationMenu(row, guts, v);
};
ArraySet<NotificationChannel> channels = new ArraySet<>();
channels.add(row.getEntry().channel);
if (row.isSummaryWithChildren()) {
// If this is a summary, then add in the children notification channels for the
// same user and pkg.
final List<ExpandableNotificationRow> childrenRows = row.getNotificationChildren();
final int numChildren = childrenRows.size();
for (int i = 0; i < numChildren; i++) {
final ExpandableNotificationRow childRow = childrenRows.get(i);
final NotificationChannel childChannel = childRow.getEntry().channel;
final StatusBarNotification childSbn = childRow.getStatusBarNotification();
if (childSbn.getUser().equals(userHandle) &&
childSbn.getPackageName().equals(pkg)) {
channels.add(childChannel);
}
}
}
try {
info.bindNotification(pmUser, iNotificationManager, pkg, new ArrayList(channels),
row.getEntry().channel.getImportance(), sbn, onSettingsClick,
onAppSettingsClick, onDoneClick, mCheckSaveListener,
mNonBlockablePkgs);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
}
}
/**
* Closes guts or notification menus that might be visible and saves any changes.
*
* @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed.
* @param force true if guts should be closed regardless of state (used for snooze only).
* @param removeControls true if controls (e.g. info) should be closed.
* @param x if closed based on touch location, this is the x touch location.
* @param y if closed based on touch location, this is the y touch location.
* @param resetMenu if any notification menus that might be revealed should be closed.
*/
public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls,
int x, int y, boolean resetMenu) {
if (mNotificationGutsExposed != null) {
mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force);
}
if (resetMenu) {
mStackScroller.resetExposedMenuView(false /* animate */, true /* force */);
}
}
/**
* Returns the exposed NotificationGuts or null if none are exposed.
*/
public NotificationGuts getExposedGuts() {
return mNotificationGutsExposed;
}
public void setExposedGuts(NotificationGuts guts) {
mNotificationGutsExposed = guts;
}
/**
* Opens guts on the given ExpandableNotificationRow |v|.
*
* @param v ExpandableNotificationRow to open guts on
* @param x x coordinate of origin of circular reveal
* @param y y coordinate of origin of circular reveal
* @param item MenuItem the guts should display
* @return true if guts was opened
*/
public boolean openGuts(View v, int x, int y,
NotificationMenuRowPlugin.MenuItem item) {
if (!(v instanceof ExpandableNotificationRow)) {
return false;
}
if (v.getWindowToken() == null) {
Log.e(TAG, "Trying to show notification guts, but not attached to window");
return false;
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
if (row.isDark()) {
return false;
}
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (row.areGutsExposed()) {
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, -1 /* x */, -1 /* y */,
true /* resetMenu */);
return false;
}
bindGuts(row, item);
NotificationGuts guts = row.getGuts();
// Assume we are a status_bar_notification_row
if (guts == null) {
// This view has no guts. Examples are the more card or the dismiss all view
return false;
}
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_CONTROLS);
// ensure that it's laid but not visible until actually laid out
guts.setVisibility(View.INVISIBLE);
// Post to ensure the the guts are properly laid out.
guts.post(new Runnable() {
@Override
public void run() {
if (row.getWindowToken() == null) {
Log.e(TAG, "Trying to show notification guts, but not attached to "
+ "window");
return;
}
closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
true /* removeControls */, -1 /* x */, -1 /* y */,
false /* resetMenu */);
guts.setVisibility(View.VISIBLE);
final double horz = Math.max(guts.getWidth() - x, x);
final double vert = Math.max(guts.getHeight() - y, y);
final float r = (float) Math.hypot(horz, vert);
final Animator a
= ViewAnimationUtils.createCircularReveal(guts, x, y, 0, r);
a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
a.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// Move the notification view back over the menu
row.resetTranslation();
}
});
a.start();
final boolean needsFalsingProtection =
(mPresenter.isPresenterLocked() &&
!mAccessibilityManager.isTouchExplorationEnabled());
guts.setExposed(true /* exposed */, needsFalsingProtection);
row.closeRemoteInput();
mStackScroller.onHeightChanged(row, true /* needsAnimation */);
mNotificationGutsExposed = guts;
mGutsMenuItem = item;
}
});
return true;
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print("mKeyToRemoveOnGutsClosed: ");
pw.println(mKeyToRemoveOnGutsClosed);
}
public interface OnSettingsClickListener {
void onClick(String key);
}
}