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 */
16package com.android.systemui.statusbar;
17
18import static android.app.AppOpsManager.OP_CAMERA;
19import static android.app.AppOpsManager.OP_RECORD_AUDIO;
20import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
21import static android.service.notification.NotificationListenerService.Ranking
22        .USER_SENTIMENT_NEGATIVE;
23
24import android.app.INotificationManager;
25import android.app.NotificationChannel;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.res.Resources;
30import android.net.Uri;
31import android.os.RemoteException;
32import android.os.ServiceManager;
33import android.os.UserHandle;
34import android.provider.Settings;
35import android.service.notification.StatusBarNotification;
36import android.support.annotation.VisibleForTesting;
37import android.util.ArraySet;
38import android.util.Log;
39import android.view.HapticFeedbackConstants;
40import android.view.View;
41import android.view.accessibility.AccessibilityManager;
42
43import com.android.internal.logging.MetricsLogger;
44import com.android.internal.logging.nano.MetricsProto;
45import com.android.systemui.Dependency;
46import com.android.systemui.Dumpable;
47import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
48import com.android.systemui.statusbar.phone.StatusBar;
49
50import java.io.FileDescriptor;
51import java.io.PrintWriter;
52import java.util.Collections;
53import java.util.HashSet;
54import java.util.List;
55import 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 */
61public 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