| /* |
| * Copyright (C) 2010 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; |
| |
| import android.app.ActivityManager; |
| import android.app.ActivityManagerNative; |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.app.TaskStackBuilder; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.res.Configuration; |
| import android.database.ContentObserver; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.service.dreams.DreamService; |
| import android.service.dreams.IDreamManager; |
| import android.service.notification.StatusBarNotification; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.IWindowManager; |
| import android.view.LayoutInflater; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewGroup.LayoutParams; |
| import android.view.WindowManager; |
| import android.view.WindowManagerGlobal; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.PopupMenu; |
| import android.widget.RemoteViews; |
| import android.widget.TextView; |
| |
| import com.android.internal.statusbar.IStatusBarService; |
| import com.android.internal.statusbar.StatusBarIcon; |
| import com.android.internal.statusbar.StatusBarIconList; |
| import com.android.internal.widget.SizeAdaptiveLayout; |
| import com.android.systemui.R; |
| import com.android.systemui.RecentsComponent; |
| import com.android.systemui.SearchPanelView; |
| import com.android.systemui.SystemUI; |
| import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; |
| import com.android.systemui.statusbar.policy.NotificationRowLayout; |
| |
| import java.util.ArrayList; |
| import java.util.Locale; |
| |
| public abstract class BaseStatusBar extends SystemUI implements |
| CommandQueue.Callbacks { |
| public static final String TAG = "StatusBar"; |
| public static final boolean DEBUG = false; |
| public static final boolean MULTIUSER_DEBUG = false; |
| |
| protected static final int MSG_TOGGLE_RECENTS_PANEL = 1020; |
| protected static final int MSG_CLOSE_RECENTS_PANEL = 1021; |
| protected static final int MSG_PRELOAD_RECENT_APPS = 1022; |
| protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023; |
| protected static final int MSG_OPEN_SEARCH_PANEL = 1024; |
| protected static final int MSG_CLOSE_SEARCH_PANEL = 1025; |
| protected static final int MSG_SHOW_HEADS_UP = 1026; |
| protected static final int MSG_HIDE_HEADS_UP = 1027; |
| protected static final int MSG_ESCALATE_HEADS_UP = 1028; |
| |
| protected static final boolean ENABLE_HEADS_UP = true; |
| // scores above this threshold should be displayed in heads up mode. |
| protected static final int INTERRUPTION_THRESHOLD = 11; |
| protected static final String SETTING_HEADS_UP = "heads_up_enabled"; |
| |
| // Should match the value in PhoneWindowManager |
| public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; |
| |
| public static final int EXPANDED_LEAVE_ALONE = -10000; |
| public static final int EXPANDED_FULL_OPEN = -10001; |
| |
| protected CommandQueue mCommandQueue; |
| protected IStatusBarService mBarService; |
| protected H mHandler = createHandler(); |
| |
| // all notifications |
| protected NotificationData mNotificationData = new NotificationData(); |
| protected NotificationRowLayout mPile; |
| |
| protected NotificationData.Entry mInterruptingNotificationEntry; |
| protected long mInterruptingNotificationTime; |
| |
| // used to notify status bar for suppressing notification LED |
| protected boolean mPanelSlightlyVisible; |
| |
| // Search panel |
| protected SearchPanelView mSearchPanelView; |
| |
| protected PopupMenu mNotificationBlamePopup; |
| |
| protected int mCurrentUserId = 0; |
| |
| protected int mLayoutDirection = -1; // invalid |
| private Locale mLocale; |
| protected boolean mUseHeadsUp = false; |
| |
| protected IDreamManager mDreamManager; |
| PowerManager mPowerManager; |
| protected int mRowHeight; |
| |
| // UI-specific methods |
| |
| /** |
| * Create all windows necessary for the status bar (including navigation, overlay panels, etc) |
| * and add them to the window manager. |
| */ |
| protected abstract void createAndAddWindows(); |
| |
| protected WindowManager mWindowManager; |
| protected IWindowManager mWindowManagerService; |
| protected abstract void refreshLayout(int layoutDirection); |
| |
| protected Display mDisplay; |
| |
| private boolean mDeviceProvisioned = false; |
| |
| private RecentsComponent mRecents; |
| |
| public IStatusBarService getStatusBarService() { |
| return mBarService; |
| } |
| |
| public boolean isDeviceProvisioned() { |
| return mDeviceProvisioned; |
| } |
| |
| private ContentObserver mProvisioningObserver = new ContentObserver(new Handler()) { |
| @Override |
| public void onChange(boolean selfChange) { |
| final boolean provisioned = 0 != Settings.Global.getInt( |
| mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0); |
| if (provisioned != mDeviceProvisioned) { |
| mDeviceProvisioned = provisioned; |
| updateNotificationIcons(); |
| } |
| } |
| }; |
| |
| private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { |
| @Override |
| public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { |
| if (DEBUG) { |
| Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); |
| } |
| final boolean isActivity = pendingIntent.isActivity(); |
| if (isActivity) { |
| try { |
| // The intent we are sending is for the application, which |
| // won't have permission to immediately start an activity after |
| // the user switches to home. We know it is safe to do at this |
| // point, so make sure new activity switches are now allowed. |
| ActivityManagerNative.getDefault().resumeAppSwitches(); |
| // Also, notifications can be launched from the lock screen, |
| // so dismiss the lock screen when the activity starts. |
| ActivityManagerNative.getDefault().dismissKeyguardOnNextActivity(); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| boolean handled = super.onClickHandler(view, pendingIntent, fillInIntent); |
| |
| if (isActivity && handled) { |
| // close the shade if it was open |
| animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); |
| visibilityChanged(false); |
| } |
| return handled; |
| } |
| }; |
| |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (Intent.ACTION_USER_SWITCHED.equals(action)) { |
| mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); |
| if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house"); |
| userSwitched(mCurrentUserId); |
| } |
| } |
| }; |
| |
| public void start() { |
| mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); |
| mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); |
| mDisplay = mWindowManager.getDefaultDisplay(); |
| |
| mDreamManager = IDreamManager.Stub.asInterface( |
| ServiceManager.checkService(DreamService.DREAM_SERVICE)); |
| mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); |
| |
| mProvisioningObserver.onChange(false); // set up |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, |
| mProvisioningObserver); |
| |
| mBarService = IStatusBarService.Stub.asInterface( |
| ServiceManager.getService(Context.STATUS_BAR_SERVICE)); |
| |
| mRecents = getComponent(RecentsComponent.class); |
| |
| mLocale = mContext.getResources().getConfiguration().locale; |
| mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale); |
| |
| // Connect in to the status bar manager service |
| StatusBarIconList iconList = new StatusBarIconList(); |
| ArrayList<IBinder> notificationKeys = new ArrayList<IBinder>(); |
| ArrayList<StatusBarNotification> notifications = new ArrayList<StatusBarNotification>(); |
| mCommandQueue = new CommandQueue(this, iconList); |
| |
| int[] switches = new int[7]; |
| ArrayList<IBinder> binders = new ArrayList<IBinder>(); |
| try { |
| mBarService.registerStatusBar(mCommandQueue, iconList, notificationKeys, notifications, |
| switches, binders); |
| } catch (RemoteException ex) { |
| // If the system process isn't there we're doomed anyway. |
| } |
| |
| createAndAddWindows(); |
| |
| disable(switches[0]); |
| setSystemUiVisibility(switches[1], 0xffffffff); |
| topAppWindowChanged(switches[2] != 0); |
| // StatusBarManagerService has a back up of IME token and it's restored here. |
| setImeWindowStatus(binders.get(0), switches[3], switches[4]); |
| setHardKeyboardStatus(switches[5] != 0, switches[6] != 0); |
| |
| // Set up the initial icon state |
| int N = iconList.size(); |
| int viewIndex = 0; |
| for (int i=0; i<N; i++) { |
| StatusBarIcon icon = iconList.getIcon(i); |
| if (icon != null) { |
| addIcon(iconList.getSlot(i), i, viewIndex, icon); |
| viewIndex++; |
| } |
| } |
| |
| // Set up the initial notification state |
| N = notificationKeys.size(); |
| if (N == notifications.size()) { |
| for (int i=0; i<N; i++) { |
| addNotification(notificationKeys.get(i), notifications.get(i)); |
| } |
| } else { |
| Log.wtf(TAG, "Notification list length mismatch: keys=" + N |
| + " notifications=" + notifications.size()); |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, String.format( |
| "init: icons=%d disabled=0x%08x lights=0x%08x menu=0x%08x imeButton=0x%08x", |
| iconList.size(), |
| switches[0], |
| switches[1], |
| switches[2], |
| switches[3] |
| )); |
| } |
| |
| mCurrentUserId = ActivityManager.getCurrentUser(); |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_USER_SWITCHED); |
| mContext.registerReceiver(mBroadcastReceiver, filter); |
| } |
| |
| public void userSwitched(int newUserId) { |
| // should be overridden |
| } |
| |
| public boolean notificationIsForCurrentUser(StatusBarNotification n) { |
| final int thisUserId = mCurrentUserId; |
| final int notificationUserId = n.getUserId(); |
| if (DEBUG && MULTIUSER_DEBUG) { |
| Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d", |
| n, thisUserId, notificationUserId)); |
| } |
| return notificationUserId == UserHandle.USER_ALL |
| || thisUserId == notificationUserId; |
| } |
| |
| @Override |
| protected void onConfigurationChanged(Configuration newConfig) { |
| final Locale locale = mContext.getResources().getConfiguration().locale; |
| final int ld = TextUtils.getLayoutDirectionFromLocale(locale); |
| if (! locale.equals(mLocale) || ld != mLayoutDirection) { |
| if (DEBUG) { |
| Log.v(TAG, String.format( |
| "config changed locale/LD: %s (%d) -> %s (%d)", mLocale, mLayoutDirection, |
| locale, ld)); |
| } |
| mLocale = locale; |
| mLayoutDirection = ld; |
| refreshLayout(ld); |
| } |
| } |
| |
| protected View updateNotificationVetoButton(View row, StatusBarNotification n) { |
| View vetoButton = row.findViewById(R.id.veto); |
| if (n.isClearable() || (mInterruptingNotificationEntry != null |
| && mInterruptingNotificationEntry.row == row)) { |
| final String _pkg = n.getPackageName(); |
| final String _tag = n.getTag(); |
| final int _id = n.getId(); |
| vetoButton.setOnClickListener(new View.OnClickListener() { |
| public void onClick(View v) { |
| // Accessibility feedback |
| v.announceForAccessibility( |
| mContext.getString(R.string.accessibility_notification_dismissed)); |
| try { |
| mBarService.onNotificationClear(_pkg, _tag, _id); |
| |
| } catch (RemoteException ex) { |
| // system process is dead if we're here. |
| } |
| } |
| }); |
| vetoButton.setVisibility(View.VISIBLE); |
| } else { |
| vetoButton.setVisibility(View.GONE); |
| } |
| vetoButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); |
| return vetoButton; |
| } |
| |
| |
| protected void applyLegacyRowBackground(StatusBarNotification sbn, View content) { |
| if (sbn.getNotification().contentView.getLayoutId() != |
| com.android.internal.R.layout.notification_template_base) { |
| int version = 0; |
| try { |
| ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(sbn.getPackageName(), 0); |
| version = info.targetSdkVersion; |
| } catch (NameNotFoundException ex) { |
| Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex); |
| } |
| if (version > 0 && version < Build.VERSION_CODES.GINGERBREAD) { |
| content.setBackgroundResource(R.drawable.notification_row_legacy_bg); |
| } else { |
| content.setBackgroundResource(com.android.internal.R.drawable.notification_bg); |
| } |
| } |
| } |
| |
| private void startApplicationDetailsActivity(String packageName) { |
| Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, |
| Uri.fromParts("package", packageName, null)); |
| intent.setComponent(intent.resolveActivity(mContext.getPackageManager())); |
| TaskStackBuilder.create(mContext).addNextIntentWithParentStack(intent).startActivities( |
| null, UserHandle.CURRENT); |
| } |
| |
| protected View.OnLongClickListener getNotificationLongClicker() { |
| return new View.OnLongClickListener() { |
| @Override |
| public boolean onLongClick(View v) { |
| final String packageNameF = (String) v.getTag(); |
| if (packageNameF == null) return false; |
| if (v.getWindowToken() == null) return false; |
| mNotificationBlamePopup = new PopupMenu(mContext, v); |
| mNotificationBlamePopup.getMenuInflater().inflate( |
| R.menu.notification_popup_menu, |
| mNotificationBlamePopup.getMenu()); |
| mNotificationBlamePopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { |
| public boolean onMenuItemClick(MenuItem item) { |
| if (item.getItemId() == R.id.notification_inspect_item) { |
| startApplicationDetailsActivity(packageNameF); |
| animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); |
| } else { |
| return false; |
| } |
| return true; |
| } |
| }); |
| mNotificationBlamePopup.show(); |
| |
| return true; |
| } |
| }; |
| } |
| |
| public void dismissPopups() { |
| if (mNotificationBlamePopup != null) { |
| mNotificationBlamePopup.dismiss(); |
| mNotificationBlamePopup = null; |
| } |
| } |
| |
| public void onHeadsUpDismissed() { |
| } |
| |
| @Override |
| public void toggleRecentApps() { |
| int msg = MSG_TOGGLE_RECENTS_PANEL; |
| mHandler.removeMessages(msg); |
| mHandler.sendEmptyMessage(msg); |
| } |
| |
| @Override |
| public void preloadRecentApps() { |
| int msg = MSG_PRELOAD_RECENT_APPS; |
| mHandler.removeMessages(msg); |
| mHandler.sendEmptyMessage(msg); |
| } |
| |
| @Override |
| public void cancelPreloadRecentApps() { |
| int msg = MSG_CANCEL_PRELOAD_RECENT_APPS; |
| mHandler.removeMessages(msg); |
| mHandler.sendEmptyMessage(msg); |
| } |
| |
| @Override |
| public void showSearchPanel() { |
| int msg = MSG_OPEN_SEARCH_PANEL; |
| mHandler.removeMessages(msg); |
| mHandler.sendEmptyMessage(msg); |
| } |
| |
| @Override |
| public void hideSearchPanel() { |
| int msg = MSG_CLOSE_SEARCH_PANEL; |
| mHandler.removeMessages(msg); |
| mHandler.sendEmptyMessage(msg); |
| } |
| |
| protected abstract WindowManager.LayoutParams getSearchLayoutParams( |
| LayoutParams layoutParams); |
| |
| protected void updateSearchPanel() { |
| // Search Panel |
| boolean visible = false; |
| if (mSearchPanelView != null) { |
| visible = mSearchPanelView.isShowing(); |
| mWindowManager.removeView(mSearchPanelView); |
| } |
| |
| // Provide SearchPanel with a temporary parent to allow layout params to work. |
| LinearLayout tmpRoot = new LinearLayout(mContext); |
| mSearchPanelView = (SearchPanelView) LayoutInflater.from(mContext).inflate( |
| R.layout.status_bar_search_panel, tmpRoot, false); |
| mSearchPanelView.setOnTouchListener( |
| new TouchOutsideListener(MSG_CLOSE_SEARCH_PANEL, mSearchPanelView)); |
| mSearchPanelView.setVisibility(View.GONE); |
| |
| WindowManager.LayoutParams lp = getSearchLayoutParams(mSearchPanelView.getLayoutParams()); |
| |
| mWindowManager.addView(mSearchPanelView, lp); |
| mSearchPanelView.setBar(this); |
| if (visible) { |
| mSearchPanelView.show(true, false); |
| } |
| } |
| |
| protected H createHandler() { |
| return new H(); |
| } |
| |
| static void sendCloseSystemWindows(Context context, String reason) { |
| if (ActivityManagerNative.isSystemReady()) { |
| try { |
| ActivityManagerNative.getDefault().closeSystemDialogs(reason); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| protected abstract View getStatusBarView(); |
| |
| protected View.OnTouchListener mRecentsPreloadOnTouchListener = new View.OnTouchListener() { |
| // additional optimization when we have software system buttons - start loading the recent |
| // tasks on touch down |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| int action = event.getAction() & MotionEvent.ACTION_MASK; |
| if (action == MotionEvent.ACTION_DOWN) { |
| preloadRecentTasksList(); |
| } else if (action == MotionEvent.ACTION_CANCEL) { |
| cancelPreloadingRecentTasksList(); |
| } else if (action == MotionEvent.ACTION_UP) { |
| if (!v.isPressed()) { |
| cancelPreloadingRecentTasksList(); |
| } |
| |
| } |
| return false; |
| } |
| }; |
| |
| protected void toggleRecentsActivity() { |
| if (mRecents != null) { |
| mRecents.toggleRecents(mDisplay, mLayoutDirection, getStatusBarView()); |
| } |
| } |
| |
| protected void preloadRecentTasksList() { |
| if (mRecents != null) { |
| mRecents.preloadRecentTasksList(); |
| } |
| } |
| |
| protected void cancelPreloadingRecentTasksList() { |
| if (mRecents != null) { |
| mRecents.cancelPreloadingRecentTasksList(); |
| } |
| } |
| |
| protected void closeRecents() { |
| if (mRecents != null) { |
| mRecents.closeRecents(); |
| } |
| } |
| |
| public abstract void resetHeadsUpDecayTimer(); |
| |
| protected class H extends Handler { |
| public void handleMessage(Message m) { |
| Intent intent; |
| switch (m.what) { |
| case MSG_TOGGLE_RECENTS_PANEL: |
| toggleRecentsActivity(); |
| break; |
| case MSG_CLOSE_RECENTS_PANEL: |
| closeRecents(); |
| break; |
| case MSG_PRELOAD_RECENT_APPS: |
| preloadRecentTasksList(); |
| break; |
| case MSG_CANCEL_PRELOAD_RECENT_APPS: |
| cancelPreloadingRecentTasksList(); |
| break; |
| case MSG_OPEN_SEARCH_PANEL: |
| if (DEBUG) Log.d(TAG, "opening search panel"); |
| if (mSearchPanelView != null && mSearchPanelView.isAssistantAvailable()) { |
| mSearchPanelView.show(true, true); |
| onShowSearchPanel(); |
| } |
| break; |
| case MSG_CLOSE_SEARCH_PANEL: |
| if (DEBUG) Log.d(TAG, "closing search panel"); |
| if (mSearchPanelView != null && mSearchPanelView.isShowing()) { |
| mSearchPanelView.show(false, true); |
| onHideSearchPanel(); |
| } |
| break; |
| } |
| } |
| } |
| |
| public class TouchOutsideListener implements View.OnTouchListener { |
| private int mMsg; |
| private StatusBarPanel mPanel; |
| |
| public TouchOutsideListener(int msg, StatusBarPanel panel) { |
| mMsg = msg; |
| mPanel = panel; |
| } |
| |
| public boolean onTouch(View v, MotionEvent ev) { |
| final int action = ev.getAction(); |
| if (action == MotionEvent.ACTION_OUTSIDE |
| || (action == MotionEvent.ACTION_DOWN |
| && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) { |
| mHandler.removeMessages(mMsg); |
| mHandler.sendEmptyMessage(mMsg); |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| protected void workAroundBadLayerDrawableOpacity(View v) { |
| } |
| |
| protected void onHideSearchPanel() { |
| } |
| |
| protected void onShowSearchPanel() { |
| } |
| |
| public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) { |
| int minHeight = |
| mContext.getResources().getDimensionPixelSize(R.dimen.notification_min_height); |
| int maxHeight = |
| mContext.getResources().getDimensionPixelSize(R.dimen.notification_max_height); |
| StatusBarNotification sbn = entry.notification; |
| RemoteViews contentView = sbn.getNotification().contentView; |
| RemoteViews bigContentView = sbn.getNotification().bigContentView; |
| if (contentView == null) { |
| return false; |
| } |
| |
| // create the row view |
| LayoutInflater inflater = (LayoutInflater)mContext.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate( |
| R.layout.status_bar_notification_row, parent, false); |
| |
| // for blaming (see SwipeHelper.setLongPressListener) |
| row.setTag(sbn.getPackageName()); |
| |
| workAroundBadLayerDrawableOpacity(row); |
| View vetoButton = updateNotificationVetoButton(row, sbn); |
| vetoButton.setContentDescription(mContext.getString( |
| R.string.accessibility_remove_notification)); |
| |
| // NB: the large icon is now handled entirely by the template |
| |
| // bind the click event to the content area |
| ViewGroup content = (ViewGroup)row.findViewById(R.id.content); |
| ViewGroup adaptive = (ViewGroup)row.findViewById(R.id.adaptive); |
| |
| content.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); |
| |
| PendingIntent contentIntent = sbn.getNotification().contentIntent; |
| if (contentIntent != null) { |
| final View.OnClickListener listener = new NotificationClicker(contentIntent, |
| sbn.getPackageName(), sbn.getTag(), sbn.getId()); |
| content.setOnClickListener(listener); |
| } else { |
| content.setOnClickListener(null); |
| } |
| |
| View contentViewLocal = null; |
| View bigContentViewLocal = null; |
| try { |
| contentViewLocal = contentView.apply(mContext, adaptive, mOnClickHandler); |
| if (bigContentView != null) { |
| bigContentViewLocal = bigContentView.apply(mContext, adaptive, mOnClickHandler); |
| } |
| } |
| catch (RuntimeException e) { |
| final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()); |
| Log.e(TAG, "couldn't inflate view for notification " + ident, e); |
| return false; |
| } |
| |
| if (contentViewLocal != null) { |
| SizeAdaptiveLayout.LayoutParams params = |
| new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams()); |
| params.minHeight = minHeight; |
| params.maxHeight = minHeight; |
| adaptive.addView(contentViewLocal, params); |
| } |
| if (bigContentViewLocal != null) { |
| SizeAdaptiveLayout.LayoutParams params = |
| new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams()); |
| params.minHeight = minHeight+1; |
| params.maxHeight = maxHeight; |
| adaptive.addView(bigContentViewLocal, params); |
| } |
| row.setDrawingCacheEnabled(true); |
| |
| applyLegacyRowBackground(sbn, content); |
| |
| if (MULTIUSER_DEBUG) { |
| TextView debug = (TextView) row.findViewById(R.id.debug_info); |
| if (debug != null) { |
| debug.setVisibility(View.VISIBLE); |
| debug.setText("U " + entry.notification.getUserId()); |
| } |
| } |
| entry.row = row; |
| entry.row.setRowHeight(mRowHeight); |
| entry.content = content; |
| entry.expanded = contentViewLocal; |
| entry.setBigContentView(bigContentViewLocal); |
| |
| return true; |
| } |
| |
| public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag, int id) { |
| return new NotificationClicker(intent, pkg, tag, id); |
| } |
| |
| protected class NotificationClicker implements View.OnClickListener { |
| private PendingIntent mIntent; |
| private String mPkg; |
| private String mTag; |
| private int mId; |
| |
| public NotificationClicker(PendingIntent intent, String pkg, String tag, int id) { |
| mIntent = intent; |
| mPkg = pkg; |
| mTag = tag; |
| mId = id; |
| } |
| |
| public void onClick(View v) { |
| try { |
| // The intent we are sending is for the application, which |
| // won't have permission to immediately start an activity after |
| // the user switches to home. We know it is safe to do at this |
| // point, so make sure new activity switches are now allowed. |
| ActivityManagerNative.getDefault().resumeAppSwitches(); |
| // Also, notifications can be launched from the lock screen, |
| // so dismiss the lock screen when the activity starts. |
| ActivityManagerNative.getDefault().dismissKeyguardOnNextActivity(); |
| } catch (RemoteException e) { |
| } |
| |
| if (mIntent != null) { |
| int[] pos = new int[2]; |
| v.getLocationOnScreen(pos); |
| Intent overlay = new Intent(); |
| overlay.setSourceBounds( |
| new Rect(pos[0], pos[1], pos[0]+v.getWidth(), pos[1]+v.getHeight())); |
| try { |
| mIntent.send(mContext, 0, overlay); |
| } catch (PendingIntent.CanceledException e) { |
| // the stack trace isn't very helpful here. Just log the exception message. |
| Log.w(TAG, "Sending contentIntent failed: " + e); |
| } |
| |
| KeyguardTouchDelegate.getInstance(mContext).dismiss(); |
| } |
| |
| try { |
| mBarService.onNotificationClick(mPkg, mTag, mId); |
| } catch (RemoteException ex) { |
| // system process is dead if we're here. |
| } |
| |
| // close the shade if it was open |
| animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); |
| visibilityChanged(false); |
| } |
| } |
| /** |
| * The LEDs are turned o)ff when the notification panel is shown, even just a little bit. |
| * This was added last-minute and is inconsistent with the way the rest of the notifications |
| * are handled, because the notification isn't really cancelled. The lights are just |
| * turned off. If any other notifications happen, the lights will turn back on. Steve says |
| * this is what he wants. (see bug 1131461) |
| */ |
| protected void visibilityChanged(boolean visible) { |
| if (mPanelSlightlyVisible != visible) { |
| mPanelSlightlyVisible = visible; |
| try { |
| mBarService.onPanelRevealed(); |
| } catch (RemoteException ex) { |
| // Won't fail unless the world has ended. |
| } |
| } |
| } |
| |
| /** |
| * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService |
| * about the failure. |
| * |
| * WARNING: this will call back into us. Don't hold any locks. |
| */ |
| void handleNotificationError(IBinder key, StatusBarNotification n, String message) { |
| removeNotification(key); |
| try { |
| mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), n.getInitialPid(), message); |
| } catch (RemoteException ex) { |
| // The end is nigh. |
| } |
| } |
| |
| protected StatusBarNotification removeNotificationViews(IBinder key) { |
| NotificationData.Entry entry = mNotificationData.remove(key); |
| if (entry == null) { |
| Log.w(TAG, "removeNotification for unknown key: " + key); |
| return null; |
| } |
| // Remove the expanded view. |
| ViewGroup rowParent = (ViewGroup)entry.row.getParent(); |
| if (rowParent != null) rowParent.removeView(entry.row); |
| updateExpansionStates(); |
| updateNotificationIcons(); |
| |
| return entry.notification; |
| } |
| |
| protected NotificationData.Entry createNotificationViews(IBinder key, |
| StatusBarNotification notification) { |
| if (DEBUG) { |
| Log.d(TAG, "createNotificationViews(key=" + key + ", notification=" + notification); |
| } |
| // Construct the icon. |
| final StatusBarIconView iconView = new StatusBarIconView(mContext, |
| notification.getPackageName() + "/0x" + Integer.toHexString(notification.getId()), |
| notification.getNotification()); |
| iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); |
| |
| final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), |
| notification.getUser(), |
| notification.getNotification().icon, |
| notification.getNotification().iconLevel, |
| notification.getNotification().number, |
| notification.getNotification().tickerText); |
| if (!iconView.set(ic)) { |
| handleNotificationError(key, notification, "Couldn't create icon: " + ic); |
| return null; |
| } |
| // Construct the expanded view. |
| NotificationData.Entry entry = new NotificationData.Entry(key, notification, iconView); |
| if (!inflateViews(entry, mPile)) { |
| handleNotificationError(key, notification, "Couldn't expand RemoteViews for: " |
| + notification); |
| return null; |
| } |
| return entry; |
| } |
| |
| protected void addNotificationViews(NotificationData.Entry entry) { |
| // Add the expanded view and icon. |
| int pos = mNotificationData.add(entry); |
| if (DEBUG) { |
| Log.d(TAG, "addNotificationViews: added at " + pos); |
| } |
| updateExpansionStates(); |
| updateNotificationIcons(); |
| } |
| |
| private void addNotificationViews(IBinder key, StatusBarNotification notification) { |
| addNotificationViews(createNotificationViews(key, notification)); |
| } |
| |
| protected void updateExpansionStates() { |
| int N = mNotificationData.size(); |
| for (int i = 0; i < N; i++) { |
| NotificationData.Entry entry = mNotificationData.get(i); |
| if (!entry.row.isUserLocked()) { |
| if (i == (N-1)) { |
| if (DEBUG) Log.d(TAG, "expanding top notification at " + i); |
| entry.row.setExpanded(true); |
| } else { |
| if (!entry.row.isUserExpanded()) { |
| if (DEBUG) Log.d(TAG, "collapsing notification at " + i); |
| entry.row.setExpanded(false); |
| } else { |
| if (DEBUG) Log.d(TAG, "ignoring user-modified notification at " + i); |
| } |
| } |
| } else { |
| if (DEBUG) Log.d(TAG, "ignoring notification being held by user at " + i); |
| } |
| } |
| } |
| |
| protected abstract void haltTicker(); |
| protected abstract void setAreThereNotifications(); |
| protected abstract void updateNotificationIcons(); |
| protected abstract void tick(IBinder key, StatusBarNotification n, boolean firstTime); |
| protected abstract void updateExpandedViewPos(int expandedPosition); |
| protected abstract int getExpandedViewMaxHeight(); |
| protected abstract boolean shouldDisableNavbarGestures(); |
| |
| protected boolean isTopNotification(ViewGroup parent, NotificationData.Entry entry) { |
| return parent != null && parent.indexOfChild(entry.row) == 0; |
| } |
| |
| public void updateNotification(IBinder key, StatusBarNotification notification) { |
| if (DEBUG) Log.d(TAG, "updateNotification(" + key + " -> " + notification + ")"); |
| |
| final NotificationData.Entry oldEntry = mNotificationData.findByKey(key); |
| if (oldEntry == null) { |
| Log.w(TAG, "updateNotification for unknown key: " + key); |
| return; |
| } |
| |
| final StatusBarNotification oldNotification = oldEntry.notification; |
| |
| // XXX: modify when we do something more intelligent with the two content views |
| final RemoteViews oldContentView = oldNotification.getNotification().contentView; |
| final RemoteViews contentView = notification.getNotification().contentView; |
| final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView; |
| final RemoteViews bigContentView = notification.getNotification().bigContentView; |
| |
| if (DEBUG) { |
| Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when |
| + " ongoing=" + oldNotification.isOngoing() |
| + " expanded=" + oldEntry.expanded |
| + " contentView=" + oldContentView |
| + " bigContentView=" + oldBigContentView |
| + " rowParent=" + oldEntry.row.getParent()); |
| Log.d(TAG, "new notification: when=" + notification.getNotification().when |
| + " ongoing=" + oldNotification.isOngoing() |
| + " contentView=" + contentView |
| + " bigContentView=" + bigContentView); |
| } |
| |
| // Can we just reapply the RemoteViews in place? If when didn't change, the order |
| // didn't change. |
| |
| // 1U is never null |
| boolean contentsUnchanged = oldEntry.expanded != null |
| && contentView.getPackage() != null |
| && oldContentView.getPackage() != null |
| && oldContentView.getPackage().equals(contentView.getPackage()) |
| && oldContentView.getLayoutId() == contentView.getLayoutId(); |
| // large view may be null |
| boolean bigContentsUnchanged = |
| (oldEntry.getBigContentView() == null && bigContentView == null) |
| || ((oldEntry.getBigContentView() != null && bigContentView != null) |
| && bigContentView.getPackage() != null |
| && oldBigContentView.getPackage() != null |
| && oldBigContentView.getPackage().equals(bigContentView.getPackage()) |
| && oldBigContentView.getLayoutId() == bigContentView.getLayoutId()); |
| ViewGroup rowParent = (ViewGroup) oldEntry.row.getParent(); |
| boolean orderUnchanged = notification.getNotification().when== oldNotification.getNotification().when |
| && notification.getScore() == oldNotification.getScore(); |
| // score now encompasses/supersedes isOngoing() |
| |
| boolean updateTicker = notification.getNotification().tickerText != null |
| && !TextUtils.equals(notification.getNotification().tickerText, |
| oldEntry.notification.getNotification().tickerText); |
| boolean isTopAnyway = isTopNotification(rowParent, oldEntry); |
| if (contentsUnchanged && bigContentsUnchanged && (orderUnchanged || isTopAnyway)) { |
| if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); |
| oldEntry.notification = notification; |
| try { |
| updateNotificationViews(oldEntry, notification); |
| |
| if (ENABLE_HEADS_UP && mInterruptingNotificationEntry != null |
| && oldNotification == mInterruptingNotificationEntry.notification) { |
| if (!shouldInterrupt(notification)) { |
| if (DEBUG) Log.d(TAG, "no longer interrupts!"); |
| mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); |
| } else { |
| if (DEBUG) Log.d(TAG, "updating the current heads up:" + notification); |
| mInterruptingNotificationEntry.notification = notification; |
| updateNotificationViews(mInterruptingNotificationEntry, notification); |
| } |
| } |
| |
| // Update the icon. |
| final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), |
| notification.getUser(), |
| notification.getNotification().icon, notification.getNotification().iconLevel, |
| notification.getNotification().number, |
| notification.getNotification().tickerText); |
| if (!oldEntry.icon.set(ic)) { |
| handleNotificationError(key, notification, "Couldn't update icon: " + ic); |
| return; |
| } |
| updateExpansionStates(); |
| } |
| catch (RuntimeException e) { |
| // It failed to add cleanly. Log, and remove the view from the panel. |
| Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e); |
| removeNotificationViews(key); |
| addNotificationViews(key, notification); |
| } |
| } else { |
| if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); |
| if (DEBUG) Log.d(TAG, "contents was " + (contentsUnchanged ? "unchanged" : "changed")); |
| if (DEBUG) Log.d(TAG, "order was " + (orderUnchanged ? "unchanged" : "changed")); |
| if (DEBUG) Log.d(TAG, "notification is " + (isTopAnyway ? "top" : "not top")); |
| final boolean wasExpanded = oldEntry.row.isUserExpanded(); |
| removeNotificationViews(key); |
| addNotificationViews(key, notification); // will also replace the heads up |
| if (wasExpanded) { |
| final NotificationData.Entry newEntry = mNotificationData.findByKey(key); |
| newEntry.row.setExpanded(true); |
| newEntry.row.setUserExpanded(true); |
| } |
| } |
| |
| // Update the veto button accordingly (and as a result, whether this row is |
| // swipe-dismissable) |
| updateNotificationVetoButton(oldEntry.row, notification); |
| |
| // Is this for you? |
| boolean isForCurrentUser = notificationIsForCurrentUser(notification); |
| if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); |
| |
| // Restart the ticker if it's still running |
| if (updateTicker && isForCurrentUser) { |
| haltTicker(); |
| tick(key, notification, false); |
| } |
| |
| // Recalculate the position of the sliding windows and the titles. |
| setAreThereNotifications(); |
| updateExpandedViewPos(EXPANDED_LEAVE_ALONE); |
| } |
| |
| private void updateNotificationViews(NotificationData.Entry entry, |
| StatusBarNotification notification) { |
| final RemoteViews contentView = notification.getNotification().contentView; |
| final RemoteViews bigContentView = notification.getNotification().bigContentView; |
| // Reapply the RemoteViews |
| contentView.reapply(mContext, entry.expanded, mOnClickHandler); |
| if (bigContentView != null && entry.getBigContentView() != null) { |
| bigContentView.reapply(mContext, entry.getBigContentView(), mOnClickHandler); |
| } |
| // update the contentIntent |
| final PendingIntent contentIntent = notification.getNotification().contentIntent; |
| if (contentIntent != null) { |
| final View.OnClickListener listener = makeClicker(contentIntent, |
| notification.getPackageName(), notification.getTag(), notification.getId()); |
| entry.content.setOnClickListener(listener); |
| } else { |
| entry.content.setOnClickListener(null); |
| } |
| } |
| |
| protected void notifyHeadsUpScreenOn(boolean screenOn) { |
| if (!screenOn && mInterruptingNotificationEntry != null) { |
| mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); |
| } |
| } |
| |
| protected boolean shouldInterrupt(StatusBarNotification sbn) { |
| Notification notification = sbn.getNotification(); |
| // some predicates to make the boolean logic legible |
| boolean isNoisy = (notification.defaults & Notification.DEFAULT_SOUND) != 0 |
| || (notification.defaults & Notification.DEFAULT_VIBRATE) != 0 |
| || notification.sound != null |
| || notification.vibrate != null; |
| boolean isHighPriority = sbn.getScore() >= INTERRUPTION_THRESHOLD; |
| boolean isFullscreen = notification.fullScreenIntent != null; |
| boolean isAllowed = notification.extras.getInt(Notification.EXTRA_AS_HEADS_UP, |
| Notification.HEADS_UP_ALLOWED) != Notification.HEADS_UP_NEVER; |
| |
| final KeyguardTouchDelegate keyguard = KeyguardTouchDelegate.getInstance(mContext); |
| boolean interrupt = (isFullscreen || (isHighPriority && isNoisy)) |
| && isAllowed |
| && mPowerManager.isScreenOn() |
| && !keyguard.isShowingAndNotHidden() |
| && !keyguard.isInputRestricted(); |
| try { |
| interrupt = interrupt && !mDreamManager.isDreaming(); |
| } catch (RemoteException e) { |
| Log.d(TAG, "failed to query dream manager", e); |
| } |
| if (DEBUG) Log.d(TAG, "interrupt: " + interrupt); |
| return interrupt; |
| } |
| |
| // Q: What kinds of notifications should show during setup? |
| // A: Almost none! Only things coming from the system (package is "android") that also |
| // have special "kind" tags marking them as relevant for setup (see below). |
| protected boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { |
| return "android".equals(sbn.getPackageName()) |
| && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); |
| } |
| |
| public boolean inKeyguardRestrictedInputMode() { |
| return KeyguardTouchDelegate.getInstance(mContext).isInputRestricted(); |
| } |
| |
| public void setInteracting(int barWindow, boolean interacting) { |
| // hook for subclasses |
| } |
| |
| public void destroy() { |
| if (mSearchPanelView != null) { |
| mWindowManager.removeViewImmediate(mSearchPanelView); |
| } |
| mContext.unregisterReceiver(mBroadcastReceiver); |
| } |
| } |