1 /*
2  * Copyright (C) 2014 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 
17 package com.android.systemui.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.app.INotificationManager;
22 import android.app.NotificationChannel;
23 import android.app.NotificationManager;
24 import android.content.Context;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.content.res.ColorStateList;
29 import android.content.res.TypedArray;
30 import android.graphics.Canvas;
31 import android.graphics.drawable.Drawable;
32 import android.os.Handler;
33 import android.os.RemoteException;
34 import android.os.ServiceManager;
35 import android.service.notification.NotificationListenerService;
36 import android.service.notification.StatusBarNotification;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.view.View;
40 import android.view.ViewAnimationUtils;
41 import android.view.ViewGroup;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.widget.FrameLayout;
44 import android.widget.ImageView;
45 import android.widget.LinearLayout;
46 import android.widget.SeekBar;
47 import android.widget.Switch;
48 import android.widget.TextView;
49 
50 import com.android.internal.logging.MetricsLogger;
51 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
52 import com.android.settingslib.Utils;
53 import com.android.systemui.Interpolators;
54 import com.android.systemui.R;
55 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
56 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
57 import com.android.systemui.statusbar.stack.StackStateAnimator;
58 
59 import java.util.Set;
60 
61 /**
62  * The guts of a notification revealed when performing a long press.
63  */
64 public class NotificationGuts extends FrameLayout {
65     private static final String TAG = "NotificationGuts";
66     private static final long CLOSE_GUTS_DELAY = 8000;
67 
68     private Drawable mBackground;
69     private int mClipTopAmount;
70     private int mClipBottomAmount;
71     private int mActualHeight;
72     private boolean mExposed;
73 
74     private Handler mHandler;
75     private Runnable mFalsingCheck;
76     private boolean mNeedsFalsingProtection;
77     private OnGutsClosedListener mClosedListener;
78     private OnHeightChangedListener mHeightListener;
79 
80     private GutsContent mGutsContent;
81 
82     public interface GutsContent {
83 
setGutsParent(NotificationGuts listener)84         public void setGutsParent(NotificationGuts listener);
85 
86         /**
87          * @return the view to be shown in the notification guts.
88          */
getContentView()89         public View getContentView();
90 
91         /**
92          * @return the actual height of the content.
93          */
getActualHeight()94         public int getActualHeight();
95 
96         /**
97          * Called when the guts view have been told to close, typically after an outside
98          * interaction.
99          *
100          * @param save whether the state should be saved.
101          * @param force whether the guts view should be forced closed regardless of state.
102          * @return if closing the view has been handled.
103          */
handleCloseControls(boolean save, boolean force)104         public boolean handleCloseControls(boolean save, boolean force);
105 
106         /**
107          * @return whether the notification associated with these guts is set to be removed.
108          */
willBeRemoved()109         public boolean willBeRemoved();
110 
111         /**
112          * @return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
113          */
isLeavebehind()114         public default boolean isLeavebehind() {
115             return false;
116         }
117     }
118 
119     public interface OnGutsClosedListener {
onGutsClosed(NotificationGuts guts)120         public void onGutsClosed(NotificationGuts guts);
121     }
122 
123     public interface OnHeightChangedListener {
onHeightChanged(NotificationGuts guts)124         public void onHeightChanged(NotificationGuts guts);
125     }
126 
127     interface OnSettingsClickListener {
onClick(View v, int appUid)128         void onClick(View v, int appUid);
129     }
130 
NotificationGuts(Context context, AttributeSet attrs)131     public NotificationGuts(Context context, AttributeSet attrs) {
132         super(context, attrs);
133         setWillNotDraw(false);
134         mHandler = new Handler();
135         mFalsingCheck = new Runnable() {
136             @Override
137             public void run() {
138                 if (mNeedsFalsingProtection && mExposed) {
139                     closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
140                 }
141             }
142         };
143         final TypedArray ta = context.obtainStyledAttributes(attrs,
144                 com.android.internal.R.styleable.Theme, 0, 0);
145         ta.recycle();
146     }
147 
NotificationGuts(Context context)148     public NotificationGuts(Context context) {
149         this(context, null);
150     }
151 
setGutsContent(GutsContent content)152     public void setGutsContent(GutsContent content) {
153         mGutsContent = content;
154         removeAllViews();
155         addView(mGutsContent.getContentView());
156     }
157 
getGutsContent()158     public GutsContent getGutsContent() {
159         return mGutsContent;
160     }
161 
resetFalsingCheck()162     public void resetFalsingCheck() {
163         mHandler.removeCallbacks(mFalsingCheck);
164         if (mNeedsFalsingProtection && mExposed) {
165             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
166         }
167     }
168 
169     @Override
onDraw(Canvas canvas)170     protected void onDraw(Canvas canvas) {
171         draw(canvas, mBackground);
172     }
173 
draw(Canvas canvas, Drawable drawable)174     private void draw(Canvas canvas, Drawable drawable) {
175         int top = mClipTopAmount;
176         int bottom = mActualHeight - mClipBottomAmount;
177         if (drawable != null && top < bottom) {
178             drawable.setBounds(0, top, getWidth(), bottom);
179             drawable.draw(canvas);
180         }
181     }
182 
183     @Override
onFinishInflate()184     protected void onFinishInflate() {
185         super.onFinishInflate();
186         mBackground = mContext.getDrawable(R.drawable.notification_guts_bg);
187         if (mBackground != null) {
188             mBackground.setCallback(this);
189         }
190     }
191 
192     @Override
verifyDrawable(Drawable who)193     protected boolean verifyDrawable(Drawable who) {
194         return super.verifyDrawable(who) || who == mBackground;
195     }
196 
197     @Override
drawableStateChanged()198     protected void drawableStateChanged() {
199         drawableStateChanged(mBackground);
200     }
201 
drawableStateChanged(Drawable d)202     private void drawableStateChanged(Drawable d) {
203         if (d != null && d.isStateful()) {
204             d.setState(getDrawableState());
205         }
206     }
207 
208     @Override
drawableHotspotChanged(float x, float y)209     public void drawableHotspotChanged(float x, float y) {
210         if (mBackground != null) {
211             mBackground.setHotspot(x, y);
212         }
213     }
214 
closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)215     public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
216         if (mGutsContent != null) {
217             if (mGutsContent.isLeavebehind() && leavebehinds) {
218                 closeControls(x, y, true /* save */, force);
219             } else if (!mGutsContent.isLeavebehind() && controls) {
220                 closeControls(x, y, true /* save */, force);
221             }
222         }
223     }
224 
closeControls(int x, int y, boolean save, boolean force)225     public void closeControls(int x, int y, boolean save, boolean force) {
226         if (getWindowToken() == null) {
227             if (mClosedListener != null) {
228                 mClosedListener.onGutsClosed(this);
229             }
230             return;
231         }
232 
233         if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) {
234             animateClose(x, y);
235             setExposed(false, mNeedsFalsingProtection);
236             if (mClosedListener != null) {
237                 mClosedListener.onGutsClosed(this);
238             }
239         }
240     }
241 
animateClose(int x, int y)242     private void animateClose(int x, int y) {
243         if (x == -1 || y == -1) {
244             x = (getLeft() + getRight()) / 2;
245             y = (getTop() + getHeight() / 2);
246         }
247         final double horz = Math.max(getWidth() - x, x);
248         final double vert = Math.max(getHeight() - y, y);
249         final float r = (float) Math.hypot(horz, vert);
250         final Animator a = ViewAnimationUtils.createCircularReveal(this,
251                 x, y, r, 0);
252         a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
253         a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
254         a.addListener(new AnimatorListenerAdapter() {
255             @Override
256             public void onAnimationEnd(Animator animation) {
257                 super.onAnimationEnd(animation);
258                 setVisibility(View.GONE);
259             }
260         });
261         a.start();
262     }
263 
setActualHeight(int actualHeight)264     public void setActualHeight(int actualHeight) {
265         mActualHeight = actualHeight;
266         invalidate();
267     }
268 
getActualHeight()269     public int getActualHeight() {
270         return mActualHeight;
271     }
272 
getIntrinsicHeight()273     public int getIntrinsicHeight() {
274         return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
275     }
276 
setClipTopAmount(int clipTopAmount)277     public void setClipTopAmount(int clipTopAmount) {
278         mClipTopAmount = clipTopAmount;
279         invalidate();
280     }
281 
setClipBottomAmount(int clipBottomAmount)282     public void setClipBottomAmount(int clipBottomAmount) {
283         mClipBottomAmount = clipBottomAmount;
284         invalidate();
285     }
286 
287     @Override
hasOverlappingRendering()288     public boolean hasOverlappingRendering() {
289         // Prevents this view from creating a layer when alpha is animating.
290         return false;
291     }
292 
setClosedListener(OnGutsClosedListener listener)293     public void setClosedListener(OnGutsClosedListener listener) {
294         mClosedListener = listener;
295     }
296 
setHeightChangedListener(OnHeightChangedListener listener)297     public void setHeightChangedListener(OnHeightChangedListener listener) {
298         mHeightListener = listener;
299     }
300 
onHeightChanged()301     protected void onHeightChanged() {
302         if (mHeightListener != null) {
303             mHeightListener.onHeightChanged(this);
304         }
305     }
306 
setExposed(boolean exposed, boolean needsFalsingProtection)307     public void setExposed(boolean exposed, boolean needsFalsingProtection) {
308         final boolean wasExposed = mExposed;
309         mExposed = exposed;
310         mNeedsFalsingProtection = needsFalsingProtection;
311         if (mExposed && mNeedsFalsingProtection) {
312             resetFalsingCheck();
313         } else {
314             mHandler.removeCallbacks(mFalsingCheck);
315         }
316         if (wasExposed != mExposed && mGutsContent != null) {
317             final View contentView = mGutsContent.getContentView();
318             contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
319             if (mExposed) {
320                 contentView.requestAccessibilityFocus();
321             }
322         }
323     }
324 
willBeRemoved()325     public boolean willBeRemoved() {
326         return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
327     }
328 
isExposed()329     public boolean isExposed() {
330         return mExposed;
331     }
332 }
333