1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.qs; 17 18 import android.app.ActivityManager; 19 import android.app.AlertDialog; 20 import android.app.admin.DevicePolicyEventLogger; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.content.Intent; 24 import android.content.pm.UserInfo; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.os.UserManager; 29 import android.provider.Settings; 30 import android.text.SpannableStringBuilder; 31 import android.text.method.LinkMovementMethod; 32 import android.text.style.ClickableSpan; 33 import android.util.Log; 34 import android.util.StatsLog; 35 import android.view.ContextThemeWrapper; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.view.ViewGroup; 40 import android.view.Window; 41 import android.widget.ImageView; 42 import android.widget.TextView; 43 44 import com.android.systemui.Dependency; 45 import com.android.systemui.FontSizeUtils; 46 import com.android.systemui.R; 47 import com.android.systemui.plugins.ActivityStarter; 48 import com.android.systemui.statusbar.phone.SystemUIDialog; 49 import com.android.systemui.statusbar.policy.SecurityController; 50 51 public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClickListener { 52 protected static final String TAG = "QSSecurityFooter"; 53 protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 54 55 private final View mRootView; 56 private final TextView mFooterText; 57 private final ImageView mFooterIcon; 58 private final Context mContext; 59 private final Callback mCallback = new Callback(); 60 private final SecurityController mSecurityController; 61 private final ActivityStarter mActivityStarter; 62 private final Handler mMainHandler; 63 private final View mDivider; 64 65 private final UserManager mUm; 66 67 private AlertDialog mDialog; 68 private QSTileHost mHost; 69 protected H mHandler; 70 71 private boolean mIsVisible; 72 private CharSequence mFooterTextContent = null; 73 private int mFooterTextId; 74 private int mFooterIconId; 75 QSSecurityFooter(QSPanel qsPanel, Context context)76 public QSSecurityFooter(QSPanel qsPanel, Context context) { 77 mRootView = LayoutInflater.from(context) 78 .inflate(R.layout.quick_settings_footer, qsPanel, false); 79 mRootView.setOnClickListener(this); 80 mFooterText = (TextView) mRootView.findViewById(R.id.footer_text); 81 mFooterIcon = (ImageView) mRootView.findViewById(R.id.footer_icon); 82 mFooterIconId = R.drawable.ic_info_outline; 83 mContext = context; 84 mMainHandler = new Handler(Looper.myLooper()); 85 mActivityStarter = Dependency.get(ActivityStarter.class); 86 mSecurityController = Dependency.get(SecurityController.class); 87 mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); 88 mDivider = qsPanel == null ? null : qsPanel.getDivider(); 89 mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 90 } 91 setHostEnvironment(QSTileHost host)92 public void setHostEnvironment(QSTileHost host) { 93 mHost = host; 94 } 95 setListening(boolean listening)96 public void setListening(boolean listening) { 97 if (listening) { 98 mSecurityController.addCallback(mCallback); 99 refreshState(); 100 } else { 101 mSecurityController.removeCallback(mCallback); 102 } 103 } 104 onConfigurationChanged()105 public void onConfigurationChanged() { 106 FontSizeUtils.updateFontSize(mFooterText, R.dimen.qs_tile_text_size); 107 } 108 getView()109 public View getView() { 110 return mRootView; 111 } 112 hasFooter()113 public boolean hasFooter() { 114 return mRootView.getVisibility() != View.GONE; 115 } 116 117 @Override onClick(View v)118 public void onClick(View v) { 119 mHandler.sendEmptyMessage(H.CLICK); 120 } 121 handleClick()122 private void handleClick() { 123 showDeviceMonitoringDialog(); 124 DevicePolicyEventLogger 125 .createEvent(StatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED) 126 .write(); 127 } 128 showDeviceMonitoringDialog()129 public void showDeviceMonitoringDialog() { 130 mHost.collapsePanels(); 131 // TODO: Delay dialog creation until after panels are collapsed. 132 createDialog(); 133 } 134 refreshState()135 public void refreshState() { 136 mHandler.sendEmptyMessage(H.REFRESH_STATE); 137 } 138 handleRefreshState()139 private void handleRefreshState() { 140 final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); 141 final UserInfo currentUser = mUm.getUserInfo(ActivityManager.getCurrentUser()); 142 final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null 143 && currentUser.isDemo(); 144 final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); 145 final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); 146 final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); 147 final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); 148 final String vpnName = mSecurityController.getPrimaryVpnName(); 149 final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); 150 final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName(); 151 final CharSequence workProfileName = mSecurityController.getWorkProfileOrganizationName(); 152 // Update visibility of footer 153 mIsVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || hasCACertsInWorkProfile || 154 vpnName != null || vpnNameWorkProfile != null; 155 // Update the string 156 mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, 157 hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, 158 vpnNameWorkProfile, organizationName, workProfileName); 159 // Update the icon 160 int footerIconId = R.drawable.ic_info_outline; 161 if (vpnName != null || vpnNameWorkProfile != null) { 162 if (mSecurityController.isVpnBranded()) { 163 footerIconId = R.drawable.stat_sys_branded_vpn; 164 } else { 165 footerIconId = R.drawable.stat_sys_vpn_ic; 166 } 167 } 168 if (mFooterIconId != footerIconId) { 169 mFooterIconId = footerIconId; 170 mMainHandler.post(mUpdateIcon); 171 } 172 mMainHandler.post(mUpdateDisplayState); 173 } 174 getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, String vpnName, String vpnNameWorkProfile, CharSequence organizationName, CharSequence workProfileName)175 protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, 176 boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, 177 String vpnName, String vpnNameWorkProfile, CharSequence organizationName, 178 CharSequence workProfileName) { 179 if (isDeviceManaged) { 180 if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { 181 if (organizationName == null) { 182 return mContext.getString( 183 R.string.quick_settings_disclosure_management_monitoring); 184 } 185 return mContext.getString( 186 R.string.quick_settings_disclosure_named_management_monitoring, 187 organizationName); 188 } 189 if (vpnName != null && vpnNameWorkProfile != null) { 190 if (organizationName == null) { 191 return mContext.getString(R.string.quick_settings_disclosure_management_vpns); 192 } 193 return mContext.getString(R.string.quick_settings_disclosure_named_management_vpns, 194 organizationName); 195 } 196 if (vpnName != null || vpnNameWorkProfile != null) { 197 if (organizationName == null) { 198 return mContext.getString( 199 R.string.quick_settings_disclosure_management_named_vpn, 200 vpnName != null ? vpnName : vpnNameWorkProfile); 201 } 202 return mContext.getString( 203 R.string.quick_settings_disclosure_named_management_named_vpn, 204 organizationName, 205 vpnName != null ? vpnName : vpnNameWorkProfile); 206 } 207 if (organizationName == null) { 208 return mContext.getString(R.string.quick_settings_disclosure_management); 209 } 210 return mContext.getString(R.string.quick_settings_disclosure_named_management, 211 organizationName); 212 } // end if(isDeviceManaged) 213 if (hasCACertsInWorkProfile) { 214 if (workProfileName == null) { 215 return mContext.getString( 216 R.string.quick_settings_disclosure_managed_profile_monitoring); 217 } 218 return mContext.getString( 219 R.string.quick_settings_disclosure_named_managed_profile_monitoring, 220 workProfileName); 221 } 222 if (hasCACerts) { 223 return mContext.getString(R.string.quick_settings_disclosure_monitoring); 224 } 225 if (vpnName != null && vpnNameWorkProfile != null) { 226 return mContext.getString(R.string.quick_settings_disclosure_vpns); 227 } 228 if (vpnNameWorkProfile != null) { 229 return mContext.getString(R.string.quick_settings_disclosure_managed_profile_named_vpn, 230 vpnNameWorkProfile); 231 } 232 if (vpnName != null) { 233 if (hasWorkProfile) { 234 return mContext.getString( 235 R.string.quick_settings_disclosure_personal_profile_named_vpn, 236 vpnName); 237 } 238 return mContext.getString(R.string.quick_settings_disclosure_named_vpn, 239 vpnName); 240 } 241 return null; 242 } 243 244 @Override onClick(DialogInterface dialog, int which)245 public void onClick(DialogInterface dialog, int which) { 246 if (which == DialogInterface.BUTTON_NEGATIVE) { 247 final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS); 248 mDialog.dismiss(); 249 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 250 } 251 } 252 createDialog()253 private void createDialog() { 254 final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); 255 final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); 256 final CharSequence deviceOwnerOrganization = 257 mSecurityController.getDeviceOwnerOrganizationName(); 258 final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); 259 final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); 260 final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); 261 final String vpnName = mSecurityController.getPrimaryVpnName(); 262 final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); 263 264 mDialog = new SystemUIDialog(mContext); 265 mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 266 View dialogView = LayoutInflater.from( 267 new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_Dialog)) 268 .inflate(R.layout.quick_settings_footer_dialog, null, false); 269 mDialog.setView(dialogView); 270 mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); 271 272 // device management section 273 CharSequence managementMessage = getManagementMessage(isDeviceManaged, 274 deviceOwnerOrganization); 275 if (managementMessage == null) { 276 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE); 277 } else { 278 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE); 279 TextView deviceManagementWarning = 280 (TextView) dialogView.findViewById(R.id.device_management_warning); 281 deviceManagementWarning.setText(managementMessage); 282 mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getSettingsButton(), this); 283 } 284 285 // ca certificate section 286 CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts, 287 hasCACertsInWorkProfile); 288 if (caCertsMessage == null) { 289 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE); 290 } else { 291 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE); 292 TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning); 293 caCertsWarning.setText(caCertsMessage); 294 // Make "Open trusted credentials"-link clickable 295 caCertsWarning.setMovementMethod(new LinkMovementMethod()); 296 } 297 298 // network logging section 299 CharSequence networkLoggingMessage = getNetworkLoggingMessage(isNetworkLoggingEnabled); 300 if (networkLoggingMessage == null) { 301 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE); 302 } else { 303 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE); 304 TextView networkLoggingWarning = 305 (TextView) dialogView.findViewById(R.id.network_logging_warning); 306 networkLoggingWarning.setText(networkLoggingMessage); 307 } 308 309 // vpn section 310 CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName, 311 vpnNameWorkProfile); 312 if (vpnMessage == null) { 313 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE); 314 } else { 315 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE); 316 TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning); 317 vpnWarning.setText(vpnMessage); 318 // Make "Open VPN Settings"-link clickable 319 vpnWarning.setMovementMethod(new LinkMovementMethod()); 320 } 321 322 // Note: if a new section is added, should update configSubtitleVisibility to include 323 // the handling of the subtitle 324 configSubtitleVisibility(managementMessage != null, 325 caCertsMessage != null, 326 networkLoggingMessage != null, 327 vpnMessage != null, 328 dialogView); 329 330 mDialog.show(); 331 mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 332 ViewGroup.LayoutParams.WRAP_CONTENT); 333 } 334 configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, boolean showNetworkLogging, boolean showVpn, View dialogView)335 protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, 336 boolean showNetworkLogging, boolean showVpn, View dialogView) { 337 // Device Management title should always been shown 338 // When there is a Device Management message, all subtitles should be shown 339 if (showDeviceManagement) { 340 return; 341 } 342 // Hide the subtitle if there is only 1 message shown 343 int mSectionCountExcludingDeviceMgt = 0; 344 if (showCaCerts) { mSectionCountExcludingDeviceMgt++; } 345 if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; } 346 if (showVpn) { mSectionCountExcludingDeviceMgt++; } 347 348 // No work needed if there is no sections or more than 1 section 349 if (mSectionCountExcludingDeviceMgt != 1) { 350 return; 351 } 352 if (showCaCerts) { 353 dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE); 354 } 355 if (showNetworkLogging) { 356 dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE); 357 } 358 if (showVpn) { 359 dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE); 360 } 361 } 362 getSettingsButton()363 private String getSettingsButton() { 364 return mContext.getString(R.string.monitoring_button_view_policies); 365 } 366 getPositiveButton()367 private String getPositiveButton() { 368 return mContext.getString(R.string.ok); 369 } 370 getManagementMessage(boolean isDeviceManaged, CharSequence organizationName)371 protected CharSequence getManagementMessage(boolean isDeviceManaged, 372 CharSequence organizationName) { 373 if (!isDeviceManaged) return null; 374 if (organizationName != null) 375 return mContext.getString( 376 R.string.monitoring_description_named_management, organizationName); 377 return mContext.getString(R.string.monitoring_description_management); 378 } 379 getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, boolean hasCACertsInWorkProfile)380 protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, 381 boolean hasCACertsInWorkProfile) { 382 if (!(hasCACerts || hasCACertsInWorkProfile)) return null; 383 if (isDeviceManaged) { 384 return mContext.getString(R.string.monitoring_description_management_ca_certificate); 385 } 386 if (hasCACertsInWorkProfile) { 387 return mContext.getString( 388 R.string.monitoring_description_managed_profile_ca_certificate); 389 } 390 return mContext.getString(R.string.monitoring_description_ca_certificate); 391 } 392 getNetworkLoggingMessage(boolean isNetworkLoggingEnabled)393 protected CharSequence getNetworkLoggingMessage(boolean isNetworkLoggingEnabled) { 394 if (!isNetworkLoggingEnabled) return null; 395 return mContext.getString(R.string.monitoring_description_management_network_logging); 396 } 397 getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile)398 protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, 399 String vpnName, String vpnNameWorkProfile) { 400 if (vpnName == null && vpnNameWorkProfile == null) return null; 401 final SpannableStringBuilder message = new SpannableStringBuilder(); 402 if (isDeviceManaged) { 403 if (vpnName != null && vpnNameWorkProfile != null) { 404 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 405 vpnName, vpnNameWorkProfile)); 406 } else { 407 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 408 vpnName != null ? vpnName : vpnNameWorkProfile)); 409 } 410 } else { 411 if (vpnName != null && vpnNameWorkProfile != null) { 412 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 413 vpnName, vpnNameWorkProfile)); 414 } else if (vpnNameWorkProfile != null) { 415 message.append(mContext.getString( 416 R.string.monitoring_description_managed_profile_named_vpn, 417 vpnNameWorkProfile)); 418 } else if (hasWorkProfile) { 419 message.append(mContext.getString( 420 R.string.monitoring_description_personal_profile_named_vpn, vpnName)); 421 } else { 422 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 423 vpnName)); 424 } 425 } 426 message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); 427 message.append(mContext.getString(R.string.monitoring_description_vpn_settings), 428 new VpnSpan(), 0); 429 return message; 430 } 431 getTitle(String deviceOwner)432 private int getTitle(String deviceOwner) { 433 if (deviceOwner != null) { 434 return R.string.monitoring_title_device_owned; 435 } else { 436 return R.string.monitoring_title; 437 } 438 } 439 440 private final Runnable mUpdateIcon = new Runnable() { 441 @Override 442 public void run() { 443 mFooterIcon.setImageResource(mFooterIconId); 444 } 445 }; 446 447 private final Runnable mUpdateDisplayState = new Runnable() { 448 @Override 449 public void run() { 450 if (mFooterTextContent != null) { 451 mFooterText.setText(mFooterTextContent); 452 } 453 mRootView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE); 454 if (mDivider != null) mDivider.setVisibility(mIsVisible ? View.GONE : View.VISIBLE); 455 } 456 }; 457 458 private class Callback implements SecurityController.SecurityControllerCallback { 459 @Override onStateChanged()460 public void onStateChanged() { 461 refreshState(); 462 } 463 } 464 465 private class H extends Handler { 466 private static final int CLICK = 0; 467 private static final int REFRESH_STATE = 1; 468 H(Looper looper)469 private H(Looper looper) { 470 super(looper); 471 } 472 473 @Override handleMessage(Message msg)474 public void handleMessage(Message msg) { 475 String name = null; 476 try { 477 if (msg.what == REFRESH_STATE) { 478 name = "handleRefreshState"; 479 handleRefreshState(); 480 } else if (msg.what == CLICK) { 481 name = "handleClick"; 482 handleClick(); 483 } 484 } catch (Throwable t) { 485 final String error = "Error in " + name; 486 Log.w(TAG, error, t); 487 mHost.warn(error, t); 488 } 489 } 490 } 491 492 protected class VpnSpan extends ClickableSpan { 493 @Override onClick(View widget)494 public void onClick(View widget) { 495 final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); 496 mDialog.dismiss(); 497 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 498 } 499 500 // for testing, to compare two CharSequences containing VpnSpans 501 @Override equals(Object object)502 public boolean equals(Object object) { 503 return object instanceof VpnSpan; 504 } 505 506 @Override hashCode()507 public int hashCode() { 508 return 314159257; // prime 509 } 510 } 511 } 512