Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2017 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 package com.android.systemui.statusbar;
     17 
     18 import static android.app.AppOpsManager.OP_CAMERA;
     19 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
     20 import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
     21 import static android.service.notification.NotificationListenerService.Ranking
     22         .USER_SENTIMENT_NEGATIVE;
     23 
     24 import android.app.INotificationManager;
     25 import android.app.NotificationChannel;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.pm.PackageManager;
     29 import android.content.res.Resources;
     30 import android.net.Uri;
     31 import android.os.RemoteException;
     32 import android.os.ServiceManager;
     33 import android.os.UserHandle;
     34 import android.provider.Settings;
     35 import android.service.notification.StatusBarNotification;
     36 import android.support.annotation.VisibleForTesting;
     37 import android.util.ArraySet;
     38 import android.util.Log;
     39 import android.view.HapticFeedbackConstants;
     40 import android.view.View;
     41 import android.view.accessibility.AccessibilityManager;
     42 
     43 import com.android.internal.logging.MetricsLogger;
     44 import com.android.internal.logging.nano.MetricsProto;
     45 import com.android.systemui.Dependency;
     46 import com.android.systemui.Dumpable;
     47 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
     48 import com.android.systemui.statusbar.phone.StatusBar;
     49 
     50 import java.io.FileDescriptor;
     51 import java.io.PrintWriter;
     52 import java.util.Collections;
     53 import java.util.HashSet;
     54 import java.util.List;
     55 import java.util.Set;
     56 
     57 /**
     58  * Handles various NotificationGuts related tasks, such as binding guts to a row, opening and
     59  * closing guts, and keeping track of the currently exposed notification guts.
     60  */
     61 public class NotificationGutsManager implements Dumpable {
     62     private static final String TAG = "NotificationGutsManager";
     63 
     64     // Must match constant in Settings. Used to highlight preferences when linking to Settings.
     65     private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
     66 
     67     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
     68     private final Context mContext;
     69     private final AccessibilityManager mAccessibilityManager;
     70 
     71     // Dependencies:
     72     private final NotificationLockscreenUserManager mLockscreenUserManager =
     73             Dependency.get(NotificationLockscreenUserManager.class);
     74 
     75     // which notification is currently being longpress-examined by the user
     76     private NotificationGuts mNotificationGutsExposed;
     77     private NotificationMenuRowPlugin.MenuItem mGutsMenuItem;
     78     protected NotificationPresenter mPresenter;
     79     protected NotificationEntryManager mEntryManager;
     80     private NotificationListContainer mListContainer;
     81     private NotificationInfo.CheckSaveListener mCheckSaveListener;
     82     private OnSettingsClickListener mOnSettingsClickListener;
     83     private String mKeyToRemoveOnGutsClosed;
     84 
     85     public NotificationGutsManager(Context context) {
     86         mContext = context;
     87         Resources res = context.getResources();
     88 
     89         mAccessibilityManager = (AccessibilityManager)
     90                 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
     91     }
     92 
     93     public void setUpWithPresenter(NotificationPresenter presenter,
     94             NotificationEntryManager entryManager, NotificationListContainer listContainer,
     95             NotificationInfo.CheckSaveListener checkSaveListener,
     96             OnSettingsClickListener onSettingsClickListener) {
     97         mPresenter = presenter;
     98         mEntryManager = entryManager;
     99         mListContainer = listContainer;
    100         mCheckSaveListener = checkSaveListener;
    101         mOnSettingsClickListener = onSettingsClickListener;
    102     }
    103 
    104     public String getKeyToRemoveOnGutsClosed() {
    105         return mKeyToRemoveOnGutsClosed;
    106     }
    107 
    108     public void setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed) {
    109         mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed;
    110     }
    111 
    112     public void onDensityOrFontScaleChanged(ExpandableNotificationRow row) {
    113         setExposedGuts(row.getGuts());
    114         bindGuts(row);
    115     }
    116 
    117     /**
    118      * Sends an intent to open the app settings for a particular package and optional
    119      * channel.
    120      */
    121     private void startAppNotificationSettingsActivity(String packageName, final int appUid,
    122             final NotificationChannel channel, ExpandableNotificationRow row) {
    123         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    124         intent.setData(Uri.fromParts("package", packageName, null));
    125         intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
    126         intent.putExtra(Settings.EXTRA_APP_UID, appUid);
    127         if (channel != null) {
    128             intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId());
    129         }
    130         mPresenter.startNotificationGutsIntent(intent, appUid, row);
    131     }
    132 
    133     protected void startAppOpsSettingsActivity(String pkg, int uid, ArraySet<Integer> ops,
    134             ExpandableNotificationRow row) {
    135         if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) {
    136             if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) {
    137                 startAppNotificationSettingsActivity(pkg, uid, null, row);
    138             } else {
    139                 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
    140                 intent.setData(Uri.fromParts("package", pkg, null));
    141                 mPresenter.startNotificationGutsIntent(intent, uid, row);
    142             }
    143         } else if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) {
    144             Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
    145             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg);
    146             mPresenter.startNotificationGutsIntent(intent, uid, row);
    147         }
    148     }
    149 
    150     public void bindGuts(final ExpandableNotificationRow row) {
    151         bindGuts(row, mGutsMenuItem);
    152     }
    153 
    154     private void bindGuts(final ExpandableNotificationRow row,
    155             NotificationMenuRowPlugin.MenuItem item) {
    156         StatusBarNotification sbn = row.getStatusBarNotification();
    157 
    158         row.inflateGuts();
    159         row.setGutsView(item);
    160         row.setTag(sbn.getPackageName());
    161         row.getGuts().setClosedListener((NotificationGuts g) -> {
    162             row.onGutsClosed();
    163             if (!g.willBeRemoved() && !row.isRemoved()) {
    164                 mListContainer.onHeightChanged(
    165                         row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */);
    166             }
    167             if (mNotificationGutsExposed == g) {
    168                 mNotificationGutsExposed = null;
    169                 mGutsMenuItem = null;
    170             }
    171             String key = sbn.getKey();
    172             if (key.equals(mKeyToRemoveOnGutsClosed)) {
    173                 mKeyToRemoveOnGutsClosed = null;
    174                 mEntryManager.removeNotification(key, mEntryManager.getLatestRankingMap());
    175             }
    176         });
    177 
    178         View gutsView = item.getGutsView();
    179         if (gutsView instanceof NotificationSnooze) {
    180             initializeSnoozeView(row, (NotificationSnooze) gutsView);
    181         } else if (gutsView instanceof AppOpsInfo) {
    182             initializeAppOpsInfo(row, (AppOpsInfo) gutsView);
    183         } else if (gutsView instanceof NotificationInfo) {
    184             initializeNotificationInfo(row, (NotificationInfo) gutsView);
    185         }
    186     }
    187 
    188     /**
    189      * Sets up the {@link NotificationSnooze} inside the notification row's guts.
    190      *
    191      * @param row view to set up the guts for
    192      * @param notificationSnoozeView view to set up/bind within {@code row}
    193      */
    194     private void initializeSnoozeView(
    195             final ExpandableNotificationRow row,
    196             NotificationSnooze notificationSnoozeView) {
    197         NotificationGuts guts = row.getGuts();
    198         StatusBarNotification sbn = row.getStatusBarNotification();
    199 
    200         notificationSnoozeView.setSnoozeListener(mListContainer.getSwipeActionHelper());
    201         notificationSnoozeView.setStatusBarNotification(sbn);
    202         notificationSnoozeView.setSnoozeOptions(row.getEntry().snoozeCriteria);
    203         guts.setHeightChangedListener((NotificationGuts g) -> {
    204             mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */);
    205         });
    206     }
    207 
    208     /**
    209      * Sets up the {@link AppOpsInfo} inside the notification row's guts.
    210      *
    211      * @param row view to set up the guts for
    212      * @param appOpsInfoView view to set up/bind within {@code row}
    213      */
    214     private void initializeAppOpsInfo(
    215             final ExpandableNotificationRow row,
    216             AppOpsInfo appOpsInfoView) {
    217         NotificationGuts guts = row.getGuts();
    218         StatusBarNotification sbn = row.getStatusBarNotification();
    219         UserHandle userHandle = sbn.getUser();
    220         PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
    221                 userHandle.getIdentifier());
    222 
    223         AppOpsInfo.OnSettingsClickListener onSettingsClick =
    224                 (View v, String pkg, int uid, ArraySet<Integer> ops) -> {
    225             mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
    226             guts.resetFalsingCheck();
    227             startAppOpsSettingsActivity(pkg, uid, ops, row);
    228         };
    229         if (!row.getEntry().mActiveAppOps.isEmpty()) {
    230             appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps);
    231         }
    232     }
    233 
    234     /**
    235      * Sets up the {@link NotificationInfo} inside the notification row's guts.
    236      *
    237      * @param row view to set up the guts for
    238      * @param notificationInfoView view to set up/bind within {@code row}
    239      */
    240     @VisibleForTesting
    241     void initializeNotificationInfo(
    242             final ExpandableNotificationRow row,
    243             NotificationInfo notificationInfoView) {
    244         NotificationGuts guts = row.getGuts();
    245         StatusBarNotification sbn = row.getStatusBarNotification();
    246         String packageName = sbn.getPackageName();
    247         // Settings link is only valid for notifications that specify a non-system user
    248         NotificationInfo.OnSettingsClickListener onSettingsClick = null;
    249         UserHandle userHandle = sbn.getUser();
    250         PackageManager pmUser = StatusBar.getPackageManagerForUser(
    251                 mContext, userHandle.getIdentifier());
    252         INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
    253                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
    254         final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick =
    255                 (View v, Intent intent) -> {
    256                     mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS);
    257                     guts.resetFalsingCheck();
    258                     mPresenter.startNotificationGutsIntent(intent, sbn.getUid(), row);
    259                 };
    260         boolean isForBlockingHelper = row.isBlockingHelperShowing();
    261 
    262         if (!userHandle.equals(UserHandle.ALL)
    263                 || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) {
    264             onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
    265                 mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
    266                 guts.resetFalsingCheck();
    267                 mOnSettingsClickListener.onClick(sbn.getKey());
    268                 startAppNotificationSettingsActivity(packageName, appUid, channel, row);
    269             };
    270         }
    271 
    272         try {
    273             notificationInfoView.bindNotification(
    274                     pmUser,
    275                     iNotificationManager,
    276                     packageName,
    277                     row.getEntry().channel,
    278                     row.getNumUniqueChannels(),
    279                     sbn,
    280                     mCheckSaveListener,
    281                     onSettingsClick,
    282                     onAppSettingsClick,
    283                     row.getIsNonblockable(),
    284                     isForBlockingHelper,
    285                     row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE);
    286         } catch (RemoteException e) {
    287             Log.e(TAG, e.toString());
    288         }
    289     }
    290 
    291     /**
    292      * Closes guts or notification menus that might be visible and saves any changes.
    293      *
    294      * @param removeLeavebehinds true if leavebehinds (e.g. snooze) should be closed.
    295      * @param force true if guts should be closed regardless of state (used for snooze only).
    296      * @param removeControls true if controls (e.g. info) should be closed.
    297      * @param x if closed based on touch location, this is the x touch location.
    298      * @param y if closed based on touch location, this is the y touch location.
    299      * @param resetMenu if any notification menus that might be revealed should be closed.
    300      */
    301     public void closeAndSaveGuts(boolean removeLeavebehinds, boolean force, boolean removeControls,
    302             int x, int y, boolean resetMenu) {
    303         if (mNotificationGutsExposed != null) {
    304             mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force);
    305         }
    306         if (resetMenu) {
    307             mListContainer.resetExposedMenuView(false /* animate */, true /* force */);
    308         }
    309     }
    310 
    311     /**
    312      * Returns the exposed NotificationGuts or null if none are exposed.
    313      */
    314     public NotificationGuts getExposedGuts() {
    315         return mNotificationGutsExposed;
    316     }
    317 
    318     public void setExposedGuts(NotificationGuts guts) {
    319         mNotificationGutsExposed = guts;
    320     }
    321 
    322     /**
    323      * Opens guts on the given ExpandableNotificationRow {@code view}. This handles opening guts for
    324      * the normal half-swipe and long-press use cases via a circular reveal. When the blocking
    325      * helper needs to be shown on the row, this will skip the circular reveal.
    326      *
    327      * @param view ExpandableNotificationRow to open guts on
    328      * @param x x coordinate of origin of circular reveal
    329      * @param y y coordinate of origin of circular reveal
    330      * @param menuItem MenuItem the guts should display
    331      * @return true if guts was opened
    332      */
    333     boolean openGuts(
    334             View view,
    335             int x,
    336             int y,
    337             NotificationMenuRowPlugin.MenuItem menuItem) {
    338         if (!(view instanceof ExpandableNotificationRow)) {
    339             return false;
    340         }
    341 
    342         if (view.getWindowToken() == null) {
    343             Log.e(TAG, "Trying to show notification guts, but not attached to window");
    344             return false;
    345         }
    346 
    347         final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
    348         if (row.isDark()) {
    349             return false;
    350         }
    351         view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    352         if (row.areGutsExposed()) {
    353             closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
    354                     true /* removeControls */, -1 /* x */, -1 /* y */,
    355                     true /* resetMenu */);
    356             return false;
    357         }
    358         bindGuts(row, menuItem);
    359         NotificationGuts guts = row.getGuts();
    360 
    361         // Assume we are a status_bar_notification_row
    362         if (guts == null) {
    363             // This view has no guts. Examples are the more card or the dismiss all view
    364             return false;
    365         }
    366 
    367         mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_CONTROLS);
    368 
    369         // ensure that it's laid but not visible until actually laid out
    370         guts.setVisibility(View.INVISIBLE);
    371         // Post to ensure the the guts are properly laid out.
    372         guts.post(new Runnable() {
    373             @Override
    374             public void run() {
    375                 if (row.getWindowToken() == null) {
    376                     Log.e(TAG, "Trying to show notification guts in post(), but not attached to "
    377                             + "window");
    378                     return;
    379                 }
    380                 closeAndSaveGuts(true /* removeLeavebehind */, true /* force */,
    381                         true /* removeControls */, -1 /* x */, -1 /* y */,
    382                         false /* resetMenu */);
    383                 guts.setVisibility(View.VISIBLE);
    384 
    385                 final boolean needsFalsingProtection =
    386                         (mPresenter.isPresenterLocked() &&
    387                                 !mAccessibilityManager.isTouchExplorationEnabled());
    388 
    389                 guts.openControls(
    390                         !row.isBlockingHelperShowing(),
    391                         x,
    392                         y,
    393                         needsFalsingProtection,
    394                         row::onGutsOpened);
    395 
    396                 row.closeRemoteInput();
    397                 mListContainer.onHeightChanged(row, true /* needsAnimation */);
    398                 mNotificationGutsExposed = guts;
    399                 mGutsMenuItem = menuItem;
    400             }
    401         });
    402         return true;
    403     }
    404 
    405     @Override
    406     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    407         pw.println("NotificationGutsManager state:");
    408         pw.print("  mKeyToRemoveOnGutsClosed: ");
    409         pw.println(mKeyToRemoveOnGutsClosed);
    410     }
    411 
    412     public interface OnSettingsClickListener {
    413         void onClick(String key);
    414     }
    415 }
    416