Merge "Add screen for notification history"

This commit is contained in:
Julia Reynolds
2020-01-21 16:21:30 +00:00
committed by Android (Google) Code Review
19 changed files with 1287 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationHistory;
import android.app.role.RoleManager;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
@@ -367,6 +368,15 @@ public class NotificationBackend {
return false;
}
public NotificationHistory getNotificationHistory(String pkg) {
try {
return sINM.getNotificationHistory(pkg);
} catch (Exception e) {
Log.w(TAG, "Error calling NoMan", e);
}
return new NotificationHistory();
}
protected void recordAggregatedUsageEvents(Context context, AppRow appRow) {
long now = System.currentTimeMillis();
long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK);

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import android.app.NotificationHistory;
import android.app.NotificationHistory.HistoricalNotification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HistoryLoader {
private final Context mContext;
private final NotificationBackend mBackend;
private final PackageManager mPm;
public HistoryLoader(Context context, NotificationBackend backend, PackageManager pm) {
mContext = context;
mBackend = backend;
mPm = pm;
}
public void load(OnHistoryLoaderListener listener) {
ThreadUtils.postOnBackgroundThread(() -> {
Map<String, NotificationHistoryPackage> historicalNotifications = new HashMap<>();
NotificationHistory history =
mBackend.getNotificationHistory(mContext.getPackageName());
while (history.hasNextNotification()) {
HistoricalNotification hn = history.getNextNotification();
String key = hn.getPackage() + "|" + hn.getUid();
NotificationHistoryPackage hnsForPackage = historicalNotifications.getOrDefault(
key,
new NotificationHistoryPackage(hn.getPackage(), hn.getUid()));
hnsForPackage.notifications.add(hn);
historicalNotifications.put(key, hnsForPackage);
}
List<NotificationHistoryPackage> packages =
new ArrayList<>(historicalNotifications.values());
Collections.sort(packages,
(o1, o2) -> -1 * Long.compare(o1.getMostRecent(), o2.getMostRecent()));
for (NotificationHistoryPackage nhp : packages) {
ApplicationInfo info;
try {
info = mPm.getApplicationInfoAsUser(
nhp.pkgName,
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_DIRECT_BOOT_AWARE,
UserHandle.getUserId(nhp.uid));
if (info != null) {
nhp.label = String.valueOf(mPm.getApplicationLabel(info));
nhp.icon = mPm.getUserBadgedIcon(mPm.getApplicationIcon(info),
UserHandle.of(UserHandle.getUserId(nhp.uid)));
}
} catch (PackageManager.NameNotFoundException e) {
// app is gone, just show package name and generic icon
nhp.icon = mPm.getDefaultActivityIcon();
}
}
ThreadUtils.postOnMainThread(() -> listener.onHistoryLoaded(packages));
});
}
interface OnHistoryLoaderListener {
void onHistoryLoaded(List<NotificationHistoryPackage> notificationsByPackage);
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.INotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.notification.NotificationBackend;
import java.util.Arrays;
public class NotificationHistoryActivity extends Activity {
private static String TAG = "NotifHistory";
private ViewGroup mTodayView;
private ViewGroup mSnoozeView;
private ViewGroup mDismissView;
private HistoryLoader mHistoryLoader;
private INotificationManager mNm;
private PackageManager mPm;
private HistoryLoader.OnHistoryLoaderListener mOnHistoryLoaderListener = notifications -> {
// for each package, new header and recycler view
for (NotificationHistoryPackage nhp : notifications) {
View viewForPackage = LayoutInflater.from(this)
.inflate(R.layout.notification_history_app_layout, null);
final View container = viewForPackage.findViewById(R.id.list_container);
container.setVisibility(View.GONE);
ImageButton expand = viewForPackage.findViewById(R.id.expand);
expand.setOnClickListener(v -> {
container.setVisibility(container.getVisibility() == View.VISIBLE
? View.GONE : View.VISIBLE);
expand.setImageResource(container.getVisibility() == View.VISIBLE
? R.drawable.ic_expand_less
: com.android.internal.R.drawable.ic_expand_more);
});
TextView label = viewForPackage.findViewById(R.id.label);
label.setText(nhp.label != null ? nhp.label : nhp.pkgName);
ImageView icon = viewForPackage.findViewById(R.id.icon);
icon.setImageDrawable(nhp.icon);
RecyclerView rv = viewForPackage.findViewById(R.id.notification_list);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new NotificationHistoryAdapter());
((NotificationHistoryAdapter) rv.getAdapter()).onRebuildComplete(nhp.notifications);
mTodayView.addView(viewForPackage);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.notification_history);
mTodayView = findViewById(R.id.apps);
mSnoozeView = findViewById(R.id.snoozed_list);
mDismissView = findViewById(R.id.recently_dismissed_list);
}
@Override
protected void onResume() {
super.onResume();
mPm = getPackageManager();
mHistoryLoader = new HistoryLoader(this, new NotificationBackend(), mPm);
mHistoryLoader.load(mOnHistoryLoaderListener);
mNm = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
try {
mListener.registerAsSystemService(this, new ComponentName(getPackageName(),
this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
} catch (RemoteException e) {
Log.e(TAG, "Cannot register listener", e);
}
}
@Override
public void onPause() {
try {
mListener.unregisterAsSystemService();
} catch (RemoteException e) {
Log.e(TAG, "Cannot unregister listener", e);
}
super.onPause();
}
private final NotificationListenerService mListener = new NotificationListenerService() {
@Override
public void onListenerConnected() {
StatusBarNotification[] snoozed = getSnoozedNotifications();
if (snoozed == null || snoozed.length == 0) {
mSnoozeView.setVisibility(View.GONE);
} else {
RecyclerView rv = mSnoozeView.findViewById(R.id.notification_list);
rv.setLayoutManager(new LinearLayoutManager(NotificationHistoryActivity.this));
rv.setAdapter(new NotificationSbnAdapter(NotificationHistoryActivity.this, mPm));
((NotificationSbnAdapter) rv.getAdapter()).onRebuildComplete(
Arrays.asList(snoozed));
}
try {
StatusBarNotification[] dismissed = mNm.getHistoricalNotifications(
NotificationHistoryActivity.this.getPackageName(), 10);
RecyclerView rv = mDismissView.findViewById(R.id.notification_list);
rv.setLayoutManager(new LinearLayoutManager(NotificationHistoryActivity.this));
rv.setAdapter(new NotificationSbnAdapter(NotificationHistoryActivity.this, mPm));
((NotificationSbnAdapter) rv.getAdapter()).onRebuildComplete(
Arrays.asList(dismissed));
mDismissView.setVisibility(View.VISIBLE);
} catch (Exception e) {
Slog.e(TAG, "Cannot load recently dismissed", e);
mDismissView.setVisibility(View.GONE);
}
}
};
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import android.app.NotificationHistory;
import android.app.NotificationHistory.HistoricalNotification;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class NotificationHistoryAdapter extends
RecyclerView.Adapter<NotificationHistoryViewHolder> {
private List<HistoricalNotification> mValues;
public NotificationHistoryAdapter() {
mValues = new ArrayList<>();
setHasStableIds(true);
}
@Override
public NotificationHistoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.notification_history_log_row, parent, false);
return new NotificationHistoryViewHolder(view);
}
@Override
public void onBindViewHolder(final @NonNull NotificationHistoryViewHolder holder,
int position) {
final HistoricalNotification hn = mValues.get(position);
holder.setTitle(hn.getTitle());
holder.setSummary(hn.getText());
holder.setPostedTime(hn.getPostedTimeMs());
holder.addOnClick(hn.getPackage(), hn.getUserId(), hn.getChannelId());
}
@Override
public int getItemCount() {
return mValues.size();
}
public void onRebuildComplete(List<HistoricalNotification> notifications) {
mValues = notifications;
notifyDataSetChanged();
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import android.app.NotificationHistory;
import android.graphics.drawable.Drawable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class NotificationHistoryPackage {
String pkgName;
int uid;
List<NotificationHistory.HistoricalNotification> notifications;
CharSequence label;
Drawable icon;
public NotificationHistoryPackage(String pkgName, int uid) {
this.pkgName = pkgName;
this.uid = uid;
notifications = new ArrayList<>();
}
public long getMostRecent() {
if (notifications.isEmpty()) {
return 0;
}
return notifications.get(0).getPostedTimeMs();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NotificationHistoryPackage that = (NotificationHistoryPackage) o;
return uid == that.uid &&
Objects.equals(pkgName, that.pkgName) &&
Objects.equals(notifications, that.notifications) &&
Objects.equals(label, that.label) &&
Objects.equals(icon, that.icon);
}
@Override
public int hashCode() {
return Objects.hash(pkgName, uid, notifications, label, icon);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import android.content.Intent;
import android.os.UserHandle;
import android.provider.Settings;
import android.view.View;
import android.widget.DateTimeView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
public class NotificationHistoryViewHolder extends RecyclerView.ViewHolder {
private final DateTimeView mTime;
private final TextView mTitle;
private final TextView mSummary;
NotificationHistoryViewHolder(View itemView) {
super(itemView);
mTime = itemView.findViewById(R.id.timestamp);
mTime.setShowRelativeTime(true);
mTitle = itemView.findViewById(R.id.title);
mSummary = itemView.findViewById(R.id.text);
}
void setSummary(CharSequence summary) {
mSummary.setText(summary);
mSummary.setVisibility(summary != null ? View.VISIBLE : View.GONE);
}
void setTitle(CharSequence title) {
mTitle.setText(title);
mTitle.setVisibility(title != null ? View.VISIBLE : View.GONE);
}
void setPostedTime(long postedTime) {
mTime.setTime(postedTime);
}
void addOnClick(String pkg, int userId, String channelId) {
itemView.setOnClickListener(v -> {
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, pkg)
.putExtra(EXTRA_CHANNEL_ID, channelId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
itemView.getContext().startActivityAsUser(intent, UserHandle.of(userId));
});
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import static android.content.pm.PackageManager.*;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class NotificationSbnAdapter extends
RecyclerView.Adapter<NotificationSbnViewHolder> {
private static final String TAG = "SbnAdapter";
private List<StatusBarNotification> mValues;
private Map<Integer, Drawable> mUserBadgeCache;
private final Context mContext;
private PackageManager mPm;
public NotificationSbnAdapter(Context context, PackageManager pm) {
mContext = context;
mPm = pm;
mUserBadgeCache = new HashMap<>();
mValues = new ArrayList<>();
setHasStableIds(true);
}
@Override
public NotificationSbnViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.notification_sbn_log_row, parent, false);
return new NotificationSbnViewHolder(view);
}
@Override
public void onBindViewHolder(final @NonNull NotificationSbnViewHolder holder,
int position) {
final StatusBarNotification sbn = mValues.get(position);
holder.setIcon(loadIcon(sbn));
holder.setPackageName(loadPackageName(sbn.getPackageName()).toString());
holder.setTitle(getTitleString(sbn.getNotification()));
holder.setSummary(getTextString(mContext, sbn.getNotification()));
holder.setPostedTime(sbn.getPostTime());
if (!mUserBadgeCache.containsKey(sbn.getUserId())) {
Drawable profile = mContext.getPackageManager().getUserBadgeForDensity(
UserHandle.of(sbn.getUserId()), -1);
mUserBadgeCache.put(sbn.getUserId(), profile);
}
holder.setProfileBadge(mUserBadgeCache.get(sbn.getUserId()));
}
@Override
public int getItemCount() {
return mValues.size();
}
public void onRebuildComplete(List<StatusBarNotification> notifications) {
mValues = notifications;
notifyDataSetChanged();
}
private @NonNull CharSequence loadPackageName(String pkg) {
try {
ApplicationInfo info = mPm.getApplicationInfo(pkg,
MATCH_ANY_USER);
if (info != null) return mPm.getApplicationLabel(info);
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot load package name", e);
}
return pkg;
}
private static String getTitleString(Notification n) {
CharSequence title = null;
if (n.extras != null) {
title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
}
return title == null? null : String.valueOf(title);
}
/**
* Returns the appropriate substring for this notification based on the style of notification.
*/
private static String getTextString(Context appContext, Notification n) {
CharSequence text = null;
if (n.extras != null) {
text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
Notification.Builder nb = Notification.Builder.recoverBuilder(appContext, n);
if (nb.getStyle() instanceof Notification.BigTextStyle) {
text = ((Notification.BigTextStyle) nb.getStyle()).getBigText();
} else if (nb.getStyle() instanceof Notification.MessagingStyle) {
Notification.MessagingStyle ms = (Notification.MessagingStyle) nb.getStyle();
final List<Notification.MessagingStyle.Message> messages = ms.getMessages();
if (messages != null && messages.size() > 0) {
text = messages.get(messages.size() - 1).getText();
}
}
if (TextUtils.isEmpty(text)) {
text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
}
}
return text == null ? null : String.valueOf(text);
}
private Drawable loadIcon(StatusBarNotification sbn) {
Drawable draw = sbn.getNotification().getSmallIcon().loadDrawableAsUser(
sbn.getPackageContext(mContext), sbn.getUserId());
if (draw == null) {
return null;
}
draw.mutate();
draw.setColorFilter(sbn.getNotification().color, PorterDuff.Mode.SRC_ATOP);
return draw;
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2020 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.settings.notification.history;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.view.View;
import android.widget.DateTimeView;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
private final TextView mPkgName;
private final ImageView mIcon;
private final DateTimeView mTime;
private final TextView mTitle;
private final TextView mSummary;
private final ImageView mProfileBadge;
NotificationSbnViewHolder(View itemView) {
super(itemView);
mPkgName = itemView.findViewById(R.id.pkgname);
mIcon = itemView.findViewById(R.id.icon);
mTime = itemView.findViewById(R.id.timestamp);
mTitle = itemView.findViewById(R.id.title);
mSummary = itemView.findViewById(R.id.text);
mProfileBadge = itemView.findViewById(R.id.profile_badge);
}
void setSummary(CharSequence summary) {
mSummary.setVisibility(TextUtils.isEmpty(summary) ? View.GONE : View.VISIBLE);
mSummary.setText(summary);
}
void setTitle(CharSequence title) {
if (title == null) {
return;
}
mTitle.setText(title);
}
void setIcon(Drawable icon) {
mIcon.setImageDrawable(icon);
}
void setPackageName(String pkg) {
mPkgName.setText(pkg);
}
void setPostedTime(long postedTime) {
mTime.setTime(postedTime);
}
void setProfileBadge(Drawable badge) {
mProfileBadge.setImageDrawable(badge);
}
}