Add NotificationListener to launcher.
- NotificationListener extends NotificationListenerService, and is added to the manifest. - Added PopupDataProvider, which contains logic for storing and interacting with data that goes into the long-press popup menu (shortcuts and notifications). A follow-up CL will rename DeepShortcutsContainer to a generic PopupContainerWithArrow. - If Launcher has notification access, NotificationListener will get callbacks when notifications are posted and removed; upon receiving these callbacks, NotificationListener passes them to PopupDataProvider via a NotificationsChangedListener interface. - Upon receiving the changed notifications, PopupDataProvider maps them to the corresponding package/user and tells launcher to update relevant icons on the workspace and all apps. This is guarded by FeatureFlags.BADGE_ICONS. Bug: 32410600 Change-Id: I59aeb31a7f92399c9c4b831ab551e51e13f44f5c
This commit is contained in:
parent
c711e6006f
commit
010d255018
|
@ -76,6 +76,13 @@
|
|||
android:process=":wallpaper_chooser">
|
||||
</service>
|
||||
|
||||
<service android:name="com.android.launcher3.badging.NotificationListener"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<meta-data android:name="android.nfc.disable_beam_default"
|
||||
android:value="true" />
|
||||
|
||||
|
|
|
@ -39,14 +39,16 @@ import android.widget.TextView;
|
|||
|
||||
import com.android.launcher3.IconCache.IconLoadRequest;
|
||||
import com.android.launcher3.IconCache.ItemInfoUpdateReceiver;
|
||||
import com.android.launcher3.badge.BadgeRenderer;
|
||||
import com.android.launcher3.badge.BadgeInfo;
|
||||
import com.android.launcher3.badge.BadgeRenderer;
|
||||
import com.android.launcher3.badging.NotificationInfo;
|
||||
import com.android.launcher3.folder.FolderIcon;
|
||||
import com.android.launcher3.graphics.DrawableFactory;
|
||||
import com.android.launcher3.graphics.HolographicOutlineHelper;
|
||||
import com.android.launcher3.model.PackageItemInfo;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
|
||||
|
@ -168,6 +170,8 @@ public class BubbleTextView extends TextView
|
|||
if (promiseStateChanged || info.isPromise()) {
|
||||
applyPromiseState(promiseStateChanged);
|
||||
}
|
||||
|
||||
applyBadgeState(info);
|
||||
}
|
||||
|
||||
public void applyFromApplicationInfo(AppInfo info) {
|
||||
|
@ -178,6 +182,8 @@ public class BubbleTextView extends TextView
|
|||
|
||||
// Verify high res immediately
|
||||
verifyHighRes();
|
||||
|
||||
applyBadgeState(info);
|
||||
}
|
||||
|
||||
public void applyFromPackageItemInfo(PackageItemInfo info) {
|
||||
|
@ -502,8 +508,9 @@ public class BubbleTextView extends TextView
|
|||
}
|
||||
}
|
||||
|
||||
public void applyBadgeState(BadgeInfo badgeInfo) {
|
||||
public void applyBadgeState(ItemInfo itemInfo) {
|
||||
if (mIcon instanceof FastBitmapDrawable) {
|
||||
BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
|
||||
BadgeRenderer badgeRenderer = mLauncher.getDeviceProfile().mBadgeRenderer;
|
||||
((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer);
|
||||
}
|
||||
|
@ -634,7 +641,8 @@ public class BubbleTextView extends TextView
|
|||
* Returns true if the view can show custom shortcuts.
|
||||
*/
|
||||
public boolean hasDeepShortcuts() {
|
||||
return !mLauncher.getShortcutIdsForItem((ItemInfo) getTag()).isEmpty();
|
||||
return !mLauncher.getPopupDataProvider().getShortcutIdsForItem((ItemInfo) getTag())
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,12 +30,13 @@ import android.graphics.PixelFormat;
|
|||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
|
||||
import com.android.launcher3.graphics.IconPalette;
|
||||
import com.android.launcher3.badge.BadgeRenderer;
|
||||
import com.android.launcher3.badge.BadgeInfo;
|
||||
import com.android.launcher3.badge.BadgeRenderer;
|
||||
import com.android.launcher3.graphics.IconPalette;
|
||||
|
||||
public class FastBitmapDrawable extends Drawable {
|
||||
private static final float DISABLED_DESATURATION = 1f;
|
||||
|
@ -123,14 +124,18 @@ public class FastBitmapDrawable extends Drawable {
|
|||
}
|
||||
|
||||
public void applyIconBadge(BadgeInfo badgeInfo, BadgeRenderer badgeRenderer) {
|
||||
boolean wasBadged = mBadgeInfo != null;
|
||||
boolean isBadged = badgeInfo != null;
|
||||
mBadgeInfo = badgeInfo;
|
||||
mBadgeRenderer = badgeRenderer;
|
||||
if (mIconPalette == null) {
|
||||
if (wasBadged || isBadged) {
|
||||
if (mBadgeInfo != null && mIconPalette == null) {
|
||||
mIconPalette = IconPalette.fromDominantColor(Utilities
|
||||
.findDominantColorByHue(mBitmap, 20));
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
|
@ -157,7 +162,7 @@ public class FastBitmapDrawable extends Drawable {
|
|||
}
|
||||
|
||||
private boolean hasBadge() {
|
||||
return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != null;
|
||||
return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -84,6 +84,8 @@ import com.android.launcher3.allapps.AllAppsContainerView;
|
|||
import com.android.launcher3.allapps.AllAppsTransitionController;
|
||||
import com.android.launcher3.allapps.DefaultAppSearchController;
|
||||
import com.android.launcher3.anim.AnimationLayerSet;
|
||||
import com.android.launcher3.badging.NotificationListener;
|
||||
import com.android.launcher3.popup.PopupDataProvider;
|
||||
import com.android.launcher3.compat.AppWidgetManagerCompat;
|
||||
import com.android.launcher3.compat.LauncherAppsCompat;
|
||||
import com.android.launcher3.compat.PinItemRequestCompat;
|
||||
|
@ -117,6 +119,7 @@ import com.android.launcher3.util.ComponentKey;
|
|||
import com.android.launcher3.util.ItemInfoMatcher;
|
||||
import com.android.launcher3.util.MultiHashMap;
|
||||
import com.android.launcher3.util.PackageManagerHelper;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
import com.android.launcher3.util.PendingRequestArgs;
|
||||
import com.android.launcher3.util.TestingUtils;
|
||||
import com.android.launcher3.util.Thunk;
|
||||
|
@ -130,10 +133,10 @@ import java.io.FileDescriptor;
|
|||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Default launcher application.
|
||||
|
@ -260,8 +263,7 @@ public class Launcher extends BaseActivity
|
|||
private boolean mHasFocus = false;
|
||||
private boolean mAttached = false;
|
||||
|
||||
/** Maps launcher activity components to their list of shortcut ids. */
|
||||
private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
|
||||
private PopupDataProvider mPopupDataProvider;
|
||||
|
||||
private View.OnTouchListener mHapticFeedbackTouchListener;
|
||||
|
||||
|
@ -394,6 +396,8 @@ public class Launcher extends BaseActivity
|
|||
mExtractedColors = new ExtractedColors();
|
||||
loadExtractedColorsAndColorItems();
|
||||
|
||||
mPopupDataProvider = new PopupDataProvider(this);
|
||||
|
||||
((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE))
|
||||
.addAccessibilityStateChangeListener(this);
|
||||
|
||||
|
@ -652,6 +656,10 @@ public class Launcher extends BaseActivity
|
|||
return (int) info.id;
|
||||
}
|
||||
|
||||
public PopupDataProvider getPopupDataProvider() {
|
||||
return mPopupDataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
|
||||
* a configuration step, this allows the proper animations to run after other transitions.
|
||||
|
@ -926,6 +934,8 @@ public class Launcher extends BaseActivity
|
|||
if (Utilities.ATLEAST_NOUGAT_MR1) {
|
||||
mAppWidgetHost.stopListening();
|
||||
}
|
||||
|
||||
NotificationListener.removeNotificationsChangedListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -940,6 +950,10 @@ public class Launcher extends BaseActivity
|
|||
if (Utilities.ATLEAST_NOUGAT_MR1) {
|
||||
mAppWidgetHost.startListening();
|
||||
}
|
||||
|
||||
if (!isWorkspaceLoading()) {
|
||||
NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1564,6 +1578,19 @@ public class Launcher extends BaseActivity
|
|||
}
|
||||
};
|
||||
|
||||
public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWorkspace.updateIconBadges(updatedBadges);
|
||||
mAppsView.updateIconBadges(updatedBadges);
|
||||
}
|
||||
};
|
||||
if (!waitUntilResume(r)) {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
@ -3672,6 +3699,8 @@ public class Launcher extends BaseActivity
|
|||
|
||||
InstallShortcutReceiver.disableAndFlushInstallQueue(this);
|
||||
|
||||
NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
|
||||
|
||||
if (mLauncherCallbacks != null) {
|
||||
mLauncherCallbacks.finishBindingItems(false);
|
||||
}
|
||||
|
@ -3741,21 +3770,7 @@ public class Launcher extends BaseActivity
|
|||
*/
|
||||
@Override
|
||||
public void bindDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
|
||||
mDeepShortcutMap = deepShortcutMapCopy;
|
||||
if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
|
||||
}
|
||||
|
||||
public List<String> getShortcutIdsForItem(ItemInfo info) {
|
||||
if (!DeepShortcutManager.supportsShortcuts(info)) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
ComponentName component = info.getTargetComponent();
|
||||
if (component == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
|
||||
return ids == null ? Collections.EMPTY_LIST : ids;
|
||||
mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
|||
import com.android.launcher3.util.ItemInfoMatcher;
|
||||
import com.android.launcher3.util.LongArrayMap;
|
||||
import com.android.launcher3.util.MultiStateAlphaController;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
import com.android.launcher3.util.Thunk;
|
||||
import com.android.launcher3.util.VerticalFlingDetector;
|
||||
import com.android.launcher3.util.WallpaperOffsetInterpolator;
|
||||
|
@ -86,6 +87,7 @@ import com.android.launcher3.widget.PendingAddWidgetInfo;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The workspace is a wide area with a wallpaper and a finite number of pages.
|
||||
|
@ -3957,6 +3959,23 @@ public class Workspace extends PagedView
|
|||
});
|
||||
}
|
||||
|
||||
public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
|
||||
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
|
||||
mapOverItems(MAP_RECURSE, new ItemOperator() {
|
||||
@Override
|
||||
public boolean evaluate(ItemInfo info, View v) {
|
||||
if (info instanceof ShortcutInfo && v instanceof BubbleTextView) {
|
||||
packageUserKey.updateFromItemInfo(info);
|
||||
if (updatedBadges.contains(packageUserKey)) {
|
||||
((BubbleTextView) v).applyBadgeState(info);
|
||||
}
|
||||
}
|
||||
// process all the shortcuts
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeAbandonedPromise(String packageName, UserHandle user) {
|
||||
HashSet<String> packages = new HashSet<>(1);
|
||||
packages.add(packageName);
|
||||
|
|
|
@ -53,9 +53,11 @@ import com.android.launcher3.graphics.TintedDrawableSpan;
|
|||
import com.android.launcher3.keyboard.FocusedItemDecorator;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The all apps view container.
|
||||
|
@ -472,4 +474,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
|
|||
navBarBg.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
|
||||
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
|
||||
for (AlphabeticalAppsList.AdapterItem app : mApps.getAdapterItems()) {
|
||||
if (app.appInfo != null) {
|
||||
packageUserKey.updateFromItemInfo(app.appInfo);
|
||||
if (updatedBadges.contains(packageUserKey)) {
|
||||
mAdapter.notifyItemChanged(app.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,60 @@
|
|||
|
||||
package com.android.launcher3.badge;
|
||||
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Contains data to be used in an icon badge.
|
||||
*/
|
||||
public class BadgeInfo {
|
||||
|
||||
private int mNotificationCount;
|
||||
/** Used to link this BadgeInfo to icons on the workspace and all apps */
|
||||
private PackageUserKey mPackageUserKey;
|
||||
/**
|
||||
* The keys of the notifications that this badge represents. These keys can later be
|
||||
* used to retrieve {@link com.android.launcher3.badging.NotificationInfo}'s.
|
||||
*/
|
||||
private Set<String> mNotificationKeys;
|
||||
|
||||
public void setNotificationCount(int count) {
|
||||
mNotificationCount = count;
|
||||
public BadgeInfo(PackageUserKey packageUserKey) {
|
||||
mPackageUserKey = packageUserKey;
|
||||
mNotificationKeys = new HashSet<>();
|
||||
}
|
||||
|
||||
public String getNotificationCount() {
|
||||
return mNotificationCount == 0 ? null : String.valueOf(mNotificationCount);
|
||||
/**
|
||||
* Returns whether the notification was added (false if it already existed).
|
||||
*/
|
||||
public boolean addNotificationKey(String notificationKey) {
|
||||
return mNotificationKeys.add(notificationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the notification was removed (false if it didn't exist).
|
||||
*/
|
||||
public boolean removeNotificationKey(String notificationKey) {
|
||||
return mNotificationKeys.remove(notificationKey);
|
||||
}
|
||||
|
||||
public Set<String> getNotificationKeys() {
|
||||
return mNotificationKeys;
|
||||
}
|
||||
|
||||
public int getNotificationCount() {
|
||||
return mNotificationKeys.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether newBadge represents the same PackageUserKey as this badge, and icons with
|
||||
* this badge should be invalidated. So, for instance, if a badge has 3 notifications
|
||||
* and one of those notifications is updated, this method should return false because
|
||||
* the badge still says "3" and the contents of those notifications are only retrieved
|
||||
* upon long-click. This method always returns true when adding or removing notifications.
|
||||
*/
|
||||
public boolean shouldBeInvalidated(BadgeInfo newBadge) {
|
||||
return mPackageUserKey.equals(newBadge.mPackageUserKey)
|
||||
&& getNotificationCount() != newBadge.getNotificationCount();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ public class BadgeRenderer {
|
|||
mBackgroundRect.set(iconBounds.right - size, iconBounds.top, iconBounds.right,
|
||||
iconBounds.top + size);
|
||||
canvas.drawOval(mBackgroundRect, mBackgroundPaint);
|
||||
String notificationCount = badgeInfo.getNotificationCount();
|
||||
String notificationCount = String.valueOf(badgeInfo.getNotificationCount());
|
||||
canvas.drawText(notificationCount,
|
||||
mBackgroundRect.centerX(),
|
||||
mBackgroundRect.centerY() + mTextHeight / 2,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2017 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.launcher3.badging;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutsContainer;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
/**
|
||||
* An object that contains relevant information from a {@link StatusBarNotification}. This should
|
||||
* only be created when we need to show the notification contents on the UI; until then, a
|
||||
* {@link com.android.launcher3.badge.BadgeInfo} with only the notification key should
|
||||
* be passed around, and then this can be constructed using the StatusBarNotification from
|
||||
* {@link NotificationListener#getNotificationsForKeys(String[])}.
|
||||
*/
|
||||
public class NotificationInfo implements View.OnClickListener {
|
||||
|
||||
public final PackageUserKey packageUserKey;
|
||||
public final String notificationKey;
|
||||
public final CharSequence title;
|
||||
public final CharSequence text;
|
||||
public final Drawable iconDrawable;
|
||||
public final PendingIntent intent;
|
||||
public final boolean autoCancel;
|
||||
|
||||
/**
|
||||
* Extracts the data that we need from the StatusBarNotification.
|
||||
*/
|
||||
public NotificationInfo(Context context, StatusBarNotification notification) {
|
||||
packageUserKey = PackageUserKey.fromNotification(notification);
|
||||
notificationKey = notification.getKey();
|
||||
title = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
|
||||
text = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TEXT);
|
||||
Icon icon = notification.getNotification().getLargeIcon();
|
||||
if (icon == null) {
|
||||
icon = notification.getNotification().getSmallIcon();
|
||||
iconDrawable = icon.loadDrawable(context);
|
||||
iconDrawable.setTint(notification.getNotification().color);
|
||||
} else {
|
||||
iconDrawable = icon.loadDrawable(context);
|
||||
}
|
||||
intent = notification.getNotification().contentIntent;
|
||||
autoCancel = (notification.getNotification().flags
|
||||
& Notification.FLAG_AUTO_CANCEL) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final Launcher launcher = Launcher.getLauncher(view.getContext());
|
||||
try {
|
||||
intent.send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (autoCancel) {
|
||||
launcher.getPopupDataProvider().cancelNotification(notificationKey);
|
||||
}
|
||||
DeepShortcutsContainer.getOpen(launcher).close(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Copyright (C) 2017 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.launcher3.badging;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.Pair;
|
||||
|
||||
import com.android.launcher3.LauncherModel;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@link NotificationListenerService} that sends updates to its
|
||||
* {@link NotificationsChangedListener} when notifications are posted or canceled,
|
||||
* as well and when this service first connects. An instance of NotificationListener,
|
||||
* and its methods for getting notifications, can be obtained via {@link #getInstance()}.
|
||||
*/
|
||||
public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
private static final int MSG_NOTIFICATION_POSTED = 1;
|
||||
private static final int MSG_NOTIFICATION_REMOVED = 2;
|
||||
private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
|
||||
|
||||
private static NotificationListener sNotificationListenerInstance = null;
|
||||
private static NotificationsChangedListener sNotificationsChangedListener;
|
||||
|
||||
private final Handler mWorkerHandler;
|
||||
private final Handler mUiHandler;
|
||||
|
||||
private Handler.Callback mWorkerCallback = new Handler.Callback() {
|
||||
@Override
|
||||
public boolean handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
case MSG_NOTIFICATION_POSTED:
|
||||
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
|
||||
break;
|
||||
case MSG_NOTIFICATION_REMOVED:
|
||||
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
|
||||
break;
|
||||
case MSG_NOTIFICATION_FULL_REFRESH:
|
||||
final List<StatusBarNotification> activeNotifications
|
||||
= filterNotifications(getActiveNotifications());
|
||||
mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private Handler.Callback mUiCallback = new Handler.Callback() {
|
||||
@Override
|
||||
public boolean handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
case MSG_NOTIFICATION_POSTED:
|
||||
if (sNotificationsChangedListener != null) {
|
||||
Pair<PackageUserKey, String> pair
|
||||
= (Pair<PackageUserKey, String>) message.obj;
|
||||
sNotificationsChangedListener.onNotificationPosted(pair.first, pair.second);
|
||||
}
|
||||
break;
|
||||
case MSG_NOTIFICATION_REMOVED:
|
||||
if (sNotificationsChangedListener != null) {
|
||||
Pair<PackageUserKey, String> pair
|
||||
= (Pair<PackageUserKey, String>) message.obj;
|
||||
sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
|
||||
}
|
||||
break;
|
||||
case MSG_NOTIFICATION_FULL_REFRESH:
|
||||
if (sNotificationsChangedListener != null) {
|
||||
sNotificationsChangedListener.onNotificationFullRefresh(
|
||||
(List<StatusBarNotification>) message.obj);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public NotificationListener() {
|
||||
super();
|
||||
mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
|
||||
mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
|
||||
}
|
||||
|
||||
public static @Nullable NotificationListener getInstance() {
|
||||
return sNotificationListenerInstance;
|
||||
}
|
||||
|
||||
public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
|
||||
if (!FeatureFlags.BADGE_ICONS) {
|
||||
return;
|
||||
}
|
||||
sNotificationsChangedListener = listener;
|
||||
|
||||
NotificationListener notificationListener = getInstance();
|
||||
if (notificationListener != null) {
|
||||
notificationListener.onNotificationFullRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeNotificationsChangedListener() {
|
||||
sNotificationsChangedListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerConnected() {
|
||||
super.onListenerConnected();
|
||||
sNotificationListenerInstance = this;
|
||||
onNotificationFullRefresh();
|
||||
}
|
||||
|
||||
private void onNotificationFullRefresh() {
|
||||
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerDisconnected() {
|
||||
super.onListenerDisconnected();
|
||||
sNotificationListenerInstance = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationPosted(final StatusBarNotification sbn) {
|
||||
super.onNotificationPosted(sbn);
|
||||
if (!shouldBeFilteredOut(sbn.getNotification())) {
|
||||
Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
|
||||
= new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
|
||||
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, packageUserKeyAndNotificationKey)
|
||||
.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationRemoved(final StatusBarNotification sbn) {
|
||||
super.onNotificationRemoved(sbn);
|
||||
if (!shouldBeFilteredOut(sbn.getNotification())) {
|
||||
Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
|
||||
= new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
|
||||
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
|
||||
.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/** This makes a potentially expensive binder call and should be run on a background thread. */
|
||||
public List<StatusBarNotification> getNotificationsForKeys(String[] keys) {
|
||||
StatusBarNotification[] notifications = NotificationListener.this
|
||||
.getActiveNotifications(keys);
|
||||
return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out notifications that don't have an intent
|
||||
* or are headers for grouped notifications.
|
||||
*
|
||||
* TODO: use the system concept of a badged notification instead
|
||||
*/
|
||||
private List<StatusBarNotification> filterNotifications(
|
||||
StatusBarNotification[] notifications) {
|
||||
if (notifications == null) return null;
|
||||
Set<Integer> removedNotifications = new HashSet<>();
|
||||
for (int i = 0; i < notifications.length; i++) {
|
||||
if (shouldBeFilteredOut(notifications[i].getNotification())) {
|
||||
removedNotifications.add(i);
|
||||
}
|
||||
}
|
||||
List<StatusBarNotification> filteredNotifications = new ArrayList<>(
|
||||
notifications.length - removedNotifications.size());
|
||||
for (int i = 0; i < notifications.length; i++) {
|
||||
if (!removedNotifications.contains(i)) {
|
||||
filteredNotifications.add(notifications[i]);
|
||||
}
|
||||
}
|
||||
return filteredNotifications;
|
||||
}
|
||||
|
||||
private boolean shouldBeFilteredOut(Notification notification) {
|
||||
boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
|
||||
return (notification.contentIntent == null || isGroupHeader);
|
||||
}
|
||||
|
||||
public interface NotificationsChangedListener {
|
||||
void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey);
|
||||
void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey);
|
||||
void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (C) 2017 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.launcher3.popup;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.badge.BadgeInfo;
|
||||
import com.android.launcher3.badging.NotificationListener;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutManager;
|
||||
import com.android.launcher3.util.ComponentKey;
|
||||
import com.android.launcher3.util.MultiHashMap;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Provides data for the popup menu that appears after long-clicking on apps.
|
||||
*/
|
||||
public class PopupDataProvider implements NotificationListener.NotificationsChangedListener {
|
||||
|
||||
private static final boolean LOGD = false;
|
||||
private static final String TAG = "PopupDataProvider";
|
||||
|
||||
private final Launcher mLauncher;
|
||||
|
||||
/** Maps launcher activity components to their list of shortcut ids. */
|
||||
private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
|
||||
/** Maps packages to their BadgeInfo's . */
|
||||
private Map<PackageUserKey, BadgeInfo> mPackageUserToBadgeInfos = new HashMap<>();
|
||||
|
||||
public PopupDataProvider(Launcher launcher) {
|
||||
mLauncher = launcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey) {
|
||||
BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(postedPackageUserKey);
|
||||
if (oldBadgeInfo == null) {
|
||||
BadgeInfo newBadgeInfo = new BadgeInfo(postedPackageUserKey);
|
||||
newBadgeInfo.addNotificationKey(notificationKey);
|
||||
mPackageUserToBadgeInfos.put(postedPackageUserKey, newBadgeInfo);
|
||||
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
|
||||
} else if (oldBadgeInfo.addNotificationKey(notificationKey)) {
|
||||
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey) {
|
||||
BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(removedPackageUserKey);
|
||||
if (oldBadgeInfo != null && oldBadgeInfo.removeNotificationKey(notificationKey)) {
|
||||
if (oldBadgeInfo.getNotificationCount() == 0) {
|
||||
mPackageUserToBadgeInfos.remove(removedPackageUserKey);
|
||||
}
|
||||
mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
|
||||
if (activeNotifications == null) return;
|
||||
// This will contain the PackageUserKeys which have updated badges.
|
||||
HashMap<PackageUserKey, BadgeInfo> updatedBadges = new HashMap<>(mPackageUserToBadgeInfos);
|
||||
mPackageUserToBadgeInfos.clear();
|
||||
for (StatusBarNotification notification : activeNotifications) {
|
||||
PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification);
|
||||
BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(packageUserKey);
|
||||
if (badgeInfo == null) {
|
||||
badgeInfo = new BadgeInfo(packageUserKey);
|
||||
mPackageUserToBadgeInfos.put(packageUserKey, badgeInfo);
|
||||
}
|
||||
badgeInfo.addNotificationKey(notification.getKey());
|
||||
}
|
||||
|
||||
// Add and remove from updatedBadges so it contains the PackageUserKeys of updated badges.
|
||||
for (PackageUserKey packageUserKey : mPackageUserToBadgeInfos.keySet()) {
|
||||
BadgeInfo prevBadge = updatedBadges.get(packageUserKey);
|
||||
BadgeInfo newBadge = mPackageUserToBadgeInfos.get(packageUserKey);
|
||||
if (prevBadge == null) {
|
||||
updatedBadges.put(packageUserKey, newBadge);
|
||||
} else {
|
||||
if (!prevBadge.shouldBeInvalidated(newBadge)) {
|
||||
updatedBadges.remove(packageUserKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updatedBadges.isEmpty()) {
|
||||
mLauncher.updateIconBadges(updatedBadges.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
|
||||
mDeepShortcutMap = deepShortcutMapCopy;
|
||||
if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
|
||||
}
|
||||
|
||||
public List<String> getShortcutIdsForItem(ItemInfo info) {
|
||||
if (!DeepShortcutManager.supportsShortcuts(info)) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
ComponentName component = info.getTargetComponent();
|
||||
if (component == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
|
||||
return ids == null ? Collections.EMPTY_LIST : ids;
|
||||
}
|
||||
|
||||
public BadgeInfo getBadgeInfoForItem(ItemInfo info) {
|
||||
if (!DeepShortcutManager.supportsShortcuts(info)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
|
||||
}
|
||||
|
||||
public String[] getNotificationKeysForItem(ItemInfo info) {
|
||||
BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
|
||||
Set<String> notificationKeys = badgeInfo.getNotificationKeys();
|
||||
return notificationKeys.toArray(new String[notificationKeys.size()]);
|
||||
}
|
||||
|
||||
/** This makes a potentially expensive binder call and should be run on a background thread. */
|
||||
public List<StatusBarNotification> getStatusBarNotificationsForKeys(String[] notificationKeys) {
|
||||
NotificationListener notificationListener = NotificationListener.getInstance();
|
||||
return notificationListener == null ? Collections.EMPTY_LIST
|
||||
: notificationListener.getNotificationsForKeys(notificationKeys);
|
||||
}
|
||||
|
||||
public void cancelNotification(String notificationKey) {
|
||||
NotificationListener notificationListener = NotificationListener.getInstance();
|
||||
if (notificationListener == null) {
|
||||
return;
|
||||
}
|
||||
notificationListener.cancelNotification(notificationKey);
|
||||
}
|
||||
}
|
|
@ -718,7 +718,8 @@ public class DeepShortcutsContainer extends AbstractFloatingView
|
|||
icon.clearFocus();
|
||||
return null;
|
||||
}
|
||||
List<String> ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag());
|
||||
List<String> ids = launcher.getPopupDataProvider().getShortcutIdsForItem(
|
||||
(ItemInfo) icon.getTag());
|
||||
if (!ids.isEmpty()) {
|
||||
final DeepShortcutsContainer container =
|
||||
(DeepShortcutsContainer) launcher.getLayoutInflater().inflate(
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.android.launcher3.util;
|
||||
|
||||
import android.os.UserHandle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import com.android.launcher3.ItemInfo;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Creates a hash key based on package name and user. */
|
||||
public class PackageUserKey {
|
||||
|
||||
private String mPackageName;
|
||||
private UserHandle mUser;
|
||||
private int mHashCode;
|
||||
|
||||
public static PackageUserKey fromItemInfo(ItemInfo info) {
|
||||
return new PackageUserKey(info.getTargetComponent().getPackageName(), info.user);
|
||||
}
|
||||
|
||||
public static PackageUserKey fromNotification(StatusBarNotification notification) {
|
||||
return new PackageUserKey(notification.getPackageName(), notification.getUser());
|
||||
}
|
||||
|
||||
public PackageUserKey(String packageName, UserHandle user) {
|
||||
update(packageName, user);
|
||||
}
|
||||
|
||||
private void update(String packageName, UserHandle user) {
|
||||
mPackageName = packageName;
|
||||
mUser = user;
|
||||
mHashCode = Arrays.hashCode(new Object[] {packageName, user});
|
||||
}
|
||||
|
||||
/** This should only be called to avoid new object creations in a loop. */
|
||||
public void updateFromItemInfo(ItemInfo info) {
|
||||
update(info.getTargetComponent().getPackageName(), info.user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof PackageUserKey)) return false;
|
||||
PackageUserKey otherKey = (PackageUserKey) obj;
|
||||
return mPackageName.equals(otherKey.mPackageName) && mUser.equals(otherKey.mUser);
|
||||
}
|
||||
}
|
|
@ -39,4 +39,6 @@ public final class FeatureFlags {
|
|||
public static final boolean LIGHT_STATUS_BAR = false;
|
||||
// When enabled allows to use any point on the fast scrollbar to start dragging.
|
||||
public static final boolean LAUNCHER3_DIRECT_SCROLL = true;
|
||||
// When enabled icons are badged with the number of notifications associated with that app.
|
||||
public static final boolean BADGE_ICONS = true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue