From 802ddf99f57e316d0fd87c2cfeed5dc3a0cfa8fe Mon Sep 17 00:00:00 2001 From: John Spurlock Date: Fri, 18 Jul 2014 11:51:13 -0400 Subject: [PATCH] Settings: New application-level notification settings. - Convert the old application-level dialog to an activity. - Move the settings icon to the new activity (out of the list). - Add a custom application header, similar to the switch bar style. - Use the ubiquitous vector gear for the settings icon. - Migrate old checkboxes to switch prefs, add new summaries. - Remove obsolete artifacts. Bug:16396715 Change-Id: I857e3cf448b79f44fe1c242e6020f5214434c00c --- AndroidManifest.xml | 15 +- res/drawable-hdpi/ic_settings_generic.png | Bin 1505 -> 0 bytes res/drawable-mdpi/ic_settings_generic.png | Bin 969 -> 0 bytes res/drawable-xhdpi/ic_settings_generic.png | Bin 2051 -> 0 bytes res/drawable-xxhdpi/ic_settings_generic.png | Bin 2968 -> 0 bytes res/drawable/ic_settings_32dp.xml | 23 + res/layout/app_notification_header.xml | 63 ++ res/layout/notification_app.xml | 82 +-- res/layout/notification_app_dialog.xml | 65 -- res/values/dimens.xml | 1 - res/values/strings.xml | 28 +- res/xml/app_notification_settings.xml | 45 ++ res/xml/notification_settings.xml | 2 +- src/com/android/settings/Settings.java | 1 + .../android/settings/SettingsActivity.java | 4 +- .../notification/AppNotificationDialog.java | 180 ----- .../notification/AppNotificationSettings.java | 631 ++++-------------- .../notification/NotificationAppList.java | 569 ++++++++++++++++ 18 files changed, 890 insertions(+), 819 deletions(-) delete mode 100644 res/drawable-hdpi/ic_settings_generic.png delete mode 100644 res/drawable-mdpi/ic_settings_generic.png delete mode 100644 res/drawable-xhdpi/ic_settings_generic.png delete mode 100644 res/drawable-xxhdpi/ic_settings_generic.png create mode 100644 res/drawable/ic_settings_32dp.xml create mode 100644 res/layout/app_notification_header.xml delete mode 100644 res/layout/notification_app_dialog.xml create mode 100644 res/xml/app_notification_settings.xml delete mode 100644 src/com/android/settings/notification/AppNotificationDialog.java create mode 100644 src/com/android/settings/notification/NotificationAppList.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 113dc1e5d8c..98ebfdd6e83 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1839,25 +1839,30 @@ android:resource="@id/notification_settings" /> - + + android:value="com.android.settings.notification.NotificationAppList" /> - + + + diff --git a/res/drawable-hdpi/ic_settings_generic.png b/res/drawable-hdpi/ic_settings_generic.png deleted file mode 100644 index 0e577bf53dfb3f9b2fdaeb25c799ca42ea1451e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1505 zcmV<71s?i|P)6;`7<1NHnRpShr9~Q2uGKv%m5CTNVy}nw7b93KK#gy^DRi#&jJJysEGZJ; zI&*W*)fNY#`}6<_xJ7jiGwN1TuAB!me5HVQt81?aK!T`d%L+cscZZV6gA^%HH`8s7 z=rJ68ztQ)6M*}VvDA7Rbe|FgJpiKN?7^kQ)U?_fhfCN#k9@S^Be|cArn02m4%74A9 zTME?{XmXQkba~pJL^W7!xutg6Y&1SGKmxvMM-bbzL1$H~^nq8r;2>tGL7zbxH&|np zT7x=WrB;CGx2QxjAOBM8>(*4nN!emr?RcEBEQnQNg4W|?NIdS49WMs4$?GXf8C%QWI% zzgCOWRvWd;)nKaQVxcKKukovhCsP0DNHY%ki|~4 zPfUdXqt2k(Y3D$t`pZh!s|4qqR*eA{J{BMWb*{w%!;XTfRwykTED1`j2s+nM!{#HB z(+MW@;2H^w&?urK;H|VIf50bTiIu^zLqrq4?7R6>E}A2Bwap$7!E{N7+^rH->a-iU z)LpUNb!J-VyUb=jyT=lzb@;7yYG z07)@=EOuQ^jWuet*H-QNMMPaNEaiZgZ1AiFwpg9l9{y^hk;`or{eESsT8ujFeFMJW z4mW$n3pUDMTQ*yz){vBGW(4JN($l$Xeg5KAs@47W>absrG52_6lErI_ziZIs7R$`^ z3!9C_F9$$^N>}M~p7_P$y-|w?d@iW8@alNfi?%r!Oy3F(7W$|Dk69fjq68(rpSLf; zDQmouYwbidX}gY-_P3z%h3Op~ldCUw9-N%u@6wwFixDjj;Jmku<#1Bo$sdlN7$6S+ zk;?Pwa8afe{V%i2ZcCLblX1b2KkK`)04Zxt3ko!C%wVujd`6$1{tuIxg=C?5M(F?m z03~!qSaf7zbY(hYa%Ew3WdJfTF*GeOG%YbOR53O>GB!FiIV~_WIxsM3XTl%=001R) zMObuXVRU6WZEs|0W_bWIFflYOFf=VOFjO%%Ix;poG&wCWG&(Ra89I8L00000NkvXX Hu0mjfSfsd< diff --git a/res/drawable-mdpi/ic_settings_generic.png b/res/drawable-mdpi/ic_settings_generic.png deleted file mode 100644 index a7ede7e8823ff488fbd51b0cc55be22bc4f12540..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 969 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8wRq zxP?KOkzv*x2?hpcZci7-5Rc~#?^pU^UM z>!jEh_vCgQXx)=*S1DI#z5nmI_q*>~KL2fN|85)WCfOsAOMN^X4xQIK_Krt^&o@rk z;XwK;l{|jIGrzX|wo%nkXeesEd1w>!nyq=~;#cZKN8}w_-2Cq2c9DuJ-G_<;6}MJB zK71r?(%~E@OG(S{%JWravgpiD6U}zN-RNZ}w9ewCcIm69 zjZ2*G&GJ-cUo<7-6NAUk3!Ls(xRwi_5!`k+>iz6ZmUnMV-Dh}lV&IVts+sS5{AB_g zU;F)JoE;Lm&}lLAsT|+$OGE0@#CPp|7n>Ly6?(fdj@QapIeh72vsD3$uclQl+iErS zVk_UpDR$f6n5;OIm$_3Q@E!BFpKb_BIcqC%+qEPi1Z{qY*CI{25dF?hxxnuQuuYv{l{K78x z;wq60%v>3?f0vZ6+ey*VD_L7OR!n~Hx%ixW@JyFgpC-jTxw+nPsjS!B2nUsY-?ye! za#Y`LSm&ekRqC6>jimX`QlSxv>-QG7TEAUTyKTw$`k5y@6i>BOyI!`@pMK$qx$Cr( zj)zi@%LcEuTidrJE&kF*hjaYD4~20)&eOQI{OAEC%lwkdMT#XilS|svi-k{i%#gZ! zZudh;-l&*M=^~zMaK(3X6 jK}Px^1r!ar`6-!cmAExXT2;*fYGCkm^>bP0l+XkK@lvo= diff --git a/res/drawable-xhdpi/ic_settings_generic.png b/res/drawable-xhdpi/ic_settings_generic.png deleted file mode 100644 index 6c907f4a0c91d45f98df0a590583d3a62ce53702..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2051 zcmV+e2>kbnP)8I~_tUJEY{qdelPq2^W# z)aAGQa(EvX7thimqS}lcSi_tyLOc{L5AWlY_=;wG($s}!ILBGWH@Vj`=^gH+#f7VQ}#+KK+;C5tg%T%)Ilng=&-}b5|XAk-Fc>qV7t9WDow$7 z%tdx&HlQ%1LEm*smaYIh+~;>1jc}TnDx*b3BqcOiuhvUeSnWs7SD`Ws_atZe?Wc!R z4)5bk@}OBE6JoP#JZ!Lq7O6_}t54dkcfZy4S}1OE2ttFiZBF+;W9r4CuCy>@Ml`s@ z<7Qdua+4H?fESzKBvZZZX&cQpD(j63yR572$w?n)mK#I%u11%5);HWCrbuonGS$(x z`iIS`m1kHZ#&{~$T$`p!AByvg&H`<7ty&BGCUj6TWu`kUQ0rQ)S+?=cO=nD>dIec6 zCh2~w&9U5}DcXeh)qBToWe#_waiW23g5|bbt;(fY?j5f}b1wRBHyZBLv`TNeQ<0@+ zrsx{nZH<;--H&j(#b&4Uz||JJ(@CbL;kR3Bce=kR5~EF>i4G4YUys|Yai*UH)2+wf zT;VZ0b?Zg1ZadX^(ZQw!4Ik@2o-@S!6jydx;g|NLj(ym+GuiJQ*I#UOhHX|kIe6hA zm)oAXgU6xLZO#l-PdP`W)h70r_gL;09qIMLPQ14*u`%f8H_dZYV0+CC+2sA@?QZZ| zpsI4DO{}l7LGV0JRD&+O;n#MCy&f8F)DTsQc1=o+3k^N6w1>5M#hnpvoNVcfns#$YIZKT4!bc_ zR9i~wG%@t7OO@hao%26!qK09YL>}~U$_z%S(b~Nq80H$auS_P+D0742R_#XjPvMU6 zdXZ3&p{_7ZUpALe7nsKQ=6<~YmNCKedI_@>VWwk+2HRQmaY|j~dm<=ww1{Wz3cB_} zGs2_xxHvOi5Dd>QwIYsDh!L(+71n@A7I}%wTpBEtIi_0grQn5Gu2CsG1*vpRN<>hn z-Vx^XmuC1ySOAt8?UcIG5@msEsBNB>baLR;6yt5u5|jGFbT|1<;7+?`*1IT0GuA|P zv9_4nT=cQQmRJ((@O`~g=}GmbnigoLIa-lsosxP*lsLp9aVMno<)c;@>qa93%^-)F zs9tL-eR{Z2WPZJrm84a+WK}75y_mIroN}7ymJKekR?PK= zXW5$kH9gTt8~a+ZkCm&=YIV6;-u9H29c=tQLhZdC^;0jH?^hX7UY~P!d9WsToKLJr zldp%Q2%{XM#dDtUo*^niHs5yZ+~j84oZ%MJL;K}wE$LZq27Rqqn}`!ba=+o`tFXy4 zp764dB=zdB*KV!eu-ZzuT4#)_EDgQ;>9EX`nP+}js9E72r>6PUY_LlCWVk&d8Q2f| zq1OG<6DQ_gW3_5huijhQjP$eq1na=^`@0pJsmh^7h~hoxd*J}`d*Vc#ZL)P5e4?vg zU1_oJ2ghlbP9JNN5LNCV#leJZ^k;waAqte3;A^TK;Vyry2|4p~-R-1F|2<8i>CUkz zBzNzVUhjLz171(vSny>deV83&=cMmzKuw5F#Pybj-w4@jkw+xcYkulVc!-++yEHX!fI?&--*P(K=3Nz{iWTbAWsfEg4ghH& zU$Wxab$c1ecdReu^DO@dVH_rV*_#Vu0000bbVXQnWMOn=I%9HWVRU5xGB7bTEig1K zF)&myHaapkIy5;gFf=+aFlcAOAOHXWC3HntbYx+4WjbwdWNBu305UK!G%YYREio`u hF*Z6fHaavpEig1XFfbW9dYk|N002ovPDHLkV1f1fmI8$OI$^2tq_yE?6cmQ_B^p6F9lbc3CH^ zM1Cb@%a(>B%ES*1G#Cje2$GeWfhoTP7A%AjXZSIL{E(Rbxck{%*Z1B%_s4tlvin`@ zt$Fv&J^P+>zUQ8^_da`{CJhY@BxpP|O}Zc!$<2bNbmRA}2GH>kd2uTpQqV$ACRR z4N$MYwE;Q;-GO!iWK{uoh{#@xHueU_s&@8y;0Y1Awb2PM##|39$#TnSU^B2z|85bH z1ICyefnNh{eIBs|Sgqw>5s^ba_2~@sS1Wo1aJi5CdL?0@N6sa{K%fj54tz3A*~ucZ zpwS30#&iVU#PK=?K|R@hJV$`XfG0&{Ys3N41IL|xJC3_L6Mhf8hvPl%iL>wEhoe9r z5jhaHeo-}z2Sao|Svvm+@DMOhM2=_Z(DQ�iA)?`tu3k5b&0Fx)YI|z`e$p>A<&f zHh0nyaYKl96lr}Y<^2sX#tZ@ejI%Qp6%D|XK!u1L_B^MJTC&4|AvzUgQlHNOyMgt< z8eqAI)O+N00%qWB^u)DUGfYI*<&6MU8UG3Nj-m4J05^-sD$g@6Rh9Isgm+2T1B-!K zBJ#RN{wUxH;KCT%@H)^pjA|R!5W6F$&VLQ)=k9!COo=gO39uEog=qT$ZB)hV8=yMyE>-)YU>z_%HP`Fj?KI#gID5Rgz)9eTz|54LVvK34 z>T=Y*hiagQh}7l!4UiZ*e=RWD)%hKP7j$mURp+C!X8?cHXQFi&t#yimi*x|x?hQ~K zdw0Z6+6oL7kz*?JVl{~Jy=k%&7@0D{j4|zi4Zvj)WYz)QMWiNIZ@@hfJO3y!&ei$d zw9$>RW7Pv#uXRA{G)`6Fu($xYGbe9=YCeY|PCpYw+v%b?AV9Gni^%<2zF0fdrg5i$9{_WM8aX;;-lfE~ zvM6!_=q)1qwEX?Rj{`;tgZ!K4PcGVab5q+vhxoDWg;>&jD;y8XGLU| zPEiLfinInEa9z*gzwT}uYy^e}>HN@^u#zxJ7cf685Rs!gs!A;#R|kP1BJz)@E3C@+ zf+`Sxn3Rek(mF2)(D|b?IS_N7s0AF!8Rwa62LOKoTBiB;J`w4r_q71twoqd$MC74d=$pov3LF=DHq-)L zQfj0zW)D!3MTH$CBGqB*)HH_CwHYUhAstq`zK^iz{2iY4B5ur43pg7(cr>&%8SXo* z(@u4i3N?-C6@M(leChVD*u;50n9FDd5qTf@g~hESGqfRvv%thSrpY+tg3?vgy@-}s zsDYK_9+mp6Y$(gnh7{(C$l5q66DI-P(~Oc`I*&HB@b|3q?q22SEvUzFg=a%gt#fLg zKbyurq0k8E=DFUluRK6k{GE7z%cK9t?^)+-om11+e$W4l3z2}uQWir5NqUiAczXfq5jr zFZb-dl;)#t=<3z*3XuS>FQ^&1S*Y$mqg&I3C52Jr#8kO0LO&Qwo!C)yE0@TM}N&1XN_efi1A?~}nla;H{ z-yFEt;?@^^I(xD)=GI0hKm%P@k~u>98c(BYEf(VP9FG2S{IhUsJ%eR43J*}G-P}Q zc2Q)vxW3Qk!;US|Seeg9ZJ4LNJ%HyeEZB3v!>;SE``q6NSgA8XehAQD)|J>Ff^m}u z^rHUrXDrHe!HHVy8$;*!#o4jzY;pTDBC<`(6#+L{lqm(OQn98vB|xLuRuk+Do;IF& z{d;MAE15{xWQ-{fvoITDni*rtRio)*QRD>heb0Jf8yepiSRIChAfbs}wDT_wL3AC^ zE0s%SjJca&TaT&0tfeAy%BP;Kfa?hsm<2FZL}sguAhAnsyhgy#lprIkf5^^H`BG-O z^!#7q?7P}V$Mpi90)AzTc@cOC*zVG*I;eZ$DxB@ab^+^GW*&Oj{SRV}J_$-PS}L zp%uV&BJ!@st{E0lc8#T_9c~f!EXY%0SPxQa+gNw1zaHQ*11H8mu0-kv9pM!svd8tT zV&D&|;YG!rA~G*u1T@15Sn3l)#j9}=wW~bO=mSh8o%3WgFrwbMO9|5uMHXaKGjk@aB;M?Ed2l5mGyWJ2tRb$EPO*$(Rnty)JpHz+Cr z#W?3A*hCrBs`a`ZCx++-A0qO7Tupt5sqV14SZW%D0Bzu8oI@|X*sD|224Itj?C_Mk z5%`Toqf-Yqt;IR8soJML7wMup47&rH>VPRCvOovanUB1@z6UtC9) z&Br+wawAT*;tL}3FN<;|N5eQbAz0X874R()*_~y&8w7j__%h%=d-PEe z`DWC2tEyMQ-doiRu=FeEBF+p^)blU zkPr|ae~noF6!3CFK;secvR@!qLO^zG!~Zy(Y}i=f?JH~o^mN;)IQzC+aN>B5=*OLv zS*O3%-~?K2!r4!lU+4rBq%Y|I2}z=MLO?=5f`ovCfCLEv2>}Tj0sjHx_6E}h&+A(N O0000 + + + diff --git a/res/layout/app_notification_header.xml b/res/layout/app_notification_header.xml new file mode 100644 index 00000000000..8c3ca4b6b0b --- /dev/null +++ b/res/layout/app_notification_header.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + diff --git a/res/layout/notification_app.xml b/res/layout/notification_app.xml index 9442de659b7..54060c1a91e 100644 --- a/res/layout/notification_app.xml +++ b/res/layout/notification_app.xml @@ -13,70 +13,44 @@ See the License for the specific language governing permissions and limitations under the License. --> + - - - - - - - - - - - + android:contentDescription="@null" + android:padding="8dp" /> + + + + - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 796ab0cf369..62162d37551 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -87,7 +87,6 @@ 64dp 20dp 4dp - 48dp 160dp 7dp 10dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 75024e9ae74..daad5d1a45b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5739,27 +5739,33 @@ Loading apps... - - Show notifications + + Block - - Display at the top of the list + + Never show notifications from this app - - Hide sensitive content when device is locked + + Priority + + + Show notifications at the top of the list and keep them coming when the device is set to priority interruptions only + + + Sensitive + + + When the device is locked, hide any sensitive content from this app’s notifications Blocked - - Top of list + + Priority Sensitive - - Done - Until you turn this off diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml new file mode 100644 index 00000000000..1d8ae4561a6 --- /dev/null +++ b/res/xml/app_notification_settings.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + diff --git a/res/xml/notification_settings.xml b/res/xml/notification_settings.xml index f077e85d6e3..e5dda19080e 100644 --- a/res/xml/notification_settings.xml +++ b/res/xml/notification_settings.xml @@ -108,7 +108,7 @@ + android:fragment="com.android.settings.notification.NotificationAppList" /> mRows = new ArrayMap(); - private final ArrayList mSortedRows = new ArrayList(); - private final ArrayList mSections = new ArrayList(); + static final String EXTRA_HAS_SETTINGS_INTENT = "has_settings_intent"; + static final String EXTRA_SETTINGS_INTENT = "settings_intent"; + + private final Backend mBackend = new Backend(); private Context mContext; - private LayoutInflater mInflater; - private NotificationAppAdapter mAdapter; - private Signature[] mSystemSignature; - private Parcelable mListViewState; - private Backend mBackend = new Backend(); - private UserSpinnerAdapter mProfileSpinnerAdapter; + private SwitchPreference mBlock; + private SwitchPreference mPriority; + private SwitchPreference mSensitive; + private AppRow mAppRow; + private boolean mCreated; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (DEBUG) Log.d(TAG, "onActivityCreated mCreated=" + mCreated); + if (mCreated) { + Log.w(TAG, "onActivityCreated: ignoring duplicate call"); + return; + } + mCreated = true; + if (mAppRow == null) return; + final View content = getActivity().findViewById(R.id.main_content); + final ViewGroup contentParent = (ViewGroup) content.getParent(); + final View bar = getActivity().getLayoutInflater().inflate(R.layout.app_notification_header, + contentParent, false); + + final ImageView appIcon = (ImageView) bar.findViewById(R.id.app_icon); + appIcon.setImageDrawable(mAppRow.icon); + + final TextView appName = (TextView) bar.findViewById(R.id.app_name); + appName.setText(mAppRow.label); + + final View appSettings = bar.findViewById(R.id.app_settings); + if (mAppRow.settingsIntent == null) { + appSettings.setVisibility(View.GONE); + } else { + appSettings.setClickable(true); + appSettings.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mContext.startActivity(mAppRow.settingsIntent); + } + }); + } + contentParent.addView(bar, 0); + } @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(), AppNotificationSettingsActivity.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; - ViewGroup appButton; - ImageView icon; - TextView title; - TextView subtitle; - View settingsDivider; - ImageView settingsButton; - View rowDivider; - } - - private class NotificationAppAdapter extends ArrayAdapter implements SectionIndexer { - public NotificationAppAdapter(Context context) { - super(context, 0, 0); + Intent intent = getActivity().getIntent(); + if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent); + if (intent == null) { + Log.w(TAG, "No intent"); + toastAndFinish(); + return; } - @Override - public boolean hasStableIds() { - return true; + final int uid = intent.getIntExtra(Settings.EXTRA_APP_UID, -1); + final String pkg = intent.getStringExtra(Settings.EXTRA_APP_PACKAGE); + if (uid == -1 || TextUtils.isEmpty(pkg)) { + Log.w(TAG, "Missing extras: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + ", " + + Settings.EXTRA_APP_UID + " was " + uid); + toastAndFinish(); + return; } - @Override - public long getItemId(int position) { - return position; + if (DEBUG) Log.d(TAG, "Load details for pkg=" + pkg + " uid=" + uid); + final PackageManager pm = getPackageManager(); + final PackageInfo info = findPackageInfo(pm, pkg, uid); + if (info == null) { + Log.w(TAG, "Failed to find package info: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + + ", " + Settings.EXTRA_APP_UID + " was " + uid); + toastAndFinish(); + return; } - @Override - public int getViewTypeCount() { - return 2; - } + addPreferencesFromResource(R.xml.app_notification_settings); + mBlock = (SwitchPreference) findPreference(KEY_BLOCK); + mPriority = (SwitchPreference) findPreference(KEY_PRIORITY); + mSensitive = (SwitchPreference) findPreference(KEY_SENSITIVE); - @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; + mAppRow = NotificationAppList.loadAppRow(pm, info, mBackend); + if (intent.hasExtra(EXTRA_HAS_SETTINGS_INTENT)) { + // use settings intent from extra + if (intent.getBooleanExtra(EXTRA_HAS_SETTINGS_INTENT, false)) { + mAppRow.settingsIntent = intent.getParcelableExtra(EXTRA_SETTINGS_INTENT); } - bindView(v, r, false /*animate*/); - return v; + } else { + // load settings intent + ArrayMap rows = new ArrayMap(); + rows.put(mAppRow.pkg, mAppRow); + NotificationAppList.collectConfigActivities(getPackageManager(), rows); } - public View newView(ViewGroup parent, Row r) { - if (!(r instanceof AppRow)) { - return mInflater.inflate(R.layout.notification_app_section, parent, false); + mBlock.setChecked(mAppRow.banned); + mPriority.setChecked(mAppRow.priority); + mSensitive.setChecked(mAppRow.sensitive); + + mBlock.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean block = (Boolean) newValue; + return mBackend.setNotificationsBanned(pkg, uid, block); } - 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.appButton = (ViewGroup) v.findViewById(android.R.id.button1); - vh.appButton.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.settingsDivider = v.findViewById(R.id.settings_divider); - vh.settingsButton = (ImageView) v.findViewById(android.R.id.button2); - 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); + mPriority.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean priority = (Boolean) newValue; + return mBackend.setHighPriority(pkg, uid, priority); } - } + }); - 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; + mSensitive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean sensitive = (Boolean) newValue; + return mBackend.setSensitive(pkg, uid, sensitive); } - - 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.appButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mContext.startActivity(new Intent(mContext, AppNotificationDialog.class) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg) - .putExtra(Settings.EXTRA_APP_UID, row.uid)); - } - }); - enableLayoutTransitions(vh.appButton, 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); - final boolean showSettings = !row.banned && row.settingsIntent != null; - vh.settingsDivider.setVisibility(showSettings ? View.VISIBLE : View.GONE); - vh.settingsButton.setVisibility(showSettings ? View.VISIBLE : View.GONE); - vh.settingsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (row.settingsIntent != null) { - getContext().startActivity(row.settingsIntent); - } - } - }); - } - - private String getSubtitle(AppRow row) { - if (row.banned) return mContext.getString(R.string.app_notification_row_banned); - if (!row.priority && !row.sensitive) return ""; - 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; + private void toastAndFinish() { + Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show(); + getActivity().finish(); } - 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; - } - - 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 - 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, - 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 = mRows.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); - } - // 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; + private static PackageInfo findPackageInfo(PackageManager pm, String pkg, int uid) { + final String[] packages = pm.getPackagesForUid(uid); + if (packages != null && pkg != null) { + final int N = packages.length; 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; + final String p = packages[i]; + if (pkg.equals(p)) { + try { + return pm.getPackageInfo(pkg, 0); + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to load package " + pkg, e); + } } - 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; - } + return null; } } diff --git a/src/com/android/settings/notification/NotificationAppList.java b/src/com/android/settings/notification/NotificationAppList.java new file mode 100644 index 00000000000..3879bef67e6 --- /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 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 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 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 void collectConfigActivities(PackageManager pm, ArrayMap rows) { + 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, + 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; + } + } +}