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