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.content.Context; 21 import android.content.DialogInterface; 22 import android.content.Intent; 23 import android.content.pm.UserInfo; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.os.UserManager; 28 import android.provider.Settings; 29 import android.text.SpannableStringBuilder; 30 import android.text.method.LinkMovementMethod; 31 import android.text.style.ClickableSpan; 32 import android.util.Log; 33 import android.view.ContextThemeWrapper; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.View.OnClickListener; 38 import android.view.Window; 39 import android.widget.ImageView; 40 import android.widget.TextView; 41 42 import com.android.systemui.Dependency; 43 import com.android.systemui.FontSizeUtils; 44 import com.android.systemui.R; 45 import com.android.systemui.plugins.ActivityStarter; 46 import com.android.systemui.statusbar.phone.SystemUIDialog; 47 import com.android.systemui.statusbar.policy.SecurityController; 48 49 import static android.provider.Settings.ACTION_VPN_SETTINGS; 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.getMainLooper()); 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 } 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 mHandler.sendEmptyMessage(H.CLICK); 119 } 120 handleClick()121 private void handleClick() { 122 showDeviceMonitoringDialog(); 123 } 124 showDeviceMonitoringDialog()125 public void showDeviceMonitoringDialog() { 126 mHost.collapsePanels(); 127 // TODO: Delay dialog creation until after panels are collapsed. 128 createDialog(); 129 } 130 refreshState()131 public void refreshState() { 132 mHandler.sendEmptyMessage(H.REFRESH_STATE); 133 } 134 handleRefreshState()135 private void handleRefreshState() { 136 final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); 137 final UserInfo currentUser = mUm.getUserInfo(ActivityManager.getCurrentUser()); 138 final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null 139 && currentUser.isDemo(); 140 final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); 141 final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); 142 final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); 143 final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); 144 final String vpnName = mSecurityController.getPrimaryVpnName(); 145 final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); 146 final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName(); 147 final CharSequence workProfileName = mSecurityController.getWorkProfileOrganizationName(); 148 // Update visibility of footer 149 mIsVisible = (isDeviceManaged && !isDemoDevice) || hasCACerts || hasCACertsInWorkProfile || 150 vpnName != null || vpnNameWorkProfile != null; 151 // Update the string 152 mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, 153 hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, 154 vpnNameWorkProfile, organizationName, workProfileName); 155 // Update the icon 156 int footerIconId = R.drawable.ic_info_outline; 157 if (vpnName != null || vpnNameWorkProfile != null) { 158 if (mSecurityController.isVpnBranded()) { 159 footerIconId = R.drawable.ic_qs_branded_vpn; 160 } else { 161 footerIconId = R.drawable.ic_qs_vpn; 162 } 163 } 164 if (mFooterIconId != footerIconId) { 165 mFooterIconId = footerIconId; 166 mMainHandler.post(mUpdateIcon); 167 } 168 mMainHandler.post(mUpdateDisplayState); 169 } 170 getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, String vpnName, String vpnNameWorkProfile, CharSequence organizationName, CharSequence workProfileName)171 protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, 172 boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, 173 String vpnName, String vpnNameWorkProfile, CharSequence organizationName, 174 CharSequence workProfileName) { 175 if (isDeviceManaged) { 176 if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { 177 if (organizationName == null) { 178 return mContext.getString( 179 R.string.quick_settings_disclosure_management_monitoring); 180 } 181 return mContext.getString( 182 R.string.quick_settings_disclosure_named_management_monitoring, 183 organizationName); 184 } 185 if (vpnName != null && vpnNameWorkProfile != null) { 186 if (organizationName == null) { 187 return mContext.getString(R.string.quick_settings_disclosure_management_vpns); 188 } 189 return mContext.getString(R.string.quick_settings_disclosure_named_management_vpns, 190 organizationName); 191 } 192 if (vpnName != null || vpnNameWorkProfile != null) { 193 if (organizationName == null) { 194 return mContext.getString( 195 R.string.quick_settings_disclosure_management_named_vpn, 196 vpnName != null ? vpnName : vpnNameWorkProfile); 197 } 198 return mContext.getString( 199 R.string.quick_settings_disclosure_named_management_named_vpn, 200 organizationName, 201 vpnName != null ? vpnName : vpnNameWorkProfile); 202 } 203 if (organizationName == null) { 204 return mContext.getString(R.string.quick_settings_disclosure_management); 205 } 206 return mContext.getString(R.string.quick_settings_disclosure_named_management, 207 organizationName); 208 } // end if(isDeviceManaged) 209 if (hasCACertsInWorkProfile) { 210 if (workProfileName == null) { 211 return mContext.getString( 212 R.string.quick_settings_disclosure_managed_profile_monitoring); 213 } 214 return mContext.getString( 215 R.string.quick_settings_disclosure_named_managed_profile_monitoring, 216 workProfileName); 217 } 218 if (hasCACerts) { 219 return mContext.getString(R.string.quick_settings_disclosure_monitoring); 220 } 221 if (vpnName != null && vpnNameWorkProfile != null) { 222 return mContext.getString(R.string.quick_settings_disclosure_vpns); 223 } 224 if (vpnNameWorkProfile != null) { 225 return mContext.getString(R.string.quick_settings_disclosure_managed_profile_named_vpn, 226 vpnNameWorkProfile); 227 } 228 if (vpnName != null) { 229 if (hasWorkProfile) { 230 return mContext.getString( 231 R.string.quick_settings_disclosure_personal_profile_named_vpn, 232 vpnName); 233 } 234 return mContext.getString(R.string.quick_settings_disclosure_named_vpn, 235 vpnName); 236 } 237 return null; 238 } 239 240 @Override onClick(DialogInterface dialog, int which)241 public void onClick(DialogInterface dialog, int which) { 242 if (which == DialogInterface.BUTTON_NEGATIVE) { 243 final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS); 244 mDialog.dismiss(); 245 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 246 } 247 } 248 createDialog()249 private void createDialog() { 250 final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); 251 final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); 252 final CharSequence deviceOwnerOrganization = 253 mSecurityController.getDeviceOwnerOrganizationName(); 254 final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); 255 final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); 256 final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); 257 final String vpnName = mSecurityController.getPrimaryVpnName(); 258 final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); 259 260 mDialog = new SystemUIDialog(mContext); 261 mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 262 View dialogView = LayoutInflater.from( 263 new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_Dialog)) 264 .inflate(R.layout.quick_settings_footer_dialog, null, false); 265 mDialog.setView(dialogView); 266 mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); 267 268 // device management section 269 CharSequence managementMessage = getManagementMessage(isDeviceManaged, 270 deviceOwnerOrganization); 271 if (managementMessage == null) { 272 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE); 273 } else { 274 dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE); 275 TextView deviceManagementWarning = 276 (TextView) dialogView.findViewById(R.id.device_management_warning); 277 deviceManagementWarning.setText(managementMessage); 278 mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getSettingsButton(), this); 279 } 280 281 // ca certificate section 282 CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts, 283 hasCACertsInWorkProfile); 284 if (caCertsMessage == null) { 285 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE); 286 } else { 287 dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE); 288 TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning); 289 caCertsWarning.setText(caCertsMessage); 290 // Make "Open trusted credentials"-link clickable 291 caCertsWarning.setMovementMethod(new LinkMovementMethod()); 292 } 293 294 // network logging section 295 CharSequence networkLoggingMessage = getNetworkLoggingMessage(isNetworkLoggingEnabled); 296 if (networkLoggingMessage == null) { 297 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE); 298 } else { 299 dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE); 300 TextView networkLoggingWarning = 301 (TextView) dialogView.findViewById(R.id.network_logging_warning); 302 networkLoggingWarning.setText(networkLoggingMessage); 303 } 304 305 // vpn section 306 CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName, 307 vpnNameWorkProfile); 308 if (vpnMessage == null) { 309 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE); 310 } else { 311 dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE); 312 TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning); 313 vpnWarning.setText(vpnMessage); 314 // Make "Open VPN Settings"-link clickable 315 vpnWarning.setMovementMethod(new LinkMovementMethod()); 316 } 317 318 // Note: if a new section is added, should update configSubtitleVisibility to include 319 // the handling of the subtitle 320 configSubtitleVisibility(managementMessage != null, 321 caCertsMessage != null, 322 networkLoggingMessage != null, 323 vpnMessage != null, 324 dialogView); 325 326 mDialog.show(); 327 mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 328 ViewGroup.LayoutParams.WRAP_CONTENT); 329 } 330 configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, boolean showNetworkLogging, boolean showVpn, View dialogView)331 protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, 332 boolean showNetworkLogging, boolean showVpn, View dialogView) { 333 // Device Management title should always been shown 334 // When there is a Device Management message, all subtitles should be shown 335 if (showDeviceManagement) { 336 return; 337 } 338 // Hide the subtitle if there is only 1 message shown 339 int mSectionCountExcludingDeviceMgt = 0; 340 if (showCaCerts) { mSectionCountExcludingDeviceMgt++; } 341 if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; } 342 if (showVpn) { mSectionCountExcludingDeviceMgt++; } 343 344 // No work needed if there is no sections or more than 1 section 345 if (mSectionCountExcludingDeviceMgt != 1) { 346 return; 347 } 348 if (showCaCerts) { 349 dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE); 350 } 351 if (showNetworkLogging) { 352 dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE); 353 } 354 if (showVpn) { 355 dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE); 356 } 357 } 358 getSettingsButton()359 private String getSettingsButton() { 360 return mContext.getString(R.string.monitoring_button_view_policies); 361 } 362 getPositiveButton()363 private String getPositiveButton() { 364 return mContext.getString(R.string.ok); 365 } 366 getManagementMessage(boolean isDeviceManaged, CharSequence organizationName)367 protected CharSequence getManagementMessage(boolean isDeviceManaged, 368 CharSequence organizationName) { 369 if (!isDeviceManaged) return null; 370 if (organizationName != null) 371 return mContext.getString( 372 R.string.monitoring_description_named_management, organizationName); 373 return mContext.getString(R.string.monitoring_description_management); 374 } 375 getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, boolean hasCACertsInWorkProfile)376 protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, 377 boolean hasCACertsInWorkProfile) { 378 if (!(hasCACerts || hasCACertsInWorkProfile)) return null; 379 if (isDeviceManaged) { 380 return mContext.getString(R.string.monitoring_description_management_ca_certificate); 381 } 382 if (hasCACertsInWorkProfile) { 383 return mContext.getString( 384 R.string.monitoring_description_managed_profile_ca_certificate); 385 } 386 return mContext.getString(R.string.monitoring_description_ca_certificate); 387 } 388 getNetworkLoggingMessage(boolean isNetworkLoggingEnabled)389 protected CharSequence getNetworkLoggingMessage(boolean isNetworkLoggingEnabled) { 390 if (!isNetworkLoggingEnabled) return null; 391 return mContext.getString(R.string.monitoring_description_management_network_logging); 392 } 393 getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile)394 protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, 395 String vpnName, String vpnNameWorkProfile) { 396 if (vpnName == null && vpnNameWorkProfile == null) return null; 397 final SpannableStringBuilder message = new SpannableStringBuilder(); 398 if (isDeviceManaged) { 399 if (vpnName != null && vpnNameWorkProfile != null) { 400 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 401 vpnName, vpnNameWorkProfile)); 402 } else { 403 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 404 vpnName != null ? vpnName : vpnNameWorkProfile)); 405 } 406 } else { 407 if (vpnName != null && vpnNameWorkProfile != null) { 408 message.append(mContext.getString(R.string.monitoring_description_two_named_vpns, 409 vpnName, vpnNameWorkProfile)); 410 } else if (vpnNameWorkProfile != null) { 411 message.append(mContext.getString( 412 R.string.monitoring_description_managed_profile_named_vpn, 413 vpnNameWorkProfile)); 414 } else if (hasWorkProfile) { 415 message.append(mContext.getString( 416 R.string.monitoring_description_personal_profile_named_vpn, vpnName)); 417 } else { 418 message.append(mContext.getString(R.string.monitoring_description_named_vpn, 419 vpnName)); 420 } 421 } 422 message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); 423 message.append(mContext.getString(R.string.monitoring_description_vpn_settings), 424 new VpnSpan(), 0); 425 return message; 426 } 427 getTitle(String deviceOwner)428 private int getTitle(String deviceOwner) { 429 if (deviceOwner != null) { 430 return R.string.monitoring_title_device_owned; 431 } else { 432 return R.string.monitoring_title; 433 } 434 } 435 436 private final Runnable mUpdateIcon = new Runnable() { 437 @Override 438 public void run() { 439 mFooterIcon.setImageResource(mFooterIconId); 440 } 441 }; 442 443 private final Runnable mUpdateDisplayState = new Runnable() { 444 @Override 445 public void run() { 446 if (mFooterTextContent != null) { 447 mFooterText.setText(mFooterTextContent); 448 } 449 mRootView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE); 450 if (mDivider != null) mDivider.setVisibility(mIsVisible ? View.GONE : View.VISIBLE); 451 } 452 }; 453 454 private class Callback implements SecurityController.SecurityControllerCallback { 455 @Override onStateChanged()456 public void onStateChanged() { 457 refreshState(); 458 } 459 } 460 461 private class H extends Handler { 462 private static final int CLICK = 0; 463 private static final int REFRESH_STATE = 1; 464 H(Looper looper)465 private H(Looper looper) { 466 super(looper); 467 } 468 469 @Override handleMessage(Message msg)470 public void handleMessage(Message msg) { 471 String name = null; 472 try { 473 if (msg.what == REFRESH_STATE) { 474 name = "handleRefreshState"; 475 handleRefreshState(); 476 } else if (msg.what == CLICK) { 477 name = "handleClick"; 478 handleClick(); 479 } 480 } catch (Throwable t) { 481 final String error = "Error in " + name; 482 Log.w(TAG, error, t); 483 mHost.warn(error, t); 484 } 485 } 486 } 487 488 protected class VpnSpan extends ClickableSpan { 489 @Override onClick(View widget)490 public void onClick(View widget) { 491 final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); 492 mDialog.dismiss(); 493 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 494 } 495 496 // for testing, to compare two CharSequences containing VpnSpans 497 @Override equals(Object object)498 public boolean equals(Object object) { 499 return object instanceof VpnSpan; 500 } 501 502 @Override hashCode()503 public int hashCode() { 504 return 314159257; // prime 505 } 506 } 507 } 508