/* * Copyright (C) 2014 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; import static com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT; import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT; import android.animation.LayoutTransition; import android.app.INotificationManager; import android.app.Notification; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.Signature; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.ArrayMap; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.SectionIndexer; import android.widget.Spinner; import android.widget.TextView; import com.android.settings.PinnedHeaderListFragment; import com.android.settings.R; import com.android.settings.Settings.NotificationAppListActivity; import com.android.settings.UserSpinnerAdapter; import com.android.settings.Utils; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** Just a sectioned list of installed applications, nothing else to index **/ public class NotificationAppList extends PinnedHeaderListFragment implements OnItemSelectedListener { private static final String TAG = "NotificationAppList"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final String EMPTY_SUBTITLE = ""; private static final String SECTION_BEFORE_A = "*"; private static final String SECTION_AFTER_Z = "**"; private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT = new Intent(Intent.ACTION_MAIN) .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES); private final Handler mHandler = new Handler(); private final ArrayMap mRows = new ArrayMap(); private final ArrayList mSortedRows = new ArrayList(); private final ArrayList mSections = new ArrayList(); private Context mContext; private LayoutInflater mInflater; private NotificationAppAdapter mAdapter; private Signature[] mSystemSignature; private Parcelable mListViewState; private Backend mBackend = new Backend(); private UserSpinnerAdapter mProfileSpinnerAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext = getActivity(); mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mAdapter = new NotificationAppAdapter(mContext); getActivity().setTitle(R.string.app_notifications_title); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.notification_app_list, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext); if (mProfileSpinnerAdapter != null) { Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate( R.layout.spinner_view, null); spinner.setAdapter(mProfileSpinnerAdapter); spinner.setOnItemSelectedListener(this); setPinnedHeaderView(spinner); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); repositionScrollbar(); getListView().setAdapter(mAdapter); } @Override public void onPause() { super.onPause(); if (DEBUG) Log.d(TAG, "Saving listView state"); mListViewState = getListView().onSaveInstanceState(); } @Override public void onDestroyView() { super.onDestroyView(); mListViewState = null; // you're dead to me } @Override public void onResume() { super.onResume(); loadAppsList(); } @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); if (selectedUser.getIdentifier() != UserHandle.myUserId()) { Intent intent = new Intent(getActivity(), NotificationAppListActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivityAsUser(intent, selectedUser); getActivity().finish(); } } @Override public void onNothingSelected(AdapterView parent) { } public void setBackend(Backend backend) { mBackend = backend; } private void loadAppsList() { AsyncTask.execute(mCollectAppsRunnable); } private String getSection(CharSequence label) { if (label == null || label.length() == 0) return SECTION_BEFORE_A; final char c = Character.toUpperCase(label.charAt(0)); if (c < 'A') return SECTION_BEFORE_A; if (c > 'Z') return SECTION_AFTER_Z; return Character.toString(c); } private void repositionScrollbar() { final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, getListView().getScrollBarSize(), getResources().getDisplayMetrics()); final View parent = (View)getView().getParent(); final int eat = Math.min(sbWidthPx, parent.getPaddingEnd()); if (eat <= 0) return; if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d", eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection())); parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(), parent.getPaddingEnd() - eat, parent.getPaddingBottom()); } private static class ViewHolder { ViewGroup row; ImageView icon; TextView title; TextView subtitle; View rowDivider; } private class NotificationAppAdapter extends ArrayAdapter implements SectionIndexer { public NotificationAppAdapter(Context context) { super(context, 0, 0); } @Override public boolean hasStableIds() { return true; } @Override public long getItemId(int position) { return position; } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { Row r = getItem(position); return r instanceof AppRow ? 1 : 0; } public View getView(int position, View convertView, ViewGroup parent) { Row r = getItem(position); View v; if (convertView == null) { v = newView(parent, r); } else { v = convertView; } bindView(v, r, false /*animate*/); return v; } public View newView(ViewGroup parent, Row r) { if (!(r instanceof AppRow)) { return mInflater.inflate(R.layout.notification_app_section, parent, false); } final View v = mInflater.inflate(R.layout.notification_app, parent, false); final ViewHolder vh = new ViewHolder(); vh.row = (ViewGroup) v; vh.row.setLayoutTransition(new LayoutTransition()); vh.row.setLayoutTransition(new LayoutTransition()); vh.icon = (ImageView) v.findViewById(android.R.id.icon); vh.title = (TextView) v.findViewById(android.R.id.title); vh.subtitle = (TextView) v.findViewById(android.R.id.text1); vh.rowDivider = v.findViewById(R.id.row_divider); v.setTag(vh); return v; } private void enableLayoutTransitions(ViewGroup vg, boolean enabled) { if (enabled) { vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING); vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING); } else { vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING); vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING); } } public void bindView(final View view, Row r, boolean animate) { if (!(r instanceof AppRow)) { // it's a section row final TextView tv = (TextView)view.findViewById(android.R.id.title); tv.setText(r.section); return; } final AppRow row = (AppRow)r; final ViewHolder vh = (ViewHolder) view.getTag(); enableLayoutTransitions(vh.row, animate); vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE); vh.row.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg) .putExtra(Settings.EXTRA_APP_UID, row.uid) .putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null) .putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent)); } }); enableLayoutTransitions(vh.row, animate); vh.icon.setImageDrawable(row.icon); vh.title.setText(row.label); final String sub = getSubtitle(row); vh.subtitle.setText(sub); vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE); } private String getSubtitle(AppRow row) { if (row.banned) { return mContext.getString(R.string.app_notification_row_banned); } if (!row.priority && !row.sensitive) { return EMPTY_SUBTITLE; } final String priString = mContext.getString(R.string.app_notification_row_priority); final String senString = mContext.getString(R.string.app_notification_row_sensitive); if (row.priority != row.sensitive) { return row.priority ? priString : senString; } return priString + mContext.getString(R.string.summary_divider_text) + senString; } @Override public Object[] getSections() { return mSections.toArray(new Object[mSections.size()]); } @Override public int getPositionForSection(int sectionIndex) { final String section = mSections.get(sectionIndex); final int n = getCount(); for (int i = 0; i < n; i++) { final Row r = getItem(i); if (r.section.equals(section)) { return i; } } return 0; } @Override public int getSectionForPosition(int position) { Row row = getItem(position); return mSections.indexOf(row.section); } } private static class Row { public String section; } public static class AppRow extends Row { public String pkg; public int uid; public Drawable icon; public CharSequence label; public Intent settingsIntent; public boolean banned; public boolean priority; public boolean sensitive; public boolean first; // first app in section } private static final Comparator mRowComparator = new Comparator() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppRow lhs, AppRow rhs) { return sCollator.compare(lhs.label, rhs.label); } }; public static AppRow loadAppRow(PackageManager pm, PackageInfo pkg, Backend backend) { final AppRow row = new AppRow(); row.pkg = pkg.packageName; row.uid = pkg.applicationInfo.uid; try { row.label = pkg.applicationInfo.loadLabel(pm); } catch (Throwable t) { Log.e(TAG, "Error loading application label for " + row.pkg, t); row.label = row.pkg; } row.icon = pkg.applicationInfo.loadIcon(pm); row.banned = backend.getNotificationsBanned(row.pkg, row.uid); row.priority = backend.getHighPriority(row.pkg, row.uid); row.sensitive = backend.getSensitive(row.pkg, row.uid); return row; } public static List queryNotificationConfigActivities(PackageManager pm) { if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is " + APP_NOTIFICATION_PREFS_CATEGORY_INTENT); final List resolveInfos = pm.queryIntentActivities( APP_NOTIFICATION_PREFS_CATEGORY_INTENT, 0 //PackageManager.MATCH_DEFAULT_ONLY ); return resolveInfos; } public static void collectConfigActivities(PackageManager pm, ArrayMap rows) { final List resolveInfos = queryNotificationConfigActivities(pm); applyConfigActivities(pm, rows, resolveInfos); } public static void applyConfigActivities(PackageManager pm, ArrayMap rows, List resolveInfos) { if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" + (resolveInfos.size() == 0 ? " ;_;" : "")); for (ResolveInfo ri : resolveInfos) { final ActivityInfo activityInfo = ri.activityInfo; final ApplicationInfo appInfo = activityInfo.applicationInfo; final AppRow row = rows.get(appInfo.packageName); if (row == null) { Log.v(TAG, "Ignoring notification preference activity (" + activityInfo.name + ") for unknown package " + activityInfo.packageName); continue; } if (row.settingsIntent != null) { Log.v(TAG, "Ignoring duplicate notification preference activity (" + activityInfo.name + ") for package " + activityInfo.packageName); continue; } row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT) .setClassName(activityInfo.packageName, activityInfo.name); } } private final Runnable mCollectAppsRunnable = new Runnable() { @Override public void run() { synchronized (mRows) { final long start = SystemClock.uptimeMillis(); if (DEBUG) Log.d(TAG, "Collecting apps..."); mRows.clear(); mSortedRows.clear(); // collect all launchable apps, plus any packages that have notification settings final PackageManager pm = mContext.getPackageManager(); final List resolvedApps = pm.queryIntentActivities( new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), PackageManager.MATCH_DEFAULT_ONLY ); final List resolvedConfigActivities = queryNotificationConfigActivities(pm); resolvedApps.addAll(resolvedConfigActivities); for (ResolveInfo info : resolvedApps) { String pkgName = info.activityInfo.packageName; if (mRows.containsKey(pkgName)) { // we already have this app, thanks continue; } PackageInfo pkg = null; try { pkg = pm.getPackageInfo(pkgName, PackageManager.GET_SIGNATURES); } catch (NameNotFoundException e) { if (DEBUG) Log.d(TAG, "Skipping (NNFE): " + pkg.packageName); continue; } if (info.activityInfo.applicationInfo == null) { if (DEBUG) Log.d(TAG, "Skipping (no applicationInfo): " + pkg.packageName); continue; } final AppRow row = loadAppRow(pm, pkg, mBackend); mRows.put(pkgName, row); } // add config activities to the list applyConfigActivities(pm, mRows, resolvedConfigActivities); // sort rows mSortedRows.addAll(mRows.values()); Collections.sort(mSortedRows, mRowComparator); // compute sections mSections.clear(); String section = null; for (AppRow r : mSortedRows) { r.section = getSection(r.label); if (!r.section.equals(section)) { section = r.section; mSections.add(section); } } mHandler.post(mRefreshAppsListRunnable); final long elapsed = SystemClock.uptimeMillis() - start; if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms"); } } }; private void refreshDisplayedItems() { if (DEBUG) Log.d(TAG, "Refreshing apps..."); mAdapter.clear(); synchronized (mSortedRows) { String section = null; final int N = mSortedRows.size(); boolean first = true; for (int i = 0; i < N; i++) { final AppRow row = mSortedRows.get(i); if (!row.section.equals(section)) { section = row.section; Row r = new Row(); r.section = section; mAdapter.add(r); first = true; } row.first = first; mAdapter.add(row); first = false; } } if (mListViewState != null) { if (DEBUG) Log.d(TAG, "Restoring listView state"); getListView().onRestoreInstanceState(mListViewState); mListViewState = null; } if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items"); } private final Runnable mRefreshAppsListRunnable = new Runnable() { @Override public void run() { refreshDisplayedItems(); } }; public static class Backend { public boolean setNotificationsBanned(String pkg, int uid, boolean banned) { INotificationManager nm = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { nm.setNotificationsEnabledForPackage(pkg, uid, !banned); return true; } catch (Exception e) { Log.w(TAG, "Error calling NoMan", e); return false; } } public boolean getNotificationsBanned(String pkg, int uid) { INotificationManager nm = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { final boolean enabled = nm.areNotificationsEnabledForPackage(pkg, uid); return !enabled; } catch (Exception e) { Log.w(TAG, "Error calling NoMan", e); return false; } } public boolean getHighPriority(String pkg, int uid) { INotificationManager nm = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { return nm.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX; } catch (Exception e) { Log.w(TAG, "Error calling NoMan", e); return false; } } public boolean setHighPriority(String pkg, int uid, boolean highPriority) { INotificationManager nm = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { nm.setPackagePriority(pkg, uid, highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT); return true; } catch (Exception e) { Log.w(TAG, "Error calling NoMan", e); return false; } } public boolean getSensitive(String pkg, int uid) { // TODO get visibility state from NoMan return false; } public boolean setSensitive(String pkg, int uid, boolean sensitive) { // TODO save visibility state to NoMan return true; } } }