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.view.ContextThemeWrapper; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.View.OnClickListener; 38 import android.view.ViewGroup; 39 import android.view.Window; 40 import android.widget.ImageView; 41 import android.widget.TextView; 42 43 import com.android.internal.util.FrameworkStatsLog; 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 private static final boolean DEBUG_FORCE_VISIBLE = false; 55 56 private final View mRootView; 57 private final TextView mFooterText; 58 private final ImageView mFooterIcon; 59 private final Context mContext; 60 private final Callback mCallback = new Callback(); 61 private final SecurityController mSecurityController; 62 private final ActivityStarter mActivityStarter; 63 private final Handler mMainHandler; 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 mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 89 } 90 setHostEnvironment(QSTileHost host)91 public void setHostEnvironment(QSTileHost host) { 92 mHost = host; 93 } 94 setListening(boolean listening)95 public void setListening(boolean listening) { 96 if (listening) { 97 mSecurityController.addCallback(mCallback); 98 refreshState(); 99 } else { 100 mSecurityController.removeCallback(mCallback); 101 } 102 } 103 onConfigurationChanged()104 public void onConfigurationChanged() { 105 FontSizeUtils.updateFontSize(mFooterText, R.dimen.qs_tile_text_size); 106 } 107 getView()108 public View getView() { 109 return mRootView; 110 } 111 hasFooter()112 public boolean hasFooter() { 113 return mRootView.getVisibility() != View.GONE; 114 } 115 116 @Override onClick(View v)117 public void onClick(View v) { 118 if (!hasFooter()) return; 119 mHandler.sendEmptyMessage(H.CLICK); 120 } 121 handleClick()122 private void handleClick() { 123 showDeviceMonitoringDialog(); 124 DevicePolicyEventLogger 125 .createEvent(FrameworkStatsLog.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 workProfileOrganizationName = 152 mSecurityController.getWorkProfileOrganizationName(); 153 final boolean isProfileOwnerOfOrganizationOwnedDevice = 154 mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); 155 // Update visibility of footer 156 mIsVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || hasCACertsInWorkProfile 157 || vpnName != null || vpnNameWorkProfile != null 158 || isProfileOwnerOfOrganizationOwnedDevice; 159 // Update the string 160 mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, 161 hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, 162 vpnNameWorkProfile, organizationName, workProfileOrganizationName, 163 isProfileOwnerOfOrganizationOwnedDevice); 164 // Update the icon 165 int footerIconId = R.drawable.ic_info_outline; 166 if (vpnName != null || vpnNameWorkProfile != null) { 167 if (mSecurityController.isVpnBranded()) { 168 footerIconId = R.drawable.stat_sys_branded_vpn; 169 } else { 170 footerIconId = R.drawable.stat_sys_vpn_ic; 171 } 172 } 173 if (mFooterIconId != footerIconId) { 174 mFooterIconId = footerIconId; 175 mMainHandler.post(mUpdateIcon); 176 } 177 mMainHandler.post(mUpdateDisplayState); 178 } 179 getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, String vpnName, String vpnNameWorkProfile, CharSequence organizationName, CharSequence workProfileOrganizationName, boolean isProfileOwnerOfOrganizationOwnedDevice)180 protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, 181 boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, 182 String vpnName, String vpnNameWorkProfile, CharSequence organizationName, 183 CharSequence workProfileOrganizationName, 184 boolean isProfileOwnerOfOrganizationOwnedDevice) { 185 if (isDeviceManaged || DEBUG_FORCE_VISIBLE) { 186 if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { 187 if (organizationName == null) { 188 return mContext.getString( 189 R.string.quick_settings_disclosure_management_monitoring); 190 } 191 return mContext.getString( 192 R.string.quick_settings_disclosure_named_management_monitoring, 193 organizationName); 194 } 195 if (vpnName != null && vpnNameWorkProfile != null) { 196 if (organizationName == null) { 197 return mContext.getString(R.string.quick_settings_disclosure_management_vpns); 198 } 199 return mContext.getString(R.string.quick_settings_disclosure_named_management_vpns, 200 organizationName); 201 } 202 if (vpnName != null || vpnNameWorkProfile != null) { 203 if (organizationName == null) { 204 return mContext.getString( 205 R.string.quick_settings_disclosure_management_named_vpn, 206 vpnName != null ? vpnName : vpnNameWorkProfile); 207 } 208 return mContext.getString( 209 R.string.quick_settings_disclosure_named_management_named_vpn, 210 organizationName, 211 vpnName != null ? vpnName : vpnNameWorkProfile); 212 } 213 if (organizationName == null) { 214 return mContext.getString(R.string.quick_settings_disclosure_management); 215 } 216 return mContext.getString(R.string.quick_settings_disclosure_named_management, 217 organizationName); 218 } // end if(isDeviceManaged) 219 if (hasCACertsInWorkProfile) { 220 if (workProfileOrganizationName == null) { 221 return mContext.getString( 222 R.string.quick_settings_disclosure_managed_profile_monitoring); 223 } 224 return mContext.getString( 225 R.string.quick_settings_disclosure_named_managed_profile_monitoring, 226 workProfileOrganizationName); 227 } 228 if (hasCACerts) { 229 return mContext.getString(R.string.quick_settings_disclosure_monitoring); 230 } 231 if (vpnName != null && vpnNameWorkProfile != null) { 232 return mContext.getString(R.string.quick_settings_disclosure_vpns); 233 } 234 if (vpnNameWorkProfile != null) { 235 return mContext.getString(R.string.quick_settings_disclosure_managed_profile_named_vpn, 236 vpnNameWorkProfile); 237 } 238 if (vpnName != null) { 239 if (hasWorkProfile) { 240 return mContext.getString( 241 R.string.quick_settings_disclosure_personal_profile_named_vpn, 242 vpnName); 243 } 244 return mContext.getString(R.string.quick_settings_disclosure_named_vpn, 245 vpnName); 246 } 247 if (isProfileOwnerOfOrganizationOwnedDevice) { 248 if (workProfileOrganizationName == null) { 249 return mContext.getString(R.string.quick_settings_disclosure_management); 250 } 251 return mContext.getString(R.string.quick_settings_disclosure_named_management, 252 workProfileOrganizationName); 253 } 254 return null; 255 } 256 257 @Override onClick(DialogInterface dialog, int which)258 public void onClick(DialogInterface dialog, int which) { 259 if (which == DialogInterface.BUTTON_NEGATIVE) { 260 final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS); 261 mDialog.dismiss(); 262 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 263 } 264 } 265 createDialog()266 private void createDialog() { 267 final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); 268 boolean isProfileOwnerOfOrganizationOwnedDevice = 269 mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); 270 final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); 271 final CharSequence deviceOwnerOrganization = 272 mSecurityController.getDeviceOwnerOrganizationName(); 273 final CharSequence workProfileOrganizationName = 274 mSecurityController.getWorkProfileOrganizationName(); 275 final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); 276 final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); 277 final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); 278 final String vpnName = mSecurityController.getPrimaryVpnName(); 279 final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); 280 281 mDialog = new SystemUIDialog(mContext); 282 mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 283 View dialogView = LayoutInflater.from( 284 new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_Dialog)) 285 .inflate(R.layout.quick_settings_footer_dialog, null, false); 286 mDialog.setView(dialogView); 287 mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); 288 289 // device management section 290 CharSequence managementMessage = getManagementMessage(isDeviceManaged, 291 deviceOwnerOrganization, isProfileOwnerOfOrganizationOwnedDevice, 292 workProfileOrganizationName); 293 if (managementMessage == null) { 294 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE); 295 } else { 296 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE); 297 TextView deviceManagementWarning = 298 (TextView) dialogView.findViewById(R.id.device_management_warning); 299 deviceManagementWarning.setText(managementMessage); 300 // Don't show the policies button for profile owner of org owned device, because there 301 // is no policies settings screen for it 302 if (!isProfileOwnerOfOrganizationOwnedDevice) { 303 mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getSettingsButton(), this); 304 } 305 } 306 307 // ca certificate section 308 CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts, 309 hasCACertsInWorkProfile); 310 if (caCertsMessage == null) { 311 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE); 312 } else { 313 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE); 314 TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning); 315 caCertsWarning.setText(caCertsMessage); 316 // Make "Open trusted credentials"-link clickable 317 caCertsWarning.setMovementMethod(new LinkMovementMethod()); 318 } 319 320 // network logging section 321 CharSequence networkLoggingMessage = getNetworkLoggingMessage(isNetworkLoggingEnabled); 322 if (networkLoggingMessage == null) { 323 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE); 324 } else { 325 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE); 326 TextView networkLoggingWarning = 327 (TextView) dialogView.findViewById(R.id.network_logging_warning); 328 networkLoggingWarning.setText(networkLoggingMessage); 329 } 330 331 // vpn section 332 CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName, 333 vpnNameWorkProfile); 334 if (vpnMessage == null) { 335 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE); 336 } else { 337 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE); 338 TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning); 339 vpnWarning.setText(vpnMessage); 340 // Make "Open VPN Settings"-link clickable 341 vpnWarning.setMovementMethod(new LinkMovementMethod()); 342 } 343 344 // Note: if a new section is added, should update configSubtitleVisibility to include 345 // the handling of the subtitle 346 configSubtitleVisibility(managementMessage != null, 347 caCertsMessage != null, 348 networkLoggingMessage != null, 349 vpnMessage != null, 350 dialogView); 351 352 mDialog.show(); 353 mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 354 ViewGroup.LayoutParams.WRAP_CONTENT); 355 } 356 configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, boolean showNetworkLogging, boolean showVpn, View dialogView)357 protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, 358 boolean showNetworkLogging, boolean showVpn, View dialogView) { 359 // Device Management title should always been shown 360 // When there is a Device Management message, all subtitles should be shown 361 if (showDeviceManagement) { 362 return; 363 } 364 // Hide the subtitle if there is only 1 message shown 365 int mSectionCountExcludingDeviceMgt = 0; 366 if (showCaCerts) { mSectionCountExcludingDeviceMgt++; } 367 if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; } 368 if (showVpn) { mSectionCountExcludingDeviceMgt++; } 369 370 // No work needed if there is no sections or more than 1 section 371 if (mSectionCountExcludingDeviceMgt != 1) { 372 return; 373 } 374 if (showCaCerts) { 375 dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE); 376 } 377 if (showNetworkLogging) { 378 dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE); 379 } 380 if (showVpn) { 381 dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE); 382 } 383 } 384 getSettingsButton()385 private String getSettingsButton() { 386 return mContext.getString(R.string.monitoring_button_view_policies); 387 } 388 getPositiveButton()389 private String getPositiveButton() { 390 return mContext.getString(R.string.ok); 391 } 392 getManagementMessage(boolean isDeviceManaged, CharSequence organizationName, boolean isProfileOwnerOfOrganizationOwnedDevice, CharSequence workProfileOrganizationName)393 protected CharSequence getManagementMessage(boolean isDeviceManaged, 394 CharSequence organizationName, boolean isProfileOwnerOfOrganizationOwnedDevice, 395 CharSequence workProfileOrganizationName) { 396 if (!isDeviceManaged && !isProfileOwnerOfOrganizationOwnedDevice) { 397 return null; 398 } 399 if (isDeviceManaged && organizationName != null) { 400 return mContext.getString( 401 R.string.monitoring_description_named_management, organizationName); 402 } else if (isProfileOwnerOfOrganizationOwnedDevice && workProfileOrganizationName != null) { 403 return mContext.getString( 404 R.string.monitoring_description_named_management, workProfileOrganizationName); 405 } 406 return mContext.getString(R.string.monitoring_description_management); 407 } 408 getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, boolean hasCACertsInWorkProfile)409 protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, 410 boolean hasCACertsInWorkProfile) { 411 if (!(hasCACerts || hasCACertsInWorkProfile)) return null; 412 if (isDeviceManaged) { 413 return mContext.getString(R.string.monitoring_description_management_ca_certificate); 414 } 415 if (hasCACertsInWorkProfile) { 416 return mContext.getString( 417 R.string.monitoring_description_managed_profile_ca_certificate); 418 } 419 return mContext.getString(R.string.monitoring_description_ca_certificate); 420 } 421 getNetworkLoggingMessage(boolean isNetworkLoggingEnabled)422 protected CharSequence getNetworkLoggingMessage(boolean isNetworkLoggingEnabled) { 423 if (!isNetworkLoggingEnabled) return null; 424 return mContext.getString(R.string.monitoring_description_management_network_logging); 425 } 426 getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile)427 protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, 428 String vpnName, String vpnNameWorkProfile) { 429 if (vpnName == null && vpnNameWorkProfile == null) return null; 430 final SpannableStringBuilder message = new SpannableStringBuilder(); 431 if (isDeviceManaged) { 432 if (vpnName != null && vpnNameWorkProfile != null) { 433 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 434 vpnName, vpnNameWorkProfile)); 435 } else { 436 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 437 vpnName != null ? vpnName : vpnNameWorkProfile)); 438 } 439 } else { 440 if (vpnName != null && vpnNameWorkProfile != null) { 441 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 442 vpnName, vpnNameWorkProfile)); 443 } else if (vpnNameWorkProfile != null) { 444 message.append(mContext.getString( 445 R.string.monitoring_description_managed_profile_named_vpn, 446 vpnNameWorkProfile)); 447 } else if (hasWorkProfile) { 448 message.append(mContext.getString( 449 R.string.monitoring_description_personal_profile_named_vpn, vpnName)); 450 } else { 451 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 452 vpnName)); 453 } 454 } 455 message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); 456 message.append(mContext.getString(R.string.monitoring_description_vpn_settings), 457 new VpnSpan(), 0); 458 return message; 459 } 460 getTitle(String deviceOwner)461 private int getTitle(String deviceOwner) { 462 if (deviceOwner != null) { 463 return R.string.monitoring_title_device_owned; 464 } else { 465 return R.string.monitoring_title; 466 } 467 } 468 469 private final Runnable mUpdateIcon = new Runnable() { 470 @Override 471 public void run() { 472 mFooterIcon.setImageResource(mFooterIconId); 473 } 474 }; 475 476 private final Runnable mUpdateDisplayState = new Runnable() { 477 @Override 478 public void run() { 479 if (mFooterTextContent != null) { 480 mFooterText.setText(mFooterTextContent); 481 } 482 mRootView.setVisibility(mIsVisible || DEBUG_FORCE_VISIBLE ? View.VISIBLE : View.GONE); 483 } 484 }; 485 486 private class Callback implements SecurityController.SecurityControllerCallback { 487 @Override onStateChanged()488 public void onStateChanged() { 489 refreshState(); 490 } 491 } 492 493 private class H extends Handler { 494 private static final int CLICK = 0; 495 private static final int REFRESH_STATE = 1; 496 H(Looper looper)497 private H(Looper looper) { 498 super(looper); 499 } 500 501 @Override handleMessage(Message msg)502 public void handleMessage(Message msg) { 503 String name = null; 504 try { 505 if (msg.what == REFRESH_STATE) { 506 name = "handleRefreshState"; 507 handleRefreshState(); 508 } else if (msg.what == CLICK) { 509 name = "handleClick"; 510 handleClick(); 511 } 512 } catch (Throwable t) { 513 final String error = "Error in " + name; 514 Log.w(TAG, error, t); 515 mHost.warn(error, t); 516 } 517 } 518 } 519 520 protected class VpnSpan extends ClickableSpan { 521 @Override onClick(View widget)522 public void onClick(View widget) { 523 final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); 524 mDialog.dismiss(); 525 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 526 } 527 528 // for testing, to compare two CharSequences containing VpnSpans 529 @Override equals(Object object)530 public boolean equals(Object object) { 531 return object instanceof VpnSpan; 532 } 533 534 @Override hashCode()535 public int hashCode() { 536 return 314159257; // prime 537 } 538 } 539 } 540