Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 47098cbf authored by Eliot Courtney's avatar Eliot Courtney
Browse files

Split NotificationGutsManager out of StatusBar.

This CL introduces NotificationDisplayer, which is an interface
abstracting the notification displaying related functionality of
StatusBar. This is the first CL in a series of refactoring CLs breaking
up StatusBar into re-usable components. In order to keep the code
reviews a reasonable size I've had to leave a few TODOs. For example,
later I plan to pull out an interface from NotificationStackScrollLayout,
which NotificationGutsManager (and others) would use. But, then
everything would end up being pulled into the same CL so for now it's a
TODO.

Bug: 63874929
Bug: 62602530
Test: runtest systemui
Test: Compile and run
Change-Id: I482a0554aa13aefce4c48807a627cd5ecd820880
parent d9db81bb
Loading
Loading
Loading
Loading
+1 −26
Original line number Diff line number Diff line
@@ -18,46 +18,21 @@ package com.android.systemui.statusbar;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;

import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;

import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
import com.android.systemui.statusbar.stack.StackStateAnimator;

import java.util.Set;

/**
 * The guts of a notification revealed when performing a long press.
 */
+355 −0
Original line number Diff line number Diff line
/*
 * 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.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 String mKeyToRemoveOnGutsClosed;

    public NotificationGutsManager(
            NotificationPresenter presenter,
            NotificationStackScrollLayout stackScroller,
            NotificationInfo.CheckSaveListener checkSaveListener,
            Context context) {
        mPresenter = presenter;
        mStackScroller = stackScroller;
        mCheckSaveListener = checkSaveListener;
        mContext = context;
        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();
                    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);
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -84,7 +84,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
    public interface CheckSaveListener {
        // Invoked when importance has changed and the NotificationInfo wants to try to save it.
        // Listener should run saveImportance unless the change should be canceled.
        void checkSave(Runnable saveImportance);
        void checkSave(Runnable saveImportance, StatusBarNotification sbn);
    }

    public interface OnSettingsClickListener {
@@ -409,7 +409,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
    public boolean handleCloseControls(boolean save, boolean force) {
        if (save && hasImportanceChanged()) {
            if (mCheckSaveListener != null) {
                mCheckSaveListener.checkSave(() -> { saveImportance(); });
                mCheckSaveListener.checkSave(this::saveImportance, mSbn);
            } else {
                saveImportance();
            }
+63 −0
Original line number Diff line number Diff line
/*
 * 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.content.Intent;
import android.service.notification.NotificationListenerService;

/**
 * An abstraction of something that presents notifications, e.g. StatusBar. Contains methods
 * for both querying the state of the system (some modularised piece of functionality may
 * want to act differently based on e.g. whether the presenter is visible to the user or not) and
 * for affecting the state of the system (e.g. starting an intent, given that the presenter may
 * want to perform some action before doing so).
 */
public interface NotificationPresenter {

    /**
     * Returns true if the presenter is not visible. For example, it may not be necessary to do
     * animations if this returns true.
     */
    boolean isPresenterFullyCollapsed();

    /**
     * Returns true if the presenter is locked. For example, if the keyguard is active.
     */
    boolean isPresenterLocked();

    /**
     * Returns the current user id. This can change if the user is switched.
     */
    int getCurrentUserId();

    /**
     * Runs the given intent. The presenter may want to run some animations or close itself when
     * this happens.
     */
    void startNotificationGutsIntent(Intent intent, int appUid);

    // TODO: Create NotificationUpdateHandler and move this method to there.
    /**
     * Removes a notification.
     */
    void removeNotification(String key, NotificationListenerService.RankingMap ranking);

    // TODO: Create NotificationEntryManager and move this method to there.
    /**
     * Gets the latest ranking map.
     */
    NotificationListenerService.RankingMap getLatestRankingMap();
}
+1 −1
Original line number Diff line number Diff line
@@ -596,7 +596,7 @@ public class NotificationPanelView extends PanelView implements
            mLastCameraLaunchSource = KeyguardBottomAreaView.CAMERA_LAUNCH_SOURCE_AFFORDANCE;
        }
        closeQs();
        mStatusBar.closeAndSaveGuts(true /* leavebehind */, true /* force */,
        mStatusBar.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */,
                true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
                true /* cancelAnimators */);
Loading