| /* |
| * 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.LauncherActivityInfo; |
| import android.content.pm.LauncherApps; |
| import android.content.pm.PackageManager; |
| 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.service.notification.NotificationListenerService; |
| 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; |
| private Spinner mSpinner; |
| |
| private PackageManager mPM; |
| private UserManager mUM; |
| private LauncherApps mLauncherApps; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mContext = getActivity(); |
| mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| mAdapter = new NotificationAppAdapter(mContext); |
| mUM = UserManager.get(mContext); |
| mPM = mContext.getPackageManager(); |
| mLauncherApps = (LauncherApps) mContext.getSystemService(Context.LAUNCHER_APPS_SERVICE); |
| 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); |
| mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(mUM, mContext); |
| if (mProfileSpinnerAdapter != null) { |
| mSpinner = (Spinner) getActivity().getLayoutInflater().inflate( |
| R.layout.spinner_view, null); |
| mSpinner.setAdapter(mProfileSpinnerAdapter); |
| mSpinner.setOnItemSelectedListener(this); |
| setPinnedHeaderView(mSpinner); |
| } |
| } |
| |
| @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); |
| intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); |
| mContext.startActivityAsUser(intent, selectedUser); |
| // Go back to default selection, which is the first one; this makes sure that pressing |
| // the back button takes you into a consistent state |
| mSpinner.setSelection(0); |
| } |
| } |
| |
| @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<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, ApplicationInfo app, |
| Backend backend) { |
| final AppRow row = new AppRow(); |
| row.pkg = app.packageName; |
| row.uid = app.uid; |
| try { |
| row.label = app.loadLabel(pm); |
| } catch (Throwable t) { |
| Log.e(TAG, "Error loading application label for " + row.pkg, t); |
| row.label = row.pkg; |
| } |
| row.icon = app.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<ResolveInfo> queryNotificationConfigActivities(PackageManager pm) { |
| 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, |
| 0 //PackageManager.MATCH_DEFAULT_ONLY |
| ); |
| return resolveInfos; |
| } |
| public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) { |
| final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm); |
| applyConfigActivities(pm, rows, resolveInfos); |
| } |
| |
| public static void applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows, |
| List<ResolveInfo> 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 List<ApplicationInfo> appInfos = new ArrayList<ApplicationInfo>(); |
| |
| final List<LauncherActivityInfo> lais |
| = mLauncherApps.getActivityList(null /* all */, |
| UserHandle.getCallingUserHandle()); |
| if (DEBUG) Log.d(TAG, " launchable activities:"); |
| for (LauncherActivityInfo lai : lais) { |
| if (DEBUG) Log.d(TAG, " " + lai.getComponentName().toString()); |
| appInfos.add(lai.getApplicationInfo()); |
| } |
| |
| final List<ResolveInfo> resolvedConfigActivities |
| = queryNotificationConfigActivities(mPM); |
| if (DEBUG) Log.d(TAG, " config activities:"); |
| for (ResolveInfo ri : resolvedConfigActivities) { |
| if (DEBUG) Log.d(TAG, " " |
| + ri.activityInfo.packageName + "/" + ri.activityInfo.name); |
| appInfos.add(ri.activityInfo.applicationInfo); |
| } |
| |
| for (ApplicationInfo info : appInfos) { |
| final String key = info.packageName; |
| if (mRows.containsKey(key)) { |
| // we already have this app, thanks |
| continue; |
| } |
| |
| final AppRow row = loadAppRow(mPM, info, mBackend); |
| mRows.put(key, row); |
| } |
| |
| // add config activities to the list |
| applyConfigActivities(mPM, 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 { |
| static INotificationManager sINM = INotificationManager.Stub.asInterface( |
| ServiceManager.getService(Context.NOTIFICATION_SERVICE)); |
| |
| public boolean setNotificationsBanned(String pkg, int uid, boolean banned) { |
| try { |
| sINM.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) { |
| try { |
| final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid); |
| return !enabled; |
| } catch (Exception e) { |
| Log.w(TAG, "Error calling NoMan", e); |
| return false; |
| } |
| } |
| |
| public boolean getHighPriority(String pkg, int uid) { |
| try { |
| return sINM.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) { |
| try { |
| sINM.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) { |
| try { |
| return sINM.getPackageVisibilityOverride(pkg, uid) == Notification.VISIBILITY_PRIVATE; |
| } catch (Exception e) { |
| Log.w(TAG, "Error calling NoMan", e); |
| return false; |
| } |
| } |
| |
| public boolean setSensitive(String pkg, int uid, boolean sensitive) { |
| try { |
| sINM.setPackageVisibilityOverride(pkg, uid, |
| sensitive ? Notification.VISIBILITY_PRIVATE |
| : NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); |
| return true; |
| } catch (Exception e) { |
| Log.w(TAG, "Error calling NoMan", e); |
| return false; |
| } |
| } |
| } |
| } |