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 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 92 public void setHostEnvironment(QSTileHost host) { 93 mHost = host; 94 } 95 96 public void setListening(boolean listening) { 97 if (listening) { 98 mSecurityController.addCallback(mCallback); 99 } else { 100 mSecurityController.removeCallback(mCallback); 101 } 102 } 103 104 public void onConfigurationChanged() { 105 FontSizeUtils.updateFontSize(mFooterText, R.dimen.qs_tile_text_size); 106 } 107 108 public View getView() { 109 return mRootView; 110 } 111 112 public boolean hasFooter() { 113 return mRootView.getVisibility() != View.GONE; 114 } 115 116 @Override 117 public void onClick(View v) { 118 mHandler.sendEmptyMessage(H.CLICK); 119 } 120 121 private void handleClick() { 122 showDeviceMonitoringDialog(); 123 } 124 125 public void showDeviceMonitoringDialog() { 126 mHost.collapsePanels(); 127 // TODO: Delay dialog creation until after panels are collapsed. 128 createDialog(); 129 } 130 131 public void refreshState() { 132 mHandler.sendEmptyMessage(H.REFRESH_STATE); 133 } 134 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 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 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 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 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 359 private String getSettingsButton() { 360 return mContext.getString(R.string.monitoring_button_view_policies); 361 } 362 363 private String getPositiveButton() { 364 return mContext.getString(R.string.ok); 365 } 366 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 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 389 protected CharSequence getNetworkLoggingMessage(boolean isNetworkLoggingEnabled) { 390 if (!isNetworkLoggingEnabled) return null; 391 return mContext.getString(R.string.monitoring_description_management_network_logging); 392 } 393 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 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 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 465 private H(Looper looper) { 466 super(looper); 467 } 468 469 @Override 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 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 498 public boolean equals(Object object) { 499 return object instanceof VpnSpan; 500 } 501 502 @Override 503 public int hashCode() { 504 return 314159257; // prime 505 } 506 } 507 } 508