Organize notification classes in row/stack

No functional changes. Reorganized logic under either row, stack,
logging, or row/wrapper. Haven't moved all classes over since there's
some classes that create conflicts due to weird use of
package-private/protected (primarily waiting for HUN and shelf classes).

Test: built, ran, used notifications
Bug: 110802404
Change-Id: Ia2152603bdbeb12c522360193511946c843b9266
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
new file mode 100644
index 0000000..3e380d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -0,0 +1,565 @@
+/*
+ * 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.notification.row;
+
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.logging.NotificationCounters;
+
+import java.util.List;
+
+/**
+ * The guts of a notification revealed when performing a long press. This also houses the blocking
+ * helper affordance that allows a user to keep/stop notifications after swiping one away.
+ */
+public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
+    private static final String TAG = "InfoGuts";
+
+    private INotificationManager mINotificationManager;
+    private PackageManager mPm;
+    private MetricsLogger mMetricsLogger;
+
+    private String mPackageName;
+    private String mAppName;
+    private int mAppUid;
+    private int mNumUniqueChannelsInRow;
+    private NotificationChannel mSingleNotificationChannel;
+    private int mStartingUserImportance;
+    private int mChosenImportance;
+    private boolean mIsSingleDefaultChannel;
+    private boolean mIsNonblockable;
+    private StatusBarNotification mSbn;
+    private AnimatorSet mExpandAnimation;
+    private boolean mIsForeground;
+
+    private CheckSaveListener mCheckSaveListener;
+    private OnSettingsClickListener mOnSettingsClickListener;
+    private OnAppSettingsClickListener mAppSettingsClickListener;
+    private NotificationGuts mGutsContainer;
+
+    /** Whether this view is being shown as part of the blocking helper. */
+    private boolean mIsForBlockingHelper;
+    private boolean mNegativeUserSentiment;
+
+    /**
+     * String that describes how the user exit or quit out of this view, also used as a counter tag.
+     */
+    private String mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED;
+
+    private OnClickListener mOnKeepShowing = v -> {
+        mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING;
+        closeControls(v);
+    };
+
+    private OnClickListener mOnStopOrMinimizeNotifications = v -> {
+        mExitReason = NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS;
+        swapContent(false);
+    };
+
+    private OnClickListener mOnUndo = v -> {
+        // Reset exit counter that we'll log and record an undo event separately (not an exit event)
+        mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED;
+        logBlockingHelperCounter(NotificationCounters.BLOCKING_HELPER_UNDO);
+        swapContent(true);
+    };
+
+    public NotificationInfo(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    // Specify a CheckSaveListener to override when/if the user's changes are committed.
+    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, StatusBarNotification sbn);
+    }
+
+    public interface OnSettingsClickListener {
+        void onClick(View v, NotificationChannel channel, int appUid);
+    }
+
+    public interface OnAppSettingsClickListener {
+        void onClick(View v, Intent intent);
+    }
+
+    @VisibleForTesting
+    void bindNotification(
+            final PackageManager pm,
+            final INotificationManager iNotificationManager,
+            final String pkg,
+            final NotificationChannel notificationChannel,
+            final int numUniqueChannelsInRow,
+            final StatusBarNotification sbn,
+            final CheckSaveListener checkSaveListener,
+            final OnSettingsClickListener onSettingsClick,
+            final OnAppSettingsClickListener onAppSettingsClick,
+            boolean isNonblockable)
+            throws RemoteException {
+        bindNotification(pm, iNotificationManager, pkg, notificationChannel,
+                numUniqueChannelsInRow, sbn, checkSaveListener, onSettingsClick,
+                onAppSettingsClick, isNonblockable, false /* isBlockingHelper */,
+                false /* isUserSentimentNegative */);
+    }
+
+    public void bindNotification(
+            PackageManager pm,
+            INotificationManager iNotificationManager,
+            String pkg,
+            NotificationChannel notificationChannel,
+            int numUniqueChannelsInRow,
+            StatusBarNotification sbn,
+            CheckSaveListener checkSaveListener,
+            OnSettingsClickListener onSettingsClick,
+            OnAppSettingsClickListener onAppSettingsClick,
+            boolean isNonblockable,
+            boolean isForBlockingHelper,
+            boolean isUserSentimentNegative)
+            throws RemoteException {
+        mINotificationManager = iNotificationManager;
+        mMetricsLogger = Dependency.get(MetricsLogger.class);
+        mPackageName = pkg;
+        mNumUniqueChannelsInRow = numUniqueChannelsInRow;
+        mSbn = sbn;
+        mPm = pm;
+        mAppSettingsClickListener = onAppSettingsClick;
+        mAppName = mPackageName;
+        mCheckSaveListener = checkSaveListener;
+        mOnSettingsClickListener = onSettingsClick;
+        mSingleNotificationChannel = notificationChannel;
+        mStartingUserImportance = mChosenImportance = mSingleNotificationChannel.getImportance();
+        mNegativeUserSentiment = isUserSentimentNegative;
+        mIsNonblockable = isNonblockable;
+        mIsForeground =
+                (mSbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
+        mIsForBlockingHelper = isForBlockingHelper;
+        mAppUid = mSbn.getUid();
+
+        int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
+                pkg, mAppUid, false /* includeDeleted */);
+        if (mNumUniqueChannelsInRow == 0) {
+            throw new IllegalArgumentException("bindNotification requires at least one channel");
+        } else  {
+            // Special behavior for the Default channel if no other channels have been defined.
+            mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1
+                    && mSingleNotificationChannel.getId().equals(
+                            NotificationChannel.DEFAULT_CHANNEL_ID)
+                    && numTotalChannels == 1;
+        }
+
+        bindHeader();
+        bindPrompt();
+        bindButtons();
+    }
+
+    private void bindHeader() throws RemoteException {
+        // Package name
+        Drawable pkgicon = null;
+        ApplicationInfo info;
+        try {
+            info = mPm.getApplicationInfo(
+                    mPackageName,
+                    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));
+                pkgicon = mPm.getApplicationIcon(info);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // app is gone, just show package name and generic icon
+            pkgicon = mPm.getDefaultActivityIcon();
+        }
+        ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
+        ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
+
+        // Set group information if this channel has an associated group.
+        CharSequence groupName = null;
+        if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
+            final NotificationChannelGroup notificationChannelGroup =
+                    mINotificationManager.getNotificationChannelGroupForPackage(
+                            mSingleNotificationChannel.getGroup(), mPackageName, mAppUid);
+            if (notificationChannelGroup != null) {
+                groupName = notificationChannelGroup.getName();
+            }
+        }
+        TextView groupNameView = findViewById(R.id.group_name);
+        TextView groupDividerView = findViewById(R.id.pkg_group_divider);
+        if (groupName != null) {
+            groupNameView.setText(groupName);
+            groupNameView.setVisibility(View.VISIBLE);
+            groupDividerView.setVisibility(View.VISIBLE);
+        } else {
+            groupNameView.setVisibility(View.GONE);
+            groupDividerView.setVisibility(View.GONE);
+        }
+
+        // Settings button.
+        final View settingsButton = findViewById(R.id.info);
+        if (mAppUid >= 0 && mOnSettingsClickListener != null) {
+            settingsButton.setVisibility(View.VISIBLE);
+            final int appUidF = mAppUid;
+            settingsButton.setOnClickListener(
+                    (View view) -> {
+                        logBlockingHelperCounter(
+                                NotificationCounters.BLOCKING_HELPER_NOTIF_SETTINGS);
+                        mOnSettingsClickListener.onClick(view,
+                                mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
+                                appUidF);
+                    });
+        } else {
+            settingsButton.setVisibility(View.GONE);
+        }
+    }
+
+    private void bindPrompt() {
+        final TextView blockPrompt = findViewById(R.id.block_prompt);
+        bindName();
+        if (mIsNonblockable) {
+            blockPrompt.setText(R.string.notification_unblockable_desc);
+        } else {
+            if (mNegativeUserSentiment) {
+                blockPrompt.setText(R.string.inline_blocking_helper);
+            }  else if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
+                blockPrompt.setText(R.string.inline_keep_showing_app);
+            } else {
+                blockPrompt.setText(R.string.inline_keep_showing);
+            }
+        }
+    }
+
+    private void bindName() {
+        final TextView channelName = findViewById(R.id.channel_name);
+        if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
+            channelName.setVisibility(View.GONE);
+        } else {
+            channelName.setText(mSingleNotificationChannel.getName());
+        }
+    }
+
+    @VisibleForTesting
+    void logBlockingHelperCounter(String counterTag) {
+        if (mIsForBlockingHelper) {
+            mMetricsLogger.count(counterTag, 1);
+        }
+    }
+
+    private boolean hasImportanceChanged() {
+        return mSingleNotificationChannel != null && mStartingUserImportance != mChosenImportance;
+    }
+
+    private void saveImportance() {
+        if (!mIsNonblockable) {
+            // Only go through the lock screen/bouncer if the user hit 'Stop notifications'.
+            // Otherwise, update the importance immediately.
+            if (mCheckSaveListener != null
+                    && NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS.equals(
+                            mExitReason)) {
+                mCheckSaveListener.checkSave(this::updateImportance, mSbn);
+            } else {
+                updateImportance();
+            }
+        }
+    }
+
+    /**
+     * Commits the updated importance values on the background thread.
+     */
+    private void updateImportance() {
+        MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
+                mChosenImportance - mStartingUserImportance);
+
+        Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
+        bgHandler.post(new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid,
+                mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null,
+                mStartingUserImportance, mChosenImportance));
+    }
+
+    private void bindButtons() {
+        // Set up stay-in-notification actions
+        View block =  findViewById(R.id.block);
+        TextView keep = findViewById(R.id.keep);
+        View minimize = findViewById(R.id.minimize);
+
+        findViewById(R.id.undo).setOnClickListener(mOnUndo);
+        block.setOnClickListener(mOnStopOrMinimizeNotifications);
+        keep.setOnClickListener(mOnKeepShowing);
+        minimize.setOnClickListener(mOnStopOrMinimizeNotifications);
+
+        if (mIsNonblockable) {
+            keep.setText(android.R.string.ok);
+            block.setVisibility(GONE);
+            minimize.setVisibility(GONE);
+        } else if (mIsForeground) {
+            block.setVisibility(GONE);
+            minimize.setVisibility(VISIBLE);
+        } else if (!mIsForeground) {
+            block.setVisibility(VISIBLE);
+            minimize.setVisibility(GONE);
+        }
+
+        // Set up app settings link (i.e. Customize)
+        TextView settingsLinkView = findViewById(R.id.app_settings);
+        Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName, mSingleNotificationChannel,
+                mSbn.getId(), mSbn.getTag());
+        if (!mIsForBlockingHelper
+                && settingsIntent != null
+                && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
+            settingsLinkView.setVisibility(VISIBLE);
+            settingsLinkView.setText(mContext.getString(R.string.notification_app_settings));
+            settingsLinkView.setOnClickListener((View view) -> {
+                mAppSettingsClickListener.onClick(view, settingsIntent);
+            });
+        } else {
+            settingsLinkView.setVisibility(View.GONE);
+        }
+    }
+
+    private void swapContent(boolean showPrompt) {
+        if (mExpandAnimation != null) {
+            mExpandAnimation.cancel();
+        }
+
+        View prompt = findViewById(R.id.prompt);
+        ViewGroup confirmation = findViewById(R.id.confirmation);
+        TextView confirmationText = findViewById(R.id.confirmation_text);
+        View header = findViewById(R.id.header);
+
+        if (showPrompt) {
+            mChosenImportance = mStartingUserImportance;
+        } else if (mIsForeground) {
+            mChosenImportance = IMPORTANCE_MIN;
+            confirmationText.setText(R.string.notification_channel_minimized);
+        } else {
+            mChosenImportance = IMPORTANCE_NONE;
+            confirmationText.setText(R.string.notification_channel_disabled);
+        }
+
+        ObjectAnimator promptAnim = ObjectAnimator.ofFloat(prompt, View.ALPHA,
+                prompt.getAlpha(), showPrompt ? 1f : 0f);
+        promptAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
+        ObjectAnimator confirmAnim = ObjectAnimator.ofFloat(confirmation, View.ALPHA,
+                confirmation.getAlpha(), showPrompt ? 0f : 1f);
+        confirmAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_OUT : Interpolators.ALPHA_IN);
+
+        prompt.setVisibility(showPrompt ? VISIBLE : GONE);
+        confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
+        header.setVisibility(showPrompt ? VISIBLE : GONE);
+
+        mExpandAnimation = new AnimatorSet();
+        mExpandAnimation.playTogether(promptAnim, confirmAnim);
+        mExpandAnimation.setDuration(150);
+        mExpandAnimation.addListener(new AnimatorListenerAdapter() {
+            boolean cancelled = false;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                cancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!cancelled) {
+                    prompt.setVisibility(showPrompt ? VISIBLE : GONE);
+                    confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
+                }
+            }
+        });
+        mExpandAnimation.start();
+
+        // Since we're swapping/update the content, reset the timeout so the UI can't close
+        // immediately after the update.
+        if (mGutsContainer != null) {
+            mGutsContainer.resetFalsingCheck();
+        }
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        if (mGutsContainer != null &&
+                event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+            if (mGutsContainer.isExposed()) {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_opened_accessibility, mAppName));
+            } else {
+                event.getText().add(mContext.getString(
+                        R.string.notification_channel_controls_closed_accessibility, mAppName));
+            }
+        }
+    }
+
+    private Intent getAppSettingsIntent(PackageManager pm, String packageName,
+            NotificationChannel channel, int id, String tag) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
+                .setPackage(packageName);
+        final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
+                intent,
+                PackageManager.MATCH_DEFAULT_ONLY
+        );
+        if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+            return null;
+        }
+        final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+        intent.setClassName(activityInfo.packageName, activityInfo.name);
+        if (channel != null) {
+            intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
+        }
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
+        intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
+        return intent;
+    }
+
+    /**
+     * Closes the controls and commits the updated importance values (indirectly). If this view is
+     * being used to show the blocking helper, this will immediately dismiss the blocking helper and
+     * commit the updated importance.
+     *
+     * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
+     * user does not have the ability to undo the action anymore. See {@link #swapContent(boolean)}
+     * for where undo is handled.
+     */
+    @VisibleForTesting
+    void closeControls(View v) {
+        int[] parentLoc = new int[2];
+        int[] targetLoc = new int[2];
+        mGutsContainer.getLocationOnScreen(parentLoc);
+        v.getLocationOnScreen(targetLoc);
+        final int centerX = v.getWidth() / 2;
+        final int centerY = v.getHeight() / 2;
+        final int x = targetLoc[0] - parentLoc[0] + centerX;
+        final int y = targetLoc[1] - parentLoc[1] + centerY;
+        mGutsContainer.closeControls(x, y, true /* save */, false /* force */);
+    }
+
+    @Override
+    public void setGutsParent(NotificationGuts guts) {
+        mGutsContainer = guts;
+    }
+
+    @Override
+    public boolean willBeRemoved() {
+        return hasImportanceChanged();
+    }
+
+    @Override
+    public boolean shouldBeSaved() {
+        return hasImportanceChanged();
+    }
+
+    @Override
+    public View getContentView() {
+        return this;
+    }
+
+    @Override
+    public boolean handleCloseControls(boolean save, boolean force) {
+        // Save regardless of the importance so we can lock the importance field if the user wants
+        // to keep getting notifications
+        if (save) {
+            saveImportance();
+        }
+        logBlockingHelperCounter(mExitReason);
+        return false;
+    }
+
+    @Override
+    public int getActualHeight() {
+        return getHeight();
+    }
+
+    /**
+     * Runnable to either update the given channel (with a new importance value) or, if no channel
+     * is provided, update notifications enabled state for the package.
+     */
+    private static class UpdateImportanceRunnable implements Runnable {
+        private final INotificationManager mINotificationManager;
+        private final String mPackageName;
+        private final int mAppUid;
+        private final @Nullable NotificationChannel mChannelToUpdate;
+        private final int mCurrentImportance;
+        private final int mNewImportance;
+
+
+        public UpdateImportanceRunnable(INotificationManager notificationManager,
+                String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
+                int currentImportance, int newImportance) {
+            mINotificationManager = notificationManager;
+            mPackageName = packageName;
+            mAppUid = appUid;
+            mChannelToUpdate = channelToUpdate;
+            mCurrentImportance = currentImportance;
+            mNewImportance = newImportance;
+        }
+
+        @Override
+        public void run() {
+            try {
+                if (mChannelToUpdate != null) {
+                    mChannelToUpdate.setImportance(mNewImportance);
+                    mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+                    mINotificationManager.updateNotificationChannelForPackage(
+                            mPackageName, mAppUid, mChannelToUpdate);
+                } else {
+                    // For notifications with more than one channel, update notification enabled
+                    // state. If the importance was lowered, we disable notifications.
+                    mINotificationManager.setNotificationsEnabledWithImportanceLockForPackage(
+                            mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Unable to update notification importance", e);
+            }
+        }
+    }
+}