diff options
Diffstat (limited to 'src/com/android/settings/notification/NotificationAppList.java')
-rw-r--r-- | src/com/android/settings/notification/NotificationAppList.java | 569 |
1 files changed, 569 insertions, 0 deletions
diff --git a/src/com/android/settings/notification/NotificationAppList.java b/src/com/android/settings/notification/NotificationAppList.java new file mode 100644 index 0000000..3879bef --- /dev/null +++ b/src/com/android/settings/notification/NotificationAppList.java @@ -0,0 +1,569 @@ +/* + * 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<String, AppRow> mRows = new ArrayMap<String, AppRow>(); + private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>(); + private final ArrayList<String> mSections = new ArrayList<String>(); + + 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 boolean isSystemApp(PackageInfo pkg) { + if (mSystemSignature == null) { + mSystemSignature = new Signature[]{ getSystemSignature() }; + } + return mSystemSignature[0] != null && mSystemSignature[0].equals(getFirstSignature(pkg)); + } + + private static Signature getFirstSignature(PackageInfo pkg) { + if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) { + return pkg.signatures[0]; + } + return null; + } + + private Signature getSystemSignature() { + final PackageManager pm = mContext.getPackageManager(); + try { + final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES); + return getFirstSignature(sys); + } catch (NameNotFoundException e) { + } + return null; + } + + private static class ViewHolder { + ViewGroup row; + ImageView icon; + TextView title; + TextView subtitle; + View rowDivider; + } + + private class NotificationAppAdapter extends ArrayAdapter<Row> 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<AppRow> mRowComparator = new Comparator<AppRow>() { + 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 void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) { + if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is " + + APP_NOTIFICATION_PREFS_CATEGORY_INTENT); + final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( + APP_NOTIFICATION_PREFS_CATEGORY_INTENT, + PackageManager.MATCH_DEFAULT_ONLY); + if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities"); + 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(Intent.ACTION_MAIN) + .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 non-system apps + final PackageManager pm = mContext.getPackageManager(); + for (PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_SIGNATURES)) { + if (pkg.applicationInfo == null || isSystemApp(pkg)) { + if (DEBUG) Log.d(TAG, "Skipping " + pkg.packageName); + continue; + } + final AppRow row = loadAppRow(pm, pkg, mBackend); + mRows.put(row.pkg, row); + } + // collect config activities + collectConfigActivities(pm, mRows); + // 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; + } + } +} |