diff options
Diffstat (limited to 'src')
149 files changed, 13722 insertions, 5511 deletions
diff --git a/src/com/android/settings/AccessibilityEnableScriptInjectionPreference.java b/src/com/android/settings/AccessibilityEnableScriptInjectionPreference.java new file mode 100644 index 0000000..a9338ed --- /dev/null +++ b/src/com/android/settings/AccessibilityEnableScriptInjectionPreference.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2011 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; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.DialogPreference; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +/** + * Preference for enabling accessibility script injection. It displays a warning + * dialog before enabling the preference. + */ +public class AccessibilityEnableScriptInjectionPreference extends DialogPreference { + + private boolean mInjectionAllowed; + private boolean mSendClickAccessibilityEvent; + + public AccessibilityEnableScriptInjectionPreference(Context context, AttributeSet attrs) { + super(context, attrs); + updateSummary(); + } + + public void setInjectionAllowed(boolean injectionAllowed) { + if (mInjectionAllowed != injectionAllowed) { + mInjectionAllowed = injectionAllowed; + persistBoolean(injectionAllowed); + updateSummary(); + } + } + + public boolean isInjectionAllowed() { + return mInjectionAllowed; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + View summaryView = view.findViewById(com.android.internal.R.id.summary); + sendAccessibilityEvent(summaryView); + } + + private void sendAccessibilityEvent(View view) { + // Since the view is still not attached we create, populate, + // and send the event directly since we do not know when it + // will be attached and posting commands is not as clean. + AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(getContext()); + if (mSendClickAccessibilityEvent && accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED); + view.onInitializeAccessibilityEvent(event); + view.dispatchPopulateAccessibilityEvent(event); + accessibilityManager.sendAccessibilityEvent(event); + } + mSendClickAccessibilityEvent = false; + } + + @Override + protected void onClick() { + if (isInjectionAllowed()) { + setInjectionAllowed(false); + // Update the system setting only upon user action. + setSystemSetting(false); + mSendClickAccessibilityEvent = true; + } else { + super.onClick(); + mSendClickAccessibilityEvent = false; + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getBoolean(index, false); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setInjectionAllowed(restoreValue + ? getPersistedBoolean(mInjectionAllowed) + : (Boolean) defaultValue); + } + + @Override + protected void onDialogClosed(boolean result) { + setInjectionAllowed(result); + if (result) { + // Update the system setting only upon user action. + setSystemSetting(true); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + return superState; + } + SavedState myState = new SavedState(superState); + myState.mInjectionAllowed = mInjectionAllowed; + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + super.onRestoreInstanceState(state); + return; + } + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setInjectionAllowed(myState.mInjectionAllowed); + } + + private void updateSummary() { + setSummary(mInjectionAllowed + ? getContext().getString(R.string.accessibility_script_injection_allowed) + : getContext().getString(R.string.accessibility_script_injection_disallowed)); + } + + private void setSystemSetting(boolean enabled) { + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, enabled ? 1 : 0); + } + + private static class SavedState extends BaseSavedState { + private boolean mInjectionAllowed; + + public SavedState(Parcel source) { + super(source); + mInjectionAllowed = (source.readInt() == 1); + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + super.writeToParcel(parcel, flags); + parcel.writeInt(mInjectionAllowed ? 1 : 0); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @SuppressWarnings("all") + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/src/com/android/settings/AccessibilitySettings.java b/src/com/android/settings/AccessibilitySettings.java index 826410d..e1b44e7 100644 --- a/src/com/android/settings/AccessibilitySettings.java +++ b/src/com/android/settings/AccessibilitySettings.java @@ -16,366 +16,515 @@ package com.android.settings; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.ActionBar; +import android.app.Activity; +import android.app.ActivityManagerNative; import android.app.AlertDialog; import android.app.Dialog; -import android.app.Service; +import android.content.ComponentName; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ServiceInfo; +import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; import android.os.SystemProperties; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; +import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; -import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.provider.Settings; import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.view.Gravity; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.widget.Switch; +import android.widget.TextView; +import com.android.internal.content.PackageMonitor; +import com.android.settings.AccessibilitySettings.ToggleSwitch.OnBeforeCheckedChangeListener; + +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Activity with the accessibility settings. */ public class AccessibilitySettings extends SettingsPreferenceFragment implements DialogCreatable, Preference.OnPreferenceChangeListener { + private static final String DEFAULT_SCREENREADER_MARKET_LINK = "market://search?q=pname:com.google.android.marvin.talkback"; - private final String TOGGLE_ACCESSIBILITY_CHECKBOX = - "toggle_accessibility_service_checkbox"; + private static final float LARGE_FONT_SCALE = 1.3f; + + private static final String SYSTEM_PROPERTY_MARKET_URL = "ro.screenreader.market"; + + // Timeout before we update the services if packages are added/removed since + // the AccessibilityManagerService has to do that processing first to generate + // the AccessibilityServiceInfo we need for proper presentation. + private static final long DELAY_UPDATE_SERVICES_PREFERENCES_MILLIS = 1000; - private static final String ACCESSIBILITY_SERVICES_CATEGORY = - "accessibility_services_category"; + private static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':'; - private static final String TOGGLE_ACCESSIBILITY_SCRIPT_INJECTION_CHECKBOX = - "toggle_accessibility_script_injection_checkbox"; + private static final String KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE = + "key_accessibility_tutorial_launched_once"; - private static final String POWER_BUTTON_CATEGORY = - "power_button_category"; + private static final String KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE = + "key_install_accessibility_service_offered_once"; - private static final String POWER_BUTTON_ENDS_CALL_CHECKBOX = - "power_button_ends_call"; + // Preference categories + private static final String SERVICES_CATEGORY = "services_category"; + private static final String SYSTEM_CATEGORY = "system_category"; - private final String KEY_TOGGLE_ACCESSIBILITY_SERVICE_CHECKBOX = - "key_toggle_accessibility_service_checkbox"; + // Preferences + private static final String TOGGLE_LARGE_TEXT_PREFERENCE = "toggle_large_text_preference"; + private static final String TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE = + "toggle_power_button_ends_call_preference"; + private static final String TOGGLE_TOUCH_EXPLORATION_PREFERENCE = + "toggle_touch_exploration_preference"; + private static final String SELECT_LONG_PRESS_TIMEOUT_PREFERENCE = + "select_long_press_timeout_preference"; + private static final String TOGGLE_SCRIPT_INJECTION_PREFERENCE = + "toggle_script_injection_preference"; - private final String KEY_LONG_PRESS_TIMEOUT_LIST_PREFERENCE = - "long_press_timeout_list_preference"; + // Extras passed to sub-fragments. + private static final String EXTRA_PREFERENCE_KEY = "preference_key"; + private static final String EXTRA_CHECKED = "checked"; + private static final String EXTRA_TITLE = "title"; + private static final String EXTRA_SUMMARY = "summary"; + private static final String EXTRA_WARNING_MESSAGE = "warning_message"; + private static final String EXTRA_SETTINGS_TITLE = "settings_title"; + private static final String EXTRA_SETTINGS_COMPONENT_NAME = "settings_component_name"; + // Dialog IDs. private static final int DIALOG_ID_DISABLE_ACCESSIBILITY = 1; - private static final int DIALOG_ID_ENABLE_SCRIPT_INJECTION = 2; - private static final int DIALOG_ID_ENABLE_ACCESSIBILITY_SERVICE = 3; - private static final int DIALOG_ID_NO_ACCESSIBILITY_SERVICES = 4; + private static final int DIALOG_ID_NO_ACCESSIBILITY_SERVICES = 2; + + // Auxiliary members. + private final SimpleStringSplitter mStringColonSplitter = + new SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR); + + private final Map<String, String> mLongPressTimeoutValuetoTitleMap = + new HashMap<String, String>(); + + private final Configuration mCurConfig = new Configuration(); - private CheckBoxPreference mToggleAccessibilityCheckBox; - private CheckBoxPreference mToggleScriptInjectionCheckBox; - private CheckBoxPreference mToggleAccessibilityServiceCheckBox; + private final PackageMonitor mSettingsPackageMonitor = new SettingsPackageMonitor(); - private PreferenceCategory mPowerButtonCategory; - private CheckBoxPreference mPowerButtonEndsCallCheckBox; + private final Handler mHandler = new Handler() { + @Override + public void dispatchMessage(Message msg) { + super.dispatchMessage(msg); + updateServicesPreferences(mToggleAccessibilitySwitch.isChecked()); + } + }; - private PreferenceGroup mAccessibilityServicesCategory; + // Preference controls. + private ToggleSwitch mToggleAccessibilitySwitch; - private ListPreference mLongPressTimeoutListPreference; + private PreferenceCategory mServicesCategory; + private PreferenceCategory mSystemsCategory; - private Map<String, ServiceInfo> mAccessibilityServices = - new LinkedHashMap<String, ServiceInfo>(); + private CheckBoxPreference mToggleLargeTextPreference; + private CheckBoxPreference mTogglePowerButtonEndsCallPreference; + private Preference mToggleTouchExplorationPreference; + private ListPreference mSelectLongPressTimeoutPreference; + private AccessibilityEnableScriptInjectionPreference mToggleScriptInjectionPreference; - private TextUtils.SimpleStringSplitter mStringColonSplitter = - new TextUtils.SimpleStringSplitter(':'); + private int mLongPressTimeoutDefault; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - addPreferencesFromResource(R.xml.accessibility_settings); + initializeAllPreferences(); + } + + @Override + public void onResume() { + super.onResume(); + final boolean accessibilityEnabled = mToggleAccessibilitySwitch.isChecked(); + updateAllPreferences(accessibilityEnabled); + if (accessibilityEnabled) { + offerInstallAccessibilitySerivceOnce(); + } + mSettingsPackageMonitor.register(getActivity(), false); + } + + @Override + public void onPause() { + mSettingsPackageMonitor.unregister(); + super.onPause(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + addToggleAccessibilitySwitch(); + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onDestroyView() { + removeToggleAccessibilitySwitch(); + super.onDestroyView(); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mSelectLongPressTimeoutPreference) { + String stringValue = (String) newValue; + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LONG_PRESS_TIMEOUT, Integer.parseInt(stringValue)); + mSelectLongPressTimeoutPreference.setSummary( + mLongPressTimeoutValuetoTitleMap.get(stringValue)); + return true; + } + return false; + } - mAccessibilityServicesCategory = - (PreferenceGroup) findPreference(ACCESSIBILITY_SERVICES_CATEGORY); + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + final String key = preference.getKey(); + if (mToggleLargeTextPreference == preference) { + handleToggleLargeTextPreferenceClick(); + return true; + } else if (mTogglePowerButtonEndsCallPreference == preference) { + handleTogglePowerButtonEndsCallPreferenceClick(); + return true; + } + return super.onPreferenceTreeClick(preferenceScreen, preference); + } - mToggleAccessibilityCheckBox = (CheckBoxPreference) findPreference( - TOGGLE_ACCESSIBILITY_CHECKBOX); + private void handleToggleLargeTextPreferenceClick() { + try { + mCurConfig.fontScale = mToggleLargeTextPreference.isChecked() ? LARGE_FONT_SCALE : 1; + ActivityManagerNative.getDefault().updatePersistentConfiguration(mCurConfig); + } catch (RemoteException re) { + /* ignore */ + } + } - mToggleScriptInjectionCheckBox = (CheckBoxPreference) findPreference( - TOGGLE_ACCESSIBILITY_SCRIPT_INJECTION_CHECKBOX); + private void handleTogglePowerButtonEndsCallPreferenceClick() { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, + (mTogglePowerButtonEndsCallPreference.isChecked() + ? Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP + : Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF)); + } - mPowerButtonCategory = (PreferenceCategory) findPreference(POWER_BUTTON_CATEGORY); - mPowerButtonEndsCallCheckBox = (CheckBoxPreference) findPreference( - POWER_BUTTON_ENDS_CALL_CHECKBOX); + private void addToggleAccessibilitySwitch() { + mToggleAccessibilitySwitch = createAndAddActionBarToggleSwitch(getActivity()); + final boolean checked = (Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1); + mToggleAccessibilitySwitch.setChecked(checked); + mToggleAccessibilitySwitch.setOnBeforeCheckedChangeListener( + new OnBeforeCheckedChangeListener() { + @Override + public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { + if (!checked) { + toggleSwitch.setCheckedInternal(true); + showDialog(DIALOG_ID_DISABLE_ACCESSIBILITY); + return true; + } + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 1); + updateAllPreferences(true); + offerInstallAccessibilitySerivceOnce(); + return false; + } + }); + } - mLongPressTimeoutListPreference = (ListPreference) findPreference( - KEY_LONG_PRESS_TIMEOUT_LIST_PREFERENCE); + public void removeToggleAccessibilitySwitch() { + mToggleAccessibilitySwitch.setOnBeforeCheckedChangeListener(null); + getActivity().getActionBar().setCustomView(null); + } - // set the accessibility script injection category - boolean scriptInjectionEnabled = (Settings.Secure.getInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1); - mToggleScriptInjectionCheckBox.setChecked(scriptInjectionEnabled); - mToggleScriptInjectionCheckBox.setEnabled(true); + private void initializeAllPreferences() { + // The basic logic here is if accessibility is not enabled all accessibility + // settings will have no effect but still their selected state should be kept + // unchanged, so the user can see what settings will be enabled when turning + // on accessibility. + + final boolean accessibilityEnabled = (Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1); + + mServicesCategory = (PreferenceCategory) findPreference(SERVICES_CATEGORY); + mSystemsCategory = (PreferenceCategory) findPreference(SYSTEM_CATEGORY); + + // Large text. + mToggleLargeTextPreference = + (CheckBoxPreference) findPreference(TOGGLE_LARGE_TEXT_PREFERENCE); + if (accessibilityEnabled) { + try { + mCurConfig.updateFrom(ActivityManagerNative.getDefault().getConfiguration()); + } catch (RemoteException re) { + /* ignore */ + } + mToggleLargeTextPreference.setChecked(mCurConfig.fontScale == LARGE_FONT_SCALE); + } + // Power button ends calls. + mTogglePowerButtonEndsCallPreference = + (CheckBoxPreference) findPreference(TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE); if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_POWER) && Utils.isVoiceCapable(getActivity())) { - int incallPowerBehavior = Settings.Secure.getInt(getContentResolver(), - Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, - Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_DEFAULT); - // The checkbox is labeled "Power button ends call"; thus the in-call - // Power button behavior is INCALL_POWER_BUTTON_BEHAVIOR_HANGUP if - // checked, and INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF if unchecked. - boolean powerButtonCheckboxEnabled = + if (accessibilityEnabled) { + final int incallPowerBehavior = Settings.Secure.getInt(getContentResolver(), + Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, + Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_DEFAULT); + final boolean powerButtonEndsCall = (incallPowerBehavior == Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP); - mPowerButtonEndsCallCheckBox.setChecked(powerButtonCheckboxEnabled); - mPowerButtonEndsCallCheckBox.setEnabled(true); + mTogglePowerButtonEndsCallPreference.setChecked(powerButtonEndsCall); + } } else { - // No POWER key on the current device or no voice capability; - // this entire category is irrelevant. - getPreferenceScreen().removePreference(mPowerButtonCategory); + mSystemsCategory.removePreference(mTogglePowerButtonEndsCallPreference); } - mLongPressTimeoutListPreference.setOnPreferenceChangeListener(this); + // Touch exploration enabled. + mToggleTouchExplorationPreference = findPreference(TOGGLE_TOUCH_EXPLORATION_PREFERENCE); + final boolean touchExplorationEnabled = (Settings.Secure.getInt(getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1); + if (touchExplorationEnabled) { + mToggleTouchExplorationPreference.setSummary( + getString(R.string.accessibility_service_state_on)); + mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, true); + } else { + mToggleTouchExplorationPreference.setSummary( + getString(R.string.accessibility_service_state_off)); + mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, false); + } + + // Long press timeout. + mSelectLongPressTimeoutPreference = + (ListPreference) findPreference(SELECT_LONG_PRESS_TIMEOUT_PREFERENCE); + mSelectLongPressTimeoutPreference.setOnPreferenceChangeListener(this); + if (mLongPressTimeoutValuetoTitleMap.size() == 0) { + String[] timeoutValues = getResources().getStringArray( + R.array.long_press_timeout_selector_values); + mLongPressTimeoutDefault = Integer.parseInt(timeoutValues[0]); + String[] timeoutTitles = getResources().getStringArray( + R.array.long_press_timeout_selector_titles); + final int timeoutValueCount = timeoutValues.length; + for (int i = 0; i < timeoutValueCount; i++) { + mLongPressTimeoutValuetoTitleMap.put(timeoutValues[i], timeoutTitles[i]); + } + } + if (accessibilityEnabled) { + final int longPressTimeout = Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LONG_PRESS_TIMEOUT, mLongPressTimeoutDefault); + String value = String.valueOf(longPressTimeout); + mSelectLongPressTimeoutPreference.setValue(value); + mSelectLongPressTimeoutPreference.setSummary( + mLongPressTimeoutValuetoTitleMap.get(value)); + } else { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.LONG_PRESS_TIMEOUT, + mLongPressTimeoutDefault); + } + + // Script injection. + mToggleScriptInjectionPreference = (AccessibilityEnableScriptInjectionPreference) + findPreference(TOGGLE_SCRIPT_INJECTION_PREFERENCE); + if (accessibilityEnabled) { + final boolean scriptInjectionAllowed = (Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1); + mToggleScriptInjectionPreference.setInjectionAllowed(scriptInjectionAllowed); + } } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - addAccessibilitServicePreferences(); + private void updateAllPreferences(boolean accessibilityEnabled) { + updateServicesPreferences(accessibilityEnabled); + updateSystemPreferences(accessibilityEnabled); + } - final HashSet<String> enabled = new HashSet<String>(); + private void updateServicesPreferences(boolean accessibilityEnabled) { + // Since services category is auto generated we have to do a pass + // to generate it since services can come and go and then based on + // the global accessibility state to decided whether it is enabled. + + // Generate. + mServicesCategory.removeAll(); + + AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(getActivity()); + + List<AccessibilityServiceInfo> installedServices = + accessibilityManager.getInstalledAccessibilityServiceList(); + + Set<ComponentName> enabledComponentNames = new HashSet<ComponentName>(); String settingValue = Settings.Secure.getString(getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); if (settingValue != null) { - TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + SimpleStringSplitter splitter = mStringColonSplitter; splitter.setString(settingValue); while (splitter.hasNext()) { - enabled.add(splitter.next()); + enabledComponentNames.add(ComponentName.unflattenFromString(splitter.next())); } } - Map<String, ServiceInfo> accessibilityServices = mAccessibilityServices; + for (int i = 0, count = installedServices.size(); i < count; ++i) { + AccessibilityServiceInfo info = installedServices.get(i); + String key = info.getId(); - for (String key : accessibilityServices.keySet()) { - CheckBoxPreference preference = (CheckBoxPreference) findPreference(key); - if (preference != null) { - preference.setChecked(enabled.contains(key)); - } - } + PreferenceScreen preference = getPreferenceManager().createPreferenceScreen( + getActivity()); + String title = info.getResolveInfo().loadLabel(getPackageManager()).toString(); - int serviceState = Settings.Secure.getInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_ENABLED, 0); + ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo; + ComponentName componentName = new ComponentName(serviceInfo.packageName, + serviceInfo.name); - if (!accessibilityServices.isEmpty()) { - if (serviceState == 1) { - mToggleAccessibilityCheckBox.setChecked(true); - if (savedInstanceState != null) { - restoreInstanceState(savedInstanceState); - } + preference.setKey(componentName.flattenToString()); + + preference.setTitle(title); + final boolean enabled = enabledComponentNames.contains(componentName); + if (enabled) { + preference.setSummary(getString(R.string.accessibility_service_state_on)); } else { - setAccessibilityServicePreferencesState(false); + preference.setSummary(getString(R.string.accessibility_service_state_off)); } - mToggleAccessibilityCheckBox.setEnabled(true); - } else { - if (serviceState == 1) { - // no service and accessibility is enabled => disable - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_ENABLED, 0); - } - mToggleAccessibilityCheckBox.setEnabled(false); - // Notify user that they do not have any accessibility apps - // installed and direct them to Market to get TalkBack - showDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES); - } - super.onActivityCreated(savedInstanceState); - } + preference.setOrder(i); + preference.setFragment(ToggleAccessibilityServiceFragment.class.getName()); + preference.setPersistent(true); - @Override - public void onPause() { - super.onPause(); + Bundle extras = preference.getExtras(); + extras.putString(EXTRA_PREFERENCE_KEY, preference.getKey()); + extras.putBoolean(EXTRA_CHECKED, enabled); + extras.putString(EXTRA_TITLE, title); - persistEnabledAccessibilityServices(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (mToggleAccessibilityServiceCheckBox != null) { - outState.putString(KEY_TOGGLE_ACCESSIBILITY_SERVICE_CHECKBOX, - mToggleAccessibilityServiceCheckBox.getKey()); - } - } + String description = info.getDescription(); + if (TextUtils.isEmpty(description)) { + description = getString(R.string.accessibility_service_default_description); + } + extras.putString(EXTRA_SUMMARY, description); + + extras.putString(EXTRA_WARNING_MESSAGE, getString( + R.string.accessibility_service_security_warning, + info.getResolveInfo().loadLabel(getPackageManager()))); + + String settingsClassName = info.getSettingsActivityName(); + if (!TextUtils.isEmpty(settingsClassName)) { + extras.putString(EXTRA_SETTINGS_TITLE, + getString(R.string.accessibility_menu_item_settings)); + extras.putString(EXTRA_SETTINGS_COMPONENT_NAME, + new ComponentName(info.getResolveInfo().serviceInfo.packageName, + settingsClassName).flattenToString()); + } - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == mLongPressTimeoutListPreference) { - int intValue = Integer.parseInt((String) newValue); - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.LONG_PRESS_TIMEOUT, intValue); - return true; + mServicesCategory.addPreference(preference); } - return false; - } - /** - * Restores the instance state from <code>savedInstanceState</code>. - */ - private void restoreInstanceState(Bundle savedInstanceState) { - String key = savedInstanceState.getString(KEY_TOGGLE_ACCESSIBILITY_SERVICE_CHECKBOX); - if (key != null) { - Preference preference = findPreference(key); - if (!(preference instanceof CheckBoxPreference)) { - throw new IllegalArgumentException( - KEY_TOGGLE_ACCESSIBILITY_SERVICE_CHECKBOX - + " must be mapped to an instance of a " - + CheckBoxPreference.class.getName()); - } - mToggleAccessibilityServiceCheckBox = (CheckBoxPreference) preference; - } + // Update enabled state. + mServicesCategory.setEnabled(accessibilityEnabled); } - /** - * Sets the state of the preferences for enabling/disabling - * AccessibilityServices. - * - * @param isEnabled If to enable or disable the preferences. - */ - private void setAccessibilityServicePreferencesState(boolean isEnabled) { - if (mAccessibilityServicesCategory == null) { - return; + private void updateSystemPreferences(boolean accessibilityEnabled) { + // The basic logic here is if accessibility is not enabled all accessibility + // settings will have no effect but still their selected state should be kept + // unchanged, so the user can see what settings will be enabled when turning + // on accessibility. + + // Large text. + mToggleLargeTextPreference.setEnabled(accessibilityEnabled); + if (accessibilityEnabled) { + mCurConfig.fontScale = + mToggleLargeTextPreference.isChecked() ? LARGE_FONT_SCALE : 1; + } else { + mCurConfig.fontScale = 1; } - - int count = mAccessibilityServicesCategory.getPreferenceCount(); - for (int i = 0; i < count; i++) { - Preference pref = mAccessibilityServicesCategory.getPreference(i); - pref.setEnabled(isEnabled); + try { + ActivityManagerNative.getDefault().updatePersistentConfiguration(mCurConfig); + } catch (RemoteException re) { + /* ignore */ } - mToggleScriptInjectionCheckBox.setEnabled(isEnabled); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - final String key = preference.getKey(); - - if (TOGGLE_ACCESSIBILITY_CHECKBOX.equals(key)) { - handleEnableAccessibilityStateChange((CheckBoxPreference) preference); - } else if (POWER_BUTTON_ENDS_CALL_CHECKBOX.equals(key)) { - boolean isChecked = ((CheckBoxPreference) preference).isChecked(); - // The checkbox is labeled "Power button ends call"; thus the in-call - // Power button behavior is INCALL_POWER_BUTTON_BEHAVIOR_HANGUP if - // checked, and INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF if unchecked. + // Power button ends calls. + if (mTogglePowerButtonEndsCallPreference != null) { + mTogglePowerButtonEndsCallPreference.setEnabled(accessibilityEnabled); + final int powerButtonEndsCall; + if (accessibilityEnabled) { + powerButtonEndsCall = mTogglePowerButtonEndsCallPreference.isChecked() + ? Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP + : Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF; + } else { + powerButtonEndsCall = Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF; + } Settings.Secure.putInt(getContentResolver(), Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, - (isChecked ? Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP - : Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF)); - } else if (TOGGLE_ACCESSIBILITY_SCRIPT_INJECTION_CHECKBOX.equals(key)) { - handleToggleAccessibilityScriptInjection((CheckBoxPreference) preference); - } else if (preference instanceof CheckBoxPreference) { - handleEnableAccessibilityServiceStateChange((CheckBoxPreference) preference); + powerButtonEndsCall); } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - /** - * Handles the change of the accessibility enabled setting state. - * - * @param preference The preference for enabling/disabling accessibility. - */ - private void handleEnableAccessibilityStateChange(CheckBoxPreference preference) { - if (preference.isChecked()) { - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_ENABLED, 1); - setAccessibilityServicePreferencesState(true); + // Touch exploration enabled. + mToggleTouchExplorationPreference.setEnabled(accessibilityEnabled); + final boolean touchExplorationEnabled = (Settings.Secure.getInt(getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1); + if (touchExplorationEnabled) { + mToggleTouchExplorationPreference.setSummary( + getString(R.string.accessibility_service_state_on)); + mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, true); } else { - // set right enabled state since the user may press back - preference.setChecked(true); - showDialog(DIALOG_ID_DISABLE_ACCESSIBILITY); + mToggleTouchExplorationPreference.setSummary( + getString(R.string.accessibility_service_state_off)); + mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, false); } - } - /** - * Handles the change of the accessibility script injection setting state. - * - * @param preference The preference for enabling/disabling accessibility script injection. - */ - private void handleToggleAccessibilityScriptInjection(CheckBoxPreference preference) { - if (preference.isChecked()) { - // set right enabled state since the user may press back - preference.setChecked(false); - showDialog(DIALOG_ID_ENABLE_SCRIPT_INJECTION); + // Long press timeout. + mSelectLongPressTimeoutPreference.setEnabled(accessibilityEnabled); + final int longPressTimeout; + if (accessibilityEnabled) { + String value = mSelectLongPressTimeoutPreference.getValue(); + longPressTimeout = (value != null) ? Integer.parseInt(value) : mLongPressTimeoutDefault; } else { - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0); + longPressTimeout = mLongPressTimeoutDefault; } - } - - /** - * Handles the change of the preference for enabling/disabling an AccessibilityService. - * - * @param preference The preference. - */ - private void handleEnableAccessibilityServiceStateChange(CheckBoxPreference preference) { - if (preference.isChecked()) { - mToggleAccessibilityServiceCheckBox = preference; - // set right enabled state since the user may press back - preference.setChecked(false); - showDialog(DIALOG_ID_ENABLE_ACCESSIBILITY_SERVICE); + Settings.Secure.putInt(getContentResolver(), Settings.Secure.LONG_PRESS_TIMEOUT, + longPressTimeout); + String value = mSelectLongPressTimeoutPreference.getValue(); + mSelectLongPressTimeoutPreference.setSummary(mLongPressTimeoutValuetoTitleMap.get(value)); + + // Script injection. + mToggleScriptInjectionPreference.setEnabled(accessibilityEnabled); + final boolean scriptInjectionAllowed; + if (accessibilityEnabled) { + scriptInjectionAllowed = mToggleScriptInjectionPreference.isInjectionAllowed(); } else { - persistEnabledAccessibilityServices(); - } - } - - /** - * Persists the Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES setting. - * The AccessibilityManagerService watches this property and manages the - * AccessibilityServices. - */ - private void persistEnabledAccessibilityServices() { - StringBuilder builder = new StringBuilder(256); - - int firstEnabled = -1; - for (String key : mAccessibilityServices.keySet()) { - CheckBoxPreference preference = (CheckBoxPreference) findPreference(key); - if (preference.isChecked()) { - builder.append(key); - builder.append(':'); - } + scriptInjectionAllowed = false; } - - Settings.Secure.putString(getContentResolver(), - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, builder.toString()); + Settings.Secure.putInt(getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + scriptInjectionAllowed ? 1 : 0); } - /** - * Adds {@link CheckBoxPreference} for enabling or disabling an accessibility services. - */ - private void addAccessibilitServicePreferences() { - AccessibilityManager accessibilityManager = - (AccessibilityManager) getSystemService(Service.ACCESSIBILITY_SERVICE); - - List<ServiceInfo> installedServices = accessibilityManager.getAccessibilityServiceList(); - - if (installedServices.isEmpty()) { - getPreferenceScreen().removePreference(mAccessibilityServicesCategory); + private void offerInstallAccessibilitySerivceOnce() { + if (mServicesCategory.getPreferenceCount() > 0) { return; } - - getPreferenceScreen().addPreference(mAccessibilityServicesCategory); - - for (int i = 0, count = installedServices.size(); i < count; ++i) { - ServiceInfo serviceInfo = installedServices.get(i); - String key = serviceInfo.packageName + "/" + serviceInfo.name; - - if (mAccessibilityServices.put(key, serviceInfo) == null) { - CheckBoxPreference preference = new CheckBoxPreference(getActivity()); - preference.setKey(key); - preference.setTitle(serviceInfo.loadLabel(getActivity().getPackageManager())); - mAccessibilityServicesCategory.addPreference(preference); - } + SharedPreferences preferences = getActivity().getPreferences(Context.MODE_PRIVATE); + final boolean offerInstallService = !preferences.getBoolean( + KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE, false); + if (offerInstallService) { + preferences.edit().putBoolean(KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE, + true).commit(); + // Notify user that they do not have any accessibility + // services installed and direct them to Market to get TalkBack. + showDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES); } } @@ -384,56 +533,28 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements switch (dialogId) { case DIALOG_ID_DISABLE_ACCESSIBILITY: return (new AlertDialog.Builder(getActivity())) - .setTitle(android.R.string.dialog_alert_title) + .setTitle(R.string.accessibility_disable_warning_title) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(getResources(). - getString(R.string.accessibility_service_disable_warning)) + getString(R.string.accessibility_disable_warning_summary)) .setCancelable(true) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Settings.Secure.putInt(getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0); - mToggleAccessibilityCheckBox.setChecked(false); - setAccessibilityServicePreferencesState(false); + mToggleAccessibilitySwitch.setCheckedInternal( + false); + updateAllPreferences(false); } }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - case DIALOG_ID_ENABLE_SCRIPT_INJECTION: - return new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(getActivity().getString( - R.string.accessibility_script_injection_security_warning)) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 1); - mToggleScriptInjectionCheckBox.setChecked(true); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - case DIALOG_ID_ENABLE_ACCESSIBILITY_SERVICE: - return new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(getResources().getString( - R.string.accessibility_service_security_warning, - mAccessibilityServices.get(mToggleAccessibilityServiceCheckBox.getKey()) - .applicationInfo.loadLabel(getActivity().getPackageManager()))) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - mToggleAccessibilityServiceCheckBox.setChecked(true); - persistEnabledAccessibilityServices(); - } - }) - .setNegativeButton(android.R.string.cancel, null) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mToggleAccessibilitySwitch.setCheckedInternal( + true); + } + }) .create(); case DIALOG_ID_NO_ACCESSIBILITY_SERVICES: return new AlertDialog.Builder(getActivity()) @@ -445,9 +566,10 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // dismiss the dialog before launching the activity otherwise // the dialog removal occurs after onSaveInstanceState which // triggers an exception - dialog.dismiss(); + removeDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES); String screenreaderMarketLink = SystemProperties.get( - "ro.screenreader.market", DEFAULT_SCREENREADER_MARKET_LINK); + SYSTEM_PROPERTY_MARKET_URL, + DEFAULT_SCREENREADER_MARKET_LINK); Uri marketUri = Uri.parse(screenreaderMarketLink); Intent marketIntent = new Intent(Intent.ACTION_VIEW, marketUri); startActivity(marketIntent); @@ -459,4 +581,309 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements return null; } } + + private class SettingsPackageMonitor extends PackageMonitor { + + @Override + public void onPackageAdded(String packageName, int uid) { + Message message = mHandler.obtainMessage(); + mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_PREFERENCES_MILLIS); + } + + @Override + public void onPackageAppeared(String packageName, int reason) { + Message message = mHandler.obtainMessage(); + mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_PREFERENCES_MILLIS); + } + + @Override + public void onPackageDisappeared(String packageName, int reason) { + Message message = mHandler.obtainMessage(); + mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_PREFERENCES_MILLIS); + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + Message message = mHandler.obtainMessage(); + mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_PREFERENCES_MILLIS); + } + } + + private static ToggleSwitch createAndAddActionBarToggleSwitch(Activity activity) { + ToggleSwitch toggleSwitch = new ToggleSwitch(activity); + final int padding = activity.getResources().getDimensionPixelSize( + R.dimen.action_bar_switch_padding); + toggleSwitch.setPadding(0, 0, padding, 0); + activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, + ActionBar.DISPLAY_SHOW_CUSTOM); + activity.getActionBar().setCustomView(toggleSwitch, + new ActionBar.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT)); + return toggleSwitch; + } + + public static class ToggleSwitch extends Switch { + + private OnBeforeCheckedChangeListener mOnBeforeListener; + + public static interface OnBeforeCheckedChangeListener { + public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked); + } + + public ToggleSwitch(Context context) { + super(context); + } + + public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) { + mOnBeforeListener = listener; + } + + @Override + public void setChecked(boolean checked) { + if (mOnBeforeListener != null + && mOnBeforeListener.onBeforeCheckedChanged(this, checked)) { + return; + } + super.setChecked(checked); + } + + public void setCheckedInternal(boolean checked) { + super.setChecked(checked); + } + } + + public static class ToggleAccessibilityServiceFragment extends TogglePreferenceFragment { + @Override + public void onPreferenceToggled(String preferenceKey, boolean enabled) { + String enabledServices = Settings.Secure.getString(getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + if (enabledServices == null) { + enabledServices = ""; + } + final int length = enabledServices.length(); + if (enabled) { + if (enabledServices.contains(preferenceKey)) { + return; + } + if (length == 0) { + enabledServices += preferenceKey; + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices); + } else if (length > 0) { + enabledServices += ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR + preferenceKey; + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices); + } + } else { + final int index = enabledServices.indexOf(preferenceKey); + if (index == 0) { + enabledServices = enabledServices.replace(preferenceKey, ""); + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices); + } else if (index > 0) { + enabledServices = enabledServices.replace( + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR + preferenceKey, ""); + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices); + } + } + } + } + + public static class ToggleTouchExplorationFragment extends TogglePreferenceFragment { + @Override + public void onPreferenceToggled(String preferenceKey, boolean enabled) { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, enabled ? 1 : 0); + if (enabled) { + SharedPreferences preferences = getActivity().getPreferences(Context.MODE_PRIVATE); + final boolean launchAccessibilityTutorial = !preferences.getBoolean( + KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE, false); + if (launchAccessibilityTutorial) { + preferences.edit().putBoolean(KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE, + true).commit(); + Intent intent = new Intent(AccessibilityTutorialActivity.ACTION); + getActivity().startActivity(intent); + } + } + } + } + + private abstract static class TogglePreferenceFragment extends SettingsPreferenceFragment + implements DialogInterface.OnClickListener { + + private static final int DIALOG_ID_WARNING = 1; + + private String mPreferenceKey; + + private ToggleSwitch mToggleSwitch; + + private CharSequence mWarningMessage; + private Preference mSummaryPreference; + + private CharSequence mSettingsTitle; + private Intent mSettingsIntent; + + // TODO: Showing sub-sub fragment does not handle the activity title + // so we do it but this is wrong. Do a real fix when there is time. + private CharSequence mOldActivityTitle; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( + getActivity()); + setPreferenceScreen(preferenceScreen); + mSummaryPreference = new Preference(getActivity()) { + @Override + protected void onBindView(View view) { + super.onBindView(view); + TextView summaryView = (TextView) view.findViewById(R.id.summary); + summaryView.setText(getSummary()); + sendAccessibilityEvent(summaryView); + } + + private void sendAccessibilityEvent(View view) { + // Since the view is still not attached we create, populate, + // and send the event directly since we do not know when it + // will be attached and posting commands is not as clean. + AccessibilityManager accessibilityManager = + AccessibilityManager.getInstance(getActivity()); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED); + view.onInitializeAccessibilityEvent(event); + view.dispatchPopulateAccessibilityEvent(event); + accessibilityManager.sendAccessibilityEvent(event); + } + } + }; + mSummaryPreference.setPersistent(false); + mSummaryPreference.setLayoutResource(R.layout.text_description_preference); + preferenceScreen.addPreference(mSummaryPreference); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + installActionBarToggleSwitch(); + processArguments(); + getListView().setDivider(null); + getListView().setEnabled(false); + } + + @Override + public void onDestroyView() { + getActivity().getActionBar().setCustomView(null); + if (mOldActivityTitle != null) { + getActivity().getActionBar().setTitle(mOldActivityTitle); + } + mToggleSwitch.setOnBeforeCheckedChangeListener(null); + super.onDestroyView(); + } + + public abstract void onPreferenceToggled(String preferenceKey, boolean value); + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + MenuItem menuItem = menu.add(mSettingsTitle); + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menuItem.setIntent(mSettingsIntent); + } + + @Override + public Dialog onCreateDialog(int dialogId) { + switch (dialogId) { + case DIALOG_ID_WARNING: + return new AlertDialog.Builder(getActivity()) + .setTitle(android.R.string.dialog_alert_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(mWarningMessage) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + // OK, we got the user consent so set checked. + mToggleSwitch.setCheckedInternal(true); + onPreferenceToggled(mPreferenceKey, true); + break; + case DialogInterface.BUTTON_NEGATIVE: + onPreferenceToggled(mPreferenceKey, false); + break; + default: + throw new IllegalArgumentException(); + } + } + + private void installActionBarToggleSwitch() { + mToggleSwitch = createAndAddActionBarToggleSwitch(getActivity()); + mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { + @Override + public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { + if (checked) { + if (!TextUtils.isEmpty(mWarningMessage)) { + toggleSwitch.setCheckedInternal(false); + showDialog(DIALOG_ID_WARNING); + return true; + } + onPreferenceToggled(mPreferenceKey, true); + } else { + onPreferenceToggled(mPreferenceKey, false); + } + return false; + } + }); + } + + private void processArguments() { + Bundle arguments = getArguments(); + + // Key. + mPreferenceKey = arguments.getString(EXTRA_PREFERENCE_KEY); + + // Enabled. + final boolean enabled = arguments.getBoolean(EXTRA_CHECKED); + mToggleSwitch.setCheckedInternal(enabled); + + // Title. + PreferenceActivity activity = (PreferenceActivity) getActivity(); + if (!activity.onIsMultiPane() || activity.onIsHidingHeaders()) { + mOldActivityTitle = getActivity().getTitle(); + String title = arguments.getString(EXTRA_TITLE); + getActivity().getActionBar().setTitle(arguments.getCharSequence(EXTRA_TITLE)); + } + + // Summary. + String summary = arguments.getString(EXTRA_SUMMARY); + mSummaryPreference.setSummary(summary); + + // Settings title and intent. + String settingsTitle = arguments.getString(EXTRA_SETTINGS_TITLE); + String settingsComponentName = arguments.getString(EXTRA_SETTINGS_COMPONENT_NAME); + if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { + Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( + ComponentName.unflattenFromString(settingsComponentName.toString())); + if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { + mSettingsTitle = settingsTitle; + mSettingsIntent = settingsIntent; + setHasOptionsMenu(true); + } + } + + // Waring message. + mWarningMessage = arguments.getCharSequence( + AccessibilitySettings.EXTRA_WARNING_MESSAGE); + } + } } diff --git a/src/com/android/settings/AccessibilityTutorialActivity.java b/src/com/android/settings/AccessibilityTutorialActivity.java new file mode 100644 index 0000000..da8350c --- /dev/null +++ b/src/com/android/settings/AccessibilityTutorialActivity.java @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * 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; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.GridView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import com.android.settings.R; + +import java.util.List; + +/** + * This class provides a short tutorial that introduces the user to the features + * available in Touch Exploration. + */ +public class AccessibilityTutorialActivity extends Activity { + + /** Intent action for launching this activity. */ + public static final String ACTION = "android.settings.ACCESSIBILITY_TUTORIAL"; + + /** Instance state saving constant for the active module. */ + private static final String KEY_ACTIVE_MODULE = "active_module"; + + /** The index of the module to show when first opening the tutorial. */ + private static final int DEFAULT_MODULE = 0; + + /** View animator for switching between modules. */ + private ViewAnimator mViewAnimator; + + private AccessibilityManager mAccessibilityManager; + + /** Should touch exploration be disabled when this activity is paused? */ + private boolean mDisableOnPause; + + private final AnimationListener mInAnimationListener = new AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + final int index = mViewAnimator.getDisplayedChild(); + final TutorialModule module = (TutorialModule) mViewAnimator.getChildAt(index); + + activateModule(module); + } + + @Override + public void onAnimationRepeat(Animation animation) { + // Do nothing. + } + + @Override + public void onAnimationStart(Animation animation) { + // Do nothing. + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Animation inAnimation = AnimationUtils.loadAnimation(this, + android.R.anim.slide_in_left); + inAnimation.setAnimationListener(mInAnimationListener); + + final Animation outAnimation = AnimationUtils.loadAnimation(this, + android.R.anim.slide_in_left); + + mViewAnimator = new ViewAnimator(this); + mViewAnimator.setInAnimation(inAnimation); + mViewAnimator.setOutAnimation(outAnimation); + mViewAnimator.addView(new TouchTutorialModule1(this, this)); + mViewAnimator.addView(new TouchTutorialModule2(this, this)); + + setContentView(mViewAnimator); + + mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE); + + if (savedInstanceState != null) { + show(savedInstanceState.getInt(KEY_ACTIVE_MODULE, DEFAULT_MODULE)); + } else { + show(DEFAULT_MODULE); + } + } + + @Override + protected void onResume() { + super.onResume(); + + final ContentResolver cr = getContentResolver(); + + if (Settings.Secure.getInt(cr, Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 0) { + Settings.Secure.putInt(cr, Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1); + mDisableOnPause = true; + } else { + mDisableOnPause = false; + } + } + + @Override + protected void onPause() { + super.onPause(); + + if (mDisableOnPause) { + final ContentResolver cr = getContentResolver(); + Settings.Secure.putInt(cr, Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putInt(KEY_ACTIVE_MODULE, mViewAnimator.getDisplayedChild()); + } + + private void activateModule(TutorialModule module) { + module.activate(); + } + + private void deactivateModule(TutorialModule module) { + mAccessibilityManager.interrupt(); + mViewAnimator.setOnKeyListener(null); + module.deactivate(); + } + + private void interrupt() { + mAccessibilityManager.interrupt(); + } + + private void next() { + show(mViewAnimator.getDisplayedChild() + 1); + } + + private void previous() { + show(mViewAnimator.getDisplayedChild() - 1); + } + + private void show(int which) { + if ((which < 0) || (which >= mViewAnimator.getChildCount())) { + return; + } + + mAccessibilityManager.interrupt(); + + final int displayedIndex = mViewAnimator.getDisplayedChild(); + final TutorialModule displayedView = (TutorialModule) mViewAnimator.getChildAt( + displayedIndex); + deactivateModule(displayedView); + + mViewAnimator.setDisplayedChild(which); + } + + /** + * Loads application labels and icons. + */ + private static class AppsAdapter extends ArrayAdapter<ResolveInfo> { + protected final int mTextViewResourceId; + + private final int mIconSize; + private final View.OnHoverListener mDefaultHoverListener; + + private View.OnHoverListener mHoverListener; + + public AppsAdapter(Context context, int resource, int textViewResourceId) { + super(context, resource, textViewResourceId); + + mIconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size); + mTextViewResourceId = textViewResourceId; + mDefaultHoverListener = new View.OnHoverListener() { + @Override + public boolean onHover(View v, MotionEvent event) { + if (mHoverListener != null) { + return mHoverListener.onHover(v, event); + } else { + return false; + } + } + }; + + loadAllApps(); + } + + public CharSequence getLabel(int position) { + final PackageManager packageManager = getContext().getPackageManager(); + final ResolveInfo appInfo = getItem(position); + return appInfo.loadLabel(packageManager); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final PackageManager packageManager = getContext().getPackageManager(); + final View view = super.getView(position, convertView, parent); + view.setOnHoverListener(mDefaultHoverListener); + view.setTag(position); + + final ResolveInfo appInfo = getItem(position); + final CharSequence label = appInfo.loadLabel(packageManager); + final Drawable icon = appInfo.loadIcon(packageManager); + final TextView text = (TextView) view.findViewById(mTextViewResourceId); + + icon.setBounds(0, 0, mIconSize, mIconSize); + + populateView(text, label, icon); + + return view; + } + + private void loadAllApps() { + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + final PackageManager pm = getContext().getPackageManager(); + final List<ResolveInfo> apps = pm.queryIntentActivities(mainIntent, 0); + + addAll(apps); + } + + protected void populateView(TextView text, CharSequence label, Drawable icon) { + text.setText(label); + text.setCompoundDrawables(null, icon, null, null); + } + + public void setOnHoverListener(View.OnHoverListener hoverListener) { + mHoverListener = hoverListener; + } + } + + /** + * Introduces using a finger to explore and interact with on-screen content. + */ + private static class TouchTutorialModule1 extends TutorialModule implements + View.OnHoverListener, AdapterView.OnItemClickListener { + /** + * Handles the case where the user overshoots the target area. + */ + private class HoverTargetHandler extends Handler { + private static final int MSG_ENTERED_TARGET = 1; + private static final int DELAY_ENTERED_TARGET = 500; + + private boolean mInsideTarget = false; + + public void enteredTarget() { + mInsideTarget = true; + mHandler.sendEmptyMessageDelayed(MSG_ENTERED_TARGET, DELAY_ENTERED_TARGET); + } + + public void exitedTarget() { + mInsideTarget = false; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ENTERED_TARGET: + if (mInsideTarget) { + addInstruction(R.string.accessibility_tutorial_lesson_1_text_4, + mTargetName); + } else { + addInstruction(R.string.accessibility_tutorial_lesson_1_text_4_exited, + mTargetName); + setFlag(FLAG_TOUCHED_TARGET, false); + } + break; + } + } + } + + private static final int FLAG_TOUCH_ITEMS = 0x1; + private static final int FLAG_TOUCHED_ITEMS = 0x2; + private static final int FLAG_TOUCHED_TARGET = 0x4; + private static final int FLAG_TAPPED_TARGET = 0x8; + + private static final int MORE_EXPLORED_COUNT = 1; + private static final int DONE_EXPLORED_COUNT = 2; + + private final HoverTargetHandler mHandler; + private final AppsAdapter mAppsAdapter; + private final GridView mAllApps; + + private int mTouched = 0; + + private int mTargetPosition; + private CharSequence mTargetName; + + public TouchTutorialModule1(Context context, AccessibilityTutorialActivity controller) { + super(context, controller, R.layout.accessibility_tutorial_1, + R.string.accessibility_tutorial_lesson_1_title); + + mHandler = new HoverTargetHandler(); + + mAppsAdapter = new AppsAdapter(context, R.layout.accessibility_tutorial_app_icon, + R.id.app_icon); + mAppsAdapter.setOnHoverListener(this); + + mAllApps = (GridView) findViewById(R.id.all_apps); + mAllApps.setAdapter(mAppsAdapter); + mAllApps.setOnItemClickListener(this); + + findViewById(R.id.next_button).setOnHoverListener(this); + + setSkipVisible(true); + } + + @Override + public boolean onHover(View v, MotionEvent event) { + switch (v.getId()) { + case R.id.app_icon: + if (hasFlag(FLAG_TOUCH_ITEMS) && !hasFlag(FLAG_TOUCHED_ITEMS) && v.isEnabled() + && (event.getAction() == MotionEvent.ACTION_HOVER_ENTER)) { + mTouched++; + + if (mTouched >= DONE_EXPLORED_COUNT) { + setFlag(FLAG_TOUCHED_ITEMS, true); + addInstruction(R.string.accessibility_tutorial_lesson_1_text_3, + mTargetName); + } else if (mTouched == MORE_EXPLORED_COUNT) { + addInstruction(R.string.accessibility_tutorial_lesson_1_text_2_more); + } + + v.setEnabled(false); + } else if (hasFlag(FLAG_TOUCHED_ITEMS) + && ((Integer) v.getTag() == mTargetPosition)) { + if (!hasFlag(FLAG_TOUCHED_TARGET) + && (event.getAction() == MotionEvent.ACTION_HOVER_ENTER)) { + mHandler.enteredTarget(); + setFlag(FLAG_TOUCHED_TARGET, true); + } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) { + mHandler.exitedTarget(); + } + } + break; + } + + return false; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (hasFlag(FLAG_TOUCHED_TARGET) && !hasFlag(FLAG_TAPPED_TARGET) + && (position == mTargetPosition)) { + setFlag(FLAG_TAPPED_TARGET, true); + final CharSequence nextText = getContext().getText( + R.string.accessibility_tutorial_next); + addInstruction(R.string.accessibility_tutorial_lesson_1_text_5, nextText); + setNextVisible(true); + } + } + + @Override + public void onShown() { + final int first = mAllApps.getFirstVisiblePosition(); + final int last = mAllApps.getLastVisiblePosition(); + + mTargetPosition = 0; + mTargetName = mAppsAdapter.getLabel(mTargetPosition); + + addInstruction(R.string.accessibility_tutorial_lesson_1_text_1); + setFlag(FLAG_TOUCH_ITEMS, true); + } + } + + /** + * Introduces using two fingers to scroll through a list. + */ + private static class TouchTutorialModule2 extends TutorialModule implements + AbsListView.OnScrollListener, View.OnHoverListener { + private static final int FLAG_EXPLORE_LIST = 0x1; + private static final int FLAG_SCROLL_LIST = 0x2; + private static final int FLAG_COMPLETED_TUTORIAL = 0x4; + + private static final int MORE_EXPLORE_COUNT = 1; + private static final int DONE_EXPLORE_COUNT = 2; + private static final int MORE_SCROLL_COUNT = 2; + private static final int DONE_SCROLL_COUNT = 4; + + private final AppsAdapter mAppsAdapter; + + private int mExploreCount = 0; + private int mInitialVisibleItem = -1; + private int mScrollCount = 0; + + public TouchTutorialModule2(Context context, AccessibilityTutorialActivity controller) { + super(context, controller, R.layout.accessibility_tutorial_2, + R.string.accessibility_tutorial_lesson_2_title); + + mAppsAdapter = new AppsAdapter(context, android.R.layout.simple_list_item_1, + android.R.id.text1) { + @Override + protected void populateView(TextView text, CharSequence label, Drawable icon) { + text.setText(label); + text.setCompoundDrawables(icon, null, null, null); + } + }; + mAppsAdapter.setOnHoverListener(this); + + ((ListView) findViewById(R.id.list_view)).setAdapter(mAppsAdapter); + ((ListView) findViewById(R.id.list_view)).setOnScrollListener(this); + + setBackVisible(true); + } + + @Override + public boolean onHover(View v, MotionEvent e) { + if (e.getAction() != MotionEvent.ACTION_HOVER_ENTER) { + return false; + } + + switch (v.getId()) { + case android.R.id.text1: + if (hasFlag(FLAG_EXPLORE_LIST) && !hasFlag(FLAG_SCROLL_LIST)) { + mExploreCount++; + + if (mExploreCount >= DONE_EXPLORE_COUNT) { + addInstruction(R.string.accessibility_tutorial_lesson_2_text_3); + setFlag(FLAG_SCROLL_LIST, true); + } else if (mExploreCount == MORE_EXPLORE_COUNT) { + addInstruction(R.string.accessibility_tutorial_lesson_2_text_2_more); + } + } + break; + } + + return false; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (hasFlag(FLAG_SCROLL_LIST) && !hasFlag(FLAG_COMPLETED_TUTORIAL)) { + if (mInitialVisibleItem < 0) { + mInitialVisibleItem = firstVisibleItem; + } + + final int scrollCount = Math.abs(mInitialVisibleItem - firstVisibleItem); + + if ((mScrollCount == scrollCount) || (scrollCount <= 0)) { + return; + } else { + mScrollCount = scrollCount; + } + + if (mScrollCount >= DONE_SCROLL_COUNT) { + final CharSequence finishText = getContext().getText( + R.string.accessibility_tutorial_finish); + addInstruction(R.string.accessibility_tutorial_lesson_2_text_4, finishText); + setFlag(FLAG_COMPLETED_TUTORIAL, true); + setFinishVisible(true); + } else if (mScrollCount == MORE_SCROLL_COUNT) { + addInstruction(R.string.accessibility_tutorial_lesson_2_text_3_more); + } + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // Do nothing. + } + + @Override + public void onShown() { + addInstruction(R.string.accessibility_tutorial_lesson_2_text_1); + setFlag(FLAG_EXPLORE_LIST, true); + } + } + + /** + * Abstract class that represents a single module within a tutorial. + */ + private static abstract class TutorialModule extends FrameLayout implements OnClickListener { + private final AccessibilityTutorialActivity mController; + private final TextView mInstructions; + private final Button mSkip; + private final Button mBack; + private final Button mNext; + private final Button mFinish; + private final int mTitleResId; + + /** Which bit flags have been set. */ + private long mFlags; + + /** Whether this module is currently focused. */ + private boolean mIsVisible; + + /** + * Constructs a new tutorial module for the given context and controller + * with the specified layout. + * + * @param context The parent context. + * @param controller The parent tutorial controller. + * @param layoutResId The layout to use for this module. + */ + public TutorialModule(Context context, AccessibilityTutorialActivity controller, + int layoutResId, int titleResId) { + super(context); + + mController = controller; + mTitleResId = titleResId; + + final View container = LayoutInflater.from(context).inflate( + R.layout.accessibility_tutorial_container, this, true); + + mInstructions = (TextView) container.findViewById(R.id.instructions); + mSkip = (Button) container.findViewById(R.id.skip_button); + mSkip.setOnClickListener(this); + mBack = (Button) container.findViewById(R.id.back_button); + mBack.setOnClickListener(this); + mNext = (Button) container.findViewById(R.id.next_button); + mNext.setOnClickListener(this); + mFinish = (Button) container.findViewById(R.id.finish_button); + mFinish.setOnClickListener(this); + + final TextView title = (TextView) container.findViewById(R.id.title); + + if (title != null) { + title.setText(titleResId); + } + + final ViewGroup contentHolder = (ViewGroup) container.findViewById(R.id.content); + LayoutInflater.from(context).inflate(layoutResId, contentHolder, true); + } + + /** + * Called when this tutorial gains focus. + */ + public final void activate() { + mIsVisible = true; + + mFlags = 0; + mInstructions.setVisibility(View.GONE); + mController.setTitle(mTitleResId); + + onShown(); + } + + /** + * Formats an instruction string and adds it to the speaking queue. + * + * @param resId The resource id of the instruction string. + * @param formatArgs Optional formatting arguments. + * @see String#format(String, Object...) + */ + protected void addInstruction(final int resId, Object... formatArgs) { + if (!mIsVisible) { + return; + } + + final String text = getContext().getString(resId, formatArgs); + + mInstructions.setVisibility(View.VISIBLE); + mInstructions.setText(text); + mInstructions.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + + /** + * Called when this tutorial loses focus. + */ + public void deactivate() { + mIsVisible = false; + + mController.interrupt(); + } + + /** + * Returns {@code true} if the flag with the specified id has been set. + * + * @param flagId The id of the flag to check for. + * @return {@code true} if the flag with the specified id has been set. + */ + protected boolean hasFlag(int flagId) { + return (mFlags & flagId) == flagId; + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.skip_button: + mController.finish(); + break; + case R.id.back_button: + mController.previous(); + break; + case R.id.next_button: + mController.next(); + break; + case R.id.finish_button: + mController.finish(); + break; + } + } + + public abstract void onShown(); + + /** + * Sets or removes the flag with the specified id. + * + * @param flagId The id of the flag to modify. + * @param value {@code true} to set the flag, {@code false} to remove + * it. + */ + protected void setFlag(int flagId, boolean value) { + if (value) { + mFlags |= flagId; + } else { + mFlags = ~(~mFlags | flagId); + } + } + + protected void setSkipVisible(boolean visible) { + mSkip.setVisibility(visible ? VISIBLE : GONE); + } + + protected void setBackVisible(boolean visible) { + mBack.setVisibility(visible ? VISIBLE : GONE); + } + + protected void setNextVisible(boolean visible) { + mNext.setVisibility(visible ? VISIBLE : GONE); + } + + protected void setFinishVisible(boolean visible) { + mFinish.setVisibility(visible ? VISIBLE : GONE); + } + } +} diff --git a/src/com/android/settings/AccountPreference.java b/src/com/android/settings/AccountPreference.java index f3d7d51..f76d5cb 100644 --- a/src/com/android/settings/AccountPreference.java +++ b/src/com/android/settings/AccountPreference.java @@ -71,6 +71,7 @@ public class AccountPreference extends Preference { setSummary(getSyncStatusMessage(mStatus)); mSyncStatusIcon = (ImageView) view.findViewById(R.id.syncStatusIcon); mSyncStatusIcon.setImageResource(getSyncStatusIcon(mStatus)); + mSyncStatusIcon.setContentDescription(getSyncContentDescription(mStatus)); } public void setProviderIcon(Drawable icon) { @@ -126,6 +127,20 @@ public class AccountPreference extends Preference { return res; } + private String getSyncContentDescription(int status) { + switch (status) { + case SYNC_ENABLED: + return getContext().getString(R.string.accessibility_sync_enabled); + case SYNC_DISABLED: + return getContext().getString(R.string.accessibility_sync_disabled); + case SYNC_ERROR: + return getContext().getString(R.string.accessibility_sync_error); + default: + Log.e(TAG, "Unknown sync status: " + status); + return getContext().getString(R.string.accessibility_sync_error); + } + } + @Override public int compareTo(Preference other) { if (!(other instanceof AccountPreference)) { diff --git a/src/com/android/settings/ActivityPicker.java b/src/com/android/settings/ActivityPicker.java index d984adb..ac79cea 100644 --- a/src/com/android/settings/ActivityPicker.java +++ b/src/com/android/settings/ActivityPicker.java @@ -399,6 +399,7 @@ public class ActivityPicker extends AlertActivity implements //noinspection deprecation icon = new BitmapDrawable(thumb); ((BitmapDrawable) icon).setTargetDensity(mMetrics); + canvas.setBitmap(null); } else if (iconWidth < width && iconHeight < height) { final Bitmap.Config c = Bitmap.Config.ARGB_8888; final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c); @@ -413,6 +414,7 @@ public class ActivityPicker extends AlertActivity implements //noinspection deprecation icon = new BitmapDrawable(thumb); ((BitmapDrawable) icon).setTargetDensity(mMetrics); + canvas.setBitmap(null); } } diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index 00c416f..94ba5a1 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -16,8 +16,6 @@ package com.android.settings; -import com.android.internal.telephony.PhoneStateIntentReceiver; - import android.content.Context; import android.content.Intent; import android.database.ContentObserver; @@ -27,8 +25,8 @@ import android.os.SystemProperties; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.provider.Settings; -import android.telephony.ServiceState; +import com.android.internal.telephony.PhoneStateIntentReceiver; import com.android.internal.telephony.TelephonyProperties; public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListener { @@ -93,10 +91,6 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene } private void setAirplaneModeOn(boolean enabling) { - - mCheckBoxPref.setSummary(enabling ? R.string.airplane_mode_turning_on - : R.string.airplane_mode_turning_off); - // Change the system setting Settings.System.putInt(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, enabling ? 1 : 0); @@ -118,10 +112,7 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene * - mobile does not send failure notification, fail on timeout. */ private void onAirplaneModeChanged() { - boolean airplaneModeEnabled = isAirplaneModeOn(mContext); mCheckBoxPref.setChecked(isAirplaneModeOn(mContext)); - mCheckBoxPref.setSummary(airplaneModeEnabled ? null : - mContext.getString(R.string.airplane_mode_summary)); } /** diff --git a/src/com/android/settings/ApplicationSettings.java b/src/com/android/settings/ApplicationSettings.java index da417ec..27fc3ec 100644 --- a/src/com/android/settings/ApplicationSettings.java +++ b/src/com/android/settings/ApplicationSettings.java @@ -16,21 +16,18 @@ package com.android.settings; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.res.Configuration; +import android.content.Intent; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceScreen; import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceScreen; import android.provider.Settings; -public class ApplicationSettings extends SettingsPreferenceFragment implements - DialogInterface.OnClickListener { +public class ApplicationSettings extends SettingsPreferenceFragment { - private static final String KEY_TOGGLE_INSTALL_APPLICATIONS = "toggle_install_applications"; + private static final String KEY_TOGGLE_ADVANCED_SETTINGS = "toggle_advanced_settings"; private static final String KEY_APP_INSTALL_LOCATION = "app_install_location"; // App installation location. Default is ask the user. @@ -42,20 +39,24 @@ public class ApplicationSettings extends SettingsPreferenceFragment implements private static final String APP_INSTALL_SDCARD_ID = "sdcard"; private static final String APP_INSTALL_AUTO_ID = "auto"; - private CheckBoxPreference mToggleAppInstallation; - + private CheckBoxPreference mToggleAdvancedSettings; private ListPreference mInstallLocation; - private DialogInterface mWarnInstallApps; - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.application_settings); - mToggleAppInstallation = (CheckBoxPreference) findPreference(KEY_TOGGLE_INSTALL_APPLICATIONS); - mToggleAppInstallation.setChecked(isNonMarketAppsAllowed()); + mToggleAdvancedSettings = (CheckBoxPreference)findPreference( + KEY_TOGGLE_ADVANCED_SETTINGS); + mToggleAdvancedSettings.setChecked(isAdvancedSettingsEnabled()); + getPreferenceScreen().removePreference(mToggleAdvancedSettings); + + // not ready for prime time yet + if (false) { + getPreferenceScreen().removePreference(mInstallLocation); + } mInstallLocation = (ListPreference) findPreference(KEY_APP_INSTALL_LOCATION); // Is app default install location set? @@ -94,43 +95,29 @@ public class ApplicationSettings extends SettingsPreferenceFragment implements } @Override - public void onDestroy() { - super.onDestroy(); - if (mWarnInstallApps != null) { - mWarnInstallApps.dismiss(); - } - } - - @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (preference == mToggleAppInstallation) { - if (mToggleAppInstallation.isChecked()) { - mToggleAppInstallation.setChecked(false); - warnAppInstallation(); - } else { - setNonMarketAppsAllowed(false); - } + if (preference == mToggleAdvancedSettings) { + boolean value = mToggleAdvancedSettings.isChecked(); + setAdvancedSettingsEnabled(value); } return super.onPreferenceTreeClick(preferenceScreen, preference); } - public void onClick(DialogInterface dialog, int which) { - if (dialog == mWarnInstallApps && which == DialogInterface.BUTTON_POSITIVE) { - setNonMarketAppsAllowed(true); - mToggleAppInstallation.setChecked(true); - } + private boolean isAdvancedSettingsEnabled() { + return Settings.System.getInt(getContentResolver(), + Settings.System.ADVANCED_SETTINGS, + Settings.System.ADVANCED_SETTINGS_DEFAULT) > 0; } - private void setNonMarketAppsAllowed(boolean enabled) { + private void setAdvancedSettingsEnabled(boolean enabled) { + int value = enabled ? 1 : 0; // Change the system setting - Settings.Secure.putInt(getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, - enabled ? 1 : 0); - } - - private boolean isNonMarketAppsAllowed() { - return Settings.Secure.getInt(getContentResolver(), - Settings.Secure.INSTALL_NON_MARKET_APPS, 0) > 0; + Settings.Secure.putInt(getContentResolver(), Settings.System.ADVANCED_SETTINGS, value); + // TODO: the settings thing should broadcast this for thread safety purposes. + Intent intent = new Intent(Intent.ACTION_ADVANCED_SETTINGS_CHANGED); + intent.putExtra("state", value); + getActivity().sendBroadcast(intent); } private String getAppInstallLocation() { @@ -147,15 +134,4 @@ public class ApplicationSettings extends SettingsPreferenceFragment implements return APP_INSTALL_AUTO_ID; } } - - private void warnAppInstallation() { - // TODO: DialogFragment? - mWarnInstallApps = new AlertDialog.Builder(getActivity()).setTitle( - getResources().getString(R.string.error_title)) - .setIcon(com.android.internal.R.drawable.ic_dialog_alert) - .setMessage(getResources().getString(R.string.install_all_warning)) - .setPositiveButton(android.R.string.yes, this) - .setNegativeButton(android.R.string.no, null) - .show(); - } } diff --git a/src/com/android/settings/BatteryInfo.java b/src/com/android/settings/BatteryInfo.java index 2f9d50e..d8046cf 100644 --- a/src/com/android/settings/BatteryInfo.java +++ b/src/com/android/settings/BatteryInfo.java @@ -26,7 +26,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.IPowerManager; import android.os.Message; -import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.text.format.DateUtils; @@ -91,26 +90,7 @@ public class BatteryInfo extends Activity { + getString(R.string.battery_info_temperature_units)); mTechnology.setText("" + intent.getStringExtra("technology")); - int status = intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN); - String statusString; - if (status == BatteryManager.BATTERY_STATUS_CHARGING) { - statusString = getString(R.string.battery_info_status_charging); - if (plugType > 0) { - statusString = statusString + " " + getString( - (plugType == BatteryManager.BATTERY_PLUGGED_AC) - ? R.string.battery_info_status_charging_ac - : R.string.battery_info_status_charging_usb); - } - } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { - statusString = getString(R.string.battery_info_status_discharging); - } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { - statusString = getString(R.string.battery_info_status_not_charging); - } else if (status == BatteryManager.BATTERY_STATUS_FULL) { - statusString = getString(R.string.battery_info_status_full); - } else { - statusString = getString(R.string.battery_info_status_unknown); - } - mStatus.setText(statusString); + mStatus.setText(Utils.getBatteryStatus(getResources(), intent)); switch (plugType) { case 0: @@ -198,7 +178,6 @@ public class BatteryInfo extends Activity { private void updateBatteryStats() { long uptime = SystemClock.elapsedRealtime(); mUptime.setText(DateUtils.formatElapsedTime(uptime / 1000)); - } } diff --git a/src/com/android/settings/BrightnessPreference.java b/src/com/android/settings/BrightnessPreference.java index 9bbb66a..df50ada 100644 --- a/src/com/android/settings/BrightnessPreference.java +++ b/src/com/android/settings/BrightnessPreference.java @@ -26,7 +26,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; -import android.preference.SeekBarPreference; +import android.preference.SeekBarDialogPreference; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.util.AttributeSet; @@ -35,7 +35,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.SeekBar; -public class BrightnessPreference extends SeekBarPreference implements +public class BrightnessPreference extends SeekBarDialogPreference implements SeekBar.OnSeekBarChangeListener, CheckBox.OnCheckedChangeListener { private SeekBar mSeekBar; diff --git a/src/com/android/settings/ChooseLockGeneric.java b/src/com/android/settings/ChooseLockGeneric.java index 118bc6f..8311c4a 100644 --- a/src/com/android/settings/ChooseLockGeneric.java +++ b/src/com/android/settings/ChooseLockGeneric.java @@ -27,6 +27,7 @@ import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.security.KeyStore; public class ChooseLockGeneric extends PreferenceActivity { @@ -48,9 +49,11 @@ public class ChooseLockGeneric extends PreferenceActivity { private static final int CONFIRM_EXISTING_REQUEST = 100; private static final String PASSWORD_CONFIRMED = "password_confirmed"; private static final String CONFIRM_CREDENTIALS = "confirm_credentials"; + public static final String MINIMUM_QUALITY_KEY = "minimum_quality"; private ChooseLockSettingsHelper mChooseLockSettingsHelper; private DevicePolicyManager mDPM; + private KeyStore mKeyStore; private boolean mPasswordConfirmed = false; @Override @@ -58,6 +61,7 @@ public class ChooseLockGeneric extends PreferenceActivity { super.onCreate(savedInstanceState); mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); + mKeyStore = KeyStore.getInstance(); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this.getActivity()); if (savedInstanceState != null) { @@ -126,8 +130,8 @@ public class ChooseLockGeneric extends PreferenceActivity { .getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, -1); if (quality == -1) { // If caller didn't specify password quality, show UI and allow the user to choose. - quality = mDPM.getPasswordQuality(null); - quality = upgradeQualityForEncryption(quality); + quality = getActivity().getIntent().getIntExtra(MINIMUM_QUALITY_KEY, -1); + quality = upgradeQuality(quality); final PreferenceScreen prefScreen = getPreferenceScreen(); if (prefScreen != null) { prefScreen.removeAll(); @@ -135,11 +139,26 @@ public class ChooseLockGeneric extends PreferenceActivity { addPreferencesFromResource(R.xml.security_settings_picker); disableUnusablePreferences(quality); } else { - quality = upgradeQualityForEncryption(quality); updateUnlockMethodAndFinish(quality, false); } } + private int upgradeQuality(int quality) { + quality = upgradeQualityForDPM(quality); + quality = upgradeQualityForEncryption(quality); + quality = upgradeQualityForKeyStore(quality); + return quality; + } + + private int upgradeQualityForDPM(int quality) { + // Compare min allowed password quality + int minQuality = mDPM.getPasswordQuality(null); + if (quality < minQuality) { + quality = minQuality; + } + return quality; + } + /** * Mix in "encryption minimums" to any given quality value. This prevents users * from downgrading the pattern/pin/password to a level below the minimums. @@ -152,8 +171,17 @@ public class ChooseLockGeneric extends PreferenceActivity { boolean encrypted = (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) || (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVATING); if (encrypted) { - if (quality < DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) { - quality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; + if (quality < CryptKeeperSettings.MIN_PASSWORD_QUALITY) { + quality = CryptKeeperSettings.MIN_PASSWORD_QUALITY; + } + } + return quality; + } + + private int upgradeQualityForKeyStore(int quality) { + if (!mKeyStore.isEmpty()) { + if (quality < CredentialStorage.MIN_PASSWORD_QUALITY) { + quality = CredentialStorage.MIN_PASSWORD_QUALITY; } } return quality; @@ -208,13 +236,7 @@ public class ChooseLockGeneric extends PreferenceActivity { throw new IllegalStateException("Tried to update password without confirming it"); } - // Compare min allowed password quality and launch appropriate security setting method - int minQuality = mDPM.getPasswordQuality(null); - if (quality < minQuality) { - quality = minQuality; - } - quality = upgradeQualityForEncryption(quality); - + quality = upgradeQuality(quality); if (quality >= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) { int minLength = mDPM.getPasswordMinimumLength(null); if (minLength < MIN_PASSWORD_LENGTH) { diff --git a/src/com/android/settings/ChooseLockPassword.java b/src/com/android/settings/ChooseLockPassword.java index a0f2346..96255eb 100644 --- a/src/com/android/settings/ChooseLockPassword.java +++ b/src/com/android/settings/ChooseLockPassword.java @@ -405,8 +405,10 @@ public class ChooseLockPassword extends PreferenceActivity { } public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Check if this was the result of hitting the enter key - if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN) { + // Check if this was the result of hitting the enter or "done" key + if (actionId == EditorInfo.IME_NULL + || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT) { handleNext(); return true; } diff --git a/src/com/android/settings/ChooseLockPattern.java b/src/com/android/settings/ChooseLockPattern.java index 55f6254..9a34f2f 100644 --- a/src/com/android/settings/ChooseLockPattern.java +++ b/src/com/android/settings/ChooseLockPattern.java @@ -235,7 +235,7 @@ public class ChooseLockPattern extends PreferenceActivity { Introduction( R.string.lockpattern_recording_intro_header, LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, - R.string.lockpattern_recording_intro_footer, true), + ID_EMPTY_MESSAGE, true), HelpScreen( R.string.lockpattern_settings_help_how_to_record, LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), diff --git a/src/com/android/settings/ChooseLockSettingsHelper.java b/src/com/android/settings/ChooseLockSettingsHelper.java index d31fe3b..a069712 100644 --- a/src/com/android/settings/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/ChooseLockSettingsHelper.java @@ -23,7 +23,10 @@ import android.app.Fragment; import android.app.admin.DevicePolicyManager; import android.content.Intent; -public class ChooseLockSettingsHelper { +public final class ChooseLockSettingsHelper { + + static final String EXTRA_KEY_PASSWORD = "password"; + private LockPatternUtils mLockPatternUtils; private Activity mActivity; private Fragment mFragment; @@ -49,8 +52,7 @@ public class ChooseLockSettingsHelper { * @return true if one exists and we launched an activity to confirm it * @see #onActivityResult(int, int, android.content.Intent) */ - protected boolean launchConfirmationActivity(int request, - CharSequence message, CharSequence details) { + boolean launchConfirmationActivity(int request, CharSequence message, CharSequence details) { boolean launched = false; switch (mLockPatternUtils.getKeyguardStoredPasswordQuality()) { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: diff --git a/src/com/android/settings/ConfirmLockPassword.java b/src/com/android/settings/ConfirmLockPassword.java index 3f4a4f3..1229046 100644 --- a/src/com/android/settings/ConfirmLockPassword.java +++ b/src/com/android/settings/ConfirmLockPassword.java @@ -27,13 +27,16 @@ import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceActivity; +import android.text.Editable; import android.text.InputType; +import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.Button; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; @@ -58,7 +61,7 @@ public class ConfirmLockPassword extends PreferenceActivity { } public static class ConfirmLockPasswordFragment extends Fragment implements OnClickListener, - OnEditorActionListener { + OnEditorActionListener, TextWatcher { private static final long ERROR_MESSAGE_TIMEOUT = 3000; private TextView mPasswordEntry; private LockPatternUtils mLockPatternUtils; @@ -66,6 +69,7 @@ public class ConfirmLockPassword extends PreferenceActivity { private Handler mHandler = new Handler(); private PasswordEntryKeyboardHelper mKeyboardHelper; private PasswordEntryKeyboardView mKeyboardView; + private Button mContinueButton; // required constructor for fragments @@ -87,9 +91,14 @@ public class ConfirmLockPassword extends PreferenceActivity { // Disable IME on our window since we provide our own keyboard view.findViewById(R.id.cancel_button).setOnClickListener(this); - view.findViewById(R.id.next_button).setOnClickListener(this); + mContinueButton = (Button) view.findViewById(R.id.next_button); + mContinueButton.setOnClickListener(this); + mContinueButton.setEnabled(false); // disable until the user enters at least one char + mPasswordEntry = (TextView) view.findViewById(R.id.password_entry); mPasswordEntry.setOnEditorActionListener(this); + mPasswordEntry.addTextChangedListener(this); + mKeyboardView = (PasswordEntryKeyboardView) view.findViewById(R.id.keyboard); mHeaderText = (TextView) view.findViewById(R.id.headerText); final boolean isAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality @@ -140,7 +149,7 @@ public class ConfirmLockPassword extends PreferenceActivity { if (mLockPatternUtils.checkPassword(pin)) { Intent intent = new Intent(); - intent.putExtra("password", pin); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin); getActivity().setResult(RESULT_OK, intent); getActivity().finish(); @@ -172,13 +181,27 @@ public class ConfirmLockPassword extends PreferenceActivity { }, ERROR_MESSAGE_TIMEOUT); } + // {@link OnEditorActionListener} methods. public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Check if this was the result of hitting the enter key - if (actionId == EditorInfo.IME_NULL) { + // Check if this was the result of hitting the enter or "done" key + if (actionId == EditorInfo.IME_NULL + || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT) { handleNext(); return true; } return false; } + + // {@link TextWatcher} methods. + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + mContinueButton.setEnabled(mPasswordEntry.getText().length() > 0); + } } } diff --git a/src/com/android/settings/ConfirmLockPattern.java b/src/com/android/settings/ConfirmLockPattern.java index 0653d3f..2892930 100644 --- a/src/com/android/settings/ConfirmLockPattern.java +++ b/src/com/android/settings/ConfirmLockPattern.java @@ -256,7 +256,12 @@ public class ConfirmLockPattern extends PreferenceActivity { public void onPatternDetected(List<LockPatternView.Cell> pattern) { if (mLockPatternUtils.checkPattern(pattern)) { - getActivity().setResult(Activity.RESULT_OK); + + Intent intent = new Intent(); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, + LockPatternUtils.patternToString(pattern)); + + getActivity().setResult(Activity.RESULT_OK, intent); getActivity().finish(); } else { if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL && diff --git a/src/com/android/settings/CredentialStorage.java b/src/com/android/settings/CredentialStorage.java index 9d5a603..e246fce 100644 --- a/src/com/android/settings/CredentialStorage.java +++ b/src/com/android/settings/CredentialStorage.java @@ -18,213 +18,417 @@ package com.android.settings; import android.app.Activity; import android.app.AlertDialog; +import android.app.admin.DevicePolicyManager; import android.content.DialogInterface; import android.content.Intent; +import android.content.res.Resources; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.RemoteException; +import android.security.KeyChain.KeyChainConnection; +import android.security.KeyChain; import android.security.KeyStore; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import com.android.internal.widget.LockPatternUtils; -import java.io.UnsupportedEncodingException; +/** + * CredentialStorage handles KeyStore reset, unlock, and install. + * + * CredentialStorage has a pretty convoluted state machine to migrate + * from the old style separate keystore password to a new key guard + * based password, as well as to deal with setting up the key guard if + * necessary. + * + * KeyStore: UNINITALIZED + * KeyGuard: OFF + * Action: set up key guard + * Notes: factory state + * + * KeyStore: UNINITALIZED + * KeyGuard: ON + * Action: confirm key guard + * Notes: user had key guard but no keystore and upgraded from pre-ICS + * OR user had key guard and pre-ICS keystore password which was then reset + * + * KeyStore: LOCKED + * KeyGuard: OFF/ON + * Action: old unlock dialog + * Notes: assume old password, need to use it to unlock. + * if unlock, ensure key guard before install. + * if reset, treat as UNINITALIZED/OFF + * + * KeyStore: UNLOCKED + * KeyGuard: OFF + * Action: set up key guard + * Notes: ensure key guard, then proceed + * + * KeyStore: UNLOCKED + * keyguard: ON + * Action: normal unlock/install + * Notes: this is the common case + */ +public final class CredentialStorage extends Activity { -public class CredentialStorage extends Activity implements TextWatcher, - DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private static final String TAG = "CredentialStorage"; public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK"; - public static final String ACTION_SET_PASSWORD = "com.android.credentials.SET_PASSWORD"; public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; public static final String ACTION_RESET = "com.android.credentials.RESET"; - private static final String TAG = "CredentialStorage"; + // This is the minimum acceptable password quality. If the current password quality is + // lower than this, keystore should not be activated. + static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; - private KeyStore mKeyStore = KeyStore.getInstance(); - private boolean mSubmit = false; - private Bundle mBundle; + private static final int CONFIRM_KEY_GUARD_REQUEST = 1; - private TextView mOldPassword; - private TextView mNewPassword; - private TextView mConfirmPassword; - private TextView mError; - private Button mButton; + private final KeyStore mKeyStore = KeyStore.getInstance(); - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + /** + * When non-null, the bundle containing credentials to install. + */ + private Bundle mInstallBundle; + + /** + * After unsuccessful KeyStore.unlock, the number of unlock + * attempts remaining before the KeyStore will reset itself. + * + * Reset to -1 on successful unlock or reset. + */ + private int mRetriesRemaining = -1; + + @Override protected void onResume() { + super.onResume(); Intent intent = getIntent(); String action = intent.getAction(); - int state = mKeyStore.test(); if (ACTION_RESET.equals(action)) { - showResetDialog(); - } else if (ACTION_SET_PASSWORD.equals(action)) { - showPasswordDialog(state == KeyStore.UNINITIALIZED); + new ResetDialog(); } else { if (ACTION_INSTALL.equals(action) && "com.android.certinstaller".equals(getCallingPackage())) { - mBundle = intent.getExtras(); - } - if (state == KeyStore.UNINITIALIZED) { - showPasswordDialog(true); - } else if (state == KeyStore.LOCKED) { - showUnlockDialog(); - } else { - install(); - finish(); + mInstallBundle = intent.getExtras(); } + // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL + handleUnlockOrInstall(); } } - private void install() { - if (mBundle != null && !mBundle.isEmpty()) { - try { - for (String key : mBundle.keySet()) { - byte[] value = mBundle.getByteArray(key); - if (value != null && !mKeyStore.put(key.getBytes("UTF-8"), value)) { - Log.e(TAG, "Failed to install " + key); - return; - } + /** + * Based on the current state of the KeyStore and key guard, try to + * make progress on unlocking or installing to the keystore. + */ + private void handleUnlockOrInstall() { + // something already decided we are done, do not proceed + if (isFinishing()) { + return; + } + switch (mKeyStore.state()) { + case UNINITIALIZED: { + ensureKeyGuard(); + return; + } + case LOCKED: { + new UnlockDialog(); + return; + } + case UNLOCKED: { + if (!checkKeyGuardQuality()) { + new ConfigureKeyGuardDialog(); + return; } - setResult(RESULT_OK); - } catch (UnsupportedEncodingException e) { - // Should never happen. - throw new RuntimeException(e); + installIfAvailable(); + finish(); + return; } } } - private void showResetDialog() { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(R.string.credentials_reset_hint) - .setNeutralButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - dialog.setOnDismissListener(this); - dialog.show(); + /** + * Make sure the user enters the key guard to set or change the + * keystore password. This can be used in UNINITIALIZED to set the + * keystore password or UNLOCKED to change the password (as is the + * case after unlocking with an old-style password). + */ + private void ensureKeyGuard() { + if (!checkKeyGuardQuality()) { + // key guard not setup, doing so will initialize keystore + new ConfigureKeyGuardDialog(); + // will return to onResume after Activity + return; + } + // force key guard confirmation + if (confirmKeyGuard()) { + // will return password value via onActivityResult + return; + } + finish(); } - private void showPasswordDialog(boolean firstTime) { - View view = View.inflate(this, R.layout.credentials_dialog, null); + /** + * Returns true if the currently set key guard matches our minimum quality requirements. + */ + private boolean checkKeyGuardQuality() { + int quality = new LockPatternUtils(this).getActivePasswordQuality(); + return (quality >= MIN_PASSWORD_QUALITY); + } - ((TextView) view.findViewById(R.id.hint)).setText(R.string.credentials_password_hint); - if (!firstTime) { - view.findViewById(R.id.old_password_prompt).setVisibility(View.VISIBLE); - mOldPassword = (TextView) view.findViewById(R.id.old_password); - mOldPassword.setVisibility(View.VISIBLE); - mOldPassword.addTextChangedListener(this); + /** + * Install credentials if available, otherwise do nothing. + */ + private void installIfAvailable() { + if (mInstallBundle != null && !mInstallBundle.isEmpty()) { + Bundle bundle = mInstallBundle; + mInstallBundle = null; + for (String key : bundle.keySet()) { + byte[] value = bundle.getByteArray(key); + if (value != null && !mKeyStore.put(key, value)) { + Log.e(TAG, "Failed to install " + key); + return; + } + } + setResult(RESULT_OK); } - view.findViewById(R.id.new_passwords).setVisibility(View.VISIBLE); - mNewPassword = (TextView) view.findViewById(R.id.new_password); - mNewPassword.addTextChangedListener(this); - mConfirmPassword = (TextView) view.findViewById(R.id.confirm_password); - mConfirmPassword.addTextChangedListener(this); - mError = (TextView) view.findViewById(R.id.error); - - AlertDialog dialog = new AlertDialog.Builder(this) - .setView(view) - .setTitle(R.string.credentials_set_password) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - dialog.setOnDismissListener(this); - dialog.show(); - mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - mButton.setEnabled(false); } - private void showUnlockDialog() { - View view = View.inflate(this, R.layout.credentials_dialog, null); - - ((TextView) view.findViewById(R.id.hint)).setText(R.string.credentials_unlock_hint); - mOldPassword = (TextView) view.findViewById(R.id.old_password); - mOldPassword.setVisibility(View.VISIBLE); - mOldPassword.addTextChangedListener(this); - mError = (TextView) view.findViewById(R.id.error); - - AlertDialog dialog = new AlertDialog.Builder(this) - .setView(view) - .setTitle(R.string.credentials_unlock) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - dialog.setOnDismissListener(this); - dialog.show(); - mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - mButton.setEnabled(false); - } + /** + * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. + */ + private class ResetDialog + implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener + { + private boolean mResetConfirmed; - public void afterTextChanged(Editable editable) { - if ((mOldPassword == null || mOldPassword.getText().length() > 0) && - (mNewPassword == null || mNewPassword.getText().length() >= 8) && - (mConfirmPassword == null || mConfirmPassword.getText().length() >= 8)) { - mButton.setEnabled(true); - } else { - mButton.setEnabled(false); + private ResetDialog() { + AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) + .setTitle(android.R.string.dialog_alert_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(R.string.credentials_reset_hint) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + dialog.setOnDismissListener(this); + dialog.show(); } - } - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + @Override public void onClick(DialogInterface dialog, int button) { + mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); + } - public void onTextChanged(CharSequence s,int start, int before, int count) { + @Override public void onDismiss(DialogInterface dialog) { + if (mResetConfirmed) { + mResetConfirmed = false; + new ResetKeyStoreAndKeyChain().execute(); + return; + } + finish(); + } } - public void onClick(DialogInterface dialog, int button) { - mSubmit = (button == DialogInterface.BUTTON_POSITIVE); - if (button == DialogInterface.BUTTON_NEUTRAL) { + /** + * Background task to handle reset of both keystore and user installed CAs. + */ + private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { + + @Override protected Boolean doInBackground(Void... unused) { + mKeyStore.reset(); - Toast.makeText(this, R.string.credentials_erased, Toast.LENGTH_SHORT).show(); + + try { + KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); + try { + return keyChainConnection.getService().reset(); + } catch (RemoteException e) { + return false; + } finally { + keyChainConnection.close(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + @Override protected void onPostExecute(Boolean success) { + if (success) { + Toast.makeText(CredentialStorage.this, + R.string.credentials_erased, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(CredentialStorage.this, + R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); + } + finish(); } } - public void onDismiss(DialogInterface dialog) { - if (mSubmit) { - mSubmit = false; - mError.setVisibility(View.VISIBLE); + /** + * Prompt for key guard configuration confirmation. + */ + private class ConfigureKeyGuardDialog + implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener + { + private boolean mConfigureConfirmed; - if (mNewPassword == null) { - mKeyStore.unlock(mOldPassword.getText().toString()); - } else { - String newPassword = mNewPassword.getText().toString(); - String confirmPassword = mConfirmPassword.getText().toString(); + private ConfigureKeyGuardDialog() { + AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) + .setTitle(android.R.string.dialog_alert_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(R.string.credentials_configure_lock_screen_hint) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + dialog.setOnDismissListener(this); + dialog.show(); + } + + @Override public void onClick(DialogInterface dialog, int button) { + mConfigureConfirmed = (button == DialogInterface.BUTTON_POSITIVE); + } - if (!newPassword.equals(confirmPassword)) { - mError.setText(R.string.credentials_passwords_mismatch); - ((AlertDialog) dialog).show(); + @Override public void onDismiss(DialogInterface dialog) { + if (mConfigureConfirmed) { + mConfigureConfirmed = false; + Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); + intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, + MIN_PASSWORD_QUALITY); + startActivity(intent); + return; + } + finish(); + } + } + + /** + * Confirm existing key guard, returning password via onActivityResult. + */ + private boolean confirmKeyGuard() { + Resources res = getResources(); + boolean launched = new ChooseLockSettingsHelper(this) + .launchConfirmationActivity(CONFIRM_KEY_GUARD_REQUEST, + res.getText(R.string.master_clear_gesture_prompt), + res.getText(R.string.master_clear_gesture_explanation)); + return launched; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + /** + * Receive key guard password initiated by confirmKeyGuard. + */ + if (requestCode == CONFIRM_KEY_GUARD_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + if (!TextUtils.isEmpty(password)) { + // success + mKeyStore.password(password); + // return to onResume return; - } else if (mOldPassword == null) { - mKeyStore.password(newPassword); - } else { - mKeyStore.password(mOldPassword.getText().toString(), newPassword); } } + // failed confirmation, bail + finish(); + } + } + + /** + * Prompt for unlock with old-style password. + * + * On successful unlock, ensure migration to key guard before continuing. + * On unsuccessful unlock, retry by calling handleUnlockOrInstall. + */ + private class UnlockDialog implements TextWatcher, + DialogInterface.OnClickListener, DialogInterface.OnDismissListener + { + private boolean mUnlockConfirmed; + + private final Button mButton; + private final TextView mOldPassword; + private final TextView mError; + + private UnlockDialog() { + View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null); + + CharSequence text; + if (mRetriesRemaining == -1) { + text = getResources().getText(R.string.credentials_unlock_hint); + } else if (mRetriesRemaining > 3) { + text = getResources().getText(R.string.credentials_wrong_password); + } else if (mRetriesRemaining == 1) { + text = getResources().getText(R.string.credentials_reset_warning); + } else { + text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining); + } - int error = mKeyStore.getLastError(); - if (error == KeyStore.NO_ERROR) { - Toast.makeText(this, R.string.credentials_enabled, Toast.LENGTH_SHORT).show(); - install(); - } else if (error == KeyStore.UNINITIALIZED) { - Toast.makeText(this, R.string.credentials_erased, Toast.LENGTH_SHORT).show(); - } else if (error >= KeyStore.WRONG_PASSWORD) { - int count = error - KeyStore.WRONG_PASSWORD + 1; - if (count > 3) { - mError.setText(R.string.credentials_wrong_password); - } else if (count == 1) { - mError.setText(R.string.credentials_reset_warning); - } else { - mError.setText(getString(R.string.credentials_reset_warning_plural, count)); + ((TextView) view.findViewById(R.id.hint)).setText(text); + mOldPassword = (TextView) view.findViewById(R.id.old_password); + mOldPassword.setVisibility(View.VISIBLE); + mOldPassword.addTextChangedListener(this); + mError = (TextView) view.findViewById(R.id.error); + + AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) + .setView(view) + .setTitle(R.string.credentials_unlock) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + dialog.setOnDismissListener(this); + dialog.show(); + mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + mButton.setEnabled(false); + } + + @Override public void afterTextChanged(Editable editable) { + mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0); + } + + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override public void onTextChanged(CharSequence s,int start, int before, int count) { + } + + @Override public void onClick(DialogInterface dialog, int button) { + mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE); + } + + @Override public void onDismiss(DialogInterface dialog) { + if (mUnlockConfirmed) { + mUnlockConfirmed = false; + mError.setVisibility(View.VISIBLE); + mKeyStore.unlock(mOldPassword.getText().toString()); + int error = mKeyStore.getLastError(); + if (error == KeyStore.NO_ERROR) { + mRetriesRemaining = -1; + Toast.makeText(CredentialStorage.this, + R.string.credentials_enabled, + Toast.LENGTH_SHORT).show(); + // aha, now we are unlocked, switch to key guard. + // we'll end up back in onResume to install + ensureKeyGuard(); + } else if (error == KeyStore.UNINITIALIZED) { + mRetriesRemaining = -1; + Toast.makeText(CredentialStorage.this, + R.string.credentials_erased, + Toast.LENGTH_SHORT).show(); + // we are reset, we can now set new password with key guard + handleUnlockOrInstall(); + } else if (error >= KeyStore.WRONG_PASSWORD) { + // we need to try again + mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1; + handleUnlockOrInstall(); } - ((AlertDialog) dialog).show(); return; } + finish(); } - finish(); } } diff --git a/src/com/android/settings/CryptKeeper.java b/src/com/android/settings/CryptKeeper.java index edf00d5..612f4c0 100644 --- a/src/com/android/settings/CryptKeeper.java +++ b/src/com/android/settings/CryptKeeper.java @@ -16,6 +16,7 @@ package com.android.settings; +import com.android.internal.telephony.ITelephony; import com.android.internal.widget.PasswordEntryKeyboardHelper; import com.android.internal.widget.PasswordEntryKeyboardView; @@ -33,9 +34,11 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.os.storage.IMountService; +import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -62,6 +65,9 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList private static final int COOL_DOWN_ATTEMPTS = 10; private static final int COOL_DOWN_INTERVAL = 30; // 30 seconds + // Intent action for launching the Emergency Dialer activity. + static final String ACTION_EMERGENCY_DIAL = "com.android.phone.EmergencyDialer.DIAL"; + private int mCooldown; PowerManager.WakeLock mWakeLock; private EditText mPasswordEntry; @@ -342,9 +348,13 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList KeyboardView keyboardView = (PasswordEntryKeyboardView) findViewById(R.id.keyboard); - PasswordEntryKeyboardHelper keyboardHelper = new PasswordEntryKeyboardHelper(this, - keyboardView, mPasswordEntry, false); - keyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA); + if (keyboardView != null) { + PasswordEntryKeyboardHelper keyboardHelper = new PasswordEntryKeyboardHelper(this, + keyboardView, mPasswordEntry, false); + keyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA); + } + + updateEmergencyCallButtonState(); } private IMountService getMountService() { @@ -357,7 +367,7 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_NULL) { + if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) { // Get the password String password = v.getText().toString(); @@ -379,4 +389,73 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList } return false; } -}
\ No newline at end of file + + // + // Code to update the state of, and handle clicks from, the "Emergency call" button. + // + // This code is mostly duplicated from the corresponding code in + // LockPatternUtils and LockPatternKeyguardView under frameworks/base. + // + + private void updateEmergencyCallButtonState() { + Button button = (Button) findViewById(R.id.emergencyCallButton); + // The button isn't present at all in some configurations. + if (button == null) return; + + if (isEmergencyCallCapable()) { + button.setVisibility(View.VISIBLE); + button.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + takeEmergencyCallAction(); + } + }); + } else { + button.setVisibility(View.GONE); + return; + } + + int newState = TelephonyManager.getDefault().getCallState(); + int textId; + if (newState == TelephonyManager.CALL_STATE_OFFHOOK) { + // show "return to call" text and show phone icon + textId = R.string.cryptkeeper_return_to_call; + int phoneCallIcon = R.drawable.stat_sys_phone_call; + button.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); + } else { + textId = R.string.cryptkeeper_emergency_call; + int emergencyIcon = R.drawable.ic_emergency; + button.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); + } + button.setText(textId); + } + + private boolean isEmergencyCallCapable() { + return getResources().getBoolean(com.android.internal.R.bool.config_voice_capable); + } + + private void takeEmergencyCallAction() { + if (TelephonyManager.getDefault().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) { + resumeCall(); + } else { + launchEmergencyDialer(); + } + } + + private void resumeCall() { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) { + try { + phone.showCallScreen(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelephony service: " + e); + } + } + } + + private void launchEmergencyDialer() { + Intent intent = new Intent(ACTION_EMERGENCY_DIAL); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivity(intent); + } +} diff --git a/src/com/android/settings/CryptKeeperConfirm.java b/src/com/android/settings/CryptKeeperConfirm.java index ba8ce10..d177cb4 100644 --- a/src/com/android/settings/CryptKeeperConfirm.java +++ b/src/com/android/settings/CryptKeeperConfirm.java @@ -62,6 +62,8 @@ public class CryptKeeperConfirm extends Fragment { public void run() { IBinder service = ServiceManager.getService("mount"); if (service == null) { + Log.e("CryptKeeper", "Failed to find the mount service"); + finish(); return; } diff --git a/src/com/android/settings/CryptKeeperSettings.java b/src/com/android/settings/CryptKeeperSettings.java index 10fa8ac..41a4be5 100644 --- a/src/com/android/settings/CryptKeeperSettings.java +++ b/src/com/android/settings/CryptKeeperSettings.java @@ -37,16 +37,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -/** - * Confirm and execute a reset of the device to a clean "just out of the box" - * state. Multiple confirmations are required: first, a general "are you sure - * you want to do this?" prompt, followed by a keyguard pattern trace if the user - * has defined one, followed by a final strongly-worded "THIS WILL ERASE EVERYTHING - * ON THE PHONE" prompt. If at any time the phone is allowed to go to sleep, is - * locked, et cetera, then the confirmation sequence is abandoned. - * - * This is the initial screen. - */ public class CryptKeeperSettings extends Fragment { private static final String TAG = "CryptKeeper"; @@ -54,7 +44,7 @@ public class CryptKeeperSettings extends Fragment { // This is the minimum acceptable password quality. If the current password quality is // lower than this, encryption should not be activated. - private static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; + static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; // Minimum battery charge level (in percent) to launch encryption. If the battery charge is // lower than this, encryption should not be activated. @@ -73,8 +63,14 @@ public class CryptKeeperSettings extends Fragment { if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + int invalidCharger = intent.getIntExtra(BatteryManager.EXTRA_INVALID_CHARGER, 0); + boolean levelOk = level >= MIN_BATTERY_LEVEL; - boolean pluggedOk = plugged == BatteryManager.BATTERY_PLUGGED_AC; + boolean pluggedOk = + (plugged == BatteryManager.BATTERY_PLUGGED_AC || + plugged == BatteryManager.BATTERY_PLUGGED_USB) && + invalidCharger == 0; + // Update UI elements based on power/battery status mInitiateButton.setEnabled(levelOk && pluggedOk); mPowerWarning.setVisibility(pluggedOk ? View.GONE : View.VISIBLE ); @@ -163,7 +159,7 @@ public class CryptKeeperSettings extends Fragment { */ private boolean runKeyguardConfirmation(int request) { // 1. Confirm that we have a sufficient PIN/Password to continue - int quality = new LockPatternUtils(getActivity()).getKeyguardStoredPasswordQuality(); + int quality = new LockPatternUtils(getActivity()).getActivePasswordQuality(); if (quality < MIN_PASSWORD_QUALITY) { return false; } @@ -186,7 +182,7 @@ public class CryptKeeperSettings extends Fragment { // If the user entered a valid keyguard trace, present the final // confirmation prompt; otherwise, go back to the initial state. if (resultCode == Activity.RESULT_OK && data != null) { - String password = data.getStringExtra("password"); + String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); if (!TextUtils.isEmpty(password)) { showFinalConfirmation(password); } diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java new file mode 100644 index 0000000..bccc5a5 --- /dev/null +++ b/src/com/android/settings/DataUsageSummary.java @@ -0,0 +1,1713 @@ +/* + * Copyright (C) 2011 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; + +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIMAX; +import static android.net.NetworkPolicy.LIMIT_DISABLED; +import static android.net.NetworkPolicyManager.EXTRA_NETWORK_TEMPLATE; +import static android.net.NetworkPolicyManager.POLICY_NONE; +import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; +import static android.net.NetworkPolicyManager.computeLastCycleBoundary; +import static android.net.NetworkPolicyManager.computeNextCycleBoundary; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; +import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER; +import static android.net.NetworkTemplate.MATCH_MOBILE_4G; +import static android.net.NetworkTemplate.MATCH_MOBILE_ALL; +import static android.net.NetworkTemplate.MATCH_WIFI; +import static android.net.NetworkTemplate.buildTemplateEthernet; +import static android.net.NetworkTemplate.buildTemplateMobile3gLower; +import static android.net.NetworkTemplate.buildTemplateMobile4g; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateWifi; +import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; +import static android.text.format.DateUtils.FORMAT_SHOW_DATE; +import static android.text.format.Time.TIMEZONE_UTC; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.animation.LayoutTransition; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.Loader; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.INetworkPolicyManager; +import android.net.INetworkStatsService; +import android.net.NetworkPolicy; +import android.net.NetworkPolicyManager; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.TrafficStats; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.preference.Preference; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.format.Formatter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.NumberPicker; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.Switch; +import android.widget.TabHost; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabHost.TabContentFactory; +import android.widget.TabHost.TabSpec; +import android.widget.TabWidget; +import android.widget.TextView; + +import com.android.internal.telephony.Phone; +import com.android.settings.net.NetworkPolicyEditor; +import com.android.settings.net.SummaryForAllUidLoader; +import com.android.settings.widget.DataUsageChartView; +import com.android.settings.widget.DataUsageChartView.DataUsageChartListener; +import com.google.android.collect.Lists; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Locale; + +import libcore.util.Objects; + +/** + * Panel show data usage history across various networks, including options to + * inspect based on usage cycle and control through {@link NetworkPolicy}. + */ +public class DataUsageSummary extends Fragment { + private static final String TAG = "DataUsage"; + private static final boolean LOGD = true; + + // TODO: remove this testing code + private static final boolean TEST_ANIM = false; + private static final boolean TEST_RADIOS = false; + private static final String TEST_RADIOS_PROP = "test.radios"; + + private static final String TAB_3G = "3g"; + private static final String TAB_4G = "4g"; + private static final String TAB_MOBILE = "mobile"; + private static final String TAB_WIFI = "wifi"; + private static final String TAB_ETHERNET = "ethernet"; + + private static final String TAG_CONFIRM_ROAMING = "confirmRoaming"; + private static final String TAG_CONFIRM_LIMIT = "confirmLimit"; + private static final String TAG_CYCLE_EDITOR = "cycleEditor"; + private static final String TAG_CONFIRM_RESTRICT = "confirmRestrict"; + private static final String TAG_CONFIRM_APP_RESTRICT = "confirmAppRestrict"; + private static final String TAG_APP_DETAILS = "appDetails"; + + private static final int LOADER_SUMMARY = 2; + + private static final long KB_IN_BYTES = 1024; + private static final long MB_IN_BYTES = KB_IN_BYTES * 1024; + private static final long GB_IN_BYTES = MB_IN_BYTES * 1024; + + private INetworkManagementService mNetworkService; + private INetworkStatsService mStatsService; + private INetworkPolicyManager mPolicyService; + private ConnectivityManager mConnService; + + private static final String PREF_FILE = "data_usage"; + private static final String PREF_SHOW_WIFI = "show_wifi"; + private static final String PREF_SHOW_ETHERNET = "show_ethernet"; + + private SharedPreferences mPrefs; + + private TabHost mTabHost; + private ViewGroup mTabsContainer; + private TabWidget mTabWidget; + private ListView mListView; + private DataUsageAdapter mAdapter; + + private ViewGroup mHeader; + + private ViewGroup mNetworkSwitchesContainer; + private LinearLayout mNetworkSwitches; + private Switch mDataEnabled; + private View mDataEnabledView; + private CheckBox mDisableAtLimit; + private View mDisableAtLimitView; + + private View mCycleView; + private Spinner mCycleSpinner; + private CycleAdapter mCycleAdapter; + + private DataUsageChartView mChart; + private TextView mUsageSummary; + private TextView mEmpty; + + private View mAppDetail; + private ImageView mAppIcon; + private ViewGroup mAppTitles; + private Button mAppSettings; + + private LinearLayout mAppSwitches; + private CheckBox mAppRestrict; + private View mAppRestrictView; + + private boolean mShowWifi = false; + private boolean mShowEthernet = false; + + private NetworkTemplate mTemplate = null; + + private static final int UID_NONE = -1; + private int mUid = UID_NONE; + + private Intent mAppSettingsIntent; + + private NetworkPolicyEditor mPolicyEditor; + + private NetworkStatsHistory mHistory; + private NetworkStatsHistory mDetailHistory; + + private String mCurrentTab = null; + private String mIntentTab = null; + + private MenuItem mMenuDataRoaming; + private MenuItem mMenuRestrictBackground; + + /** Flag used to ignore listeners during binding. */ + private boolean mBinding; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mNetworkService = INetworkManagementService.Stub.asInterface( + ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); + mStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + mPolicyService = INetworkPolicyManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_POLICY_SERVICE)); + mConnService = (ConnectivityManager) getActivity().getSystemService( + Context.CONNECTIVITY_SERVICE); + + mPrefs = getActivity().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); + + mPolicyEditor = new NetworkPolicyEditor(mPolicyService); + mPolicyEditor.read(); + + mShowWifi = mPrefs.getBoolean(PREF_SHOW_WIFI, false); + mShowEthernet = mPrefs.getBoolean(PREF_SHOW_ETHERNET, false); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final Context context = inflater.getContext(); + final View view = inflater.inflate(R.layout.data_usage_summary, container, false); + + mTabHost = (TabHost) view.findViewById(android.R.id.tabhost); + mTabsContainer = (ViewGroup) view.findViewById(R.id.tabs_container); + mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs); + mListView = (ListView) view.findViewById(android.R.id.list); + + mTabHost.setup(); + mTabHost.setOnTabChangedListener(mTabListener); + + mHeader = (ViewGroup) inflater.inflate(R.layout.data_usage_header, mListView, false); + mListView.addHeaderView(mHeader, null, false); + + { + // bind network switches + mNetworkSwitchesContainer = (ViewGroup) mHeader.findViewById( + R.id.network_switches_container); + mNetworkSwitches = (LinearLayout) mHeader.findViewById(R.id.network_switches); + + mDataEnabled = new Switch(inflater.getContext()); + mDataEnabledView = inflatePreference(inflater, mNetworkSwitches, mDataEnabled); + mDataEnabled.setOnCheckedChangeListener(mDataEnabledListener); + mNetworkSwitches.addView(mDataEnabledView); + + mDisableAtLimit = new CheckBox(inflater.getContext()); + mDisableAtLimit.setClickable(false); + mDisableAtLimitView = inflatePreference(inflater, mNetworkSwitches, mDisableAtLimit); + mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener); + mNetworkSwitches.addView(mDisableAtLimitView); + } + + // bind cycle dropdown + mCycleView = mHeader.findViewById(R.id.cycles); + mCycleSpinner = (Spinner) mCycleView.findViewById(R.id.cycles_spinner); + mCycleAdapter = new CycleAdapter(context); + mCycleSpinner.setAdapter(mCycleAdapter); + mCycleSpinner.setOnItemSelectedListener(mCycleListener); + + mChart = (DataUsageChartView) mHeader.findViewById(R.id.chart); + mChart.setListener(mChartListener); + + { + // bind app detail controls + mAppDetail = mHeader.findViewById(R.id.app_detail); + mAppIcon = (ImageView) mAppDetail.findViewById(R.id.app_icon); + mAppTitles = (ViewGroup) mAppDetail.findViewById(R.id.app_titles); + mAppSwitches = (LinearLayout) mAppDetail.findViewById(R.id.app_switches); + + mAppSettings = (Button) mAppDetail.findViewById(R.id.app_settings); + mAppSettings.setOnClickListener(mAppSettingsListener); + + mAppRestrict = new CheckBox(inflater.getContext()); + mAppRestrict.setClickable(false); + mAppRestrictView = inflatePreference(inflater, mAppSwitches, mAppRestrict); + setPreferenceTitle(mAppRestrictView, R.string.data_usage_app_restrict_background); + setPreferenceSummary( + mAppRestrictView, R.string.data_usage_app_restrict_background_summary); + mAppRestrictView.setOnClickListener(mAppRestrictListener); + mAppSwitches.addView(mAppRestrictView); + } + + mUsageSummary = (TextView) mHeader.findViewById(R.id.usage_summary); + mEmpty = (TextView) mHeader.findViewById(android.R.id.empty); + + // only assign layout transitions once first layout is finished + mListView.getViewTreeObserver().addOnGlobalLayoutListener(mFirstLayoutListener); + + mAdapter = new DataUsageAdapter(); + mListView.setOnItemClickListener(mListListener); + mListView.setAdapter(mAdapter); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // pick default tab based on incoming intent + final Intent intent = getActivity().getIntent(); + mIntentTab = computeTabFromIntent(intent); + + // this kicks off chain reaction which creates tabs, binds the body to + // selected network, and binds chart, cycles and detail list. + updateTabs(); + + // kick off background task to update stats + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + try { + mStatsService.forceUpdate(); + } catch (RemoteException e) { + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (isAdded()) { + updateBody(); + } + } + }.execute(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.data_usage, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + final Context context = getActivity(); + + mMenuDataRoaming = menu.findItem(R.id.data_usage_menu_roaming); + mMenuDataRoaming.setVisible(hasMobileRadio(context)); + mMenuDataRoaming.setChecked(getDataRoaming()); + + mMenuRestrictBackground = menu.findItem(R.id.data_usage_menu_restrict_background); + mMenuRestrictBackground.setChecked(getRestrictBackground()); + + final MenuItem split4g = menu.findItem(R.id.data_usage_menu_split_4g); + split4g.setVisible(hasMobile4gRadio(context)); + split4g.setChecked(isMobilePolicySplit()); + + final MenuItem showWifi = menu.findItem(R.id.data_usage_menu_show_wifi); + if (hasWifiRadio(context) && hasMobileRadio(context)) { + showWifi.setVisible(true); + showWifi.setChecked(mShowWifi); + } else { + showWifi.setVisible(false); + mShowWifi = true; + } + + final MenuItem showEthernet = menu.findItem(R.id.data_usage_menu_show_ethernet); + if (hasEthernet(context) && hasMobileRadio(context)) { + showEthernet.setVisible(true); + showEthernet.setChecked(mShowEthernet); + } else { + showEthernet.setVisible(false); + mShowEthernet = true; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.data_usage_menu_roaming: { + final boolean dataRoaming = !item.isChecked(); + if (dataRoaming) { + ConfirmDataRoamingFragment.show(this); + } else { + // no confirmation to disable roaming + setDataRoaming(false); + } + return true; + } + case R.id.data_usage_menu_restrict_background: { + final boolean restrictBackground = !item.isChecked(); + if (restrictBackground) { + ConfirmRestrictFragment.show(this); + } else { + // no confirmation to drop restriction + setRestrictBackground(false); + } + return true; + } + case R.id.data_usage_menu_split_4g: { + final boolean mobileSplit = !item.isChecked(); + setMobilePolicySplit(mobileSplit); + item.setChecked(isMobilePolicySplit()); + updateTabs(); + return true; + } + case R.id.data_usage_menu_show_wifi: { + mShowWifi = !item.isChecked(); + mPrefs.edit().putBoolean(PREF_SHOW_WIFI, mShowWifi).apply(); + item.setChecked(mShowWifi); + updateTabs(); + return true; + } + case R.id.data_usage_menu_show_ethernet: { + mShowEthernet = !item.isChecked(); + mPrefs.edit().putBoolean(PREF_SHOW_ETHERNET, mShowEthernet).apply(); + item.setChecked(mShowEthernet); + updateTabs(); + return true; + } + } + return false; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + mDataEnabledView = null; + mDisableAtLimitView = null; + } + + /** + * Listener to setup {@link LayoutTransition} after first layout pass. + */ + private OnGlobalLayoutListener mFirstLayoutListener = new OnGlobalLayoutListener() { + /** {@inheritDoc} */ + public void onGlobalLayout() { + mListView.getViewTreeObserver().removeGlobalOnLayoutListener(mFirstLayoutListener); + + mTabsContainer.setLayoutTransition(buildLayoutTransition()); + mHeader.setLayoutTransition(buildLayoutTransition()); + mNetworkSwitchesContainer.setLayoutTransition(buildLayoutTransition()); + + final LayoutTransition chartTransition = buildLayoutTransition(); + chartTransition.setStartDelay(LayoutTransition.APPEARING, 0); + chartTransition.setStartDelay(LayoutTransition.DISAPPEARING, 0); + mChart.setLayoutTransition(chartTransition); + } + }; + + private static LayoutTransition buildLayoutTransition() { + final LayoutTransition transition = new LayoutTransition(); + if (TEST_ANIM) { + transition.setDuration(1500); + } + transition.setAnimateParentHierarchy(false); + return transition; + } + + /** + * Rebuild all tabs based on {@link NetworkPolicyEditor} and + * {@link #mShowWifi}, hiding the tabs entirely when applicable. Selects + * first tab, and kicks off a full rebind of body contents. + */ + private void updateTabs() { + final Context context = getActivity(); + mTabHost.clearAllTabs(); + + final boolean mobileSplit = isMobilePolicySplit(); + if (mobileSplit && hasMobile4gRadio(context)) { + mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g)); + mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g)); + } else if (hasMobileRadio(context)) { + mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile)); + } + if (mShowWifi && hasWifiRadio(context)) { + mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi)); + } + if (mShowEthernet && hasEthernet(context)) { + mTabHost.addTab(buildTabSpec(TAB_ETHERNET, R.string.data_usage_tab_ethernet)); + } + + final boolean multipleTabs = mTabWidget.getTabCount() > 1; + mTabWidget.setVisibility(multipleTabs ? View.VISIBLE : View.GONE); + if (mIntentTab != null) { + if (Objects.equal(mIntentTab, mTabHost.getCurrentTabTag())) { + updateBody(); + } else { + mTabHost.setCurrentTabByTag(mIntentTab); + } + mIntentTab = null; + } else { + if (mTabHost.getCurrentTab() == 0) { + updateBody(); + } else { + mTabHost.setCurrentTab(0); + } + } + } + + /** + * Factory that provide empty {@link View} to make {@link TabHost} happy. + */ + private TabContentFactory mEmptyTabContent = new TabContentFactory() { + /** {@inheritDoc} */ + public View createTabContent(String tag) { + return new View(mTabHost.getContext()); + } + }; + + /** + * Build {@link TabSpec} with thin indicator, and empty content. + */ + private TabSpec buildTabSpec(String tag, int titleRes) { + final LayoutInflater inflater = LayoutInflater.from(mTabWidget.getContext()); + final View indicator = inflater.inflate( + R.layout.tab_indicator_thin_holo, mTabWidget, false); + final TextView title = (TextView) indicator.findViewById(android.R.id.title); + title.setText(titleRes); + return mTabHost.newTabSpec(tag).setIndicator(indicator).setContent(mEmptyTabContent); + } + + private OnTabChangeListener mTabListener = new OnTabChangeListener() { + /** {@inheritDoc} */ + public void onTabChanged(String tabId) { + // user changed tab; update body + updateBody(); + } + }; + + /** + * Update body content based on current tab. Loads + * {@link NetworkStatsHistory} and {@link NetworkPolicy} from system, and + * binds them to visible controls. + */ + private void updateBody() { + mBinding = true; + + final Context context = getActivity(); + final String currentTab = mTabHost.getCurrentTabTag(); + + if (currentTab == null) { + Log.w(TAG, "no tab selected; hiding body"); + mListView.setVisibility(View.GONE); + return; + } else { + mListView.setVisibility(View.VISIBLE); + } + + final boolean tabChanged = !currentTab.equals(mCurrentTab); + mCurrentTab = currentTab; + + if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab); + + if (TAB_WIFI.equals(currentTab)) { + // wifi doesn't have any controls + mDataEnabledView.setVisibility(View.GONE); + mDisableAtLimitView.setVisibility(View.GONE); + mTemplate = buildTemplateWifi(); + + } else if (TAB_ETHERNET.equals(currentTab)) { + // ethernet doesn't have any controls + mDataEnabledView.setVisibility(View.GONE); + mDisableAtLimitView.setVisibility(View.GONE); + mTemplate = buildTemplateEthernet(); + + } else { + mDataEnabledView.setVisibility(View.VISIBLE); + mDisableAtLimitView.setVisibility(View.VISIBLE); + } + + if (TAB_MOBILE.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_mobile); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_mobile_limit); + mDataEnabled.setChecked(mConnService.getMobileDataEnabled()); + mTemplate = buildTemplateMobileAll(getActiveSubscriberId(context)); + + } else if (TAB_3G.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_3g); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_3g_limit); + // TODO: bind mDataEnabled to 3G radio state + mTemplate = buildTemplateMobile3gLower(getActiveSubscriberId(context)); + + } else if (TAB_4G.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_4g); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_4g_limit); + // TODO: bind mDataEnabled to 4G radio state + mTemplate = buildTemplateMobile4g(getActiveSubscriberId(context)); + } + + try { + // load stats for current template + mHistory = mStatsService.getHistoryForNetwork( + mTemplate, FIELD_RX_BYTES | FIELD_TX_BYTES); + } catch (RemoteException e) { + // since we can't do much without policy or history, and we don't + // want to leave with half-baked UI, we bail hard. + throw new RuntimeException("problem reading network policy or stats", e); + } + + // bind chart to historical stats + mChart.bindNetworkStats(mHistory); + + // only update policy when switching tabs + updatePolicy(tabChanged); + updateAppDetail(); + + // force scroll to top of body + mListView.smoothScrollToPosition(0); + + mBinding = false; + } + + private boolean isAppDetailMode() { + return mUid != UID_NONE; + } + + /** + * Update UID details panels to match {@link #mUid}, showing or hiding them + * depending on {@link #isAppDetailMode()}. + */ + private void updateAppDetail() { + final Context context = getActivity(); + final PackageManager pm = context.getPackageManager(); + final LayoutInflater inflater = getActivity().getLayoutInflater(); + + if (isAppDetailMode()) { + mAppDetail.setVisibility(View.VISIBLE); + mCycleAdapter.setChangeVisible(false); + } else { + mAppDetail.setVisibility(View.GONE); + mCycleAdapter.setChangeVisible(true); + + // hide detail stats when not in detail mode + mChart.bindDetailNetworkStats(null); + return; + } + + // remove warning/limit sweeps while in detail mode + mChart.bindNetworkPolicy(null); + + // show icon and all labels appearing under this app + final UidDetail detail = resolveDetailForUid(context, mUid); + mAppIcon.setImageDrawable(detail.icon); + + mAppTitles.removeAllViews(); + if (detail.detailLabels != null) { + for (CharSequence label : detail.detailLabels) { + mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, label)); + } + } else { + mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, detail.label)); + } + + // enable settings button when package provides it + // TODO: target torwards entire UID instead of just first package + final String[] packageNames = pm.getPackagesForUid(mUid); + if (packageNames != null && packageNames.length > 0) { + mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE); + mAppSettingsIntent.setPackage(packageNames[0]); + mAppSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT); + + final boolean matchFound = pm.resolveActivity(mAppSettingsIntent, 0) != null; + mAppSettings.setEnabled(matchFound); + + } else { + mAppSettingsIntent = null; + mAppSettings.setEnabled(false); + } + + try { + // load stats for current uid and template + // TODO: read template from extras + mDetailHistory = mStatsService.getHistoryForUid( + mTemplate, mUid, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES); + } catch (RemoteException e) { + // since we can't do much without history, and we don't want to + // leave with half-baked UI, we bail hard. + throw new RuntimeException("problem reading network stats", e); + } + + // bind chart to historical stats + mChart.bindDetailNetworkStats(mDetailHistory); + + updateDetailData(); + + if (NetworkPolicyManager.isUidValidForPolicy(context, mUid) && !getRestrictBackground() + && isBandwidthControlEnabled()) { + mAppRestrictView.setVisibility(View.VISIBLE); + mAppRestrict.setChecked(getAppRestrictBackground()); + + } else { + mAppRestrictView.setVisibility(View.GONE); + } + + } + + private void setPolicyCycleDay(int cycleDay) { + if (LOGD) Log.d(TAG, "setPolicyCycleDay()"); + mPolicyEditor.setPolicyCycleDay(mTemplate, cycleDay); + updatePolicy(true); + } + + private void setPolicyWarningBytes(long warningBytes) { + if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); + mPolicyEditor.setPolicyWarningBytes(mTemplate, warningBytes); + updatePolicy(false); + } + + private void setPolicyLimitBytes(long limitBytes) { + if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); + mPolicyEditor.setPolicyLimitBytes(mTemplate, limitBytes); + updatePolicy(false); + } + + private boolean isNetworkPolicyModifiable() { + return isBandwidthControlEnabled() && mDataEnabled.isChecked(); + } + + private boolean isBandwidthControlEnabled() { + try { + return mNetworkService.isBandwidthControlEnabled(); + } catch (RemoteException e) { + Log.w(TAG, "problem talking with INetworkManagementService: " + e); + return false; + } + } + + private boolean getDataRoaming() { + final ContentResolver resolver = getActivity().getContentResolver(); + return Settings.Secure.getInt(resolver, Settings.Secure.DATA_ROAMING, 0) != 0; + } + + private void setDataRoaming(boolean enabled) { + // TODO: teach telephony DataConnectionTracker to watch and apply + // updates when changed. + final ContentResolver resolver = getActivity().getContentResolver(); + Settings.Secure.putInt(resolver, Settings.Secure.DATA_ROAMING, enabled ? 1 : 0); + mMenuDataRoaming.setChecked(enabled); + } + + private boolean getRestrictBackground() { + try { + return mPolicyService.getRestrictBackground(); + } catch (RemoteException e) { + Log.w(TAG, "problem talking with policy service: " + e); + return false; + } + } + + private void setRestrictBackground(boolean restrictBackground) { + if (LOGD) Log.d(TAG, "setRestrictBackground()"); + try { + mPolicyService.setRestrictBackground(restrictBackground); + mMenuRestrictBackground.setChecked(restrictBackground); + } catch (RemoteException e) { + Log.w(TAG, "problem talking with policy service: " + e); + } + } + + private boolean getAppRestrictBackground() { + final int uidPolicy; + try { + uidPolicy = mPolicyService.getUidPolicy(mUid); + } catch (RemoteException e) { + // since we can't do much without policy, we bail hard. + throw new RuntimeException("problem reading network policy", e); + } + + return (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0; + } + + private void setAppRestrictBackground(boolean restrictBackground) { + if (LOGD) Log.d(TAG, "setAppRestrictBackground()"); + try { + mPolicyService.setUidPolicy( + mUid, restrictBackground ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE); + } catch (RemoteException e) { + throw new RuntimeException("unable to save policy", e); + } + + mAppRestrict.setChecked(restrictBackground); + } + + /** + * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for + * current {@link #mTemplate}. + */ + private void updatePolicy(boolean refreshCycle) { + if (isAppDetailMode()) { + mNetworkSwitches.setVisibility(View.GONE); + // we fall through to update cycle list for detail mode + } else { + mNetworkSwitches.setVisibility(View.VISIBLE); + + // when heading back to summary without cycle refresh, kick details + // update to repopulate list. + if (!refreshCycle) { + updateDetailData(); + } + } + + final NetworkPolicy policy = mPolicyEditor.getPolicy(mTemplate); + if (isNetworkPolicyModifiable()) { + mDisableAtLimitView.setVisibility(View.VISIBLE); + mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED); + if (!isAppDetailMode()) { + mChart.bindNetworkPolicy(policy); + } + + } else { + // controls are disabled; don't bind warning/limit sweeps + mDisableAtLimitView.setVisibility(View.GONE); + mChart.bindNetworkPolicy(null); + } + + if (refreshCycle) { + // generate cycle list based on policy and available history + updateCycleList(policy); + } + } + + /** + * Rebuild {@link #mCycleAdapter} based on {@link NetworkPolicy#cycleDay} + * and available {@link NetworkStatsHistory} data. Always selects the newest + * item, updating the inspection range on {@link #mChart}. + */ + private void updateCycleList(NetworkPolicy policy) { + mCycleAdapter.clear(); + + final Context context = mCycleSpinner.getContext(); + long historyStart = mHistory.getStart(); + long historyEnd = mHistory.getEnd(); + + if (historyStart == Long.MAX_VALUE || historyEnd == Long.MIN_VALUE) { + historyStart = System.currentTimeMillis(); + historyEnd = System.currentTimeMillis(); + } + + boolean hasCycles = false; + if (policy != null) { + // find the next cycle boundary + long cycleEnd = computeNextCycleBoundary(historyEnd, policy); + + int guardCount = 0; + + // walk backwards, generating all valid cycle ranges + while (cycleEnd > historyStart) { + final long cycleStart = computeLastCycleBoundary(cycleEnd, policy); + Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs=" + + historyStart); + mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd)); + cycleEnd = cycleStart; + hasCycles = true; + + // TODO: remove this guard once we have better testing + if (guardCount++ > 50) { + Log.wtf(TAG, "stuck generating ranges for historyStart=" + historyStart + + ", historyEnd=" + historyEnd + " and policy=" + policy); + } + } + + // one last cycle entry to modify policy cycle day + mCycleAdapter.setChangePossible(isNetworkPolicyModifiable()); + } + + if (!hasCycles) { + // no valid cycles; show all data + // TODO: offer simple ranges like "last week" etc + mCycleAdapter.add(new CycleItem(context, historyStart, historyEnd)); + mCycleAdapter.setChangePossible(false); + } + + // force pick the current cycle (first item) + mCycleSpinner.setSelection(0); + mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0); + } + + private OnCheckedChangeListener mDataEnabledListener = new OnCheckedChangeListener() { + /** {@inheritDoc} */ + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mBinding) return; + + final boolean dataEnabled = isChecked; + mDataEnabled.setChecked(dataEnabled); + + final String currentTab = mCurrentTab; + if (TAB_MOBILE.equals(currentTab)) { + mConnService.setMobileDataEnabled(dataEnabled); + } + + // rebind policy to match radio state + updatePolicy(true); + } + }; + + private View.OnClickListener mDisableAtLimitListener = new View.OnClickListener() { + /** {@inheritDoc} */ + public void onClick(View v) { + final boolean disableAtLimit = !mDisableAtLimit.isChecked(); + if (disableAtLimit) { + // enabling limit; show confirmation dialog which eventually + // calls setPolicyLimitBytes() once user confirms. + ConfirmLimitFragment.show(DataUsageSummary.this); + } else { + setPolicyLimitBytes(LIMIT_DISABLED); + } + } + }; + + private View.OnClickListener mAppRestrictListener = new View.OnClickListener() { + /** {@inheritDoc} */ + public void onClick(View v) { + final boolean restrictBackground = !mAppRestrict.isChecked(); + + if (restrictBackground) { + // enabling restriction; show confirmation dialog which + // eventually calls setRestrictBackground() once user confirms. + ConfirmAppRestrictFragment.show(DataUsageSummary.this); + } else { + setAppRestrictBackground(false); + } + } + }; + + private OnClickListener mAppSettingsListener = new OnClickListener() { + /** {@inheritDoc} */ + public void onClick(View v) { + // TODO: target torwards entire UID instead of just first package + startActivity(mAppSettingsIntent); + } + }; + + private OnItemClickListener mListListener = new OnItemClickListener() { + /** {@inheritDoc} */ + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + final AppUsageItem app = (AppUsageItem) parent.getItemAtPosition(position); + AppDetailsFragment.show(DataUsageSummary.this, app.uid); + } + }; + + private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { + /** {@inheritDoc} */ + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + final CycleItem cycle = (CycleItem) parent.getItemAtPosition(position); + if (cycle instanceof CycleChangeItem) { + // show cycle editor; will eventually call setPolicyCycleDay() + // when user finishes editing. + CycleEditorFragment.show(DataUsageSummary.this); + + // reset spinner to something other than "change cycle..." + mCycleSpinner.setSelection(0); + + } else { + if (LOGD) { + Log.d(TAG, "showing cycle " + cycle + ", start=" + cycle.start + ", end=" + + cycle.end + "]"); + } + + // update chart to show selected cycle, and update detail data + // to match updated sweep bounds. + mChart.setVisibleRange(cycle.start, cycle.end); + + updateDetailData(); + } + } + + /** {@inheritDoc} */ + public void onNothingSelected(AdapterView<?> parent) { + // ignored + } + }; + + /** + * Update details based on {@link #mChart} inspection range depending on + * current mode. In network mode, updates {@link #mAdapter} with sorted list + * of applications data usage, and when {@link #isAppDetailMode()} update + * app details. + */ + private void updateDetailData() { + if (LOGD) Log.d(TAG, "updateDetailData()"); + + final long start = mChart.getInspectStart(); + final long end = mChart.getInspectEnd(); + final long now = System.currentTimeMillis(); + + final Context context = getActivity(); + final NetworkStatsHistory.Entry entry; + + if (isAppDetailMode()) { + if (mDetailHistory != null) { + entry = mDetailHistory.getValues(start, end, now, null); + } else { + entry = null; + } + + getLoaderManager().destroyLoader(LOADER_SUMMARY); + + } else { + entry = mHistory.getValues(start, end, now, null); + + // kick off loader for detailed stats + getLoaderManager().restartLoader(LOADER_SUMMARY, + SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryForAllUid); + } + + final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0; + final String totalPhrase = Formatter.formatFileSize(context, totalBytes); + final String rangePhrase = formatDateRange(context, start, end, false); + + mUsageSummary.setText( + getString(R.string.data_usage_total_during_range, totalPhrase, rangePhrase)); + } + + private final LoaderCallbacks<NetworkStats> mSummaryForAllUid = new LoaderCallbacks< + NetworkStats>() { + /** {@inheritDoc} */ + public Loader<NetworkStats> onCreateLoader(int id, Bundle args) { + return new SummaryForAllUidLoader(getActivity(), mStatsService, args); + } + + /** {@inheritDoc} */ + public void onLoadFinished(Loader<NetworkStats> loader, NetworkStats data) { + mAdapter.bindStats(data); + updateEmptyVisible(); + } + + /** {@inheritDoc} */ + public void onLoaderReset(Loader<NetworkStats> loader) { + mAdapter.bindStats(null); + updateEmptyVisible(); + } + + private void updateEmptyVisible() { + final boolean isEmpty = mAdapter.isEmpty() && !isAppDetailMode(); + mEmpty.setVisibility(isEmpty ? View.VISIBLE : View.GONE); + } + }; + + private boolean isMobilePolicySplit() { + final Context context = getActivity(); + if (hasMobileRadio(context)) { + final String subscriberId = getActiveSubscriberId(context); + return mPolicyEditor.isMobilePolicySplit(subscriberId); + } else { + return false; + } + } + + private void setMobilePolicySplit(boolean split) { + final String subscriberId = getActiveSubscriberId(getActivity()); + mPolicyEditor.setMobilePolicySplit(subscriberId, split); + } + + private static String getActiveSubscriberId(Context context) { + final TelephonyManager telephony = (TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE); + return telephony.getSubscriberId(); + } + + private DataUsageChartListener mChartListener = new DataUsageChartListener() { + /** {@inheritDoc} */ + public void onInspectRangeChanged() { + if (LOGD) Log.d(TAG, "onInspectRangeChanged()"); + updateDetailData(); + } + + /** {@inheritDoc} */ + public void onWarningChanged() { + setPolicyWarningBytes(mChart.getWarningBytes()); + } + + /** {@inheritDoc} */ + public void onLimitChanged() { + setPolicyLimitBytes(mChart.getLimitBytes()); + } + }; + + + /** + * List item that reflects a specific data usage cycle. + */ + public static class CycleItem { + public CharSequence label; + public long start; + public long end; + + CycleItem(CharSequence label) { + this.label = label; + } + + public CycleItem(Context context, long start, long end) { + this.label = formatDateRange(context, start, end, true); + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return label.toString(); + } + } + + private static final StringBuilder sBuilder = new StringBuilder(50); + private static final java.util.Formatter sFormatter = new java.util.Formatter( + sBuilder, Locale.getDefault()); + + public static String formatDateRange(Context context, long start, long end, boolean utcTime) { + final int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH; + final String timezone = utcTime ? TIMEZONE_UTC : null; + + synchronized (sBuilder) { + sBuilder.setLength(0); + return DateUtils + .formatDateRange(context, sFormatter, start, end, flags, timezone).toString(); + } + } + + /** + * Special-case data usage cycle that triggers dialog to change + * {@link NetworkPolicy#cycleDay}. + */ + public static class CycleChangeItem extends CycleItem { + public CycleChangeItem(Context context) { + super(context.getString(R.string.data_usage_change_cycle)); + } + } + + public static class CycleAdapter extends ArrayAdapter<CycleItem> { + private boolean mChangePossible = false; + private boolean mChangeVisible = false; + + private final CycleChangeItem mChangeItem; + + public CycleAdapter(Context context) { + super(context, android.R.layout.simple_spinner_item); + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mChangeItem = new CycleChangeItem(context); + } + + public void setChangePossible(boolean possible) { + mChangePossible = possible; + updateChange(); + } + + public void setChangeVisible(boolean visible) { + mChangeVisible = visible; + updateChange(); + } + + private void updateChange() { + remove(mChangeItem); + if (mChangePossible && mChangeVisible) { + add(mChangeItem); + } + } + } + + private static class AppUsageItem implements Comparable<AppUsageItem> { + public int uid; + public long total; + + /** {@inheritDoc} */ + public int compareTo(AppUsageItem another) { + return Long.compare(another.total, total); + } + } + + /** + * Adapter of applications, sorted by total usage descending. + */ + public static class DataUsageAdapter extends BaseAdapter { + private ArrayList<AppUsageItem> mItems = Lists.newArrayList(); + private long mLargest; + + /** + * Bind the given {@link NetworkStats}, or {@code null} to clear list. + */ + public void bindStats(NetworkStats stats) { + mItems.clear(); + + if (stats != null) { + final AppUsageItem systemItem = new AppUsageItem(); + systemItem.uid = android.os.Process.SYSTEM_UID; + + NetworkStats.Entry entry = null; + for (int i = 0; i < stats.size(); i++) { + entry = stats.getValues(i, entry); + + final boolean isApp = entry.uid >= android.os.Process.FIRST_APPLICATION_UID + && entry.uid <= android.os.Process.LAST_APPLICATION_UID; + if (isApp || entry.uid == TrafficStats.UID_REMOVED) { + final AppUsageItem item = new AppUsageItem(); + item.uid = entry.uid; + item.total = entry.rxBytes + entry.txBytes; + mItems.add(item); + } else { + systemItem.total += entry.rxBytes + entry.txBytes; + } + } + + if (systemItem.total > 0) { + mItems.add(systemItem); + } + } + + Collections.sort(mItems); + mLargest = (mItems.size() > 0) ? mItems.get(0).total : 0; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return mItems.get(position).uid; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()).inflate( + R.layout.data_usage_item, parent, false); + } + + final Context context = parent.getContext(); + + final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); + final TextView title = (TextView) convertView.findViewById(android.R.id.title); + final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); + final ProgressBar progress = (ProgressBar) convertView.findViewById( + android.R.id.progress); + + final AppUsageItem item = mItems.get(position); + final UidDetail detail = resolveDetailForUid(context, item.uid); + + icon.setImageDrawable(detail.icon); + title.setText(detail.label); + summary.setText(Formatter.formatFileSize(context, item.total)); + + final int percentTotal = mLargest != 0 ? (int) (item.total * 100 / mLargest) : 0; + progress.setProgress(percentTotal); + + return convertView; + } + + } + + /** + * Empty {@link Fragment} that controls display of UID details in + * {@link DataUsageSummary}. + */ + public static class AppDetailsFragment extends Fragment { + private static final String EXTRA_UID = "uid"; + + public static void show(DataUsageSummary parent, int uid) { + final Bundle args = new Bundle(); + args.putInt(EXTRA_UID, uid); + + final AppDetailsFragment fragment = new AppDetailsFragment(); + fragment.setArguments(args); + fragment.setTargetFragment(parent, 0); + + final FragmentTransaction ft = parent.getFragmentManager().beginTransaction(); + ft.add(fragment, TAG_APP_DETAILS); + ft.addToBackStack(TAG_APP_DETAILS); + ft.commit(); + } + + @Override + public void onStart() { + super.onStart(); + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + target.mUid = getArguments().getInt(EXTRA_UID); + target.updateBody(); + } + + @Override + public void onStop() { + super.onStop(); + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + target.mUid = UID_NONE; + target.updateBody(); + } + } + + /** + * Dialog to request user confirmation before setting + * {@link NetworkPolicy#limitBytes}. + */ + public static class ConfirmLimitFragment extends DialogFragment { + private static final String EXTRA_MESSAGE_ID = "messageId"; + private static final String EXTRA_LIMIT_BYTES = "limitBytes"; + + public static void show(DataUsageSummary parent) { + final Bundle args = new Bundle(); + + // TODO: customize default limits based on network template + final String currentTab = parent.mCurrentTab; + if (TAB_3G.equals(currentTab)) { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_3g); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + } else if (TAB_4G.equals(currentTab)) { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_4g); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + } else if (TAB_MOBILE.equals(currentTab)) { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_mobile); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + } + + final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); + dialog.setArguments(args); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final int messageId = getArguments().getInt(EXTRA_MESSAGE_ID); + final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.data_usage_limit_dialog_title); + builder.setMessage(messageId); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setPolicyLimitBytes(limitBytes); + } + } + }); + + return builder.create(); + } + } + + /** + * Dialog to edit {@link NetworkPolicy#cycleDay}. + */ + public static class CycleEditorFragment extends DialogFragment { + private static final String EXTRA_CYCLE_DAY = "cycleDay"; + + public static void show(DataUsageSummary parent) { + final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate); + final Bundle args = new Bundle(); + args.putInt(CycleEditorFragment.EXTRA_CYCLE_DAY, policy.cycleDay); + + final CycleEditorFragment dialog = new CycleEditorFragment(); + dialog.setArguments(args); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + + final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); + final NumberPicker cycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); + + final int oldCycleDay = getArguments().getInt(EXTRA_CYCLE_DAY, 1); + + cycleDayPicker.setMinValue(1); + cycleDayPicker.setMaxValue(31); + cycleDayPicker.setValue(oldCycleDay); + cycleDayPicker.setWrapSelectorWheel(true); + + builder.setTitle(R.string.data_usage_cycle_editor_title); + builder.setView(view); + + builder.setPositiveButton(R.string.data_usage_cycle_editor_positive, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final int cycleDay = cycleDayPicker.getValue(); + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setPolicyCycleDay(cycleDay); + } + } + }); + + return builder.create(); + } + } + + /** + * Dialog to request user confirmation before setting + * {@link Settings.Secure#DATA_ROAMING}. + */ + public static class ConfirmDataRoamingFragment extends DialogFragment { + public static void show(DataUsageSummary parent) { + final Bundle args = new Bundle(); + + final ConfirmDataRoamingFragment dialog = new ConfirmDataRoamingFragment(); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_ROAMING); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.roaming_reenable_title); + builder.setMessage(R.string.roaming_warning); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setDataRoaming(true); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + return builder.create(); + } + } + + /** + * Dialog to request user confirmation before setting + * {@link INetworkPolicyManager#setRestrictBackground(boolean)}. + */ + public static class ConfirmRestrictFragment extends DialogFragment { + public static void show(DataUsageSummary parent) { + final Bundle args = new Bundle(); + + final ConfirmRestrictFragment dialog = new ConfirmRestrictFragment(); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_RESTRICT); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.data_usage_restrict_background_title); + builder.setMessage(R.string.data_usage_restrict_background); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setRestrictBackground(true); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + return builder.create(); + } + } + + /** + * Dialog to request user confirmation before setting + * {@link #POLICY_REJECT_METERED_BACKGROUND}. + */ + public static class ConfirmAppRestrictFragment extends DialogFragment { + public static void show(DataUsageSummary parent) { + final ConfirmAppRestrictFragment dialog = new ConfirmAppRestrictFragment(); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_APP_RESTRICT); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.data_usage_app_restrict_dialog_title); + builder.setMessage(R.string.data_usage_app_restrict_dialog); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setAppRestrictBackground(true); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + return builder.create(); + } + } + + /** + * Compute default tab that should be selected, based on + * {@link NetworkPolicyManager#EXTRA_NETWORK_TEMPLATE} extra. + */ + private static String computeTabFromIntent(Intent intent) { + final NetworkTemplate template = intent.getParcelableExtra(EXTRA_NETWORK_TEMPLATE); + if (template == null) return null; + + switch (template.getMatchRule()) { + case MATCH_MOBILE_3G_LOWER: + return TAB_3G; + case MATCH_MOBILE_4G: + return TAB_4G; + case MATCH_MOBILE_ALL: + return TAB_MOBILE; + case MATCH_WIFI: + return TAB_WIFI; + default: + return null; + } + } + + public static class UidDetail { + public CharSequence label; + public CharSequence[] detailLabels; + public Drawable icon; + } + + /** + * Resolve best descriptive label for the given UID. + */ + public static UidDetail resolveDetailForUid(Context context, int uid) { + final Resources res = context.getResources(); + final PackageManager pm = context.getPackageManager(); + + final UidDetail detail = new UidDetail(); + detail.label = pm.getNameForUid(uid); + detail.icon = pm.getDefaultActivityIcon(); + + // handle special case labels + switch (uid) { + case android.os.Process.SYSTEM_UID: + detail.label = res.getString(R.string.process_kernel_label); + detail.icon = pm.getDefaultActivityIcon(); + return detail; + case TrafficStats.UID_REMOVED: + detail.label = res.getString(R.string.data_usage_uninstalled_apps); + detail.icon = pm.getDefaultActivityIcon(); + return detail; + } + + // otherwise fall back to using packagemanager labels + final String[] packageNames = pm.getPackagesForUid(uid); + final int length = packageNames != null ? packageNames.length : 0; + + try { + if (length == 1) { + final ApplicationInfo info = pm.getApplicationInfo(packageNames[0], 0); + detail.label = info.loadLabel(pm).toString(); + detail.icon = info.loadIcon(pm); + } else if (length > 1) { + detail.detailLabels = new CharSequence[length]; + for (int i = 0; i < length; i++) { + final String packageName = packageNames[i]; + final PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); + final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); + + detail.detailLabels[i] = appInfo.loadLabel(pm).toString(); + if (packageInfo.sharedUserLabel != 0) { + detail.label = pm.getText(packageName, packageInfo.sharedUserLabel, + packageInfo.applicationInfo).toString(); + detail.icon = appInfo.loadIcon(pm); + } + } + } + } catch (NameNotFoundException e) { + } + + if (TextUtils.isEmpty(detail.label)) { + detail.label = Integer.toString(uid); + } + return detail; + } + + /** + * Test if device has a mobile data radio. + */ + private static boolean hasMobileRadio(Context context) { + if (TEST_RADIOS) { + return SystemProperties.get(TEST_RADIOS_PROP).contains("mobile"); + } + + final ConnectivityManager conn = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + + // mobile devices should have MOBILE network tracker regardless of + // connection status. + return conn.getNetworkInfo(TYPE_MOBILE) != null; + } + + /** + * Test if device has a mobile 4G data radio. + */ + private static boolean hasMobile4gRadio(Context context) { + if (TEST_RADIOS) { + return SystemProperties.get(TEST_RADIOS_PROP).contains("4g"); + } + + final ConnectivityManager conn = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + final TelephonyManager telephony = (TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE); + + // WiMAX devices should have WiMAX network tracker regardless of + // connection status. + final boolean hasWimax = conn.getNetworkInfo(TYPE_WIMAX) != null; + final boolean hasLte = telephony.getLteOnCdmaMode() == Phone.LTE_ON_CDMA_TRUE; + return hasWimax || hasLte; + } + + /** + * Test if device has a Wi-Fi data radio. + */ + private static boolean hasWifiRadio(Context context) { + if (TEST_RADIOS) { + return SystemProperties.get(TEST_RADIOS_PROP).contains("wifi"); + } + + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); + } + + /** + * Test if device has an ethernet network connection. + */ + private static boolean hasEthernet(Context context) { + if (TEST_RADIOS) { + return SystemProperties.get(TEST_RADIOS_PROP).contains("ethernet"); + } + + final ConnectivityManager conn = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + return conn.getNetworkInfo(TYPE_ETHERNET) != null; + } + + /** + * Inflate a {@link Preference} style layout, adding the given {@link View} + * widget into {@link android.R.id#widget_frame}. + */ + private static View inflatePreference(LayoutInflater inflater, ViewGroup root, View widget) { + final View view = inflater.inflate(R.layout.preference, root, false); + final LinearLayout widgetFrame = (LinearLayout) view.findViewById( + android.R.id.widget_frame); + widgetFrame.addView(widget, new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + return view; + } + + private static View inflateAppTitle( + LayoutInflater inflater, ViewGroup root, CharSequence label) { + final TextView view = (TextView) inflater.inflate( + R.layout.data_usage_app_title, root, false); + view.setText(label); + return view; + } + + /** + * Set {@link android.R.id#title} for a preference view inflated with + * {@link #inflatePreference(LayoutInflater, ViewGroup, View)}. + */ + private static void setPreferenceTitle(View parent, int resId) { + final TextView title = (TextView) parent.findViewById(android.R.id.title); + title.setText(resId); + } + + /** + * Set {@link android.R.id#summary} for a preference view inflated with + * {@link #inflatePreference(LayoutInflater, ViewGroup, View)}. + */ + private static void setPreferenceSummary(View parent, int resId) { + final TextView summary = (TextView) parent.findViewById(android.R.id.summary); + summary.setVisibility(View.VISIBLE); + summary.setText(resId); + } +} diff --git a/src/com/android/settings/DateTimeSettings.java b/src/com/android/settings/DateTimeSettings.java index d2c5973..3935d59 100644 --- a/src/com/android/settings/DateTimeSettings.java +++ b/src/com/android/settings/DateTimeSettings.java @@ -88,7 +88,6 @@ public class DateTimeSettings extends SettingsPreferenceFragment boolean isFirstRun = intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false); mDummyDate = Calendar.getInstance(); - mDummyDate.set(mDummyDate.get(Calendar.YEAR), 11, 31, 13, 0, 0); mAutoTimePref = (CheckBoxPreference) findPreference(KEY_AUTO_TIME); mAutoTimePref.setChecked(autoTimeEnabled); @@ -171,6 +170,8 @@ public class DateTimeSettings extends SettingsPreferenceFragment public void updateTimeAndDateDisplay(Context context) { java.text.DateFormat shortDateFormat = DateFormat.getDateFormat(context); final Calendar now = Calendar.getInstance(); + mDummyDate.setTimeZone(now.getTimeZone()); + mDummyDate.set(now.get(Calendar.YEAR), 11, 31, 13, 0, 0); Date dummyDate = mDummyDate.getTime(); mTimePref.setSummary(DateFormat.getTimeFormat(getActivity()).format(now.getTime())); mTimeZone.setSummary(getTimeZoneText(now.getTimeZone())); @@ -353,6 +354,8 @@ public class DateTimeSettings extends SettingsPreferenceFragment c.set(Calendar.HOUR_OF_DAY, hourOfDay); c.set(Calendar.MINUTE, minute); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); long when = c.getTimeInMillis(); if (when / 1000 < Integer.MAX_VALUE) { diff --git a/src/com/android/settings/DateTimeSettingsSetupWizard.java b/src/com/android/settings/DateTimeSettingsSetupWizard.java index 4ff7fc8..e63153e 100644 --- a/src/com/android/settings/DateTimeSettingsSetupWizard.java +++ b/src/com/android/settings/DateTimeSettingsSetupWizard.java @@ -31,7 +31,6 @@ import android.preference.Preference; import android.preference.PreferenceFragment; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; -import android.text.format.DateFormat; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; @@ -46,6 +45,7 @@ import android.widget.DatePicker; import android.widget.LinearLayout; import android.widget.ListPopupWindow; import android.widget.SimpleAdapter; +import android.widget.TextView; import android.widget.TimePicker; import java.util.Calendar; @@ -136,8 +136,6 @@ public class DateTimeSettingsSetupWizard extends Activity mAutoDateTimeButton = (CompoundButton)findViewById(R.id.date_time_auto_button); mAutoDateTimeButton.setChecked(autoDateTimeEnabled); - mAutoDateTimeButton.setText(autoDateTimeEnabled ? R.string.date_time_auto_summaryOn : - R.string.date_time_auto_summaryOff); mAutoDateTimeButton.setOnCheckedChangeListener(this); mTimePicker = (TimePicker)findViewById(R.id.time_picker); diff --git a/src/com/android/settings/DefaultRingtonePreference.java b/src/com/android/settings/DefaultRingtonePreference.java index 0933d62..0801b1f 100644 --- a/src/com/android/settings/DefaultRingtonePreference.java +++ b/src/com/android/settings/DefaultRingtonePreference.java @@ -23,7 +23,6 @@ import android.media.RingtoneManager; import android.net.Uri; import android.preference.RingtonePreference; import android.util.AttributeSet; -import android.util.Config; import android.util.Log; public class DefaultRingtonePreference extends RingtonePreference { diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java index 2508454..2ca28e9 100644 --- a/src/com/android/settings/DevelopmentSettings.java +++ b/src/com/android/settings/DevelopmentSettings.java @@ -16,13 +16,20 @@ package com.android.settings; +import android.app.ActivityManagerNative; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; import android.content.DialogInterface; +import android.content.Intent; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.StrictMode; import android.os.SystemProperties; import android.preference.CheckBoxPreference; import android.preference.ListPreference; @@ -31,6 +38,8 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.preference.Preference.OnPreferenceChangeListener; import android.provider.Settings; +import android.text.TextUtils; +import android.view.IWindowManager; /* * Displays preferences for application developers. @@ -45,10 +54,37 @@ public class DevelopmentSettings extends PreferenceFragment private static final String HDCP_CHECKING_KEY = "hdcp_checking"; private static final String HDCP_CHECKING_PROPERTY = "persist.sys.hdcp_checking"; + private static final String STRICT_MODE_KEY = "strict_mode"; + private static final String POINTER_LOCATION_KEY = "pointer_location"; + private static final String SHOW_SCREEN_UPDATES_KEY = "show_screen_updates"; + private static final String SHOW_CPU_USAGE_KEY = "show_cpu_usage"; + private static final String WINDOW_ANIMATION_SCALE_KEY = "window_animation_scale"; + private static final String TRANSITION_ANIMATION_SCALE_KEY = "transition_animation_scale"; + + private static final String IMMEDIATELY_DESTROY_ACTIVITIES_KEY + = "immediately_destroy_activities"; + private static final String APP_PROCESS_LIMIT_KEY = "app_process_limit"; + + private static final String SHOW_ALL_ANRS_KEY = "show_all_anrs"; + + private IWindowManager mWindowManager; + private CheckBoxPreference mEnableAdb; private CheckBoxPreference mKeepScreenOn; private CheckBoxPreference mAllowMockLocation; + private CheckBoxPreference mStrictMode; + private CheckBoxPreference mPointerLocation; + private CheckBoxPreference mShowScreenUpdates; + private CheckBoxPreference mShowCpuUsage; + private ListPreference mWindowAnimationScale; + private ListPreference mTransitionAnimationScale; + + private CheckBoxPreference mImmediatelyDestroyActivities; + private ListPreference mAppProcessLimit; + + private CheckBoxPreference mShowAllANRs; + // To track whether Yes was clicked in the adb warning dialog private boolean mOkClicked; @@ -58,12 +94,31 @@ public class DevelopmentSettings extends PreferenceFragment public void onCreate(Bundle icicle) { super.onCreate(icicle); + mWindowManager = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + addPreferencesFromResource(R.xml.development_prefs); mEnableAdb = (CheckBoxPreference) findPreference(ENABLE_ADB); mKeepScreenOn = (CheckBoxPreference) findPreference(KEEP_SCREEN_ON); mAllowMockLocation = (CheckBoxPreference) findPreference(ALLOW_MOCK_LOCATION); + mStrictMode = (CheckBoxPreference) findPreference(STRICT_MODE_KEY); + mPointerLocation = (CheckBoxPreference) findPreference(POINTER_LOCATION_KEY); + mShowScreenUpdates = (CheckBoxPreference) findPreference(SHOW_SCREEN_UPDATES_KEY); + mShowCpuUsage = (CheckBoxPreference) findPreference(SHOW_CPU_USAGE_KEY); + mWindowAnimationScale = (ListPreference) findPreference(WINDOW_ANIMATION_SCALE_KEY); + mWindowAnimationScale.setOnPreferenceChangeListener(this); + mTransitionAnimationScale = (ListPreference) findPreference(TRANSITION_ANIMATION_SCALE_KEY); + mTransitionAnimationScale.setOnPreferenceChangeListener(this); + + mImmediatelyDestroyActivities = (CheckBoxPreference) findPreference( + IMMEDIATELY_DESTROY_ACTIVITIES_KEY); + mAppProcessLimit = (ListPreference) findPreference(APP_PROCESS_LIMIT_KEY); + mAppProcessLimit.setOnPreferenceChangeListener(this); + + mShowAllANRs = (CheckBoxPreference) findPreference( + SHOW_ALL_ANRS_KEY); + removeHdcpOptionsForProduction(); } @@ -89,6 +144,14 @@ public class DevelopmentSettings extends PreferenceFragment mAllowMockLocation.setChecked(Settings.Secure.getInt(cr, Settings.Secure.ALLOW_MOCK_LOCATION, 0) != 0); updateHdcpValues(); + updateStrictModeVisualOptions(); + updatePointerLocationOptions(); + updateFlingerOptions(); + updateCpuUsageOptions(); + updateAnimationScaleOptions(); + updateImmediatelyDestroyActivitiesOptions(); + updateAppProcessLimitOptions(); + updateShowAllANRsOptions(); } private void updateHdcpValues() { @@ -110,6 +173,182 @@ public class DevelopmentSettings extends PreferenceFragment } } + // Returns the current state of the system property that controls + // strictmode flashes. One of: + // 0: not explicitly set one way or another + // 1: on + // 2: off + private int currentStrictModeActiveIndex() { + if (TextUtils.isEmpty(SystemProperties.get(StrictMode.VISUAL_PROPERTY))) { + return 0; + } + boolean enabled = SystemProperties.getBoolean(StrictMode.VISUAL_PROPERTY, false); + return enabled ? 1 : 2; + } + + private void writeStrictModeVisualOptions() { + try { + mWindowManager.setStrictModeVisualIndicatorPreference(mStrictMode.isChecked() + ? "1" : ""); + } catch (RemoteException e) { + } + } + + private void updateStrictModeVisualOptions() { + mStrictMode.setChecked(currentStrictModeActiveIndex() == 1); + } + + private void writePointerLocationOptions() { + Settings.System.putInt(getActivity().getContentResolver(), + Settings.System.POINTER_LOCATION, mPointerLocation.isChecked() ? 1 : 0); + } + + private void updatePointerLocationOptions() { + mPointerLocation.setChecked(Settings.System.getInt(getActivity().getContentResolver(), + Settings.System.POINTER_LOCATION, 0) != 0); + } + + private void updateFlingerOptions() { + // magic communication with surface flinger. + try { + IBinder flinger = ServiceManager.getService("SurfaceFlinger"); + if (flinger != null) { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + flinger.transact(1010, data, reply, 0); + @SuppressWarnings("unused") + int showCpu = reply.readInt(); + @SuppressWarnings("unused") + int enableGL = reply.readInt(); + int showUpdates = reply.readInt(); + mShowScreenUpdates.setChecked(showUpdates != 0); + @SuppressWarnings("unused") + int showBackground = reply.readInt(); + reply.recycle(); + data.recycle(); + } + } catch (RemoteException ex) { + } + } + + private void writeFlingerOptions() { + try { + IBinder flinger = ServiceManager.getService("SurfaceFlinger"); + if (flinger != null) { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + data.writeInt(mShowScreenUpdates.isChecked() ? 1 : 0); + flinger.transact(1002, data, null, 0); + data.recycle(); + + updateFlingerOptions(); + } + } catch (RemoteException ex) { + } + } + + private void updateCpuUsageOptions() { + mShowCpuUsage.setChecked(Settings.System.getInt(getActivity().getContentResolver(), + Settings.System.SHOW_PROCESSES, 0) != 0); + } + + private void writeCpuUsageOptions() { + boolean value = mShowCpuUsage.isChecked(); + Settings.System.putInt(getActivity().getContentResolver(), + Settings.System.SHOW_PROCESSES, value ? 1 : 0); + Intent service = (new Intent()) + .setClassName("com.android.systemui", "com.android.systemui.LoadAverageService"); + if (value) { + getActivity().startService(service); + } else { + getActivity().stopService(service); + } + } + + private void writeImmediatelyDestroyActivitiesOptions() { + try { + ActivityManagerNative.getDefault().setAlwaysFinish( + mImmediatelyDestroyActivities.isChecked()); + } catch (RemoteException ex) { + } + } + + private void updateImmediatelyDestroyActivitiesOptions() { + mImmediatelyDestroyActivities.setChecked(Settings.System.getInt( + getActivity().getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES, 0) != 0); + } + + private void updateAnimationScaleValue(int which, ListPreference pref) { + try { + float scale = mWindowManager.getAnimationScale(which); + CharSequence[] values = pref.getEntryValues(); + for (int i=0; i<values.length; i++) { + float val = Float.parseFloat(values[i].toString()); + if (scale <= val) { + pref.setValueIndex(i); + pref.setSummary(pref.getEntries()[i]); + return; + } + } + pref.setValueIndex(values.length-1); + pref.setSummary(pref.getEntries()[0]); + } catch (RemoteException e) { + } + } + + private void updateAnimationScaleOptions() { + updateAnimationScaleValue(0, mWindowAnimationScale); + updateAnimationScaleValue(1, mTransitionAnimationScale); + } + + private void writeAnimationScaleOption(int which, ListPreference pref, Object newValue) { + try { + float scale = Float.parseFloat(newValue.toString()); + mWindowManager.setAnimationScale(which, scale); + updateAnimationScaleValue(which, pref); + } catch (RemoteException e) { + } + } + + private void updateAppProcessLimitOptions() { + try { + int limit = ActivityManagerNative.getDefault().getProcessLimit(); + CharSequence[] values = mAppProcessLimit.getEntryValues(); + for (int i=0; i<values.length; i++) { + int val = Integer.parseInt(values[i].toString()); + if (val >= limit) { + mAppProcessLimit.setValueIndex(i); + mAppProcessLimit.setSummary(mAppProcessLimit.getEntries()[i]); + return; + } + } + mAppProcessLimit.setValueIndex(0); + mAppProcessLimit.setSummary(mAppProcessLimit.getEntries()[0]); + } catch (RemoteException e) { + } + } + + private void writeAppProcessLimitOptions(Object newValue) { + try { + int limit = Integer.parseInt(newValue.toString()); + ActivityManagerNative.getDefault().setProcessLimit(limit); + updateAppProcessLimitOptions(); + } catch (RemoteException e) { + } + } + + private void writeShowAllANRsOptions() { + Settings.Secure.putInt(getActivity().getContentResolver(), + Settings.Secure.ANR_SHOW_BACKGROUND, + mShowAllANRs.isChecked() ? 1 : 0); + } + + private void updateShowAllANRsOptions() { + mShowAllANRs.setChecked(Settings.Secure.getInt( + getActivity().getContentResolver(), Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0); + } + @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { @@ -142,11 +381,42 @@ public class DevelopmentSettings extends PreferenceFragment Settings.Secure.putInt(getActivity().getContentResolver(), Settings.Secure.ALLOW_MOCK_LOCATION, mAllowMockLocation.isChecked() ? 1 : 0); + } else if (preference == mStrictMode) { + writeStrictModeVisualOptions(); + } else if (preference == mPointerLocation) { + writePointerLocationOptions(); + } else if (preference == mShowScreenUpdates) { + writeFlingerOptions(); + } else if (preference == mShowCpuUsage) { + writeCpuUsageOptions(); + } else if (preference == mImmediatelyDestroyActivities) { + writeImmediatelyDestroyActivitiesOptions(); + } else if (preference == mShowAllANRs) { + writeShowAllANRsOptions(); } return false; } + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (HDCP_CHECKING_KEY.equals(preference.getKey())) { + SystemProperties.set(HDCP_CHECKING_PROPERTY, newValue.toString()); + updateHdcpValues(); + return true; + } else if (preference == mWindowAnimationScale) { + writeAnimationScaleOption(0, mWindowAnimationScale, newValue); + return true; + } else if (preference == mTransitionAnimationScale) { + writeAnimationScaleOption(1, mTransitionAnimationScale, newValue); + return true; + } else if (preference == mAppProcessLimit) { + writeAppProcessLimitOptions(newValue); + return true; + } + return false; + } + private void dismissDialog() { if (mOkDialog == null) return; mOkDialog.dismiss(); @@ -176,14 +446,4 @@ public class DevelopmentSettings extends PreferenceFragment dismissDialog(); super.onDestroy(); } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (HDCP_CHECKING_KEY.equals(preference.getKey())) { - SystemProperties.set(HDCP_CHECKING_PROPERTY, newValue.toString()); - updateHdcpValues(); - return true; - } - return false; - } } diff --git a/src/com/android/settings/DeviceInfoSettings.java b/src/com/android/settings/DeviceInfoSettings.java index d041c05..76f5a8e 100644 --- a/src/com/android/settings/DeviceInfoSettings.java +++ b/src/com/android/settings/DeviceInfoSettings.java @@ -40,7 +40,7 @@ import java.util.regex.Pattern; public class DeviceInfoSettings extends SettingsPreferenceFragment { - private static final String TAG = "DeviceInfoSettings"; + private static final String LOG_TAG = "DeviceInfoSettings"; private static final String KEY_CONTAINER = "container"; private static final String KEY_TEAM = "team"; @@ -55,8 +55,6 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { private static final String KEY_DEVICE_MODEL = "device_model"; private static final String KEY_BASEBAND_VERSION = "baseband_version"; private static final String KEY_FIRMWARE_VERSION = "firmware_version"; - private static final String KEY_EQUIPMENT_ID = "fcc_equipment_id"; - private static final String PROPERTY_EQUIPMENT_ID = "ro.ril.fccid"; long[] mHits = new long[3]; @@ -80,7 +78,6 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { setStringSummary(KEY_FIRMWARE_VERSION, Build.VERSION.RELEASE); findPreference(KEY_FIRMWARE_VERSION).setEnabled(true); setValueSummary(KEY_BASEBAND_VERSION, "gsm.version.baseband"); - setValueSummary(KEY_EQUIPMENT_ID, PROPERTY_EQUIPMENT_ID); setStringSummary(KEY_DEVICE_MODEL, Build.MODEL); setStringSummary(KEY_BUILD_NUMBER, Build.DISPLAY); findPreference(KEY_KERNEL_VERSION).setSummary(getFormattedKernelVersion()); @@ -89,11 +86,6 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { removePreferenceIfPropertyMissing(getPreferenceScreen(), "safetylegal", PROPERTY_URL_SAFETYLEGAL); - // Remove Equipment id preference if FCC ID is not set by RIL - removePreferenceIfPropertyMissing(getPreferenceScreen(), KEY_EQUIPMENT_ID, - PROPERTY_EQUIPMENT_ID); - - // Remove Baseband version if wifi-only device if (Utils.isWifiOnly()) { getPreferenceScreen().removePreference(findPreference(KEY_BASEBAND_VERSION)); @@ -136,6 +128,7 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { try { startActivity(intent); } catch (Exception e) { + Log.e(LOG_TAG, "Unable to start activity " + intent.toString()); } } } @@ -150,7 +143,7 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { try { preferenceGroup.removePreference(findPreference(preference)); } catch (RuntimeException e) { - Log.d(TAG, "Property '" + property + "' missing and no '" + Log.d(LOG_TAG, "Property '" + property + "' missing and no '" + preference + "' preference"); } } @@ -171,7 +164,7 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { SystemProperties.get(property, getResources().getString(R.string.device_info_default))); } catch (RuntimeException e) { - + // No recovery } } @@ -200,10 +193,10 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { Matcher m = p.matcher(procVersionStr); if (!m.matches()) { - Log.e(TAG, "Regex did not match on /proc/version: " + procVersionStr); + Log.e(LOG_TAG, "Regex did not match on /proc/version: " + procVersionStr); return "Unavailable"; } else if (m.groupCount() < 4) { - Log.e(TAG, "Regex match on /proc/version only returned " + m.groupCount() + Log.e(LOG_TAG, "Regex match on /proc/version only returned " + m.groupCount() + " groups"); return "Unavailable"; } else { @@ -212,7 +205,7 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment { .append(m.group(4))).toString(); } } catch (IOException e) { - Log.e(TAG, + Log.e(LOG_TAG, "IO Exception when getting kernel version for Device Info screen", e); diff --git a/src/com/android/settings/Display.java b/src/com/android/settings/Display.java index f90e0f0..fa29318 100644 --- a/src/com/android/settings/Display.java +++ b/src/com/android/settings/Display.java @@ -105,7 +105,7 @@ public class Display extends Activity implements View.OnClickListener { public void onClick(View v) { try { - ActivityManagerNative.getDefault().updateConfiguration(mCurConfig); + ActivityManagerNative.getDefault().updatePersistentConfiguration(mCurConfig); } catch (RemoteException e) { } finish(); diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java index cdb0147..7520ab3 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -18,21 +18,22 @@ package com.android.settings; import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; +import android.app.ActivityManagerNative; import android.app.admin.DevicePolicyManager; import android.content.ContentResolver; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.database.ContentObserver; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; -import android.os.ServiceManager; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceScreen; import android.provider.Settings; import android.util.Log; -import android.view.IWindowManager; import java.util.ArrayList; @@ -44,15 +45,14 @@ public class DisplaySettings extends SettingsPreferenceFragment implements private static final int FALLBACK_SCREEN_TIMEOUT_VALUE = 30000; private static final String KEY_SCREEN_TIMEOUT = "screen_timeout"; - private static final String KEY_ANIMATIONS = "animations"; private static final String KEY_ACCELEROMETER = "accelerometer"; + private static final String KEY_FONT_SIZE = "font_size"; - private ListPreference mAnimations; private CheckBoxPreference mAccelerometer; - private float[] mAnimationScales; - - private IWindowManager mWindowManager; + private ListPreference mFontSizePref; + private final Configuration mCurConfig = new Configuration(); + private ListPreference mScreenTimeoutPreference; private ContentObserver mAccelerometerRotationObserver = new ContentObserver(new Handler()) { @@ -66,12 +66,9 @@ public class DisplaySettings extends SettingsPreferenceFragment implements public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ContentResolver resolver = getActivity().getContentResolver(); - mWindowManager = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); addPreferencesFromResource(R.xml.display_settings); - mAnimations = (ListPreference) findPreference(KEY_ANIMATIONS); - mAnimations.setOnPreferenceChangeListener(this); mAccelerometer = (CheckBoxPreference) findPreference(KEY_ACCELEROMETER); mAccelerometer.setPersistent(false); @@ -81,22 +78,41 @@ public class DisplaySettings extends SettingsPreferenceFragment implements mScreenTimeoutPreference.setValue(String.valueOf(currentTimeout)); mScreenTimeoutPreference.setOnPreferenceChangeListener(this); disableUnusableTimeouts(mScreenTimeoutPreference); - updateTimeoutPreferenceDescription(resolver, currentTimeout); + updateTimeoutPreferenceDescription(mScreenTimeoutPreference, + R.string.screen_timeout_summary, currentTimeout); + + mFontSizePref = (ListPreference) findPreference(KEY_FONT_SIZE); + mFontSizePref.setOnPreferenceChangeListener(this); } - private void updateTimeoutPreferenceDescription(ContentResolver resolver, long currentTimeout) { - final CharSequence[] entries = mScreenTimeoutPreference.getEntries(); - final CharSequence[] values = mScreenTimeoutPreference.getEntryValues(); - int best = 0; - for (int i = 0; i < values.length; i++) { - long timeout = Long.valueOf(values[i].toString()); - if (currentTimeout >= timeout) { - best = i; + private void updateTimeoutPreferenceDescription( + ListPreference pref, + int summaryStrings, + long currentTimeout) { + updateTimeoutPreferenceDescription(pref, summaryStrings, 0, currentTimeout); + } + + private void updateTimeoutPreferenceDescription( + ListPreference pref, + int summaryStrings, + int zeroString, + long currentTimeout) { + String summary; + if (currentTimeout == 0) { + summary = pref.getContext().getString(zeroString); + } else { + final CharSequence[] entries = pref.getEntries(); + final CharSequence[] values = pref.getEntryValues(); + int best = 0; + for (int i = 0; i < values.length; i++) { + long timeout = Long.valueOf(values[i].toString()); + if (currentTimeout >= timeout) { + best = i; + } } + summary = pref.getContext().getString(summaryStrings, entries[best]); } - String summary = mScreenTimeoutPreference.getContext() - .getString(R.string.screen_timeout_summary, entries[best]); - mScreenTimeoutPreference.setSummary(summary); + pref.setSummary(summary); } private void disableUnusableTimeouts(ListPreference screenTimeoutPreference) { @@ -135,11 +151,42 @@ public class DisplaySettings extends SettingsPreferenceFragment implements screenTimeoutPreference.setEnabled(revisedEntries.size() > 0); } + int floatToIndex(float val) { + String[] indices = getResources().getStringArray(R.array.entryvalues_font_size); + float lastVal = Float.parseFloat(indices[0]); + for (int i=1; i<indices.length; i++) { + float thisVal = Float.parseFloat(indices[i]); + if (val < (lastVal + (thisVal-lastVal)*.5f)) { + return i-1; + } + lastVal = thisVal; + } + return indices.length-1; + } + + public void readFontSizePreference(ListPreference pref) { + try { + mCurConfig.updateFrom(ActivityManagerNative.getDefault().getConfiguration()); + } catch (RemoteException e) { + Log.w(TAG, "Unable to retrieve font size"); + } + + // mark the appropriate item in the preferences list + int index = floatToIndex(mCurConfig.fontScale); + pref.setValueIndex(index); + + // report the current size in the summary text + final Resources res = getResources(); + String[] fontSizeNames = res.getStringArray(R.array.entries_font_size); + pref.setSummary(String.format(res.getString(R.string.summary_font_size), + fontSizeNames[index])); + } + @Override public void onResume() { super.onResume(); - updateState(true); + updateState(); getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), true, mAccelerometerRotationObserver); @@ -152,33 +199,9 @@ public class DisplaySettings extends SettingsPreferenceFragment implements getContentResolver().unregisterContentObserver(mAccelerometerRotationObserver); } - private void updateState(boolean force) { - int animations = 0; - try { - mAnimationScales = mWindowManager.getAnimationScales(); - } catch (RemoteException e) { - } - if (mAnimationScales != null) { - if (mAnimationScales.length >= 1) { - animations = ((int)(mAnimationScales[0]+.5f)) % 10; - } - if (mAnimationScales.length >= 2) { - animations += (((int)(mAnimationScales[1]+.5f)) & 0x7) * 10; - } - } - int idx = 0; - int best = 0; - CharSequence[] aents = mAnimations.getEntryValues(); - for (int i=0; i<aents.length; i++) { - int val = Integer.parseInt(aents[i].toString()); - if (val <= animations && val > best) { - best = val; - idx = i; - } - } - mAnimations.setValueIndex(idx); - updateAnimationsSummary(mAnimations.getValue()); + private void updateState() { updateAccelerometerRotationCheckbox(); + readFontSizePreference(mFontSizePref); } private void updateAccelerometerRotationCheckbox() { @@ -187,19 +210,15 @@ public class DisplaySettings extends SettingsPreferenceFragment implements Settings.System.ACCELEROMETER_ROTATION, 0) != 0); } - private void updateAnimationsSummary(Object value) { - CharSequence[] summaries = getResources().getTextArray(R.array.animations_summaries); - CharSequence[] values = mAnimations.getEntryValues(); - for (int i=0; i<values.length; i++) { - //Log.i("foo", "Comparing entry "+ values[i] + " to current " - // + mAnimations.getValue()); - if (values[i].equals(value)) { - mAnimations.setSummary(summaries[i]); - break; - } + public void writeFontSizePreference(Object objValue) { + try { + mCurConfig.fontScale = Float.parseFloat(objValue.toString()); + ActivityManagerNative.getDefault().updatePersistentConfiguration(mCurConfig); + } catch (RemoteException e) { + Log.w(TAG, "Unable to save font size"); } } - + @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { if (preference == mAccelerometer) { @@ -207,40 +226,25 @@ public class DisplaySettings extends SettingsPreferenceFragment implements Settings.System.ACCELEROMETER_ROTATION, mAccelerometer.isChecked() ? 1 : 0); } - return true; + return super.onPreferenceTreeClick(preferenceScreen, preference); } public boolean onPreferenceChange(Preference preference, Object objValue) { final String key = preference.getKey(); - if (KEY_ANIMATIONS.equals(key)) { - try { - int value = Integer.parseInt((String) objValue); - if (mAnimationScales.length >= 1) { - mAnimationScales[0] = value%10; - } - if (mAnimationScales.length >= 2) { - mAnimationScales[1] = (value/10)%10; - } - try { - mWindowManager.setAnimationScales(mAnimationScales); - } catch (RemoteException e) { - } - updateAnimationsSummary(objValue); - } catch (NumberFormatException e) { - Log.e(TAG, "could not persist animation setting", e); - } - - } if (KEY_SCREEN_TIMEOUT.equals(key)) { int value = Integer.parseInt((String) objValue); try { Settings.System.putInt(getContentResolver(), SCREEN_OFF_TIMEOUT, value); - updateTimeoutPreferenceDescription(getContentResolver(), value); + updateTimeoutPreferenceDescription(mScreenTimeoutPreference, + R.string.screen_timeout_summary, value); } catch (NumberFormatException e) { Log.e(TAG, "could not persist screen timeout setting", e); } } + if (KEY_FONT_SIZE.equals(key)) { + writeFontSizePreference(objValue); + } return true; } diff --git a/src/com/android/settings/DreamComponentPreference.java b/src/com/android/settings/DreamComponentPreference.java new file mode 100644 index 0000000..3bc0eb4 --- /dev/null +++ b/src/com/android/settings/DreamComponentPreference.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2011 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; + +import static android.provider.Settings.Secure.DREAM_COMPONENT; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.preference.Preference; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class DreamComponentPreference extends Preference { + private static final String TAG = "DreamComponentPreference"; + + private final PackageManager pm; + private final ContentResolver resolver; + + public DreamComponentPreference(Context context, AttributeSet attrs) { + super(context, attrs); + pm = getContext().getPackageManager(); + resolver = getContext().getContentResolver(); + + refreshFromSettings(); + } + + private void refreshFromSettings() { + String component = Settings.Secure.getString(resolver, DREAM_COMPONENT); + if (component != null) { + ComponentName cn = ComponentName.unflattenFromString(component); + try { + setSummary(pm.getActivityInfo(cn, 0).loadLabel(pm)); + } catch (PackageManager.NameNotFoundException ex) { + setSummary("(unknown)"); + } + } + } + + public class DreamListAdapter extends BaseAdapter implements ListAdapter { + private ArrayList<ResolveInfo> results; + private final LayoutInflater inflater; + + public DreamListAdapter(Context context) { + Intent choosy = new Intent(Intent.ACTION_MAIN) + .addCategory("android.intent.category.DREAM"); + + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + results = new ArrayList<ResolveInfo>(pm.queryIntentActivities(choosy, 0)); + } + + @Override + public int getCount() { + return results.size(); + } + + @Override + public Object getItem(int position) { + return results.get(position); + } + + @Override + public long getItemId (int position) { + return (long) position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row = (convertView != null) + ? convertView + : inflater.inflate(R.layout.dream_picker_row, parent, false); + ResolveInfo ri = results.get(position); + ((TextView)row.findViewById(R.id.title)).setText(ri.loadLabel(pm)); + ((ImageView)row.findViewById(R.id.icon)).setImageDrawable(ri.loadIcon(pm)); + return row; + } + } + + @Override + protected void onClick() { + final DreamListAdapter list = new DreamListAdapter(getContext()); + AlertDialog alert = new AlertDialog.Builder(getContext()) + .setAdapter( + list, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ResolveInfo ri = (ResolveInfo)list.getItem(which); + ActivityInfo act = ri.activityInfo; + ComponentName cn = new ComponentName( + act.applicationInfo.packageName, + act.name); + Intent intent = new Intent(Intent.ACTION_MAIN).setComponent(cn); + + setSummary(ri.loadLabel(pm)); + //getContext().startActivity(intent); + + Settings.Secure.putString(resolver, DREAM_COMPONENT, cn.flattenToString()); + } + }) + .create(); + alert.show(); + } +} diff --git a/src/com/android/settings/DreamSettings.java b/src/com/android/settings/DreamSettings.java new file mode 100644 index 0000000..7ddab31 --- /dev/null +++ b/src/com/android/settings/DreamSettings.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010 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; + +import static android.provider.Settings.Secure.DREAM_TIMEOUT; + +import android.app.ActivityManagerNative; +import android.app.admin.DevicePolicyManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Configuration; +import android.database.ContentObserver; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.util.Log; +import android.view.IWindowManager; + +import java.util.ArrayList; + +public class DreamSettings extends SettingsPreferenceFragment implements + Preference.OnPreferenceChangeListener { + private static final String TAG = "DreamSettings"; + + private static final String KEY_DREAM_TIMEOUT = "dream_timeout"; + + private ListPreference mScreenTimeoutPreference; + private ListPreference mDreamTimeoutPreference; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ContentResolver resolver = getActivity().getContentResolver(); + + addPreferencesFromResource(R.xml.dream_settings); + + mDreamTimeoutPreference = (ListPreference) findPreference(KEY_DREAM_TIMEOUT); + final long currentSaverTimeout = Settings.Secure.getLong(resolver, DREAM_TIMEOUT, + 0); + mDreamTimeoutPreference.setValue(String.valueOf(currentSaverTimeout)); + mDreamTimeoutPreference.setOnPreferenceChangeListener(this); + updateTimeoutPreferenceDescription(resolver, mDreamTimeoutPreference, + R.string.dream_timeout_summary, + R.string.dream_timeout_zero_summary, + currentSaverTimeout); + } + + private void updateTimeoutPreferenceDescription( + ContentResolver resolver, + ListPreference pref, + int summaryStrings, + long currentTimeout) { + updateTimeoutPreferenceDescription(resolver, pref, summaryStrings, 0, currentTimeout); + } + private void updateTimeoutPreferenceDescription( + ContentResolver resolver, + ListPreference pref, + int summaryStrings, + int zeroString, + long currentTimeout) { + String summary; + if (currentTimeout == 0) { + summary = pref.getContext().getString(zeroString); + } else { + final CharSequence[] entries = pref.getEntries(); + final CharSequence[] values = pref.getEntryValues(); + int best = 0; + for (int i = 0; i < values.length; i++) { + long timeout = Long.valueOf(values[i].toString()); + if (currentTimeout >= timeout) { + best = i; + } + } + summary = pref.getContext().getString(summaryStrings, entries[best]); + } + pref.setSummary(summary); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + } + + public boolean onPreferenceChange(Preference preference, Object objValue) { + final String key = preference.getKey(); + if (KEY_DREAM_TIMEOUT.equals(key)) { + int value = Integer.parseInt((String) objValue); + try { + Settings.Secure.putInt(getContentResolver(), + DREAM_TIMEOUT, value); + updateTimeoutPreferenceDescription(getContentResolver(), + mDreamTimeoutPreference, + R.string.dream_timeout_summary, + R.string.dream_timeout_zero_summary, + value); + } catch (NumberFormatException e) { + Log.e(TAG, "could not persist dream timeout setting", e); + } + } + + return true; + } +} diff --git a/src/com/android/settings/DreamTesterPreference.java b/src/com/android/settings/DreamTesterPreference.java new file mode 100644 index 0000000..7ff5667 --- /dev/null +++ b/src/com/android/settings/DreamTesterPreference.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 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; + +import static android.provider.Settings.Secure.DREAM_COMPONENT; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.preference.Preference; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class DreamTesterPreference extends Preference { + private static final String TAG = "DreamTesterPreference"; + + private final PackageManager pm; + private final ContentResolver resolver; + + public DreamTesterPreference(Context context, AttributeSet attrs) { + super(context, attrs); + pm = getContext().getPackageManager(); + resolver = getContext().getContentResolver(); + } + + @Override + protected void onClick() { + String component = Settings.Secure.getString(resolver, DREAM_COMPONENT); + if (component != null) { + ComponentName cn = ComponentName.unflattenFromString(component); + Intent intent = new Intent(Intent.ACTION_MAIN) + .setComponent(cn) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_SINGLE_TOP) + .putExtra("android.dreams.TEST", true); + getContext().startActivity(intent); + } + } +} diff --git a/src/com/android/settings/GoogleLocationSettingHelper.java b/src/com/android/settings/GoogleLocationSettingHelper.java index 0d4861e..be4a02c 100644 --- a/src/com/android/settings/GoogleLocationSettingHelper.java +++ b/src/com/android/settings/GoogleLocationSettingHelper.java @@ -122,6 +122,7 @@ public class GoogleLocationSettingHelper { try { context.startActivity(i); } catch (ActivityNotFoundException e) { + Log.e("GoogleLocationSettingHelper", "Problem while starting GSF location activity"); } } diff --git a/src/com/android/settings/LocationSettings.java b/src/com/android/settings/LocationSettings.java new file mode 100644 index 0000000..0824aab --- /dev/null +++ b/src/com/android/settings/LocationSettings.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2011 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; + + +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.location.LocationManager; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceScreen; +import android.provider.Settings; + +import java.util.Observable; +import java.util.Observer; + +/** + * Gesture lock pattern settings. + */ +public class LocationSettings extends SettingsPreferenceFragment + implements OnPreferenceChangeListener { + + // Location Settings + private static final String KEY_LOCATION_NETWORK = "location_network"; + private static final String KEY_LOCATION_GPS = "location_gps"; + private static final String KEY_ASSISTED_GPS = "assisted_gps"; + private static final String KEY_USE_LOCATION = "location_use_for_services"; + + private CheckBoxPreference mNetwork; + private CheckBoxPreference mGps; + private CheckBoxPreference mAssistedGps; + private CheckBoxPreference mUseLocation; + + // These provide support for receiving notification when Location Manager settings change. + // This is necessary because the Network Location Provider can change settings + // if the user does not confirm enabling the provider. + private ContentQueryMap mContentQueryMap; + + private Observer mSettingsObserver; + + @Override + public void onStart() { + super.onStart(); + // listen for Location Manager settings changes + Cursor settingsCursor = getContentResolver().query(Settings.Secure.CONTENT_URI, null, + "(" + Settings.System.NAME + "=?)", + new String[]{Settings.Secure.LOCATION_PROVIDERS_ALLOWED}, + null); + mContentQueryMap = new ContentQueryMap(settingsCursor, Settings.System.NAME, true, null); + } + + @Override + public void onStop() { + super.onStop(); + if (mSettingsObserver != null) { + mContentQueryMap.deleteObserver(mSettingsObserver); + } + } + + private PreferenceScreen createPreferenceHierarchy() { + PreferenceScreen root = getPreferenceScreen(); + if (root != null) { + root.removeAll(); + } + addPreferencesFromResource(R.xml.location_settings); + root = getPreferenceScreen(); + + mNetwork = (CheckBoxPreference) root.findPreference(KEY_LOCATION_NETWORK); + mGps = (CheckBoxPreference) root.findPreference(KEY_LOCATION_GPS); + mAssistedGps = (CheckBoxPreference) root.findPreference(KEY_ASSISTED_GPS); + if (GoogleLocationSettingHelper.isAvailable(getActivity())) { + // GSF present, Add setting for 'Use My Location' + CheckBoxPreference useLocation = new CheckBoxPreference(getActivity()); + useLocation.setKey(KEY_USE_LOCATION); + useLocation.setTitle(R.string.use_location_title); + useLocation.setSummary(R.string.use_location_summary); + useLocation.setChecked( + GoogleLocationSettingHelper.getUseLocationForServices(getActivity()) + == GoogleLocationSettingHelper.USE_LOCATION_FOR_SERVICES_ON); + useLocation.setPersistent(false); + useLocation.setOnPreferenceChangeListener(this); + getPreferenceScreen().addPreference(useLocation); + mUseLocation = useLocation; + } + + // Change the summary for wifi-only devices + if (Utils.isWifiOnly()) { + mNetwork.setSummaryOn(R.string.location_neighborhood_level_wifi); + } + + return root; + } + + @Override + public void onResume() { + super.onResume(); + + // Make sure we reload the preference hierarchy since some of these settings + // depend on others... + createPreferenceHierarchy(); + updateLocationToggles(); + + if (mSettingsObserver == null) { + mSettingsObserver = new Observer() { + public void update(Observable o, Object arg) { + updateLocationToggles(); + } + }; + mContentQueryMap.addObserver(mSettingsObserver); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + + if (preference == mNetwork) { + Settings.Secure.setLocationProviderEnabled(getContentResolver(), + LocationManager.NETWORK_PROVIDER, mNetwork.isChecked()); + } else if (preference == mGps) { + boolean enabled = mGps.isChecked(); + Settings.Secure.setLocationProviderEnabled(getContentResolver(), + LocationManager.GPS_PROVIDER, enabled); + if (mAssistedGps != null) { + mAssistedGps.setEnabled(enabled); + } + } else if (preference == mAssistedGps) { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.ASSISTED_GPS_ENABLED, + mAssistedGps.isChecked() ? 1 : 0); + } else { + // If we didn't handle it, let preferences handle it. + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + + return true; + } + + /* + * Creates toggles for each available location provider + */ + private void updateLocationToggles() { + ContentResolver res = getContentResolver(); + boolean gpsEnabled = Settings.Secure.isLocationProviderEnabled( + res, LocationManager.GPS_PROVIDER); + mNetwork.setChecked(Settings.Secure.isLocationProviderEnabled( + res, LocationManager.NETWORK_PROVIDER)); + mGps.setChecked(gpsEnabled); + if (mAssistedGps != null) { + mAssistedGps.setChecked(Settings.Secure.getInt(res, + Settings.Secure.ASSISTED_GPS_ENABLED, 2) == 1); + mAssistedGps.setEnabled(gpsEnabled); + } + } + + /** + * see confirmPatternThenDisableAndClear + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + createPreferenceHierarchy(); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + if (preference == mUseLocation) { + boolean newValue = (value == null ? false : (Boolean) value); + GoogleLocationSettingHelper.setUseLocationForServices(getActivity(), newValue); + // We don't want to change the value immediately here, since the user may click + // disagree in the dialog that pops up. When the activity we just launched exits, this + // activity will be restated and the new value re-read, so the checkbox will get its + // new value then. + return false; + } + return true; + } +} diff --git a/src/com/android/settings/MasterClear.java b/src/com/android/settings/MasterClear.java index 1b045ea..29a92b1 100644 --- a/src/com/android/settings/MasterClear.java +++ b/src/com/android/settings/MasterClear.java @@ -196,13 +196,14 @@ public class MasterClear extends Fragment { + " type=" + account.type); continue; } - Drawable icon; + Drawable icon = null; try { - Context authContext = context.createPackageContext(desc.packageName, 0); - icon = authContext.getResources().getDrawable(desc.iconId); + if (desc.iconId != 0) { + Context authContext = context.createPackageContext(desc.packageName, 0); + icon = authContext.getResources().getDrawable(desc.iconId); + } } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No icon for account type " + desc.type); - icon = null; } TextView child = (TextView)inflater.inflate(R.layout.master_clear_account, diff --git a/src/com/android/settings/PointerSpeedPreference.java b/src/com/android/settings/PointerSpeedPreference.java index e9704fd..6a6a71f 100644 --- a/src/com/android/settings/PointerSpeedPreference.java +++ b/src/com/android/settings/PointerSpeedPreference.java @@ -25,7 +25,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; -import android.preference.SeekBarPreference; +import android.preference.SeekBarDialogPreference; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.util.AttributeSet; @@ -33,7 +33,7 @@ import android.view.IWindowManager; import android.view.View; import android.widget.SeekBar; -public class PointerSpeedPreference extends SeekBarPreference implements +public class PointerSpeedPreference extends SeekBarDialogPreference implements SeekBar.OnSeekBarChangeListener { private SeekBar mSeekBar; diff --git a/src/com/android/settings/PrivacySettings.java b/src/com/android/settings/PrivacySettings.java index 28dc93a..6759500 100644 --- a/src/com/android/settings/PrivacySettings.java +++ b/src/com/android/settings/PrivacySettings.java @@ -43,11 +43,13 @@ public class PrivacySettings extends SettingsPreferenceFragment implements private static final String BACKUP_DATA = "backup_data"; private static final String AUTO_RESTORE = "auto_restore"; private static final String CONFIGURE_ACCOUNT = "configure_account"; + private static final String LOCAL_BACKUP_PASSWORD = "local_backup_password"; private IBackupManager mBackupManager; private CheckBoxPreference mBackup; private CheckBoxPreference mAutoRestore; private Dialog mConfirmDialog; private PreferenceScreen mConfigure; + private PreferenceScreen mPassword; private static final int DIALOG_ERASE_BACKUP = 2; private int mDialogType; @@ -64,6 +66,7 @@ public class PrivacySettings extends SettingsPreferenceFragment implements mBackup = (CheckBoxPreference) screen.findPreference(BACKUP_DATA); mAutoRestore = (CheckBoxPreference) screen.findPreference(AUTO_RESTORE); mConfigure = (PreferenceScreen) screen.findPreference(CONFIGURE_ACCOUNT); + mPassword = (PreferenceScreen) screen.findPreference(LOCAL_BACKUP_PASSWORD); // Vendor specific if (getActivity().getPackageManager(). @@ -158,7 +161,9 @@ public class PrivacySettings extends SettingsPreferenceFragment implements mConfigure.setEnabled(configureEnabled); mConfigure.setIntent(configIntent); setConfigureSummary(configSummary); - } + + updatePasswordSummary(); +} private void setConfigureSummary(String summary) { if (summary != null) { @@ -178,6 +183,18 @@ public class PrivacySettings extends SettingsPreferenceFragment implements } } + private void updatePasswordSummary() { + try { + if (mBackupManager.hasBackupPassword()) { + mPassword.setSummary(R.string.local_backup_password_summary_change); + } else { + mPassword.setSummary(R.string.local_backup_password_summary_none); + } + } catch (RemoteException e) { + // Not much we can do here + } + } + public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { //updateProviders(); diff --git a/src/com/android/settings/ProgressCategory.java b/src/com/android/settings/ProgressCategory.java index c5b68b6..eee19bc 100644 --- a/src/com/android/settings/ProgressCategory.java +++ b/src/com/android/settings/ProgressCategory.java @@ -17,13 +17,15 @@ package com.android.settings; import android.content.Context; +import android.preference.Preference; import android.util.AttributeSet; import android.view.View; +import android.widget.TextView; public class ProgressCategory extends ProgressCategoryBase { private boolean mProgress = false; - private View oldView = null; + private Preference mNoDeviceFoundPreference; public ProgressCategory(Context context, AttributeSet attrs) { super(context, attrs); @@ -33,19 +35,27 @@ public class ProgressCategory extends ProgressCategoryBase { @Override public void onBindView(View view) { super.onBindView(view); - final View textView = view.findViewById(R.id.scanning_text); + final TextView textView = (TextView) view.findViewById(R.id.scanning_text); final View progressBar = view.findViewById(R.id.scanning_progress); - final int visibility = mProgress ? View.VISIBLE : View.INVISIBLE; - textView.setVisibility(visibility); - progressBar.setVisibility(visibility); + textView.setText(mProgress ? R.string.progress_scanning : R.string.progress_tap_to_pair); + boolean noDeviceFound = getPreferenceCount() == 0; + textView.setVisibility(noDeviceFound ? View.INVISIBLE : View.VISIBLE); + progressBar.setVisibility(mProgress ? View.VISIBLE : View.INVISIBLE); - if (oldView != null) { - oldView.findViewById(R.id.scanning_progress).setVisibility(View.GONE); - oldView.findViewById(R.id.scanning_text).setVisibility(View.GONE); - oldView.setVisibility(View.GONE); + if (mProgress) { + if (mNoDeviceFoundPreference != null) { + removePreference(mNoDeviceFoundPreference); + } + } else { + if (noDeviceFound) { + if (mNoDeviceFoundPreference == null) { + mNoDeviceFoundPreference = new Preference(getContext()); + mNoDeviceFoundPreference.setSummary(R.string.bluetooth_no_devices_found); + } + addPreference(mNoDeviceFoundPreference); + } } - oldView = view; } @Override diff --git a/src/com/android/settings/RadioInfo.java b/src/com/android/settings/RadioInfo.java index ba07fb5..d45616e 100644 --- a/src/com/android/settings/RadioInfo.java +++ b/src/com/android/settings/RadioInfo.java @@ -24,11 +24,11 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.LinkProperties; +import android.net.TrafficStats; import android.net.Uri; import android.os.AsyncResult; import android.os.Bundle; import android.os.Handler; -import android.os.INetStatService; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; @@ -133,7 +133,6 @@ public class RadioInfo extends Activity { private TelephonyManager mTelephonyManager; private Phone phone = null; private PhoneStateIntentReceiver mPhoneStateReceiver; - private INetStatService netstat; private String mPingIpAddrResult; private String mPingHostnameResult; @@ -311,8 +310,6 @@ public class RadioInfo extends Activity { phone.getNeighboringCids( mHandler.obtainMessage(EVENT_QUERY_NEIGHBORING_CIDS_DONE)); - netstat = INetStatService.Stub.asInterface(ServiceManager.getService("netstat")); - CellLocation.requestLocationUpdate(); } @@ -625,19 +622,16 @@ public class RadioInfo extends Activity { private final void updateDataStats2() { Resources r = getResources(); - try { - long txPackets = netstat.getMobileTxPackets(); - long rxPackets = netstat.getMobileRxPackets(); - long txBytes = netstat.getMobileTxBytes(); - long rxBytes = netstat.getMobileRxBytes(); + long txPackets = TrafficStats.getMobileTxPackets(); + long rxPackets = TrafficStats.getMobileRxPackets(); + long txBytes = TrafficStats.getMobileTxBytes(); + long rxBytes = TrafficStats.getMobileRxBytes(); - String packets = r.getString(R.string.radioInfo_display_packets); - String bytes = r.getString(R.string.radioInfo_display_bytes); + String packets = r.getString(R.string.radioInfo_display_packets); + String bytes = r.getString(R.string.radioInfo_display_bytes); - sent.setText(txPackets + " " + packets + ", " + txBytes + " " + bytes); - received.setText(rxPackets + " " + packets + ", " + rxBytes + " " + bytes); - } catch (RemoteException e) { - } + sent.setText(txPackets + " " + packets + ", " + txBytes + " " + bytes); + received.setText(rxPackets + " " + packets + ", " + rxBytes + " " + bytes); } /** diff --git a/src/com/android/settings/RingerVolumePreference.java b/src/com/android/settings/RingerVolumePreference.java index b546265..a626903 100644 --- a/src/com/android/settings/RingerVolumePreference.java +++ b/src/com/android/settings/RingerVolumePreference.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; +import android.media.AudioSystem; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -36,6 +37,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.preference.VolumePreference; import android.provider.Settings; +import android.provider.Settings.System; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; @@ -51,49 +53,46 @@ import android.widget.TextView; * Special preference type that allows configuration of both the ring volume and * notification volume. */ -public class RingerVolumePreference extends VolumePreference implements - CheckBox.OnCheckedChangeListener, OnClickListener { +public class RingerVolumePreference extends VolumePreference implements OnClickListener { private static final String TAG = "RingerVolumePreference"; private static final int MSG_RINGER_MODE_CHANGED = 101; - private CheckBox mNotificationsUseRingVolumeCheckbox; private SeekBarVolumizer [] mSeekBarVolumizer; private boolean mIgnoreVolumeKeys; // These arrays must all match in length and order private static final int[] SEEKBAR_ID = new int[] { - R.id.notification_volume_seekbar, R.id.media_volume_seekbar, + R.id.ringer_volume_seekbar, + R.id.notification_volume_seekbar, R.id.alarm_volume_seekbar }; - private static final int[] NEED_VOICE_CAPABILITY_ID = new int[] { - R.id.ringtone_label, - com.android.internal.R.id.seekbar, - R.id.same_notification_volume - }; - private static final int[] SEEKBAR_TYPE = new int[] { - AudioManager.STREAM_NOTIFICATION, AudioManager.STREAM_MUSIC, + AudioManager.STREAM_RING, + AudioManager.STREAM_NOTIFICATION, AudioManager.STREAM_ALARM }; private static final int[] CHECKBOX_VIEW_ID = new int[] { + R.id.media_mute_button, + R.id.ringer_mute_button, R.id.notification_mute_button, - R.id.volume_mute_button, R.id.alarm_mute_button }; private static final int[] SEEKBAR_MUTED_RES_ID = new int[] { - com.android.internal.R.drawable.ic_audio_notification_mute, com.android.internal.R.drawable.ic_audio_vol_mute, + com.android.internal.R.drawable.ic_audio_ring_notif_mute, + com.android.internal.R.drawable.ic_audio_notification_mute, com.android.internal.R.drawable.ic_audio_alarm_mute }; private static final int[] SEEKBAR_UNMUTED_RES_ID = new int[] { - com.android.internal.R.drawable.ic_audio_notification, com.android.internal.R.drawable.ic_audio_vol, + com.android.internal.R.drawable.ic_audio_ring_notif, + com.android.internal.R.drawable.ic_audio_notification, com.android.internal.R.drawable.ic_audio_alarm }; @@ -167,22 +166,15 @@ public class RingerVolumePreference extends VolumePreference implements } } - //mNotificationVolumeTitle = (TextView) view.findViewById(R.id.notification_volume_title); - mNotificationsUseRingVolumeCheckbox = - (CheckBox) view.findViewById(R.id.same_notification_volume); - mNotificationsUseRingVolumeCheckbox.setOnCheckedChangeListener(this); - mNotificationsUseRingVolumeCheckbox.setChecked( - Utils.isVoiceCapable(getContext()) - && Settings.System.getInt( - getContext().getContentResolver(), - Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1) == 1); - setNotificationVolumeVisibility(!mNotificationsUseRingVolumeCheckbox.isChecked()); - disableSettingsThatNeedVoice(view); - + final int silentableStreams = System.getInt(getContext().getContentResolver(), + System.MODE_RINGER_STREAMS_AFFECTED, + ((1 << AudioSystem.STREAM_NOTIFICATION) | (1 << AudioSystem.STREAM_RING))); // Register callbacks for mute/unmute buttons for (int i = 0; i < mCheckBoxes.length; i++) { ImageView checkbox = (ImageView) view.findViewById(CHECKBOX_VIEW_ID[i]); - checkbox.setOnClickListener(this); + if ((silentableStreams & (1 << SEEKBAR_TYPE[i])) != 0) { + checkbox.setOnClickListener(this); + } mCheckBoxes[i] = checkbox; } @@ -197,13 +189,23 @@ public class RingerVolumePreference extends VolumePreference implements public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_RINGER_MODE_CHANGED, - intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1), 0)); + mHandler.sendMessage(mHandler.obtainMessage(MSG_RINGER_MODE_CHANGED, intent + .getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1), 0)); } } }; getContext().registerReceiver(mRingModeChangedReceiver, filter); } + + // Disable either ringer+notifications or notifications + int id; + if (!Utils.isVoiceCapable(getContext())) { + id = R.id.ringer_section; + } else { + id = R.id.notification_section; + } + View hideSection = view.findViewById(id); + hideSection.setVisibility(View.GONE); } private Uri getMediaVolumeUri(Context context) { @@ -212,15 +214,6 @@ public class RingerVolumePreference extends VolumePreference implements + "/" + R.raw.media_volume); } - private void disableSettingsThatNeedVoice(View parent) { - final boolean voiceCapable = Utils.isVoiceCapable(getContext()); - if (!voiceCapable) { - for (int id : NEED_VOICE_CAPABILITY_ID) { - parent.findViewById(id).setVisibility(View.GONE); - } - } - } - @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); @@ -236,20 +229,9 @@ public class RingerVolumePreference extends VolumePreference implements @Override public void onActivityStop() { super.onActivityStop(); - cleanup(); - } - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - setNotificationVolumeVisibility(!isChecked); - - Settings.System.putInt(getContext().getContentResolver(), - Settings.System.NOTIFICATIONS_USE_RING_VOLUME, isChecked ? 1 : 0); - - if (isChecked) { - // The user wants the notification to be same as ring, so do a - // one-time sync right now - mAudioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, - mAudioManager.getStreamVolume(AudioManager.STREAM_RING), 0); + for (SeekBarVolumizer vol : mSeekBarVolumizer) { + if (vol != null) vol.stopSample(); } } @@ -278,14 +260,6 @@ public class RingerVolumePreference extends VolumePreference implements } } - private void setNotificationVolumeVisibility(boolean visible) { - if (mSeekBarVolumizer[0] != null) { - mSeekBarVolumizer[0].getSeekBar().setVisibility( - visible ? View.VISIBLE : View.GONE); - } - // mNotificationVolumeTitle.setVisibility(visible ? View.VISIBLE : View.GONE); - } - private void cleanup() { for (int i = 0; i < SEEKBAR_ID.length; i++) { if (mSeekBarVolumizer[i] != null) { diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index dc4c42b..9a83311 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -19,15 +19,11 @@ package com.android.settings; import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; -import com.android.internal.widget.LockPatternUtils; - +import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; -import android.content.ContentQueryMap; -import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; -import android.database.Cursor; -import android.location.LocationManager; import android.os.Bundle; import android.os.Vibrator; import android.preference.CheckBoxPreference; @@ -41,15 +37,15 @@ import android.security.KeyStore; import android.telephony.TelephonyManager; import android.util.Log; +import com.android.internal.widget.LockPatternUtils; + import java.util.ArrayList; -import java.util.Observable; -import java.util.Observer; /** * Gesture lock pattern settings. */ public class SecuritySettings extends SettingsPreferenceFragment - implements OnPreferenceChangeListener { + implements OnPreferenceChangeListener, DialogInterface.OnClickListener { // Lock Settings private static final String KEY_UNLOCK_SET_OR_CHANGE = "unlock_set_or_change"; @@ -60,47 +56,28 @@ public class SecuritySettings extends SettingsPreferenceFragment private static final String KEY_LOCK_AFTER_TIMEOUT = "lock_after_timeout"; private static final int SET_OR_CHANGE_LOCK_METHOD_REQUEST = 123; - // Location Settings - private static final String KEY_LOCATION_CATEGORY = "location_category"; - private static final String KEY_LOCATION_NETWORK = "location_network"; - private static final String KEY_LOCATION_GPS = "location_gps"; - private static final String KEY_ASSISTED_GPS = "assisted_gps"; - private static final String KEY_USE_LOCATION = "location_use_for_services"; - // Misc Settings private static final String KEY_SIM_LOCK = "sim_lock"; private static final String KEY_SHOW_PASSWORD = "show_password"; - private static final String KEY_ENABLE_CREDENTIALS = "enable_credentials"; private static final String KEY_RESET_CREDENTIALS = "reset_credentials"; - - private static final String TAG = "SecuritySettings"; - - private CheckBoxPreference mNetwork; - private CheckBoxPreference mGps; - private CheckBoxPreference mAssistedGps; - private CheckBoxPreference mUseLocation; + private static final String KEY_TOGGLE_INSTALL_APPLICATIONS = "toggle_install_applications"; DevicePolicyManager mDPM; - // These provide support for receiving notification when Location Manager settings change. - // This is necessary because the Network Location Provider can change settings - // if the user does not confirm enabling the provider. - private ContentQueryMap mContentQueryMap; - private ChooseLockSettingsHelper mChooseLockSettingsHelper; private LockPatternUtils mLockPatternUtils; private ListPreference mLockAfter; - private Observer mSettingsObserver; - private CheckBoxPreference mVisiblePattern; private CheckBoxPreference mTactileFeedback; private CheckBoxPreference mShowPassword; - private CheckBoxPreference mEnableCredentials; private Preference mResetCredentials; + private CheckBoxPreference mToggleAppInstallation; + private DialogInterface mWarnInstallApps; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -112,25 +89,6 @@ public class SecuritySettings extends SettingsPreferenceFragment mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); } - @Override - public void onStart() { - super.onStart(); - // listen for Location Manager settings changes - Cursor settingsCursor = getContentResolver().query(Settings.Secure.CONTENT_URI, null, - "(" + Settings.System.NAME + "=?)", - new String[]{Settings.Secure.LOCATION_PROVIDERS_ALLOWED}, - null); - mContentQueryMap = new ContentQueryMap(settingsCursor, Settings.System.NAME, true, null); - } - - @Override - public void onStop() { - super.onStop(); - if (mSettingsObserver != null) { - mContentQueryMap.deleteObserver(mSettingsObserver); - } - } - private PreferenceScreen createPreferenceHierarchy() { PreferenceScreen root = getPreferenceScreen(); if (root != null) { @@ -139,32 +97,6 @@ public class SecuritySettings extends SettingsPreferenceFragment addPreferencesFromResource(R.xml.security_settings); root = getPreferenceScreen(); - mNetwork = (CheckBoxPreference) root.findPreference(KEY_LOCATION_NETWORK); - mGps = (CheckBoxPreference) root.findPreference(KEY_LOCATION_GPS); - mAssistedGps = (CheckBoxPreference) root.findPreference(KEY_ASSISTED_GPS); - if (GoogleLocationSettingHelper.isAvailable(getActivity())) { - // GSF present, Add setting for 'Use My Location' - PreferenceGroup locationCat = - (PreferenceGroup) root.findPreference(KEY_LOCATION_CATEGORY); - CheckBoxPreference useLocation = new CheckBoxPreference(getActivity()); - useLocation.setKey(KEY_USE_LOCATION); - useLocation.setTitle(R.string.use_location_title); - useLocation.setSummaryOn(R.string.use_location_summary_enabled); - useLocation.setSummaryOff(R.string.use_location_summary_disabled); - useLocation.setChecked( - GoogleLocationSettingHelper.getUseLocationForServices(getActivity()) - == GoogleLocationSettingHelper.USE_LOCATION_FOR_SERVICES_ON); - useLocation.setPersistent(false); - useLocation.setOnPreferenceChangeListener(this); - locationCat.addPreference(useLocation); - mUseLocation = useLocation; - } - - // Change the summary for wifi-only devices - if (Utils.isWifiOnly()) { - mNetwork.setSummaryOn(R.string.location_neighborhood_level_wifi); - } - // Add options for lock/unlock screen int resid = 0; if (!mLockPatternUtils.isSecure()) { @@ -239,13 +171,52 @@ public class SecuritySettings extends SettingsPreferenceFragment mShowPassword = (CheckBoxPreference) root.findPreference(KEY_SHOW_PASSWORD); // Credential storage - mEnableCredentials = (CheckBoxPreference) root.findPreference(KEY_ENABLE_CREDENTIALS); - mEnableCredentials.setOnPreferenceChangeListener(this); mResetCredentials = root.findPreference(KEY_RESET_CREDENTIALS); + mToggleAppInstallation = (CheckBoxPreference) findPreference( + KEY_TOGGLE_INSTALL_APPLICATIONS); + mToggleAppInstallation.setChecked(isNonMarketAppsAllowed()); + return root; } + private boolean isNonMarketAppsAllowed() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.INSTALL_NON_MARKET_APPS, 0) > 0; + } + + private void setNonMarketAppsAllowed(boolean enabled) { + // Change the system setting + Settings.Secure.putInt(getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, + enabled ? 1 : 0); + } + + private void warnAppInstallation() { + // TODO: DialogFragment? + mWarnInstallApps = new AlertDialog.Builder(getActivity()).setTitle( + getResources().getString(R.string.error_title)) + .setIcon(com.android.internal.R.drawable.ic_dialog_alert) + .setMessage(getResources().getString(R.string.install_all_warning)) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + public void onClick(DialogInterface dialog, int which) { + if (dialog == mWarnInstallApps && which == DialogInterface.BUTTON_POSITIVE) { + setNonMarketAppsAllowed(true); + mToggleAppInstallation.setChecked(true); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mWarnInstallApps != null) { + mWarnInstallApps.dismiss(); + } + } + private void setupLockAfterPreference() { // Compatible with pre-Froyo long currentTimeout = Settings.Secure.getLong(getContentResolver(), @@ -266,7 +237,7 @@ public class SecuritySettings extends SettingsPreferenceFragment private void updateLockAfterPreferenceSummary() { // Update summary message with current value long currentTimeout = Settings.Secure.getLong(getContentResolver(), - Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, 0); + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, 5000); final CharSequence[] entries = mLockAfter.getEntries(); final CharSequence[] values = mLockAfter.getEntryValues(); int best = 0; @@ -315,16 +286,6 @@ public class SecuritySettings extends SettingsPreferenceFragment // Make sure we reload the preference hierarchy since some of these settings // depend on others... createPreferenceHierarchy(); - updateLocationToggles(); - - if (mSettingsObserver == null) { - mSettingsObserver = new Observer() { - public void update(Observable o, Object arg) { - updateLocationToggles(); - } - }; - mContentQueryMap.addObserver(mSettingsObserver); - } final LockPatternUtils lockPatternUtils = mChooseLockSettingsHelper.utils(); if (mVisiblePattern != null) { @@ -337,15 +298,12 @@ public class SecuritySettings extends SettingsPreferenceFragment mShowPassword.setChecked(Settings.System.getInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1) != 0); - int state = KeyStore.getInstance().test(); - mEnableCredentials.setChecked(state == KeyStore.NO_ERROR); - mEnableCredentials.setEnabled(state != KeyStore.UNINITIALIZED); - mResetCredentials.setEnabled(state != KeyStore.UNINITIALIZED); + KeyStore.State state = KeyStore.getInstance().state(); + mResetCredentials.setEnabled(state != KeyStore.State.UNINITIALIZED); } @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { final String key = preference.getKey(); final LockPatternUtils lockPatternUtils = mChooseLockSettingsHelper.utils(); @@ -361,19 +319,13 @@ public class SecuritySettings extends SettingsPreferenceFragment } else if (preference == mShowPassword) { Settings.System.putInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, mShowPassword.isChecked() ? 1 : 0); - } else if (preference == mNetwork) { - Settings.Secure.setLocationProviderEnabled(getContentResolver(), - LocationManager.NETWORK_PROVIDER, mNetwork.isChecked()); - } else if (preference == mGps) { - boolean enabled = mGps.isChecked(); - Settings.Secure.setLocationProviderEnabled(getContentResolver(), - LocationManager.GPS_PROVIDER, enabled); - if (mAssistedGps != null) { - mAssistedGps.setEnabled(enabled); + } else if (preference == mToggleAppInstallation) { + if (mToggleAppInstallation.isChecked()) { + mToggleAppInstallation.setChecked(false); + warnAppInstallation(); + } else { + setNonMarketAppsAllowed(false); } - } else if (preference == mAssistedGps) { - Settings.Secure.putInt(getContentResolver(), Settings.Secure.ASSISTED_GPS_ENABLED, - mAssistedGps.isChecked() ? 1 : 0); } else { // If we didn't handle it, let preferences handle it. return super.onPreferenceTreeClick(preferenceScreen, preference); @@ -382,29 +334,12 @@ public class SecuritySettings extends SettingsPreferenceFragment return true; } - /* - * Creates toggles for each available location provider - */ - private void updateLocationToggles() { - ContentResolver res = getContentResolver(); - boolean gpsEnabled = Settings.Secure.isLocationProviderEnabled( - res, LocationManager.GPS_PROVIDER); - mNetwork.setChecked(Settings.Secure.isLocationProviderEnabled( - res, LocationManager.NETWORK_PROVIDER)); - mGps.setChecked(gpsEnabled); - if (mAssistedGps != null) { - mAssistedGps.setChecked(Settings.Secure.getInt(res, - Settings.Secure.ASSISTED_GPS_ENABLED, 2) == 1); - mAssistedGps.setEnabled(gpsEnabled); - } - } - private boolean isToggled(Preference pref) { return ((CheckBoxPreference) pref).isChecked(); } /** - * @see #confirmPatternThenDisableAndClear + * see confirmPatternThenDisableAndClear */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { @@ -422,21 +357,6 @@ public class SecuritySettings extends SettingsPreferenceFragment Log.e("SecuritySettings", "could not persist lockAfter timeout setting", e); } updateLockAfterPreferenceSummary(); - } else if (preference == mUseLocation) { - boolean newValue = (value == null ? false : (Boolean) value); - GoogleLocationSettingHelper.setUseLocationForServices(getActivity(), newValue); - // We don't want to change the value immediately here, since the user may click - // disagree in the dialog that pops up. When the activity we just launched exits, this - // activity will be restated and the new value re-read, so the checkbox will get its - // new value then. - return false; - } else if (preference == mEnableCredentials) { - if (value != null && (Boolean) value) { - getActivity().startActivity(new Intent(CredentialStorage.ACTION_UNLOCK)); - return false; - } else { - KeyStore.getInstance().lock(); - } } return true; } diff --git a/src/com/android/settings/SetFullBackupPassword.java b/src/com/android/settings/SetFullBackupPassword.java new file mode 100644 index 0000000..9f3f29f --- /dev/null +++ b/src/com/android/settings/SetFullBackupPassword.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 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; + +import android.app.Activity; +import android.app.backup.IBackupManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +public class SetFullBackupPassword extends Activity { + static final String TAG = "SetFullBackupPassword"; + + IBackupManager mBackupManager; + TextView mCurrentPw, mNewPw, mConfirmNewPw; + Button mCancel, mSet; + + OnClickListener mButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (v == mSet) { + final String curPw = mCurrentPw.getText().toString(); + final String newPw = mNewPw.getText().toString(); + final String confirmPw = mConfirmNewPw.getText().toString(); + + if (!newPw.equals(confirmPw)) { + // Mismatch between new pw and its confirmation re-entry +Log.i(TAG, "password mismatch"); + Toast.makeText(SetFullBackupPassword.this, + "!!! New password and confirmation don't match !!!", + Toast.LENGTH_LONG).show(); + return; + } + + // TODO: should we distinguish cases of has/hasn't set a pw before? + + if (setBackupPassword(curPw, newPw)) { + // success +Log.i(TAG, "password set successfully"); + Toast.makeText(SetFullBackupPassword.this, + "!!! New backup password set !!!", + Toast.LENGTH_LONG).show(); + finish(); + } else { + // failure -- bad existing pw, usually +Log.i(TAG, "failure; password mismatch?"); + Toast.makeText(SetFullBackupPassword.this, + "!!! Failure setting backup password !!!", + Toast.LENGTH_LONG).show(); + } + } else if (v == mCancel) { + finish(); + } else { + Log.w(TAG, "Click on unknown view"); + } + } + }; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mBackupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup")); + + setContentView(R.layout.set_backup_pw); + + mCurrentPw = (TextView) findViewById(R.id.current_backup_pw); + mNewPw = (TextView) findViewById(R.id.new_backup_pw); + mConfirmNewPw = (TextView) findViewById(R.id.confirm_new_backup_pw); + + mCancel = (Button) findViewById(R.id.backup_pw_cancel_button); + mSet = (Button) findViewById(R.id.backup_pw_set_button); + + mCancel.setOnClickListener(mButtonListener); + mSet.setOnClickListener(mButtonListener); + } + + private boolean setBackupPassword(String currentPw, String newPw) { + try { + return mBackupManager.setBackupPassword(currentPw, newPw); + } catch (RemoteException e) { + Log.e(TAG, "Unable to communicate with backup manager"); + return false; + } + } +} diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 2532747..272c0d1 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -16,18 +16,35 @@ package com.android.settings; +import com.android.settings.accounts.AccountSyncSettings; +import com.android.settings.bluetooth.BluetoothEnabler; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.wifi.WifiEnabler; + import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.text.TextUtils; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.Switch; +import android.widget.TextView; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,15 +53,18 @@ import java.util.List; */ public class Settings extends PreferenceActivity implements ButtonBarHandler { + private static final String LOG_TAG = "Settings"; private static final String META_DATA_KEY_HEADER_ID = - "com.android.settings.TOP_LEVEL_HEADER_ID"; + "com.android.settings.TOP_LEVEL_HEADER_ID"; private static final String META_DATA_KEY_FRAGMENT_CLASS = - "com.android.settings.FRAGMENT_CLASS"; + "com.android.settings.FRAGMENT_CLASS"; private static final String META_DATA_KEY_PARENT_TITLE = "com.android.settings.PARENT_FRAGMENT_TITLE"; private static final String META_DATA_KEY_PARENT_FRAGMENT_CLASS = "com.android.settings.PARENT_FRAGMENT_CLASS"; + private static final String EXTRA_THEME = "settings:theme"; + private static final String SAVE_KEY_CURRENT_HEADER = "com.android.settings.CURRENT_HEADER"; private static final String SAVE_KEY_PARENT_HEADER = "com.android.settings.PARENT_HEADER"; @@ -58,9 +78,14 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { // TODO: Update Call Settings based on airplane mode state. protected HashMap<Integer, Integer> mHeaderIndexMap = new HashMap<Integer, Integer>(); + private List<Header> mHeaders; @Override protected void onCreate(Bundle savedInstanceState) { + final int theme = getIntent().getIntExtra( + EXTRA_THEME, android.R.style.Theme_Holo); + setTheme(theme); + getMetaData(); mInLocalHeaderSwitch = true; super.onCreate(savedInstanceState); @@ -92,6 +117,10 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { } }); } + + // TODO Add support for android.R.id.home in all Setting's onOptionsItemSelected + // getActionBar().setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, + // ActionBar.DISPLAY_HOME_AS_UP); } @Override @@ -107,6 +136,26 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { } } + @Override + public void onResume() { + super.onResume(); + + ListAdapter listAdapter = getListAdapter(); + if (listAdapter instanceof HeaderAdapter) { + ((HeaderAdapter) listAdapter).resume(); + } + } + + @Override + public void onPause() { + super.onPause(); + + ListAdapter listAdapter = getListAdapter(); + if (listAdapter instanceof HeaderAdapter) { + ((HeaderAdapter) listAdapter).pause(); + } + } + private void switchToHeaderLocal(Header header) { mInLocalHeaderSwitch = true; switchToHeader(header); @@ -148,7 +197,7 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { mParentHeader.title = parentInfo.metaData.getString(META_DATA_KEY_PARENT_TITLE); } } catch (NameNotFoundException nnfe) { - Log.w("Settings", "Could not find parent activity : " + className); + Log.w(LOG_TAG, "Could not find parent activity : " + className); } } @@ -158,7 +207,7 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { // If it is not launched from history, then reset to top-level if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0 - && mFirstHeader != null) { + && mFirstHeader != null && !onIsHidingHeaders() && onIsMultiPane()) { switchToHeaderLocal(mFirstHeader); } } @@ -174,21 +223,24 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { @Override public Intent getIntent() { - String startingFragment = getStartingFragmentClass(super.getIntent()); + Intent superIntent = super.getIntent(); + String startingFragment = getStartingFragmentClass(superIntent); + // This is called from super.onCreate, isMultiPane() is not yet reliable + // Do not use onIsHidingHeaders either, which relies itself on this method if (startingFragment != null && !onIsMultiPane()) { - Intent modIntent = new Intent(super.getIntent()); + Intent modIntent = new Intent(superIntent); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment); - Bundle args = super.getIntent().getExtras(); + Bundle args = superIntent.getExtras(); if (args != null) { args = new Bundle(args); } else { args = new Bundle(); } - args.putParcelable("intent", super.getIntent()); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, super.getIntent().getExtras()); + args.putParcelable("intent", superIntent); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, superIntent.getExtras()); return modIntent; } - return super.getIntent(); + return superIntent; } /** @@ -204,7 +256,7 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { if ("com.android.settings.ManageApplications".equals(intentClass) || "com.android.settings.RunningServices".equals(intentClass) || "com.android.settings.applications.StorageUse".equals(intentClass)) { - // Old name of manage apps. + // Old names of manage apps. intentClass = com.android.settings.applications.ManageApplications.class.getName(); } @@ -226,17 +278,38 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { mCurrentHeader = header; return header; } - return super.onGetInitialHeader(); + + return mFirstHeader; } + @Override + public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, + int titleRes, int shortTitleRes) { + Intent intent = super.onBuildStartFragmentIntent(fragmentName, args, + titleRes, shortTitleRes); + + // some fragments would like a custom activity theme + if (DataUsageSummary.class.getName().equals(fragmentName) || + PowerUsageSummary.class.getName().equals(fragmentName) || + AccountSyncSettings.class.getName().equals(fragmentName) || + UserDictionarySettings.class.getName().equals(fragmentName)) { + intent.putExtra(EXTRA_THEME, android.R.style.Theme_Holo); + } + + intent.setClass(this, SubSettings.class); + return intent; + } + /** * Populate the activity with the top-level headers. */ @Override - public void onBuildHeaders(List<Header> target) { - loadHeadersFromResource(R.xml.settings_headers, target); + public void onBuildHeaders(List<Header> headers) { + loadHeadersFromResource(R.xml.settings_headers, headers); + + updateHeaderList(headers); - updateHeaderList(target); + mHeaders = headers; } private void updateHeaderList(List<Header> target) { @@ -250,14 +323,25 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { target.remove(header); } else if (id == R.id.operator_settings || id == R.id.manufacturer_settings) { Utils.updateHeaderToSpecificActivityFromMetaDataOrRemove(this, target, header); - } else if (id == R.id.call_settings) { - if (!Utils.isVoiceCapable(this)) + } else if (id == R.id.wifi_settings) { + // Remove WiFi Settings if WiFi service is not available. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { target.remove(header); + } + } else if (id == R.id.bluetooth_settings) { + // Remove Bluetooth Settings if Bluetooth service is not available. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { + target.remove(header); + } } + // Increment if the current one wasn't removed by the Utils code. if (target.get(i) == header) { // Hold on to the first header, when we need to reset to the top-level - if (i == 0) mFirstHeader = header; + if (mFirstHeader == null && + HeaderAdapter.getHeaderType(header) != HeaderAdapter.HEADER_TYPE_CATEGORY) { + mFirstHeader = header; + } mHeaderIndexMap.put(id, i); i++; } @@ -287,6 +371,7 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { } } } catch (NameNotFoundException nnfe) { + // No recovery } } @@ -300,38 +385,220 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { return super.getNextButton(); } + private static class HeaderAdapter extends ArrayAdapter<Header> { + static final int HEADER_TYPE_CATEGORY = 0; + static final int HEADER_TYPE_NORMAL = 1; + static final int HEADER_TYPE_SWITCH = 2; + private static final int HEADER_TYPE_COUNT = HEADER_TYPE_SWITCH + 1; + + private final WifiEnabler mWifiEnabler; + private final BluetoothEnabler mBluetoothEnabler; + + private static class HeaderViewHolder { + ImageView icon; + TextView title; + TextView summary; + Switch switch_; + } + + private LayoutInflater mInflater; + + static int getHeaderType(Header header) { + if (header.fragment == null && header.intent == null) { + return HEADER_TYPE_CATEGORY; + } else if (header.id == R.id.wifi_settings || header.id == R.id.bluetooth_settings) { + return HEADER_TYPE_SWITCH; + } else { + return HEADER_TYPE_NORMAL; + } + } + + @Override + public int getItemViewType(int position) { + Header header = getItem(position); + return getHeaderType(header); + } + + @Override + public boolean areAllItemsEnabled() { + return false; // because of categories + } + + @Override + public boolean isEnabled(int position) { + return getItemViewType(position) != HEADER_TYPE_CATEGORY; + } + + @Override + public int getViewTypeCount() { + return HEADER_TYPE_COUNT; + } + + @Override + public boolean hasStableIds() { + return true; + } + + public HeaderAdapter(Context context, List<Header> objects) { + super(context, 0, objects); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + // Temp Switches provided as placeholder until the adapter replaces these with actual + // Switches inflated from their layouts. Must be done before adapter is set in super + mWifiEnabler = new WifiEnabler(context, new Switch(context)); + mBluetoothEnabler = new BluetoothEnabler(context, new Switch(context)); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + Header header = getItem(position); + int headerType = getHeaderType(header); + View view = null; + + if (convertView == null) { + holder = new HeaderViewHolder(); + switch (headerType) { + case HEADER_TYPE_CATEGORY: + view = new TextView(getContext(), null, + android.R.attr.listSeparatorTextViewStyle); + holder.title = (TextView) view; + break; + + case HEADER_TYPE_SWITCH: + view = mInflater.inflate(R.layout.preference_header_switch_item, parent, + false); + holder.icon = (ImageView) view.findViewById(R.id.icon); + holder.title = (TextView) + view.findViewById(com.android.internal.R.id.title); + holder.summary = (TextView) + view.findViewById(com.android.internal.R.id.summary); + holder.switch_ = (Switch) view.findViewById(R.id.switchWidget); + break; + + case HEADER_TYPE_NORMAL: + view = mInflater.inflate( + com.android.internal.R.layout.preference_header_item, parent, + false); + holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon); + holder.title = (TextView) + view.findViewById(com.android.internal.R.id.title); + holder.summary = (TextView) + view.findViewById(com.android.internal.R.id.summary); + break; + } + view.setTag(holder); + } else { + view = convertView; + holder = (HeaderViewHolder) view.getTag(); + } + + // All view fields must be updated every time, because the view may be recycled + switch (headerType) { + case HEADER_TYPE_CATEGORY: + holder.title.setText(header.getTitle(getContext().getResources())); + break; + + case HEADER_TYPE_SWITCH: + // Would need a different treatment if the main menu had more switches + if (header.id == R.id.wifi_settings) { + mWifiEnabler.setSwitch(holder.switch_); + } else { + mBluetoothEnabler.setSwitch(holder.switch_); + } + // No break, fall through on purpose to update common fields + + //$FALL-THROUGH$ + case HEADER_TYPE_NORMAL: + holder.icon.setImageResource(header.iconRes); + holder.title.setText(header.getTitle(getContext().getResources())); + CharSequence summary = header.getSummary(getContext().getResources()); + if (!TextUtils.isEmpty(summary)) { + holder.summary.setVisibility(View.VISIBLE); + holder.summary.setText(summary); + } else { + holder.summary.setVisibility(View.GONE); + } + break; + } + + return view; + } + + public void resume() { + mWifiEnabler.resume(); + mBluetoothEnabler.resume(); + } + + public void pause() { + mWifiEnabler.pause(); + mBluetoothEnabler.pause(); + } + } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { + // Override the fragment title for Wallpaper settings + CharSequence title = pref.getTitle(); + if (pref.getFragment().equals(WallpaperTypeSettings.class.getName())) { + title = getString(R.string.wallpaper_settings_fragment_title); + } + startPreferencePanel(pref.getFragment(), pref.getExtras(), 0, title, null, 0); + return true; + } + + @Override + public void setListAdapter(ListAdapter adapter) { + if (mHeaders == null) { + mHeaders = new ArrayList<Header>(); + // When the saved state provides the list of headers, onBuildHeaders is not called + // Copy the list of Headers from the adapter, preserving their order + for (int i = 0; i < adapter.getCount(); i++) { + mHeaders.add((Header) adapter.getItem(i)); + } + } + + // Ignore the adapter provided by PreferenceActivity and substitute ours instead + super.setListAdapter(new HeaderAdapter(this, mHeaders)); + } + /* * Settings subclasses for launching independently. */ - - public static class BluetoothSettingsActivity extends Settings { } - public static class WirelessSettingsActivity extends Settings { } - public static class TetherSettingsActivity extends Settings { } - public static class VpnSettingsActivity extends Settings { } - public static class DateTimeSettingsActivity extends Settings { } - public static class StorageSettingsActivity extends Settings { } - public static class WifiSettingsActivity extends Settings { } - public static class InputMethodAndLanguageSettingsActivity extends Settings { } - public static class InputMethodConfigActivity extends Settings { } - public static class InputMethodAndSubtypeEnablerActivity extends Settings { } - public static class LocalePickerActivity extends Settings { } - public static class UserDictionarySettingsActivity extends Settings { } - public static class SoundSettingsActivity extends Settings { } - public static class DisplaySettingsActivity extends Settings { } - public static class DeviceInfoSettingsActivity extends Settings { } - public static class ApplicationSettingsActivity extends Settings { } - public static class ManageApplicationsActivity extends Settings { } - public static class StorageUseActivity extends Settings { } - public static class DevelopmentSettingsActivity extends Settings { } - public static class AccessibilitySettingsActivity extends Settings { } - public static class SecuritySettingsActivity extends Settings { } - public static class PrivacySettingsActivity extends Settings { } - public static class DockSettingsActivity extends Settings { } - public static class RunningServicesActivity extends Settings { } - public static class ManageAccountsSettingsActivity extends Settings { } - public static class PowerUsageSummaryActivity extends Settings { } - public static class AccountSyncSettingsActivity extends Settings { } - public static class AccountSyncSettingsInAddAccountActivity extends Settings { } - public static class CryptKeeperSettingsActivity extends Settings { } - public static class DeviceAdminSettingsActivity extends Settings { } + public static class BluetoothSettingsActivity extends Settings { /* empty */ } + public static class WirelessSettingsActivity extends Settings { /* empty */ } + public static class TetherSettingsActivity extends Settings { /* empty */ } + public static class VpnSettingsActivity extends Settings { /* empty */ } + public static class DateTimeSettingsActivity extends Settings { /* empty */ } + public static class StorageSettingsActivity extends Settings { /* empty */ } + public static class WifiSettingsActivity extends Settings { /* empty */ } + public static class WifiP2pSettingsActivity extends Settings { /* empty */ } + public static class InputMethodAndLanguageSettingsActivity extends Settings { /* empty */ } + public static class InputMethodAndSubtypeEnablerActivity extends Settings { /* empty */ } + public static class SpellCheckersSettingsActivity extends Settings { /* empty */ } + public static class LocalePickerActivity extends Settings { /* empty */ } + public static class UserDictionarySettingsActivity extends Settings { /* empty */ } + public static class SoundSettingsActivity extends Settings { /* empty */ } + public static class DisplaySettingsActivity extends Settings { /* empty */ } + public static class DeviceInfoSettingsActivity extends Settings { /* empty */ } + public static class ApplicationSettingsActivity extends Settings { /* empty */ } + public static class ManageApplicationsActivity extends Settings { /* empty */ } + public static class StorageUseActivity extends Settings { /* empty */ } + public static class DevelopmentSettingsActivity extends Settings { /* empty */ } + public static class AccessibilitySettingsActivity extends Settings { /* empty */ } + public static class SecuritySettingsActivity extends Settings { /* empty */ } + public static class LocationSettingsActivity extends Settings { /* empty */ } + public static class PrivacySettingsActivity extends Settings { /* empty */ } + public static class DockSettingsActivity extends Settings { /* empty */ } + public static class RunningServicesActivity extends Settings { /* empty */ } + public static class ManageAccountsSettingsActivity extends Settings { /* empty */ } + public static class PowerUsageSummaryActivity extends Settings { /* empty */ } + public static class AccountSyncSettingsActivity extends Settings { /* empty */ } + public static class AccountSyncSettingsInAddAccountActivity extends Settings { /* empty */ } + public static class CryptKeeperSettingsActivity extends Settings { /* empty */ } + public static class DeviceAdminSettingsActivity extends Settings { /* empty */ } + public static class DataUsageSummaryActivity extends Settings { /* empty */ } + public static class AdvancedWifiSettingsActivity extends Settings { /* empty */ } + public static class TextToSpeechSettingsActivity extends Settings { /* empty */ } + public static class NfcSharingSettingsActivity extends Settings { /* empty */ } } diff --git a/src/com/android/settings/SettingsLicenseActivity.java b/src/com/android/settings/SettingsLicenseActivity.java index 99828ce..2960180 100644 --- a/src/com/android/settings/SettingsLicenseActivity.java +++ b/src/com/android/settings/SettingsLicenseActivity.java @@ -21,7 +21,6 @@ import android.os.Handler; import android.os.Message; import android.os.SystemProperties; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -46,7 +45,7 @@ import java.util.zip.GZIPInputStream; public class SettingsLicenseActivity extends Activity { private static final String TAG = "SettingsLicenseActivity"; - private static final boolean LOGV = false || Config.LOGV; + private static final boolean LOGV = false || false; private static final String DEFAULT_LICENSE_PATH = "/system/etc/NOTICE.html.gz"; private static final String PROPERTY_LICENSE_PATH = "ro.config.license_path"; diff --git a/src/com/android/settings/SoundSettings.java b/src/com/android/settings/SoundSettings.java index 4ca7d4a..d1f8247 100644 --- a/src/com/android/settings/SoundSettings.java +++ b/src/com/android/settings/SoundSettings.java @@ -21,22 +21,36 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.media.audiofx.AudioEffect; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.os.Vibrator; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.provider.MediaStore; import android.provider.Settings; +import android.provider.MediaStore.Images.Media; import android.provider.Settings.SettingNotFoundException; import android.telephony.TelephonyManager; import android.util.Log; +import java.util.List; + public class SoundSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener { - private static final String TAG = "SoundAndDisplaysSettings"; + private static final String TAG = "SoundSettings"; /** If there is no setting in the provider, use this. */ private static final int FALLBACK_EMERGENCY_TONE_VALUE = 0; @@ -44,6 +58,7 @@ public class SoundSettings extends SettingsPreferenceFragment implements private static final String KEY_SILENT = "silent"; private static final String KEY_VIBRATE = "vibrate"; private static final String KEY_RING_VOLUME = "ring_volume"; + private static final String KEY_MUSICFX = "musicfx"; private static final String KEY_DTMF_TONE = "dtmf_tone"; private static final String KEY_SOUND_EFFECTS = "sound_effects"; private static final String KEY_HAPTIC_FEEDBACK = "haptic_feedback"; @@ -54,7 +69,6 @@ public class SoundSettings extends SettingsPreferenceFragment implements private static final String KEY_RINGTONE = "ringtone"; private static final String KEY_NOTIFICATION_SOUND = "notification_sound"; private static final String KEY_CATEGORY_CALLS = "category_calls"; - private static final String KEY_CATEGORY_NOTIFICATION = "category_notification"; private static final String VALUE_VIBRATE_NEVER = "never"; private static final String VALUE_VIBRATE_ALWAYS = "always"; @@ -66,6 +80,9 @@ public class SoundSettings extends SettingsPreferenceFragment implements KEY_EMERGENCY_TONE }; + private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1; + private static final int MSG_UPDATE_NOTIFICATION_SUMMARY = 2; + private CheckBoxPreference mSilent; /* @@ -80,7 +97,12 @@ public class SoundSettings extends SettingsPreferenceFragment implements private CheckBoxPreference mSoundEffects; private CheckBoxPreference mHapticFeedback; private CheckBoxPreference mNotificationPulse; + private Preference mMusicFx; private CheckBoxPreference mLockSounds; + private Preference mRingtonePreference; + private Preference mNotificationPreference; + + private Runnable mRingtoneLookupRunnable; private AudioManager mAudioManager; @@ -93,6 +115,19 @@ public class SoundSettings extends SettingsPreferenceFragment implements } }; + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_RINGTONE_SUMMARY: + mRingtonePreference.setSummary((CharSequence) msg.obj); + break; + case MSG_UPDATE_NOTIFICATION_SUMMARY: + mNotificationPreference.setSummary((CharSequence) msg.obj); + break; + } + } + }; + private PreferenceGroup mSoundSettings; @Override @@ -126,16 +161,19 @@ public class SoundSettings extends SettingsPreferenceFragment implements mSoundEffects = (CheckBoxPreference) findPreference(KEY_SOUND_EFFECTS); mSoundEffects.setPersistent(false); mSoundEffects.setChecked(Settings.System.getInt(resolver, - Settings.System.SOUND_EFFECTS_ENABLED, 0) != 0); + Settings.System.SOUND_EFFECTS_ENABLED, 1) != 0); mHapticFeedback = (CheckBoxPreference) findPreference(KEY_HAPTIC_FEEDBACK); mHapticFeedback.setPersistent(false); mHapticFeedback.setChecked(Settings.System.getInt(resolver, - Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0); + Settings.System.HAPTIC_FEEDBACK_ENABLED, 1) != 0); mLockSounds = (CheckBoxPreference) findPreference(KEY_LOCK_SOUNDS); mLockSounds.setPersistent(false); mLockSounds.setChecked(Settings.System.getInt(resolver, Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1) != 0); + mRingtonePreference = findPreference(KEY_RINGTONE); + mNotificationPreference = findPreference(KEY_NOTIFICATION_SOUND); + if (!((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) { getPreferenceScreen().removePreference(mVibrate); getPreferenceScreen().removePreference(mHapticFeedback); @@ -165,6 +203,19 @@ public class SoundSettings extends SettingsPreferenceFragment implements } } + mMusicFx = mSoundSettings.findPreference(KEY_MUSICFX); + Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + PackageManager p = getPackageManager(); + List<ResolveInfo> ris = p.queryIntentActivities(i, PackageManager.GET_DISABLED_COMPONENTS); + if (ris.size() <= 2) { + // no need to show the item if there is no choice for the user to make + // note: the built in musicfx panel has two activities (one being a + // compatibility shim that launches either the other activity, or a + // third party one), hence the check for <=2. If the implementation + // of the compatbility layer changes, this check may need to be updated. + mSoundSettings.removePreference(mMusicFx); + } + if (!Utils.isVoiceCapable(getActivity())) { for (String prefKey : NEED_VOICE_CAPABILITY) { Preference pref = findPreference(prefKey); @@ -173,6 +224,19 @@ public class SoundSettings extends SettingsPreferenceFragment implements } } } + + mRingtoneLookupRunnable = new Runnable() { + public void run() { + if (mRingtonePreference != null) { + updateRingtoneName(RingtoneManager.TYPE_RINGTONE, mRingtonePreference, + MSG_UPDATE_RINGTONE_SUMMARY); + } + if (mNotificationPreference != null) { + updateRingtoneName(RingtoneManager.TYPE_NOTIFICATION, mNotificationPreference, + MSG_UPDATE_NOTIFICATION_SUMMARY); + } + } + }; } @Override @@ -180,6 +244,7 @@ public class SoundSettings extends SettingsPreferenceFragment implements super.onResume(); updateState(true); + lookupRingtoneNames(); IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); getActivity().registerReceiver(mReceiver, filter); @@ -286,13 +351,34 @@ public class SoundSettings extends SettingsPreferenceFragment implements mVibrate.setValue(phoneVibrateSetting); } mVibrate.setSummary(mVibrate.getEntry()); + } + + private void updateRingtoneName(int type, Preference preference, int msg) { + if (preference == null) return; + Context context = getActivity(); + if (context == null) return; + Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type); + CharSequence summary = context.getString(com.android.internal.R.string.ringtone_unknown); + // Is it a silent ringtone? + if (ringtoneUri == null) { + summary = context.getString(com.android.internal.R.string.ringtone_silent); + } else { + // Fetch the ringtone title from the media provider + try { + Cursor cursor = context.getContentResolver().query(ringtoneUri, + new String[] { MediaStore.Audio.Media.TITLE }, null, null, null); + if (cursor.moveToFirst()) { + summary = cursor.getString(0); + } + } catch (SQLiteException sqle) { + // Unknown title for the ringtone + } + } + mHandler.sendMessage(mHandler.obtainMessage(msg, summary)); + } - int silentModeStreams = Settings.System.getInt(getContentResolver(), - Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - boolean isAlarmInclSilentMode = (silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0; - mSilent.setSummary(isAlarmInclSilentMode ? - R.string.silent_mode_incl_alarm_summary : - R.string.silent_mode_summary); + private void lookupRingtoneNames() { + new Thread(mRingtoneLookupRunnable).start(); } @Override @@ -335,6 +421,9 @@ public class SoundSettings extends SettingsPreferenceFragment implements boolean value = mNotificationPulse.isChecked(); Settings.System.putInt(getContentResolver(), Settings.System.NOTIFICATION_LIGHT_PULSE, value ? 1 : 0); + } else if (preference == mMusicFx) { + // let the framework fire off the intent + return false; } return true; diff --git a/src/com/android/settings/SubSettings.java b/src/com/android/settings/SubSettings.java new file mode 100644 index 0000000..9cd3c31 --- /dev/null +++ b/src/com/android/settings/SubSettings.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2011 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; + +/** + * Stub class for showing sub-settings; we can't use the main Settings class + * since for our app it is a special singleTask class. + */ +public class SubSettings extends Settings { +} diff --git a/src/com/android/settings/TestingSettingsBroadcastReceiver.java b/src/com/android/settings/TestingSettingsBroadcastReceiver.java index c6cd7e1..cea12c5 100644 --- a/src/com/android/settings/TestingSettingsBroadcastReceiver.java +++ b/src/com/android/settings/TestingSettingsBroadcastReceiver.java @@ -6,7 +6,6 @@ import static android.provider.Telephony.Intents.SECRET_CODE_ACTION; import android.content.Context; import android.content.Intent; import android.content.BroadcastReceiver; -import android.util.Config; import android.util.Log; import android.view.KeyEvent; diff --git a/src/com/android/settings/TetherSettings.java b/src/com/android/settings/TetherSettings.java index 1513d43..f5bee3a 100644 --- a/src/com/android/settings/TetherSettings.java +++ b/src/com/android/settings/TetherSettings.java @@ -31,6 +31,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.AssetManager; +import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; @@ -95,6 +96,9 @@ public class TetherSettings extends SettingsPreferenceFragment private WifiManager mWifiManager; private WifiConfiguration mWifiConfig = null; + private boolean mUsbConnected; + private boolean mMassStorageActive; + private boolean mBluetoothEnableForTether; @Override @@ -253,8 +257,14 @@ public class TetherSettings extends SettingsPreferenceFragment updateState(available.toArray(new String[available.size()]), active.toArray(new String[active.size()]), errored.toArray(new String[errored.size()])); - } else if (action.equals(Intent.ACTION_MEDIA_SHARED) || - action.equals(Intent.ACTION_MEDIA_UNSHARED)) { + } else if (action.equals(Intent.ACTION_MEDIA_SHARED)) { + mMassStorageActive = true; + updateState(); + } else if (action.equals(Intent.ACTION_MEDIA_UNSHARED)) { + mMassStorageActive = false; + updateState(); + } else if (action.equals(UsbManager.ACTION_USB_STATE)) { + mUsbConnected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false); updateState(); } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { if (mBluetoothEnableForTether) { @@ -285,11 +295,16 @@ public class TetherSettings extends SettingsPreferenceFragment final Activity activity = getActivity(); + mMassStorageActive = Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState()); mTetherChangeReceiver = new TetherChangeReceiver(); IntentFilter filter = new IntentFilter(ConnectivityManager.ACTION_TETHER_STATE_CHANGED); Intent intent = activity.registerReceiver(mTetherChangeReceiver, filter); filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_STATE); + activity.registerReceiver(mTetherChangeReceiver, filter); + + filter = new IntentFilter(); filter.addAction(Intent.ACTION_MEDIA_SHARED); filter.addAction(Intent.ACTION_MEDIA_UNSHARED); filter.addDataScheme("file"); @@ -334,14 +349,11 @@ public class TetherSettings extends SettingsPreferenceFragment String[] errored) { ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - boolean usbAvailable = false; + boolean usbAvailable = mUsbConnected && !mMassStorageActive; int usbError = ConnectivityManager.TETHER_ERROR_NO_ERROR; - boolean massStorageActive = - Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState()); for (String s : available) { for (String regex : mUsbRegexs) { if (s.matches(regex)) { - usbAvailable = true; if (usbError == ConnectivityManager.TETHER_ERROR_NO_ERROR) { usbError = cm.getLastTetherError(s); } @@ -377,7 +389,7 @@ public class TetherSettings extends SettingsPreferenceFragment mUsbTether.setSummary(R.string.usb_tethering_errored_subtext); mUsbTether.setEnabled(false); mUsbTether.setChecked(false); - } else if (massStorageActive) { + } else if (mMassStorageActive) { mUsbTether.setSummary(R.string.usb_tethering_storage_active_subtext); mUsbTether.setEnabled(false); mUsbTether.setChecked(false); @@ -434,40 +446,18 @@ public class TetherSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + ConnectivityManager cm = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + if (preference == mUsbTether) { boolean newState = mUsbTether.isChecked(); - ConnectivityManager cm = - (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - - if (newState) { - String[] available = cm.getTetherableIfaces(); - - String usbIface = findIface(available, mUsbRegexs); - if (usbIface == null) { - updateState(); - return true; - } - if (cm.tether(usbIface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { - mUsbTether.setChecked(false); - mUsbTether.setSummary(R.string.usb_tethering_errored_subtext); - return true; - } - mUsbTether.setSummary(""); - } else { - String [] tethered = cm.getTetheredIfaces(); - - String usbIface = findIface(tethered, mUsbRegexs); - if (usbIface == null) { - updateState(); - return true; - } - if (cm.untether(usbIface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { - mUsbTether.setSummary(R.string.usb_tethering_errored_subtext); - return true; - } - mUsbTether.setSummary(""); + if (cm.setUsbTethering(newState) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { + mUsbTether.setChecked(false); + mUsbTether.setSummary(R.string.usb_tethering_errored_subtext); + return true; } + mUsbTether.setSummary(""); } else if (preference == mBluetoothTether) { boolean bluetoothTetherState = mBluetoothTether.isChecked(); @@ -486,8 +476,6 @@ public class TetherSettings extends SettingsPreferenceFragment } else { boolean errored = false; - ConnectivityManager cm = - (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); String [] tethered = cm.getTetheredIfaces(); String bluetoothIface = findIface(tethered, mBluetoothRegexs); if (bluetoothIface != null && @@ -528,16 +516,13 @@ public class TetherSettings extends SettingsPreferenceFragment mWifiConfig = mDialog.getConfig(); if (mWifiConfig != null) { /** - * if soft AP is running, bring up with new config - * else update the configuration alone + * if soft AP is stopped, bring up + * else restart with new config + * TODO: update config on a running access point when framework support is added */ if (mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED) { + mWifiManager.setWifiApEnabled(null, false); mWifiManager.setWifiApEnabled(mWifiConfig, true); - /** - * There is no tether notification on changing AP - * configuration. Update status with new config. - */ - mWifiApEnabler.updateConfigSummary(mWifiConfig); } else { mWifiManager.setWifiApConfiguration(mWifiConfig); } diff --git a/src/com/android/settings/TextToSpeechSettings.java b/src/com/android/settings/TextToSpeechSettings.java index 62edac9..d76f08f 100644 --- a/src/com/android/settings/TextToSpeechSettings.java +++ b/src/com/android/settings/TextToSpeechSettings.java @@ -21,28 +21,23 @@ import static android.provider.Settings.Secure.TTS_DEFAULT_LANG; import static android.provider.Settings.Secure.TTS_DEFAULT_RATE; import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; import static android.provider.Settings.Secure.TTS_DEFAULT_VARIANT; -import static android.provider.Settings.Secure.TTS_ENABLED_PLUGINS; import static android.provider.Settings.Secure.TTS_USE_DEFAULTS; -import android.app.Activity; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.ContentResolver; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceGroup; -import android.preference.PreferenceScreen; import android.preference.Preference.OnPreferenceClickListener; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.EngineInfo; +import android.speech.tts.TtsEngines; +import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; @@ -56,7 +51,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private static final String TAG = "TextToSpeechSettings"; - private static final String SYSTEM_TTS = "com.svox.pico"; private static final String KEY_TTS_PLAY_EXAMPLE = "tts_play_example"; private static final String KEY_TTS_INSTALL_DATA = "tts_install_data"; private static final String KEY_TTS_USE_DEFAULT = "toggle_use_default_tts_settings"; @@ -65,6 +59,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private static final String KEY_TTS_DEFAULT_COUNTRY = "tts_default_country"; private static final String KEY_TTS_DEFAULT_VARIANT = "tts_default_variant"; private static final String KEY_TTS_DEFAULT_SYNTH = "tts_default_synth"; + private static final String KEY_TTS_ENGINE_SETTINGS = "tts_engine_settings"; private static final String KEY_PLUGIN_ENABLED_PREFIX = "ENABLED_"; private static final String KEY_PLUGIN_SETTINGS_PREFIX = "SETTINGS_"; @@ -76,19 +71,18 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private static final String LOCALE_DELIMITER = "-"; - private static final String FALLBACK_TTS_DEFAULT_SYNTH = - TextToSpeech.Engine.DEFAULT_SYNTH; + private Preference mPlayExample = null; + + private ListPreference mDefaultRatePref = null; + private ListPreference mDefaultLocPref = null; + private ListPreference mDefaultSynthPref = null; + + private Preference mInstallData = null; + private Preference mEngineSettings = null; - private Preference mPlayExample = null; - private Preference mInstallData = null; - private CheckBoxPreference mUseDefaultPref = null; - private ListPreference mDefaultRatePref = null; - private ListPreference mDefaultLocPref = null; - private ListPreference mDefaultSynthPref = null; private String mDefaultLanguage = null; private String mDefaultCountry = null; private String mDefaultLocVariant = null; - private String mDefaultEng = ""; private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE; // Index of the current string to use for the demo. @@ -98,6 +92,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private boolean mVoicesMissing = false; private TextToSpeech mTts = null; + private TtsEngines mEnginesHelper = null; private boolean mTtsStarted = false; /** @@ -112,10 +107,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.tts_settings); - final Activity activity = getActivity(); - addEngineSpecificSettings(activity); - - activity.setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); + getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); mEnableDemo = false; mTtsStarted = false; @@ -125,10 +117,25 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements mDefaultCountry = currentLocale.getISO3Country(); mDefaultLocVariant = currentLocale.getVariant(); - mTts = new TextToSpeech(activity, this); - initClickers(); - } + mPlayExample = findPreference(KEY_TTS_PLAY_EXAMPLE); + mPlayExample.setOnPreferenceClickListener(this); + mInstallData = findPreference(KEY_TTS_INSTALL_DATA); + mInstallData.setOnPreferenceClickListener(this); + + mDefaultSynthPref = (ListPreference) findPreference(KEY_TTS_DEFAULT_SYNTH); + mDefaultRatePref = (ListPreference) findPreference(KEY_TTS_DEFAULT_RATE); + mDefaultLocPref = (ListPreference) findPreference(KEY_TTS_DEFAULT_LANG); + + mEngineSettings = findPreference(KEY_TTS_ENGINE_SETTINGS); + mEngineSettings.setEnabled(false); + + mTts = new TextToSpeech(getActivity().getApplicationContext(), this); + mEnginesHelper = new TtsEngines(getActivity().getApplicationContext()); + + initDefaultSettings(); + initEngineSpecificSettings(); + } @Override public void onStart() { @@ -142,7 +149,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements } } - @Override public void onDestroy() { super.onDestroy(); @@ -165,97 +171,39 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements } } - private void addEngineSpecificSettings(Context context) { - PreferenceGroup enginesCategory = (PreferenceGroup) findPreference("tts_engines_section"); - Intent intent = new Intent("android.intent.action.START_TTS_ENGINE"); - ResolveInfo[] enginesArray = new ResolveInfo[0]; - PackageManager pm = getPackageManager(); - enginesArray = pm.queryIntentActivities(intent, 0).toArray(enginesArray); - for (int i = 0; i < enginesArray.length; i++) { - String prefKey = ""; - final String pluginPackageName = enginesArray[i].activityInfo.packageName; - if (!enginesArray[i].activityInfo.packageName.equals(SYSTEM_TTS)) { - CheckBoxPreference chkbxPref = new CheckBoxPreference(context); - prefKey = KEY_PLUGIN_ENABLED_PREFIX + pluginPackageName; - chkbxPref.setKey(prefKey); - chkbxPref.setTitle(enginesArray[i].loadLabel(pm)); - enginesCategory.addPreference(chkbxPref); - } - if (pluginHasSettings(pluginPackageName)) { - Preference pref = new Preference(context); - prefKey = KEY_PLUGIN_SETTINGS_PREFIX + pluginPackageName; - pref.setKey(prefKey); - pref.setTitle(enginesArray[i].loadLabel(pm)); - CharSequence settingsLabel = getResources().getString( - R.string.tts_engine_name_settings, enginesArray[i].loadLabel(pm)); - pref.setSummary(settingsLabel); - pref.setOnPreferenceClickListener(new OnPreferenceClickListener(){ - public boolean onPreferenceClick(Preference preference){ - Intent i = new Intent(); - i.setClassName(pluginPackageName, - pluginPackageName + ".EngineSettings"); - startActivity(i); - return true; - } - }); - enginesCategory.addPreference(pref); - } - } - } - - private boolean pluginHasSettings(String pluginPackageName) { - PackageManager pm = getPackageManager(); - Intent i = new Intent(); - i.setClassName(pluginPackageName, pluginPackageName + ".EngineSettings"); - if (pm.resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY) != null){ - return true; - } - return false; - } + private void initEngineSpecificSettings() { + final String engineName = mEnginesHelper.getDefaultEngine(); + final EngineInfo engine = mEnginesHelper.getEngineInfo(engineName); + mEngineSettings.setTitle(getResources().getString(R.string.tts_engine_settings_title, + engine.label)); - private void initClickers() { - mPlayExample = findPreference(KEY_TTS_PLAY_EXAMPLE); - mPlayExample.setOnPreferenceClickListener(this); + final Intent settingsIntent = mEnginesHelper.getSettingsIntent(engineName); + if (settingsIntent != null) { + mEngineSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + startActivity(settingsIntent); + return true; + } + }); + mEngineSettings.setEnabled(true); + } else { + mEngineSettings.setEnabled(false); + } - mInstallData = findPreference(KEY_TTS_INSTALL_DATA); - mInstallData.setOnPreferenceClickListener(this); } - private void initDefaultSettings() { ContentResolver resolver = getContentResolver(); // Find the default TTS values in the settings, initialize and store the // settings if they are not found. - // "Use Defaults" - int useDefault = 0; - mUseDefaultPref = (CheckBoxPreference) findPreference(KEY_TTS_USE_DEFAULT); - try { - useDefault = Settings.Secure.getInt(resolver, TTS_USE_DEFAULTS); - } catch (SettingNotFoundException e) { - // "use default" setting not found, initialize it - useDefault = TextToSpeech.Engine.USE_DEFAULTS; - Settings.Secure.putInt(resolver, TTS_USE_DEFAULTS, useDefault); - } - mUseDefaultPref.setChecked(useDefault == 1); - mUseDefaultPref.setOnPreferenceChangeListener(this); - // Default synthesis engine - mDefaultSynthPref = (ListPreference) findPreference(KEY_TTS_DEFAULT_SYNTH); loadEngines(); mDefaultSynthPref.setOnPreferenceChangeListener(this); - String engine = Settings.Secure.getString(resolver, TTS_DEFAULT_SYNTH); - if (engine == null) { - // TODO move FALLBACK_TTS_DEFAULT_SYNTH to TextToSpeech - engine = FALLBACK_TTS_DEFAULT_SYNTH; - Settings.Secure.putString(resolver, TTS_DEFAULT_SYNTH, engine); - } - mDefaultEng = engine; // Default rate - mDefaultRatePref = (ListPreference) findPreference(KEY_TTS_DEFAULT_RATE); try { mDefaultRate = Settings.Secure.getInt(resolver, TTS_DEFAULT_RATE); } catch (SettingNotFoundException e) { @@ -265,34 +213,27 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements } mDefaultRatePref.setValue(String.valueOf(mDefaultRate)); mDefaultRatePref.setOnPreferenceChangeListener(this); - // apply the default rate so the TTS demo in the Settings screen uses it, even if - // the use of default settings is not enforced - mTts.setSpeechRate(mDefaultRate/100.0f); // Default language / country / variant : these three values map to a single ListPref // representing the matching Locale - mDefaultLocPref = (ListPreference) findPreference(KEY_TTS_DEFAULT_LANG); initDefaultLang(); mDefaultLocPref.setOnPreferenceChangeListener(this); } - /** * Ask the current default engine to launch the matching CHECK_TTS_DATA activity * to check the required TTS files are properly installed. */ private void checkVoiceData() { - PackageManager pm = getPackageManager(); - Intent intent = new Intent(); - intent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); - List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0); - // query only the package that matches that of the default engine - for (int i = 0; i < resolveInfos.size(); i++) { - ActivityInfo currentActivityInfo = resolveInfos.get(i).activityInfo; - if (mDefaultEng.equals(currentActivityInfo.packageName)) { - intent.setClassName(mDefaultEng, currentActivityInfo.name); - this.startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); - } + String defaultEngine = mTts.getDefaultEngine(); + if (TextUtils.isEmpty(defaultEngine)) return; + Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); + intent.setPackage(defaultEngine); + try { + Log.v(TAG, "Checking voice data: " + intent.toUri(0)); + startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Failed to check TTS data, no acitivty found for " + intent + ")"); } } @@ -302,18 +243,16 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements * so the required TTS files are properly installed. */ private void installVoiceData() { - PackageManager pm = getPackageManager(); - Intent intent = new Intent(); - intent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); + String defaultEngine = mTts.getDefaultEngine(); + if (TextUtils.isEmpty(defaultEngine)) return; + Intent intent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0); - // query only the package that matches that of the default engine - for (int i = 0; i < resolveInfos.size(); i++) { - ActivityInfo currentActivityInfo = resolveInfos.get(i).activityInfo; - if (mDefaultEng.equals(currentActivityInfo.packageName)) { - intent.setClassName(mDefaultEng, currentActivityInfo.name); - this.startActivity(intent); - } + intent.setPackage(defaultEngine); + try { + Log.v(TAG, "Installing voice data: " + intent.toUri(0)); + startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Failed to install TTS data, no acitivty found for " + intent + ")"); } } @@ -322,26 +261,22 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements * spoken to the user. */ private void getSampleText() { - PackageManager pm = getPackageManager(); - Intent intent = new Intent(); - // TODO (clchen): Replace Intent string with the actual - // Intent defined in the list of platform Intents. - intent.setAction("android.speech.tts.engine.GET_SAMPLE_TEXT"); + String defaultEngine = mTts.getDefaultEngine(); + if (TextUtils.isEmpty(defaultEngine)) return; + Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); intent.putExtra("language", mDefaultLanguage); intent.putExtra("country", mDefaultCountry); intent.putExtra("variant", mDefaultLocVariant); - List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0); - // query only the package that matches that of the default engine - for (int i = 0; i < resolveInfos.size(); i++) { - ActivityInfo currentActivityInfo = resolveInfos.get(i).activityInfo; - if (mDefaultEng.equals(currentActivityInfo.packageName)) { - intent.setClassName(mDefaultEng, currentActivityInfo.name); - this.startActivityForResult(intent, GET_SAMPLE_TEXT); - } + intent.setPackage(defaultEngine); + + try { + Log.v(TAG, "Getting sample text: " + intent.toUri(0)); + startActivityForResult(intent, GET_SAMPLE_TEXT); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Failed to get sample text, no acitivty found for " + intent + ")"); } } - /** * Called when the TTS engine is initialized. */ @@ -358,7 +293,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements mDefaultLocVariant = new String(); } mTts.setLanguage(new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); - initDefaultSettings(); updateWidgetState(); checkVoiceData(); mTtsStarted = true; @@ -370,142 +304,155 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements updateWidgetState(); } - /** * Called when voice data integrity check returns */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { - if (data == null){ - // The CHECK_TTS_DATA activity for the plugin did not run properly; - // disable the preview and install controls and return. - mEnableDemo = false; - mVoicesMissing = false; - updateWidgetState(); - return; - } - ArrayList<String> available = - data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); - ArrayList<String> unavailable = - data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES); - if ((available == null) || (unavailable == null)){ - // The CHECK_TTS_DATA activity for the plugin did not run properly; - // disable the preview and install controls and return. - mEnableDemo = false; - mVoicesMissing = false; - updateWidgetState(); - return; + onVoiceDataIntegrityCheckDone(data); + } else if (requestCode == GET_SAMPLE_TEXT) { + onSampleTextReceived(resultCode, data); + } + } + + private void onVoiceDataIntegrityCheckDone(Intent data) { + if (data == null){ + Log.e(TAG, "TTS data check failed data = null"); + // The CHECK_TTS_DATA activity for the plugin did not run properly; + // disable the preview and install controls and return. + mEnableDemo = false; + mVoicesMissing = false; + updateWidgetState(); + return; + } + Log.v(TAG, "TTS data check completed, data = " + data.toUri(0)); + ArrayList<String> available = + data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); + ArrayList<String> unavailable = + data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES); + if (available == null || unavailable == null){ + Log.e(TAG, "TTS data check failed (available == == null)"); + // The CHECK_TTS_DATA activity for the plugin did not run properly; + // disable the preview and install controls and return. + mEnableDemo = false; + mVoicesMissing = false; + updateWidgetState(); + return; + } + if (available.size() > 0){ + if (mTts == null) { + mTts = new TextToSpeech(getActivity(), this); } - if (available.size() > 0){ - if (mTts == null) { - mTts = new TextToSpeech(getActivity(), this); - } - ListPreference ttsLanguagePref = - (ListPreference) findPreference("tts_default_lang"); - CharSequence[] entries = new CharSequence[available.size()]; - CharSequence[] entryValues = new CharSequence[available.size()]; - int selectedLanguageIndex = -1; - String selectedLanguagePref = mDefaultLanguage; - if (mDefaultCountry.length() > 0) { - selectedLanguagePref = selectedLanguagePref + LOCALE_DELIMITER + - mDefaultCountry; - } - if (mDefaultLocVariant.length() > 0) { - selectedLanguagePref = selectedLanguagePref + LOCALE_DELIMITER + - mDefaultLocVariant; - } - for (int i = 0; i < available.size(); i++) { - String[] langCountryVariant = available.get(i).split("-"); - Locale loc = null; - if (langCountryVariant.length == 1){ - loc = new Locale(langCountryVariant[0]); - } else if (langCountryVariant.length == 2){ - loc = new Locale(langCountryVariant[0], langCountryVariant[1]); - } else if (langCountryVariant.length == 3){ - loc = new Locale(langCountryVariant[0], langCountryVariant[1], - langCountryVariant[2]); - } - if (loc != null){ - entries[i] = loc.getDisplayName(); - entryValues[i] = available.get(i); - if (entryValues[i].equals(selectedLanguagePref)) { - selectedLanguageIndex = i; - } - } - } - ttsLanguagePref.setEntries(entries); - ttsLanguagePref.setEntryValues(entryValues); - if (selectedLanguageIndex > -1) { - ttsLanguagePref.setValueIndex(selectedLanguageIndex); - } - mEnableDemo = true; - // Make sure that the default language can be used. - int languageResult = mTts.setLanguage( + + updateDefaultLocPref(available); + + mEnableDemo = true; + // Make sure that the default language can be used. + int languageResult = mTts.setLanguage( + new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); + if (languageResult < TextToSpeech.LANG_AVAILABLE){ + Locale currentLocale = Locale.getDefault(); + mDefaultLanguage = currentLocale.getISO3Language(); + mDefaultCountry = currentLocale.getISO3Country(); + mDefaultLocVariant = currentLocale.getVariant(); + languageResult = mTts.setLanguage( new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); + // If the default Locale isn't supported, just choose the first available + // language so that there is at least something. if (languageResult < TextToSpeech.LANG_AVAILABLE){ - Locale currentLocale = Locale.getDefault(); - mDefaultLanguage = currentLocale.getISO3Language(); - mDefaultCountry = currentLocale.getISO3Country(); - mDefaultLocVariant = currentLocale.getVariant(); - languageResult = mTts.setLanguage( + parseLocaleInfo(mDefaultLocPref.getEntryValues()[0].toString()); + mTts.setLanguage( new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); - // If the default Locale isn't supported, just choose the first available - // language so that there is at least something. - if (languageResult < TextToSpeech.LANG_AVAILABLE){ - parseLocaleInfo(ttsLanguagePref.getEntryValues()[0].toString()); - mTts.setLanguage( - new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); - } - ContentResolver resolver = getContentResolver(); - Settings.Secure.putString(resolver, TTS_DEFAULT_LANG, mDefaultLanguage); - Settings.Secure.putString(resolver, TTS_DEFAULT_COUNTRY, mDefaultCountry); - Settings.Secure.putString(resolver, TTS_DEFAULT_VARIANT, mDefaultLocVariant); } - } else { - mEnableDemo = false; + ContentResolver resolver = getContentResolver(); + Settings.Secure.putString(resolver, TTS_DEFAULT_LANG, mDefaultLanguage); + Settings.Secure.putString(resolver, TTS_DEFAULT_COUNTRY, mDefaultCountry); + Settings.Secure.putString(resolver, TTS_DEFAULT_VARIANT, mDefaultLocVariant); } + } else { + mEnableDemo = false; + } - if (unavailable.size() > 0){ - mVoicesMissing = true; - } else { - mVoicesMissing = false; - } + if (unavailable.size() > 0){ + mVoicesMissing = true; + } else { + mVoicesMissing = false; + } - updateWidgetState(); - } else if (requestCode == GET_SAMPLE_TEXT) { - if (resultCode == TextToSpeech.LANG_AVAILABLE) { - String sample = getActivity().getString(R.string.tts_demo); - if ((data != null) && (data.getStringExtra("sampleText") != null)) { - sample = data.getStringExtra("sampleText"); - } - if (mTts != null) { - mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, null); + updateWidgetState(); + } + + private void updateDefaultLocPref(ArrayList<String> availableLangs) { + CharSequence[] entries = new CharSequence[availableLangs.size()]; + CharSequence[] entryValues = new CharSequence[availableLangs.size()]; + int selectedLanguageIndex = -1; + String selectedLanguagePref = mDefaultLanguage; + if (mDefaultCountry.length() > 0) { + selectedLanguagePref = selectedLanguagePref + LOCALE_DELIMITER + + mDefaultCountry; + } + if (mDefaultLocVariant.length() > 0) { + selectedLanguagePref = selectedLanguagePref + LOCALE_DELIMITER + + mDefaultLocVariant; + } + for (int i = 0; i < availableLangs.size(); i++) { + String[] langCountryVariant = availableLangs.get(i).split("-"); + Locale loc = null; + if (langCountryVariant.length == 1){ + loc = new Locale(langCountryVariant[0]); + } else if (langCountryVariant.length == 2){ + loc = new Locale(langCountryVariant[0], langCountryVariant[1]); + } else if (langCountryVariant.length == 3){ + loc = new Locale(langCountryVariant[0], langCountryVariant[1], + langCountryVariant[2]); + } + if (loc != null){ + entries[i] = loc.getDisplayName(); + entryValues[i] = availableLangs.get(i); + if (entryValues[i].equals(selectedLanguagePref)) { + selectedLanguageIndex = i; } - } else { - // TODO: Display an error here to the user. - Log.e(TAG, "Did not have a sample string for the requested language"); } } + mDefaultLocPref.setEntries(entries); + mDefaultLocPref.setEntryValues(entryValues); + if (selectedLanguageIndex > -1) { + mDefaultLocPref.setValueIndex(selectedLanguageIndex); + } + } + + private void onSampleTextReceived(int resultCode, Intent data) { + if (resultCode == TextToSpeech.LANG_AVAILABLE) { + String sample = getActivity().getString(R.string.tts_demo); + if (data != null && data.getStringExtra("sampleText") != null) { + sample = data.getStringExtra("sampleText"); + } + Log.v(TAG, "Got sample text: " + sample); + if (mTts != null) { + mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, null); + } + } else { + // TODO: Display an error here to the user. + Log.e(TAG, "Did not have a sample string for the requested language"); + } } public boolean onPreferenceChange(Preference preference, Object objValue) { if (KEY_TTS_USE_DEFAULT.equals(preference.getKey())) { // "Use Defaults" - int value = (Boolean)objValue ? 1 : 0; - Settings.Secure.putInt(getContentResolver(), TTS_USE_DEFAULTS, - value); - Log.i(TAG, "TTS use default settings is "+objValue.toString()); + int value = ((Boolean) objValue) ? 1 : 0; + Settings.Secure.putInt(getContentResolver(), TTS_USE_DEFAULTS, value); + Log.i(TAG, "TTS 'use default' settings changed, now " + value); } else if (KEY_TTS_DEFAULT_RATE.equals(preference.getKey())) { // Default rate mDefaultRate = Integer.parseInt((String) objValue); try { - Settings.Secure.putInt(getContentResolver(), - TTS_DEFAULT_RATE, mDefaultRate); + Settings.Secure.putInt(getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate); if (mTts != null) { - mTts.setSpeechRate(mDefaultRate/100.0f); + mTts.setSpeechRate(mDefaultRate / 100.0f); } - Log.i(TAG, "TTS default rate is " + mDefaultRate); + Log.v(TAG, "TTS default rate changed, now " + mDefaultRate); } catch (NumberFormatException e) { Log.e(TAG, "could not persist default TTS rate setting", e); } @@ -522,19 +469,24 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements mTts.setLanguage(new Locale(mDefaultLanguage, mDefaultCountry, mDefaultLocVariant)); } int newIndex = mDefaultLocPref.findIndexOfValue((String)objValue); - Log.v("Settings", " selected is " + newIndex); + Log.v(TAG, " selected is " + newIndex); mDemoStringIndex = newIndex > -1 ? newIndex : 0; } else if (KEY_TTS_DEFAULT_SYNTH.equals(preference.getKey())) { - mDefaultEng = objValue.toString(); - Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, mDefaultEng); - if (mTts != null) { - mTts.setEngineByPackageName(mDefaultEng); - mEnableDemo = false; - mVoicesMissing = false; - updateWidgetState(); - checkVoiceData(); + final String name = objValue.toString(); + final EngineInfo info = mEnginesHelper.getEngineInfo(name); + + if (info.system) { + // For system engines, do away with the alert dialog. + updateDefaultEngine(name); + initEngineSpecificSettings(); + } else { + // For all other engines, display a warning message before + // turning them on. + displayDataAlert(preference, name); } - Log.v("Settings", "The default synth is: " + objValue.toString()); + + // We'll deal with updating the UI ourselves. + return false; } return true; @@ -550,61 +502,18 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements // the actual speaking getSampleText(); return true; - } - if (preference == mInstallData) { + } else if (preference == mInstallData) { installVoiceData(); // quit this activity so it needs to be restarted after installation of the voice data finish(); return true; } - return false; - } - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (Utils.isMonkeyRunning()) { - return false; - } - - if (preference instanceof CheckBoxPreference) { - final CheckBoxPreference chkPref = (CheckBoxPreference) preference; - if (!chkPref.getKey().equals(KEY_TTS_USE_DEFAULT)){ - if (chkPref.isChecked()) { - chkPref.setChecked(false); - AlertDialog d = (new AlertDialog.Builder(getActivity())) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage( - getActivity().getString(R.string.tts_engine_security_warning, - chkPref.getTitle())) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - chkPref.setChecked(true); - loadEngines(); - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - } - }) - .create(); - d.show(); - } else { - loadEngines(); - } - return true; - } - } return false; } - private void updateWidgetState() { mPlayExample.setEnabled(mEnableDemo); - mUseDefaultPref.setEnabled(mEnableDemo); mDefaultRatePref.setEnabled(mEnableDemo); mDefaultLocPref.setEnabled(mEnableDemo); @@ -617,14 +526,18 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements mDefaultLanguage = ""; mDefaultCountry = ""; mDefaultLocVariant = ""; - if (tokenizer.hasMoreTokens()) { - mDefaultLanguage = tokenizer.nextToken().trim(); - } - if (tokenizer.hasMoreTokens()) { - mDefaultCountry = tokenizer.nextToken().trim(); - } - if (tokenizer.hasMoreTokens()) { - mDefaultLocVariant = tokenizer.nextToken().trim(); + + if (locale != null) { + String[] components = locale.split(LOCALE_DELIMITER); + if (components.length > 0) { + mDefaultLanguage = components[0]; + } + if (components.length > 1) { + mDefaultCountry = components[1]; + } + if (components.length > 2) { + mDefaultLocVariant = components[2]; + } } } @@ -716,50 +629,81 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements Settings.Secure.putString(resolver, TTS_DEFAULT_VARIANT, DEFAULT_VARIANT_VAL); } - private void loadEngines() { - mDefaultSynthPref = (ListPreference) findPreference(KEY_TTS_DEFAULT_SYNTH); - - // TODO (clchen): Try to see if it is possible to be more efficient here - // and not search for plugins again. - Intent intent = new Intent("android.intent.action.START_TTS_ENGINE"); - ResolveInfo[] enginesArray = new ResolveInfo[0]; - PackageManager pm = getPackageManager(); - enginesArray = pm.queryIntentActivities(intent, 0).toArray(enginesArray); - ArrayList<CharSequence> entries = new ArrayList<CharSequence>(); - ArrayList<CharSequence> values = new ArrayList<CharSequence>(); - String enabledEngines = ""; - for (int i = 0; i < enginesArray.length; i++) { - String pluginPackageName = enginesArray[i].activityInfo.packageName; - if (pluginPackageName.equals(SYSTEM_TTS)) { - entries.add(enginesArray[i].loadLabel(pm)); - values.add(pluginPackageName); - } else { - CheckBoxPreference pref = (CheckBoxPreference) findPreference( - KEY_PLUGIN_ENABLED_PREFIX + pluginPackageName); - if ((pref != null) && pref.isChecked()){ - entries.add(enginesArray[i].loadLabel(pm)); - values.add(pluginPackageName); - enabledEngines = enabledEngines + pluginPackageName + " "; - } - } + List<EngineInfo> engines = mEnginesHelper.getEngines(); + CharSequence entries[] = new CharSequence[engines.size()]; + CharSequence values[] = new CharSequence[engines.size()]; + + final int count = engines.size(); + for (int i = 0; i < count; ++i) { + final EngineInfo engine = engines.get(i); + entries[i] = engine.label; + values[i] = engine.name; } - ContentResolver resolver = getContentResolver(); - Settings.Secure.putString(resolver, TTS_ENABLED_PLUGINS, enabledEngines); - - CharSequence entriesArray[] = new CharSequence[entries.size()]; - CharSequence valuesArray[] = new CharSequence[values.size()]; - mDefaultSynthPref.setEntries(entries.toArray(entriesArray)); - mDefaultSynthPref.setEntryValues(values.toArray(valuesArray)); + mDefaultSynthPref.setEntries(entries); + mDefaultSynthPref.setEntryValues(values); // Set the selected engine based on the saved preference String selectedEngine = Settings.Secure.getString(getContentResolver(), TTS_DEFAULT_SYNTH); int selectedEngineIndex = mDefaultSynthPref.findIndexOfValue(selectedEngine); if (selectedEngineIndex == -1){ - selectedEngineIndex = mDefaultSynthPref.findIndexOfValue(SYSTEM_TTS); + selectedEngineIndex = mDefaultSynthPref.findIndexOfValue( + mEnginesHelper.getHighestRankedEngineName()); + } + if (selectedEngineIndex >= 0) { + mDefaultSynthPref.setValueIndex(selectedEngineIndex); + } + } + + private void displayDataAlert(Preference pref, final String key) { + Log.v(TAG, "Displaying data alert for :" + key); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(android.R.string.dialog_alert_title); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getActivity().getString( + R.string.tts_engine_security_warning, pref.getTitle())); + builder.setCancelable(true); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + updateDefaultEngine(key); + loadEngines(); + initEngineSpecificSettings(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private void updateDefaultEngine(String engine) { + Log.v(TAG, "Updating default synth to : " + engine); + if (mTts != null) { + try { + mTts.shutdown(); + mTts = null; + } catch (Exception e) { + Log.e(TAG, "Error shutting down TTS engine" + e); + } } - mDefaultSynthPref.setValueIndex(selectedEngineIndex); + + mTts = new TextToSpeech(getActivity().getApplicationContext(), this, engine); + mEnableDemo = false; + mVoicesMissing = false; + + // Persist this value to settings and update the UI before we check + // voice data because if the TTS class connected without any exception, "engine" + // will be the default engine irrespective of whether the voice check + // passes or not. + Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); + mDefaultSynthPref.setValue(engine); + updateWidgetState(); + + checkVoiceData(); + + Log.v(TAG, "The default synth is now: " + engine); } } diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java new file mode 100644 index 0000000..687663a --- /dev/null +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2011 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; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.Fragment; +import android.content.DialogInterface; +import android.net.http.SslCertificate; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.security.IKeyChainService; +import android.security.KeyChain; +import android.security.KeyChain.KeyChainConnection; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TabHost; +import android.widget.TextView; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.apache.harmony.xnet.provider.jsse.TrustedCertificateStore; + +public class TrustedCredentialsSettings extends Fragment { + + private static final String TAG = "TrustedCredentialsSettings"; + + private enum Tab { + SYSTEM("system", + R.string.trusted_credentials_system_tab, + R.id.system_tab, + R.id.system_progress, + R.id.system_list, + true), + USER("user", + R.string.trusted_credentials_user_tab, + R.id.user_tab, + R.id.user_progress, + R.id.user_list, + false); + + private final String mTag; + private final int mLabel; + private final int mView; + private final int mProgress; + private final int mList; + private final boolean mCheckbox; + private Tab(String tag, int label, int view, int progress, int list, boolean checkbox) { + mTag = tag; + mLabel = label; + mView = view; + mProgress = progress; + mList = list; + mCheckbox = checkbox; + } + private Set<String> getAliases(TrustedCertificateStore store) { + switch (this) { + case SYSTEM: + return store.allSystemAliases(); + case USER: + return store.userAliases(); + } + throw new AssertionError(); + } + private boolean deleted(TrustedCertificateStore store, String alias) { + switch (this) { + case SYSTEM: + return !store.containsAlias(alias); + case USER: + return false; + } + throw new AssertionError(); + } + private int getButtonLabel(CertHolder certHolder) { + switch (this) { + case SYSTEM: + if (certHolder.mDeleted) { + return R.string.trusted_credentials_enable_label; + } + return R.string.trusted_credentials_disable_label; + case USER: + return R.string.trusted_credentials_remove_label; + } + throw new AssertionError(); + } + private int getButtonConfirmation(CertHolder certHolder) { + switch (this) { + case SYSTEM: + if (certHolder.mDeleted) { + return R.string.trusted_credentials_enable_confirmation; + } + return R.string.trusted_credentials_disable_confirmation; + case USER: + return R.string.trusted_credentials_remove_confirmation; + } + throw new AssertionError(); + } + private void postOperationUpdate(boolean ok, CertHolder certHolder) { + if (ok) { + if (certHolder.mTab.mCheckbox) { + certHolder.mDeleted = !certHolder.mDeleted; + } else { + certHolder.mAdapter.mCertHolders.remove(certHolder); + } + certHolder.mAdapter.notifyDataSetChanged(); + } else { + // bail, reload to reset to known state + certHolder.mAdapter.load(); + } + } + } + + // be careful not to use this on the UI thread since it is does file operations + private final TrustedCertificateStore mStore = new TrustedCertificateStore(); + + private TabHost mTabHost; + + @Override public View onCreateView( + LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { + mTabHost = (TabHost) inflater.inflate(R.layout.trusted_credentials, parent, false); + mTabHost.setup(); + addTab(Tab.SYSTEM); + // TODO add Install button on Tab.USER to go to CertInstaller like KeyChainActivity + addTab(Tab.USER); + return mTabHost; + } + + private void addTab(Tab tab) { + TabHost.TabSpec systemSpec = mTabHost.newTabSpec(tab.mTag) + .setIndicator(getActivity().getString(tab.mLabel)) + .setContent(tab.mView); + mTabHost.addTab(systemSpec); + + ListView lv = (ListView) mTabHost.findViewById(tab.mList); + final TrustedCertificateAdapter adapter = new TrustedCertificateAdapter(tab); + lv.setAdapter(adapter); + lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { + showCertDialog(adapter.getItem(pos)); + } + }); + } + + private class TrustedCertificateAdapter extends BaseAdapter { + private final List<CertHolder> mCertHolders = new ArrayList<CertHolder>(); + private final Tab mTab; + private TrustedCertificateAdapter(Tab tab) { + mTab = tab; + load(); + } + private void load() { + new AliasLoader().execute(); + } + @Override public int getCount() { + return mCertHolders.size(); + } + @Override public CertHolder getItem(int position) { + return mCertHolders.get(position); + } + @Override public long getItemId(int position) { + return position; + } + @Override public View getView(int position, View view, ViewGroup parent) { + ViewHolder holder; + if (view == null) { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + view = inflater.inflate(R.layout.trusted_credential, parent, false); + holder = new ViewHolder(); + holder.mSubjectPrimaryView = (TextView) + view.findViewById(R.id.trusted_credential_subject_primary); + holder.mSubjectSecondaryView = (TextView) + view.findViewById(R.id.trusted_credential_subject_secondary); + holder.mCheckBox = (CheckBox) view.findViewById(R.id.trusted_credential_status); + view.setTag(holder); + } else { + holder = (ViewHolder) view.getTag(); + } + CertHolder certHolder = mCertHolders.get(position); + holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary); + holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary); + if (mTab.mCheckbox) { + holder.mCheckBox.setChecked(!certHolder.mDeleted); + holder.mCheckBox.setVisibility(View.VISIBLE); + } + return view; + }; + + private class AliasLoader extends AsyncTask<Void, Integer, List<CertHolder>> { + ProgressBar mProgressBar; + View mList; + @Override protected void onPreExecute() { + View content = mTabHost.getTabContentView(); + mProgressBar = (ProgressBar) content.findViewById(mTab.mProgress); + mList = content.findViewById(mTab.mList); + mProgressBar.setVisibility(View.VISIBLE); + mList.setVisibility(View.GONE); + } + @Override protected List<CertHolder> doInBackground(Void... params) { + Set<String> aliases = mTab.getAliases(mStore); + int max = aliases.size(); + int progress = 0; + List<CertHolder> certHolders = new ArrayList<CertHolder>(max); + for (String alias : aliases) { + X509Certificate cert = (X509Certificate) mStore.getCertificate(alias, true); + certHolders.add(new CertHolder(mStore, + TrustedCertificateAdapter.this, + mTab, + alias, + cert)); + publishProgress(++progress, max); + } + Collections.sort(certHolders); + return certHolders; + } + @Override protected void onProgressUpdate(Integer... progressAndMax) { + int progress = progressAndMax[0]; + int max = progressAndMax[1]; + if (max != mProgressBar.getMax()) { + mProgressBar.setMax(max); + } + mProgressBar.setProgress(progress); + } + @Override protected void onPostExecute(List<CertHolder> certHolders) { + mCertHolders.clear(); + mCertHolders.addAll(certHolders); + notifyDataSetChanged(); + View content = mTabHost.getTabContentView(); + mProgressBar.setVisibility(View.GONE); + mList.setVisibility(View.VISIBLE); + mProgressBar.setProgress(0); + } + } + } + + private static class CertHolder implements Comparable<CertHolder> { + private final TrustedCertificateStore mStore; + private final TrustedCertificateAdapter mAdapter; + private final Tab mTab; + private final String mAlias; + private final X509Certificate mX509Cert; + + private final SslCertificate mSslCert; + private final String mSubjectPrimary; + private final String mSubjectSecondary; + private boolean mDeleted; + + private CertHolder(TrustedCertificateStore store, + TrustedCertificateAdapter adapter, + Tab tab, + String alias, + X509Certificate x509Cert) { + mStore = store; + mAdapter = adapter; + mTab = tab; + mAlias = alias; + mX509Cert = x509Cert; + + mSslCert = new SslCertificate(x509Cert); + + String cn = mSslCert.getIssuedTo().getCName(); + String o = mSslCert.getIssuedTo().getOName(); + String ou = mSslCert.getIssuedTo().getUName(); + // if we have a O, use O as primary subject, secondary prefer CN over OU + // if we don't have an O, use CN as primary, empty secondary + // if we don't have O or CN, use DName as primary, empty secondary + if (!o.isEmpty()) { + if (!cn.isEmpty()) { + mSubjectPrimary = o; + mSubjectSecondary = cn; + } else { + mSubjectPrimary = o; + mSubjectSecondary = ou; + } + } else { + if (!cn.isEmpty()) { + mSubjectPrimary = cn; + mSubjectSecondary = ""; + } else { + mSubjectPrimary = mSslCert.getIssuedTo().getDName(); + mSubjectSecondary = ""; + } + } + mDeleted = mTab.deleted(mStore, mAlias); + } + @Override public int compareTo(CertHolder o) { + int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary); + if (primary != 0) { + return primary; + } + return this.mSubjectSecondary.compareToIgnoreCase(o.mSubjectSecondary); + } + @Override public boolean equals(Object o) { + if (!(o instanceof CertHolder)) { + return false; + } + CertHolder other = (CertHolder) o; + return mAlias.equals(other.mAlias); + } + @Override public int hashCode() { + return mAlias.hashCode(); + } + } + + private static class ViewHolder { + private TextView mSubjectPrimaryView; + private TextView mSubjectSecondaryView; + private CheckBox mCheckBox; + } + + private void showCertDialog(final CertHolder certHolder) { + View view = certHolder.mSslCert.inflateCertificateView(getActivity()); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(com.android.internal.R.string.ssl_certificate); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + final Dialog certDialog = builder.create(); + + ViewGroup body = (ViewGroup) view.findViewById(com.android.internal.R.id.body); + LayoutInflater inflater = LayoutInflater.from(getActivity()); + Button removeButton = (Button) inflater.inflate(R.layout.trusted_credential_details, + body, + false); + body.addView(removeButton); + removeButton.setText(certHolder.mTab.getButtonLabel(certHolder)); + removeButton.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(certHolder.mTab.getButtonConfirmation(certHolder)); + builder.setPositiveButton( + android.R.string.yes, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int id) { + new AliasOperation(certHolder).execute(); + dialog.dismiss(); + certDialog.dismiss(); + } + }); + builder.setNegativeButton( + android.R.string.no, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + } + }); + + certDialog.show(); + } + + private class AliasOperation extends AsyncTask<Void, Void, Boolean> { + private final CertHolder mCertHolder; + private AliasOperation(CertHolder certHolder) { + mCertHolder = certHolder; + } + @Override protected Boolean doInBackground(Void... params) { + try { + KeyChainConnection keyChainConnection = KeyChain.bind(getActivity()); + IKeyChainService service = keyChainConnection.getService(); + try { + if (mCertHolder.mDeleted) { + byte[] bytes = mCertHolder.mX509Cert.getEncoded(); + service.installCaCertificate(bytes); + return true; + } else { + return service.deleteCaCertificate(mCertHolder.mAlias); + } + } finally { + keyChainConnection.close(); + } + } catch (CertificateEncodingException e) { + return false; + } catch (IllegalStateException e) { + // used by installCaCertificate to report errors + return false; + } catch (RemoteException e) { + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + @Override protected void onPostExecute(Boolean ok) { + mCertHolder.mTab.postOperationUpdate(ok, mCertHolder); + } + } +} diff --git a/src/com/android/settings/UserDictionarySettings.java b/src/com/android/settings/UserDictionarySettings.java index f712cb6..fa4359c 100644 --- a/src/com/android/settings/UserDictionarySettings.java +++ b/src/com/android/settings/UserDictionarySettings.java @@ -63,23 +63,29 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata // Either the locale is empty (means the word is applicable to all locales) // or the word equals our current locale - private static final String QUERY_SELECTION = UserDictionary.Words.LOCALE + "=? OR " - + UserDictionary.Words.LOCALE + " is null"; + private static final String QUERY_SELECTION = + UserDictionary.Words.LOCALE + "=?"; + private static final String QUERY_SELECTION_ALL_LOCALES = + UserDictionary.Words.LOCALE + " is null"; private static final String DELETE_SELECTION = UserDictionary.Words.WORD + "=?"; private static final String EXTRA_WORD = "word"; - + private static final int OPTIONS_MENU_ADD = Menu.FIRST; private static final int DIALOG_ADD_OR_EDIT = 0; - + + private static final int FREQUENCY_FOR_USER_DICTIONARY_ADDS = 250; + /** The word being edited in the dialog (null means the user is adding a word). */ private String mDialogEditingWord; private View mView; private Cursor mCursor; - + + protected String mLocale; + private boolean mAddedWordAlready; private boolean mAutoReturn; @@ -93,7 +99,7 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mView = inflater.inflate(R.layout.list_content_with_empty_view, container, false); + mView = inflater.inflate(R.layout.custom_preference_list_fragment, container, false); return mView; } @@ -101,7 +107,25 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mCursor = createCursor(); + final Intent intent = getActivity().getIntent(); + final String localeFromIntent = + null == intent ? null : intent.getStringExtra("locale"); + + final Bundle arguments = getArguments(); + final String localeFromArguments = + null == arguments ? null : arguments.getString("locale"); + + final String locale; + if (null != localeFromArguments) { + locale = localeFromArguments; + } else if (null != localeFromIntent) { + locale = localeFromIntent; + } else { + locale = null; + } + + mLocale = locale; + mCursor = createCursor(locale); TextView emptyView = (TextView)mView.findViewById(R.id.empty); emptyView.setText(R.string.user_dict_settings_empty_text); @@ -117,12 +141,12 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata mAddedWordAlready = savedInstanceState.getBoolean(INSTANCE_KEY_ADDED_WORD, false); } } - + @Override public void onResume() { super.onResume(); final Intent intent = getActivity().getIntent(); - if (!mAddedWordAlready + if (!mAddedWordAlready && intent.getAction().equals("com.android.settings.USER_DICTIONARY_INSERT")) { final String word = intent.getStringExtra(EXTRA_WORD); mAutoReturn = true; @@ -139,12 +163,28 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata outState.putBoolean(INSTANCE_KEY_ADDED_WORD, mAddedWordAlready); } - private Cursor createCursor() { - String currentLocale = Locale.getDefault().toString(); - // Case-insensitive sort - return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, - QUERY_SELECTION, new String[] { currentLocale }, - "UPPER(" + UserDictionary.Words.WORD + ")"); + private Cursor createCursor(final String locale) { + // Locale can be any of: + // - The string representation of a locale, as returned by Locale#toString() + // - The empty string. This means we want a cursor returning words valid for all locales. + // - null. This means we want a cursor for the current locale, whatever this is. + // Note that this contrasts with the data inside the database, where NULL means "all + // locales" and there should never be an empty string. The confusion is called by the + // historical use of null for "all locales". + // TODO: it should be easy to make this more readable by making the special values + // human-readable, like "all_locales" and "current_locales" strings, provided they + // can be guaranteed not to match locales that may exist. + if ("".equals(locale)) { + // Case-insensitive sort + return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, + QUERY_SELECTION_ALL_LOCALES, null, + "UPPER(" + UserDictionary.Words.WORD + ")"); + } else { + final String queryLocale = null != locale ? locale : Locale.getDefault().toString(); + return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, + QUERY_SELECTION, new String[] { queryLocale }, + "UPPER(" + UserDictionary.Words.WORD + ")"); + } } private ListAdapter createAdapter() { @@ -153,7 +193,7 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata new String[] { UserDictionary.Words.WORD, UserDictionary.Words._ID }, new int[] { android.R.id.text1, R.id.delete_button }, this); } - + @Override public void onListItemClick(ListView l, View v, int position, long id) { String word = getWord(position); @@ -167,7 +207,8 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata MenuItem actionItem = menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) .setIcon(R.drawable.ic_menu_add); - actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | + MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @Override @@ -182,6 +223,7 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata } private String getWord(int position) { + if (null == mCursor) return null; mCursor.moveToPosition(position); // Handle a possible race-condition if (mCursor.isAfterLast()) return null; @@ -235,14 +277,29 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata // The user was editing a word, so do a delete/add deleteWord(mDialogEditingWord); } - + // Disallow duplicates deleteWord(word); - + // TODO: present UI for picking whether to add word to all locales, or current. - UserDictionary.Words.addWord(getActivity(), word.toString(), - 250, UserDictionary.Words.LOCALE_TYPE_ALL); - if (!mCursor.requery()) { + if (null == mLocale) { + // Null means insert with the default system locale. + UserDictionary.Words.addWord(getActivity(), word.toString(), + FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT); + } else if ("".equals(mLocale)) { + // Empty string means insert for all languages. + UserDictionary.Words.addWord(getActivity(), word.toString(), + FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_ALL); + } else { + // TODO: fix the framework so that it can accept a locale when we add a word + // to the user dictionary instead of querying the system locale. + final Locale prevLocale = Locale.getDefault(); + Locale.setDefault(Utils.createLocaleFromString(mLocale)); + UserDictionary.Words.addWord(getActivity(), word.toString(), + FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT); + Locale.setDefault(prevLocale); + } + if (null != mCursor && !mCursor.requery()) { throw new IllegalStateException("can't requery on already-closed cursor."); } mAddedWordAlready = true; @@ -277,23 +334,25 @@ public class UserDictionarySettings extends ListFragment implements DialogCreata super(context, layout, c, from, to); mSettings = settings; - int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); - String alphabet = context.getString( - com.android.internal.R.string.fast_scroll_alphabet); - mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); + if (null != c) { + final String alphabet = context.getString( + com.android.internal.R.string.fast_scroll_alphabet); + final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); + mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); + } setViewBinder(mViewBinder); } public int getPositionForSection(int section) { - return mIndexer.getPositionForSection(section); + return null == mIndexer ? 0 : mIndexer.getPositionForSection(section); } public int getSectionForPosition(int position) { - return mIndexer.getSectionForPosition(position); + return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position); } public Object[] getSections() { - return mIndexer.getSections(); + return null == mIndexer ? null : mIndexer.getSections(); } public void onClick(View v) { diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 18c6159..b725d56 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -27,6 +27,7 @@ import android.content.res.Resources.NotFoundException; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.LinkProperties; +import android.os.BatteryManager; import android.os.Bundle; import android.os.SystemProperties; import android.preference.Preference; @@ -36,8 +37,10 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import java.net.InetAddress; +import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Locale; public class Utils { @@ -308,4 +311,58 @@ public class Utils { } return addresses; } + + public static Locale createLocaleFromString(String localeStr) { + // TODO: is there a better way to actually construct a locale that will match? + // The main problem is, on top of Java specs, locale.toString() and + // new Locale(locale.toString()).toString() do not return equal() strings in + // many cases, because the constructor takes the only string as the language + // code. So : new Locale("en", "US").toString() => "en_US" + // And : new Locale("en_US").toString() => "en_us" + if (null == localeStr) + return Locale.getDefault(); + String[] brokenDownLocale = localeStr.split("_", 3); + // split may not return a 0-length array. + if (1 == brokenDownLocale.length) { + return new Locale(brokenDownLocale[0]); + } else if (2 == brokenDownLocale.length) { + return new Locale(brokenDownLocale[0], brokenDownLocale[1]); + } else { + return new Locale(brokenDownLocale[0], brokenDownLocale[1], brokenDownLocale[2]); + } + } + + public static String getBatteryPercentage(Intent batteryChangedIntent) { + int level = batteryChangedIntent.getIntExtra("level", 0); + int scale = batteryChangedIntent.getIntExtra("scale", 100); + return String.valueOf(level * 100 / scale) + "%"; + } + + public static String getBatteryStatus(Resources res, Intent batteryChangedIntent) { + final Intent intent = batteryChangedIntent; + + int plugType = intent.getIntExtra("plugged", 0); + int status = intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN); + String statusString; + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + statusString = res.getString(R.string.battery_info_status_charging); + if (plugType > 0) { + statusString = statusString + + " " + + res.getString((plugType == BatteryManager.BATTERY_PLUGGED_AC) + ? R.string.battery_info_status_charging_ac + : R.string.battery_info_status_charging_usb); + } + } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { + statusString = res.getString(R.string.battery_info_status_discharging); + } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { + statusString = res.getString(R.string.battery_info_status_not_charging); + } else if (status == BatteryManager.BATTERY_STATUS_FULL) { + statusString = res.getString(R.string.battery_info_status_full); + } else { + statusString = res.getString(R.string.battery_info_status_unknown); + } + + return statusString; + } } diff --git a/src/com/android/settings/WallpaperTypeSettings.java b/src/com/android/settings/WallpaperTypeSettings.java new file mode 100644 index 0000000..fa5f0ac --- /dev/null +++ b/src/com/android/settings/WallpaperTypeSettings.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2011 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; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceScreen; + +import java.util.List; + +public class WallpaperTypeSettings extends SettingsPreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.wallpaper_settings); + populateWallpaperTypes(); + } + + private void populateWallpaperTypes() { + // Search for activities that satisfy the ACTION_SET_WALLPAPER action + Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + final PackageManager pm = getPackageManager(); + List<ResolveInfo> rList = pm.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + + final PreferenceScreen parent = getPreferenceScreen(); + parent.setOrderingAsAdded(false); + // Add Preference items for each of the matching activities + for (ResolveInfo info : rList) { + Preference pref = new Preference(getActivity()); + Intent prefIntent = new Intent(intent); + prefIntent.setComponent(new ComponentName( + info.activityInfo.packageName, info.activityInfo.name)); + pref.setIntent(prefIntent); + CharSequence label = info.loadLabel(pm); + if (label == null) label = info.activityInfo.packageName; + pref.setTitle(label); + parent.addPreference(pref); + } + } +} diff --git a/src/com/android/settings/WirelessSettings.java b/src/com/android/settings/WirelessSettings.java index 2844f3b..636799d 100644 --- a/src/com/android/settings/WirelessSettings.java +++ b/src/com/android/settings/WirelessSettings.java @@ -16,36 +16,34 @@ package com.android.settings; -import com.android.internal.telephony.TelephonyIntents; -import com.android.internal.telephony.TelephonyProperties; -import com.android.settings.bluetooth.BluetoothEnabler; -import com.android.settings.wifi.WifiEnabler; -import com.android.settings.nfc.NfcEnabler; - import android.app.Activity; import android.app.admin.DevicePolicyManager; -import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; +import android.net.wifi.p2p.WifiP2pManager; import android.nfc.NfcAdapter; import android.os.Bundle; -import android.os.ServiceManager; import android.os.SystemProperties; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceScreen; import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Switch; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.telephony.TelephonyProperties; +import com.android.settings.nfc.NfcEnabler; public class WirelessSettings extends SettingsPreferenceFragment { private static final String KEY_TOGGLE_AIRPLANE = "toggle_airplane"; - private static final String KEY_TOGGLE_BLUETOOTH = "toggle_bluetooth"; - private static final String KEY_TOGGLE_WIFI = "toggle_wifi"; private static final String KEY_TOGGLE_NFC = "toggle_nfc"; - private static final String KEY_WIFI_SETTINGS = "wifi_settings"; - private static final String KEY_BT_SETTINGS = "bt_settings"; + private static final String KEY_ZEROCLICK_SETTINGS = "zeroclick_settings"; private static final String KEY_VPN_SETTINGS = "vpn_settings"; + private static final String KEY_WIFI_P2P_SETTINGS = "wifi_p2p_settings"; private static final String KEY_TETHER_SETTINGS = "tether_settings"; private static final String KEY_PROXY_SETTINGS = "proxy_settings"; private static final String KEY_MOBILE_NETWORK_SETTINGS = "mobile_network_settings"; @@ -55,9 +53,8 @@ public class WirelessSettings extends SettingsPreferenceFragment { private AirplaneModeEnabler mAirplaneModeEnabler; private CheckBoxPreference mAirplaneModePreference; - private WifiEnabler mWifiEnabler; private NfcEnabler mNfcEnabler; - private BluetoothEnabler mBtEnabler; + private NfcAdapter mNfcAdapter; /** * Invoked on each preference click in this hierarchy, overrides @@ -95,42 +92,38 @@ public class WirelessSettings extends SettingsPreferenceFragment { addPreferencesFromResource(R.xml.wireless_settings); final Activity activity = getActivity(); - CheckBoxPreference airplane = (CheckBoxPreference) findPreference(KEY_TOGGLE_AIRPLANE); - CheckBoxPreference wifi = (CheckBoxPreference) findPreference(KEY_TOGGLE_WIFI); - CheckBoxPreference bt = (CheckBoxPreference) findPreference(KEY_TOGGLE_BLUETOOTH); + mAirplaneModePreference = (CheckBoxPreference) findPreference(KEY_TOGGLE_AIRPLANE); CheckBoxPreference nfc = (CheckBoxPreference) findPreference(KEY_TOGGLE_NFC); + PreferenceScreen zeroclick = (PreferenceScreen) findPreference(KEY_ZEROCLICK_SETTINGS); - mAirplaneModeEnabler = new AirplaneModeEnabler(activity, airplane); - mAirplaneModePreference = (CheckBoxPreference) findPreference(KEY_TOGGLE_AIRPLANE); - mWifiEnabler = new WifiEnabler(activity, wifi); - mBtEnabler = new BluetoothEnabler(activity, bt); - mNfcEnabler = new NfcEnabler(activity, nfc); + mAirplaneModeEnabler = new AirplaneModeEnabler(activity, mAirplaneModePreference); + mNfcEnabler = new NfcEnabler(activity, nfc, zeroclick); String toggleable = Settings.System.getString(activity.getContentResolver(), Settings.System.AIRPLANE_MODE_TOGGLEABLE_RADIOS); // Manually set dependencies for Wifi when not toggleable. if (toggleable == null || !toggleable.contains(Settings.System.RADIO_WIFI)) { - wifi.setDependency(KEY_TOGGLE_AIRPLANE); - findPreference(KEY_WIFI_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); findPreference(KEY_VPN_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); } // Manually set dependencies for Bluetooth when not toggleable. if (toggleable == null || !toggleable.contains(Settings.System.RADIO_BLUETOOTH)) { - bt.setDependency(KEY_TOGGLE_AIRPLANE); - findPreference(KEY_BT_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); + // No bluetooth-dependent items in the list. Code kept in case one is added later. } - // Remove Bluetooth Settings if Bluetooth service is not available. - if (ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE) == null) { - getPreferenceScreen().removePreference(bt); - getPreferenceScreen().removePreference(findPreference(KEY_BT_SETTINGS)); + // Manually set dependencies for NFC when not toggleable. + if (toggleable == null || !toggleable.contains(Settings.System.RADIO_NFC)) { + findPreference(KEY_TOGGLE_NFC).setDependency(KEY_TOGGLE_AIRPLANE); + findPreference(KEY_ZEROCLICK_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); } // Remove NFC if its not available - if (NfcAdapter.getDefaultAdapter(activity) == null) { + mNfcAdapter = NfcAdapter.getDefaultAdapter(activity); + if (mNfcAdapter == null) { getPreferenceScreen().removePreference(nfc); + getPreferenceScreen().removePreference(zeroclick); + mNfcEnabler = null; } // Remove Mobile Network Settings if it's a wifi-only device. @@ -138,6 +131,11 @@ public class WirelessSettings extends SettingsPreferenceFragment { getPreferenceScreen().removePreference(findPreference(KEY_MOBILE_NETWORK_SETTINGS)); } + WifiP2pManager wifiP2p = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE); + if (!wifiP2p.isP2pSupported()) { + getPreferenceScreen().removePreference(findPreference(KEY_WIFI_P2P_SETTINGS)); + } + // Enable Proxy selector settings if allowed. Preference mGlobalProxy = findPreference(KEY_PROXY_SETTINGS); DevicePolicyManager mDPM = (DevicePolicyManager) @@ -163,25 +161,18 @@ public class WirelessSettings extends SettingsPreferenceFragment { Preference p = findPreference(KEY_TETHER_SETTINGS); if (wifiAvailable && usbAvailable && bluetoothAvailable) { p.setTitle(R.string.tether_settings_title_all); - p.setSummary(R.string.tether_settings_summary_all); } else if (wifiAvailable && usbAvailable) { p.setTitle(R.string.tether_settings_title_all); - p.setSummary(R.string.tether_settings_summary_usb_wifi); } else if (wifiAvailable && bluetoothAvailable) { p.setTitle(R.string.tether_settings_title_all); - p.setSummary(R.string.tether_settings_summary_wifi_bluetooth); } else if (wifiAvailable) { p.setTitle(R.string.tether_settings_title_wifi); - p.setSummary(R.string.tether_settings_summary_wifi); } else if (usbAvailable && bluetoothAvailable) { p.setTitle(R.string.tether_settings_title_usb_bluetooth); - p.setSummary(R.string.tether_settings_summary_usb_bluetooth); } else if (usbAvailable) { p.setTitle(R.string.tether_settings_title_usb); - p.setSummary(R.string.tether_settings_summary_usb); } else { p.setTitle(R.string.tether_settings_title_bluetooth); - p.setSummary(R.string.tether_settings_summary_bluetooth); } } } @@ -191,9 +182,9 @@ public class WirelessSettings extends SettingsPreferenceFragment { super.onResume(); mAirplaneModeEnabler.resume(); - mWifiEnabler.resume(); - mBtEnabler.resume(); - mNfcEnabler.resume(); + if (mNfcEnabler != null) { + mNfcEnabler.resume(); + } } @Override @@ -201,9 +192,9 @@ public class WirelessSettings extends SettingsPreferenceFragment { super.onPause(); mAirplaneModeEnabler.pause(); - mWifiEnabler.pause(); - mBtEnabler.pause(); - mNfcEnabler.pause(); + if (mNfcEnabler != null) { + mNfcEnabler.pause(); + } } @Override diff --git a/src/com/android/settings/accounts/AccountPreferenceBase.java b/src/com/android/settings/accounts/AccountPreferenceBase.java index a84bece..a0d6a7f 100644 --- a/src/com/android/settings/accounts/AccountPreferenceBase.java +++ b/src/com/android/settings/accounts/AccountPreferenceBase.java @@ -32,6 +32,7 @@ import android.content.Context; import android.content.SyncAdapterType; import android.content.SyncStatusObserver; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; @@ -113,7 +114,7 @@ class AccountPreferenceBase extends SettingsPreferenceFragment mAccountTypeToAuthorities.put(sa.accountType, authorities); } if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "added authority " + sa.authority + " to accountType " + Log.d(TAG, "added authority " + sa.authority + " to accountType " + sa.accountType); } authorities.add(sa.authority); @@ -136,7 +137,10 @@ class AccountPreferenceBase extends SettingsPreferenceFragment icon = authContext.getResources().getDrawable(desc.iconId); } catch (PackageManager.NameNotFoundException e) { // TODO: place holder icon for missing account icons? - Log.w(TAG, "No icon for account type " + accountType); + Log.w(TAG, "No icon name for account type " + accountType); + } catch (Resources.NotFoundException e) { + // TODO: place holder icon for missing account icons? + Log.w(TAG, "No icon resource for account type " + accountType); } } return icon; @@ -155,7 +159,9 @@ class AccountPreferenceBase extends SettingsPreferenceFragment Context authContext = getActivity().createPackageContext(desc.packageName, 0); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "No label for account type " + ", type " + accountType); + Log.w(TAG, "No label name for account type " + accountType); + } catch (Resources.NotFoundException e) { + Log.w(TAG, "No label icon for account type " + accountType); } } return label; @@ -179,6 +185,8 @@ class AccountPreferenceBase extends SettingsPreferenceFragment } } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); + } catch (Resources.NotFoundException e) { + Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); } } return prefs; diff --git a/src/com/android/settings/accounts/AccountSyncSettings.java b/src/com/android/settings/accounts/AccountSyncSettings.java index 9ef0481..e70cbad 100644 --- a/src/com/android/settings/accounts/AccountSyncSettings.java +++ b/src/com/android/settings/accounts/AccountSyncSettings.java @@ -16,10 +16,6 @@ package com.android.settings.accounts; -import com.android.settings.R; -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; - import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; @@ -53,6 +49,10 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.android.settings.R; +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -175,7 +175,13 @@ public class AccountSyncSettings extends AccountPreferenceBase { mDateFormat = DateFormat.getDateFormat(activity); mTimeFormat = DateFormat.getTimeFormat(activity); - mAccount = (Account) getArguments().getParcelable(ACCOUNT_KEY); + Bundle arguments = getArguments(); + if (arguments == null) { + Log.e(TAG, "No arguments provided when starting intent. ACCOUNT_KEY needed."); + return; + } + + mAccount = (Account) arguments.getParcelable(ACCOUNT_KEY); if (mAccount != null) { if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Got account: " + mAccount); mUserId.setText(mAccount.name); @@ -230,10 +236,12 @@ public class AccountSyncSettings extends AccountPreferenceBase { getString(R.string.sync_menu_sync_cancel)) .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel); - removeAccount.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS - | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - syncNow.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - syncCancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + removeAccount.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | + MenuItem.SHOW_AS_ACTION_WITH_TEXT); + syncNow.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | + MenuItem.SHOW_AS_ACTION_WITH_TEXT); + syncCancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | + MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @Override @@ -486,11 +494,13 @@ public class AccountSyncSettings extends AccountPreferenceBase { protected void onAuthDescriptionsUpdated() { super.onAuthDescriptionsUpdated(); getPreferenceScreen().removeAll(); - mProviderIcon.setImageDrawable(getDrawableForType(mAccount.type)); - mProviderId.setText(getLabelForType(mAccount.type)); - PreferenceScreen prefs = addPreferencesForType(mAccount.type); - if (prefs != null) { - updatePreferenceIntents(prefs); + if (mAccount != null) { + mProviderIcon.setImageDrawable(getDrawableForType(mAccount.type)); + mProviderId.setText(getLabelForType(mAccount.type)); + PreferenceScreen prefs = addPreferencesForType(mAccount.type); + if (prefs != null) { + updatePreferenceIntents(prefs); + } } addPreferencesFromResource(R.xml.account_sync_settings); } diff --git a/src/com/android/settings/accounts/AddAccountSettings.java b/src/com/android/settings/accounts/AddAccountSettings.java index 72ef130..382481e 100644 --- a/src/com/android/settings/accounts/AddAccountSettings.java +++ b/src/com/android/settings/accounts/AddAccountSettings.java @@ -22,6 +22,7 @@ import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; +import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -44,18 +45,37 @@ import java.io.IOException; * when returning from each account setup, which doesn't look good. */ public class AddAccountSettings extends Activity { + /** + * + */ + private static final String KEY_ADD_CALLED = "AddAccountCalled"; + + /** + * Extra parameter to identify the caller. Applications may display a + * different UI if the calls is made from Settings or from a specific + * application. + */ + private static final String KEY_CALLER_IDENTITY = "pendingIntent"; + private static final String TAG = "AccountSettings"; /* package */ static final String EXTRA_SELECTED_ACCOUNT = "selected_account"; private static final int CHOOSE_ACCOUNT_REQUEST = 1; + private PendingIntent mPendingIntent; + private AccountManagerCallback<Bundle> mCallback = new AccountManagerCallback<Bundle>() { public void run(AccountManagerFuture<Bundle> future) { try { Bundle bundle = future.getResult(); bundle.keySet(); setResult(RESULT_OK); + + if (mPendingIntent != null) { + mPendingIntent.cancel(); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "account added: " + bundle); } catch (OperationCanceledException e) { if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount was canceled"); @@ -69,10 +89,22 @@ public class AddAccountSettings extends Activity { } }; + private boolean mAddAccountCalled = false; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mAddAccountCalled = savedInstanceState.getBoolean(KEY_ADD_CALLED); + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "restored"); + } + + if (mAddAccountCalled) { + // We already called add account - maybe the callback was lost. + finish(); + return; + } final String[] authorities = getIntent().getStringArrayExtra(AccountPreferenceBase.AUTHORITIES_FILTER_KEY); final String[] accountTypes = @@ -102,14 +134,24 @@ public class AddAccountSettings extends Activity { } } + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_ADD_CALLED, mAddAccountCalled); + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "saved"); + } + private void addAccount(String accountType) { + Bundle addAccountOptions = new Bundle(); + mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(), 0); + addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent); AccountManager.get(this).addAccount( accountType, null, /* authTokenType */ null, /* requiredFeatures */ - null, /* addAccountOptions */ + addAccountOptions, this, mCallback, null /* handler */); + mAddAccountCalled = true; } } diff --git a/src/com/android/settings/accounts/ChooseAccountActivity.java b/src/com/android/settings/accounts/ChooseAccountActivity.java index 9576dee..631fe47 100644 --- a/src/com/android/settings/accounts/ChooseAccountActivity.java +++ b/src/com/android/settings/accounts/ChooseAccountActivity.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.SyncAdapterType; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.Preference; @@ -199,7 +200,10 @@ public class ChooseAccountActivity extends PreferenceActivity { icon = authContext.getResources().getDrawable(desc.iconId); } catch (PackageManager.NameNotFoundException e) { // TODO: place holder icon for missing account icons? - Log.w(TAG, "No icon for account type " + accountType); + Log.w(TAG, "No icon name for account type " + accountType); + } catch (Resources.NotFoundException e) { + // TODO: place holder icon for missing account icons? + Log.w(TAG, "No icon resource for account type " + accountType); } } return icon; @@ -218,7 +222,9 @@ public class ChooseAccountActivity extends PreferenceActivity { Context authContext = createPackageContext(desc.packageName, 0); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "No label for account type " + ", type " + accountType); + Log.w(TAG, "No label name for account type " + accountType); + } catch (Resources.NotFoundException e) { + Log.w(TAG, "No label resource for account type " + accountType); } } return label; diff --git a/src/com/android/settings/accounts/ManageAccountsSettings.java b/src/com/android/settings/accounts/ManageAccountsSettings.java index 06c5ff0..d9dea00 100644 --- a/src/com/android/settings/accounts/ManageAccountsSettings.java +++ b/src/com/android/settings/accounts/ManageAccountsSettings.java @@ -19,8 +19,6 @@ package com.android.settings.accounts; import com.android.settings.AccountPreference; import com.android.settings.DialogCreatable; import com.android.settings.R; -import com.android.settings.vpn.VpnTypeSelection; -import com.google.android.collect.Maps; import android.accounts.Account; import android.accounts.AccountManager; @@ -36,7 +34,6 @@ import android.content.Intent; import android.content.SyncAdapterType; import android.content.SyncInfo; import android.content.SyncStatusInfo; -import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.os.Bundle; @@ -202,6 +199,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase return null; } + @Override public void showDialog(int dialogId) { if (mDialogFragment != null) { Log.e(TAG, "Old dialog fragment not null!"); @@ -213,8 +211,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem actionItem = - menu.add(0, MENU_ADD_ACCOUNT, 0, R.string.add_account_label) - .setIcon(R.drawable.ic_menu_add); + menu.add(0, MENU_ADD_ACCOUNT, 0, R.string.add_account_label); actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @@ -236,6 +233,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase connManager.setBackgroundDataSetting(enabled); } + @Override protected void onSyncStateUpdated() { // Catch any delayed delivery of update messages if (getActivity() == null) return; @@ -343,6 +341,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase onSyncStateUpdated(); } + @Override protected void onAuthDescriptionsUpdated() { // Update account icons for all account preference items for (int i = 0; i < mManageAccountsCategory.getPreferenceCount(); i++) { diff --git a/src/com/android/settings/applications/ApplicationsState.java b/src/com/android/settings/applications/ApplicationsState.java index 11e4aae..e0899cb 100644 --- a/src/com/android/settings/applications/ApplicationsState.java +++ b/src/com/android/settings/applications/ApplicationsState.java @@ -5,14 +5,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageStatsObserver; import android.content.pm.PackageManager; import android.content.pm.PackageStats; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; @@ -74,7 +71,8 @@ public class ApplicationsState { long cacheSize; long codeSize; long dataSize; - long externalSize; + long externalCodeSize; + long externalDataSize; } public static class AppEntry extends SizeInfo { @@ -82,6 +80,8 @@ public class ApplicationsState { final long id; String label; long size; + long internalSize; + long externalSize; boolean mounted; @@ -97,6 +97,8 @@ public class ApplicationsState { ApplicationInfo info; Drawable icon; String sizeStr; + String internalSizeStr; + String externalSizeStr; boolean sizeStale; long sizeLoadStart; @@ -155,7 +157,8 @@ public class ApplicationsState { } }; - public static final Comparator<AppEntry> SIZE_COMPARATOR = new Comparator<AppEntry>() { + public static final Comparator<AppEntry> SIZE_COMPARATOR + = new Comparator<AppEntry>() { private final Collator sCollator = Collator.getInstance(); @Override public int compare(AppEntry object1, AppEntry object2) { @@ -165,6 +168,28 @@ public class ApplicationsState { } }; + public static final Comparator<AppEntry> INTERNAL_SIZE_COMPARATOR + = new Comparator<AppEntry>() { + private final Collator sCollator = Collator.getInstance(); + @Override + public int compare(AppEntry object1, AppEntry object2) { + if (object1.internalSize < object2.internalSize) return 1; + if (object1.internalSize > object2.internalSize) return -1; + return sCollator.compare(object1.label, object2.label); + } + }; + + public static final Comparator<AppEntry> EXTERNAL_SIZE_COMPARATOR + = new Comparator<AppEntry>() { + private final Collator sCollator = Collator.getInstance(); + @Override + public int compare(AppEntry object1, AppEntry object2) { + if (object1.externalSize < object2.externalSize) return 1; + if (object1.externalSize > object2.externalSize) return -1; + return sCollator.compare(object1.label, object2.label); + } + }; + public static final AppFilter THIRD_PARTY_FILTER = new AppFilter() { public void init() { } @@ -392,6 +417,14 @@ public class ApplicationsState { for (int i=0; i<mApplications.size(); i++) { final ApplicationInfo info = mApplications.get(i); + // Need to trim out any applications that are disabled by + // something different than the user. + if (!info.enabled && info.enabledSetting + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + mApplications.remove(i); + i--; + continue; + } final AppEntry entry = mEntriesMap.get(info.packageName); if (entry != null) { entry.info = info; @@ -652,8 +685,8 @@ public class ApplicationsState { private long getTotalExternalSize(PackageStats ps) { if (ps != null) { - return ps.externalDataSize + ps.externalMediaSize + ps.externalCacheSize - + ps.externalObbSize; + return ps.externalCodeSize + ps.externalDataSize + + ps.externalMediaSize + ps.externalObbSize; } return SIZE_INVALID; } @@ -685,19 +718,29 @@ public class ApplicationsState { synchronized (entry) { entry.sizeStale = false; entry.sizeLoadStart = 0; - long externalSize = getTotalExternalSize(stats); - long newSize = externalSize + getTotalInternalSize(stats); + long externalCodeSize = stats.externalCodeSize + + stats.externalObbSize; + long externalDataSize = stats.externalDataSize + + stats.externalMediaSize + stats.externalCacheSize; + long newSize = externalCodeSize + externalDataSize + + getTotalInternalSize(stats); if (entry.size != newSize || entry.cacheSize != stats.cacheSize || entry.codeSize != stats.codeSize || entry.dataSize != stats.dataSize || - entry.externalSize != externalSize) { + entry.externalCodeSize != externalCodeSize || + entry.externalDataSize != externalDataSize) { entry.size = newSize; entry.cacheSize = stats.cacheSize; entry.codeSize = stats.codeSize; entry.dataSize = stats.dataSize; - entry.externalSize = externalSize; + entry.externalCodeSize = externalCodeSize; + entry.externalDataSize = externalDataSize; entry.sizeStr = getSizeStr(entry.size); + entry.internalSize = getTotalInternalSize(stats); + entry.internalSizeStr = getSizeStr(entry.internalSize); + entry.externalSize = getTotalExternalSize(stats); + entry.externalSizeStr = getSizeStr(entry.externalSize); if (DEBUG) Log.i(TAG, "Set size of " + entry.label + " " + entry + ": " + entry.sizeStr); sizeChanged = true; diff --git a/src/com/android/settings/applications/InstalledAppDetails.java b/src/com/android/settings/applications/InstalledAppDetails.java index 629bac5..a84b8bb 100644 --- a/src/com/android/settings/applications/InstalledAppDetails.java +++ b/src/com/android/settings/applications/InstalledAppDetails.java @@ -79,7 +79,7 @@ public class InstalledAppDetails extends Fragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, ApplicationsState.Callbacks { private static final String TAG="InstalledAppDetails"; - static final boolean SUPPORT_DISABLE_APPS = false; + static final boolean SUPPORT_DISABLE_APPS = true; private static final boolean localLOGV = false; public static final String ARG_PACKAGE_NAME = "package"; @@ -103,7 +103,8 @@ public class InstalledAppDetails extends Fragment private TextView mTotalSize; private TextView mAppSize; private TextView mDataSize; - private TextView mExternalSize; + private TextView mExternalCodeSize; + private TextView mExternalDataSize; private ClearUserDataObserver mClearDataObserver; // Views related to cache info private TextView mCacheSize; @@ -118,7 +119,8 @@ public class InstalledAppDetails extends Fragment private boolean mHaveSizes = false; private long mLastCodeSize = -1; private long mLastDataSize = -1; - private long mLastExternalSize = -1; + private long mLastExternalCodeSize = -1; + private long mLastExternalDataSize = -1; private long mLastCacheSize = -1; private long mLastTotalSize = -1; @@ -145,6 +147,7 @@ public class InstalledAppDetails extends Fragment private static final int DLG_CANNOT_CLEAR_DATA = DLG_BASE + 4; private static final int DLG_FORCE_STOP = DLG_BASE + 5; private static final int DLG_MOVE_FAILED = DLG_BASE + 6; + private static final int DLG_DISABLE = DLG_BASE + 7; private Handler mHandler = new Handler() { public void handleMessage(Message msg) { @@ -282,7 +285,7 @@ public class InstalledAppDetails extends Fragment intent.setPackage(mAppEntry.info.packageName); List<ResolveInfo> homes = mPm.queryIntentActivities(intent, 0); if ((homes != null && homes.size() > 0) || - (mPackageInfo != null && + (mPackageInfo != null && mPackageInfo.signatures != null && sys.signatures[0].equals(mPackageInfo.signatures[0]))) { // Disable button for core system applications. mUninstallButton.setText(R.string.disable_text); @@ -331,7 +334,8 @@ public class InstalledAppDetails extends Fragment mTotalSize = (TextView)view.findViewById(R.id.total_size_text); mAppSize = (TextView)view.findViewById(R.id.application_size_text); mDataSize = (TextView)view.findViewById(R.id.data_size_text); - mExternalSize = (TextView)view.findViewById(R.id.external_size_text); + mExternalCodeSize = (TextView)view.findViewById(R.id.external_code_size_text); + mExternalDataSize = (TextView)view.findViewById(R.id.external_data_size_text); // Get Control button panel View btnPanel = view.findViewById(R.id.control_buttons_panel); @@ -547,9 +551,13 @@ public class InstalledAppDetails extends Fragment mLastDataSize = mAppEntry.dataSize; mDataSize.setText(getSizeStr(mAppEntry.dataSize)); } - if (mLastExternalSize != mAppEntry.externalSize) { - mLastExternalSize = mAppEntry.externalSize; - mExternalSize.setText(getSizeStr(mAppEntry.externalSize)); + if (mLastExternalCodeSize != mAppEntry.externalCodeSize) { + mLastExternalCodeSize = mAppEntry.externalCodeSize; + mExternalCodeSize.setText(getSizeStr(mAppEntry.externalCodeSize)); + } + if (mLastExternalDataSize != mAppEntry.externalDataSize) { + mLastExternalDataSize = mAppEntry.externalDataSize; + mExternalDataSize.setText(getSizeStr(mAppEntry.externalDataSize)); } if (mLastCacheSize != mAppEntry.cacheSize) { mLastCacheSize = mAppEntry.cacheSize; @@ -747,6 +755,22 @@ public class InstalledAppDetails extends Fragment .setMessage(msg) .setNeutralButton(R.string.dlg_ok, null) .create(); + case DLG_DISABLE: + return new AlertDialog.Builder(getActivity()) + .setTitle(getActivity().getText(R.string.app_disable_dlg_title)) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(getActivity().getText(R.string.app_disable_dlg_text)) + .setPositiveButton(R.string.dlg_ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Disable the app + new DisableChanger(getOwner(), getOwner().mAppEntry.info, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) + .execute((Object)null); + } + }) + .setNegativeButton(R.string.dlg_cancel, null) + .create(); } throw new IllegalArgumentException("unknown id " + id); } @@ -830,9 +854,13 @@ public class InstalledAppDetails extends Fragment showDialogInner(DLG_FACTORY_RESET, 0); } else { if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - new DisableChanger(this, mAppEntry.info, mAppEntry.info.enabled ? - PackageManager.COMPONENT_ENABLED_STATE_DISABLED - : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT).execute((Object)null); + if (mAppEntry.info.enabled) { + showDialogInner(DLG_DISABLE, 0); + } else { + new DisableChanger(this, mAppEntry.info, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) + .execute((Object)null); + } } else { uninstallPkg(packageName); } diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index 85db45e..68c942d 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -121,6 +121,10 @@ public class ManageApplications extends Fragment implements // constant value that can be used to check return code from sub activity. private static final int INSTALLED_APP_DETAILS = 1; + public static final int SIZE_TOTAL = 0; + public static final int SIZE_INTERNAL = 1; + public static final int SIZE_EXTERNAL = 2; + // sort order that can be changed through the menu can be sorted alphabetically // or size(descending) private static final int MENU_OPTIONS_BASE = 0; @@ -208,11 +212,21 @@ public class ManageApplications extends Fragment implements TextView disabled; CheckBox checkBox; - void updateSizeText(ManageApplications ma) { + void updateSizeText(ManageApplications ma, int whichSize) { if (DEBUG) Log.i(TAG, "updateSizeText of " + entry.label + " " + entry + ": " + entry.sizeStr); if (entry.sizeStr != null) { - appSize.setText(entry.sizeStr); + switch (whichSize) { + case SIZE_INTERNAL: + appSize.setText(entry.internalSizeStr); + break; + case SIZE_EXTERNAL: + appSize.setText(entry.externalSizeStr); + break; + default: + appSize.setText(entry.sizeStr); + break; + } } else if (entry.size == ApplicationsState.SIZE_INVALID) { appSize.setText(ma.mInvalidSizeStr); } @@ -237,6 +251,7 @@ public class ManageApplications extends Fragment implements private boolean mResumed; private int mLastFilterMode=-1, mLastSortMode=-1; private boolean mWaitingForData; + private int mWhichSize = SIZE_TOTAL; CharSequence mCurFilterPrefix; private Filter mFilter = new Filter() { @@ -296,12 +311,21 @@ public class ManageApplications extends Fragment implements if (DEBUG) Log.i(TAG, "Rebuilding app list..."); ApplicationsState.AppFilter filterObj; Comparator<AppEntry> comparatorObj; + boolean emulated = Environment.isExternalStorageEmulated(); + if (emulated) { + mWhichSize = SIZE_TOTAL; + } else { + mWhichSize = SIZE_INTERNAL; + } switch (mLastFilterMode) { case FILTER_APPS_THIRD_PARTY: filterObj = ApplicationsState.THIRD_PARTY_FILTER; break; case FILTER_APPS_SDCARD: filterObj = ApplicationsState.ON_SD_CARD_FILTER; + if (!emulated) { + mWhichSize = SIZE_EXTERNAL; + } break; default: filterObj = null; @@ -309,7 +333,17 @@ public class ManageApplications extends Fragment implements } switch (mLastSortMode) { case SORT_ORDER_SIZE: - comparatorObj = ApplicationsState.SIZE_COMPARATOR; + switch (mWhichSize) { + case SIZE_INTERNAL: + comparatorObj = ApplicationsState.INTERNAL_SIZE_COMPARATOR; + break; + case SIZE_EXTERNAL: + comparatorObj = ApplicationsState.EXTERNAL_SIZE_COMPARATOR; + break; + default: + comparatorObj = ApplicationsState.SIZE_COMPARATOR; + break; + } break; default: comparatorObj = ApplicationsState.ALPHA_COMPARATOR; @@ -399,7 +433,7 @@ public class ManageApplications extends Fragment implements AppViewHolder holder = (AppViewHolder)mActive.get(i).getTag(); if (holder.entry.info.packageName.equals(packageName)) { synchronized (holder.entry) { - holder.updateSizeText(ManageApplications.this); + holder.updateSizeText(ManageApplications.this, mWhichSize); } if (holder.entry.info.packageName.equals(mCurrentPkgName) && mLastSortMode == SORT_ORDER_SIZE) { @@ -478,7 +512,7 @@ public class ManageApplications extends Fragment implements if (entry.icon != null) { holder.appIcon.setImageDrawable(entry.icon); } - holder.updateSizeText(ManageApplications.this); + holder.updateSizeText(ManageApplications.this, mWhichSize); if (InstalledAppDetails.SUPPORT_DISABLE_APPS) { holder.disabled.setVisibility(entry.info.enabled ? View.GONE : View.VISIBLE); } else { @@ -777,6 +811,11 @@ public class ManageApplications extends Fragment implements } catch (IllegalArgumentException e) { // use the old value of mFreeMem } + final int N = mApplicationsAdapter.getCount(); + for (int i=0; i<N; i++) { + ApplicationsState.AppEntry ae = mApplicationsAdapter.getAppEntry(i); + appStorage += ae.externalCodeSize + ae.externalDataSize; + } } else { if (!mLastShowedInternalStorage) { mLastShowedInternalStorage = true; @@ -790,10 +829,14 @@ public class ManageApplications extends Fragment implements mDataFileStats.getBlockSize(); } catch (IllegalArgumentException e) { } + final boolean emulatedStorage = Environment.isExternalStorageEmulated(); final int N = mApplicationsAdapter.getCount(); for (int i=0; i<N; i++) { ApplicationsState.AppEntry ae = mApplicationsAdapter.getAppEntry(i); appStorage += ae.codeSize + ae.dataSize; + if (emulatedStorage) { + appStorage += ae.externalCodeSize + ae.externalDataSize; + } } freeStorage += mApplicationsState.sumCacheSizes(); } diff --git a/src/com/android/settings/applications/RunningProcessesView.java b/src/com/android/settings/applications/RunningProcessesView.java index 1f91c33..7c3ebb0 100644 --- a/src/com/android/settings/applications/RunningProcessesView.java +++ b/src/com/android/settings/applications/RunningProcessesView.java @@ -16,6 +16,7 @@ package com.android.settings.applications; +import com.android.internal.util.MemInfoReader; import com.android.settings.R; import android.app.ActivityManager; @@ -24,9 +25,7 @@ import android.app.Fragment; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; -import android.os.StrictMode; import android.os.SystemClock; -import android.os.SystemProperties; import android.preference.PreferenceActivity; import android.text.format.DateUtils; import android.text.format.Formatter; @@ -42,7 +41,6 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.AbsListView.RecyclerListener; -import java.io.FileInputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -51,9 +49,6 @@ public class RunningProcessesView extends FrameLayout implements AdapterView.OnItemClickListener, RecyclerListener, RunningState.OnRefreshUiListener { - // Memory pages are 4K. - static final long PAGE_SIZE = 4*1024; - long SECONDARY_SERVER_MEM; final HashMap<View, ActiveItem> mActiveItems = new HashMap<View, ActiveItem>(); @@ -85,9 +80,9 @@ public class RunningProcessesView extends FrameLayout long mLastAvailMemory = -1; Dialog mCurDialog; - - byte[] mBuffer = new byte[1024]; - + + MemInfoReader mMemInfoReader = new MemInfoReader(); + public static class ActiveItem { View mRootView; RunningState.BaseItem mItem; @@ -304,71 +299,7 @@ public class RunningProcessesView extends FrameLayout } } } - - private boolean matchText(byte[] buffer, int index, String text) { - int N = text.length(); - if ((index+N) >= buffer.length) { - return false; - } - for (int i=0; i<N; i++) { - if (buffer[index+i] != text.charAt(i)) { - return false; - } - } - return true; - } - - private long extractMemValue(byte[] buffer, int index) { - while (index < buffer.length && buffer[index] != '\n') { - if (buffer[index] >= '0' && buffer[index] <= '9') { - int start = index; - index++; - while (index < buffer.length && buffer[index] >= '0' - && buffer[index] <= '9') { - index++; - } - String str = new String(buffer, 0, start, index-start); - return ((long)Integer.parseInt(str)) * 1024; - } - index++; - } - return 0; - } - - private long readAvailMem() { - // Permit disk reads here, as /proc/meminfo isn't really "on - // disk" and should be fast. TODO: make BlockGuard ignore - // /proc/ and /sys/ files perhaps? - StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); - try { - long memFree = 0; - long memCached = 0; - FileInputStream is = new FileInputStream("/proc/meminfo"); - int len = is.read(mBuffer); - is.close(); - final int BUFLEN = mBuffer.length; - for (int i=0; i<len && (memFree == 0 || memCached == 0); i++) { - if (matchText(mBuffer, i, "MemFree")) { - i += 7; - memFree = extractMemValue(mBuffer, i); - } else if (matchText(mBuffer, i, "Cached")) { - i += 6; - memCached = extractMemValue(mBuffer, i); - } - while (i < BUFLEN && mBuffer[i] != '\n') { - i++; - } - } - return memFree + memCached; - } catch (java.io.FileNotFoundException e) { - } catch (java.io.IOException e) { - } finally { - StrictMode.setThreadPolicy(savedPolicy); - } - return 0; - } - void refreshUi(boolean dataChanged) { if (dataChanged) { ServiceListAdapter adapter = (ServiceListAdapter)(mListView.getAdapter()); @@ -383,11 +314,13 @@ public class RunningProcessesView extends FrameLayout // This is the amount of available memory until we start killing // background services. - long availMem = readAvailMem() - SECONDARY_SERVER_MEM; + mMemInfoReader.readMemInfo(); + long availMem = mMemInfoReader.getFreeSize() + mMemInfoReader.getCachedSize() + - SECONDARY_SERVER_MEM; if (availMem < 0) { availMem = 0; } - + synchronized (mState.mLock) { if (mLastNumBackgroundProcesses != mState.mNumBackgroundProcesses || mLastBackgroundProcessMemory != mState.mBackgroundProcessMemory @@ -395,10 +328,14 @@ public class RunningProcessesView extends FrameLayout mLastNumBackgroundProcesses = mState.mNumBackgroundProcesses; mLastBackgroundProcessMemory = mState.mBackgroundProcessMemory; mLastAvailMemory = availMem; - String sizeStr = Formatter.formatShortFileSize(getContext(), - mLastAvailMemory + mLastBackgroundProcessMemory); + long freeMem = mLastAvailMemory + mLastBackgroundProcessMemory; + String sizeStr = Formatter.formatShortFileSize(getContext(), freeMem); mBackgroundProcessText.setText(getResources().getString( R.string.service_background_processes, sizeStr)); + sizeStr = Formatter.formatShortFileSize(getContext(), + mMemInfoReader.getTotalSize() - freeMem); + mForegroundProcessText.setText(getResources().getString( + R.string.service_foreground_processes, sizeStr)); } if (mLastNumForegroundProcesses != mState.mNumForegroundProcesses || mLastForegroundProcessMemory != mState.mForegroundProcessMemory @@ -408,15 +345,18 @@ public class RunningProcessesView extends FrameLayout mLastForegroundProcessMemory = mState.mForegroundProcessMemory; mLastNumServiceProcesses = mState.mNumServiceProcesses; mLastServiceProcessMemory = mState.mServiceProcessMemory; + /* String sizeStr = Formatter.formatShortFileSize(getContext(), mLastForegroundProcessMemory + mLastServiceProcessMemory); mForegroundProcessText.setText(getResources().getString( R.string.service_foreground_processes, sizeStr)); + */ } - float totalMem = availMem + mLastBackgroundProcessMemory - + mLastForegroundProcessMemory + mLastServiceProcessMemory; - mColorBar.setRatios(mLastForegroundProcessMemory/totalMem, + float totalMem = mMemInfoReader.getTotalSize(); + float totalShownMem = availMem + mLastBackgroundProcessMemory + + mLastServiceProcessMemory; + mColorBar.setRatios((totalMem-totalShownMem)/totalMem, mLastServiceProcessMemory/totalMem, mLastBackgroundProcessMemory/totalMem); } @@ -482,10 +422,10 @@ public class RunningProcessesView extends FrameLayout mAdapter.setShowBackground(false); } }); - - // Magic! Implementation detail! Don't count on this! - SECONDARY_SERVER_MEM = - Integer.valueOf(SystemProperties.get("ro.SECONDARY_SERVER_MEM"))*PAGE_SIZE; + + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + mAm.getMemoryInfo(memInfo); + SECONDARY_SERVER_MEM = memInfo.secondaryServerThreshold; } public void doPause() { diff --git a/src/com/android/settings/applications/RunningState.java b/src/com/android/settings/applications/RunningState.java index 8ca17a5..552aa56 100644 --- a/src/com/android/settings/applications/RunningState.java +++ b/src/com/android/settings/applications/RunningState.java @@ -28,7 +28,6 @@ import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.res.Resources; -import android.os.Debug; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -250,7 +249,9 @@ public class RunningState { ActivityManager.RunningAppProcessInfo mRunningProcessInfo; MergedItem mMergedItem; - + + boolean mInteresting; + // Purely for sorting. boolean mIsSystem; boolean mIsStarted; @@ -388,8 +389,8 @@ public class RunningState { return changed; } - boolean updateSize(Context context, Debug.MemoryInfo mem, int curSeq) { - mSize = ((long)mem.getTotalPss()) * 1024; + boolean updateSize(Context context, long pss, int curSeq) { + mSize = pss * 1024; if (mCurSeq == curSeq) { String sizeStr = Formatter.formatShortFileSize( context, mSize); @@ -616,7 +617,8 @@ public class RunningState { return true; } if ((pi.flags&ActivityManager.RunningAppProcessInfo.FLAG_PERSISTENT) == 0 - && pi.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && pi.importance >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && pi.importance < ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE && pi.importanceReasonCode == ActivityManager.RunningAppProcessInfo.REASON_UNKNOWN) { return true; @@ -718,13 +720,16 @@ public class RunningState { mInterestingProcesses.add(proc); } proc.mCurSeq = mSequence; + proc.mInteresting = true; proc.ensureLabel(pm); + } else { + proc.mInteresting = false; } proc.mRunningSeq = mSequence; proc.mRunningProcessInfo = pi; } - + // Build the chains from client processes to the process they are // dependent on; also remove any old running processes. int NRP = mRunningProcesses.size(); @@ -756,7 +761,7 @@ public class RunningState { int NHP = mInterestingProcesses.size(); for (int i=0; i<NHP; i++) { ProcessItem proc = mInterestingProcesses.get(i); - if (mRunningProcesses.get(proc.mPid) == null) { + if (!proc.mInteresting || mRunningProcesses.get(proc.mPid) == null) { changed = true; mInterestingProcesses.remove(i); i--; @@ -964,12 +969,12 @@ public class RunningState { for (int i=0; i<numProc; i++) { pids[i] = mAllProcessItems.get(i).mPid; } - Debug.MemoryInfo[] mem = ActivityManagerNative.getDefault() - .getProcessMemoryInfo(pids); + long[] pss = ActivityManagerNative.getDefault() + .getProcessPss(pids); int bgIndex = 0; for (int i=0; i<pids.length; i++) { ProcessItem proc = mAllProcessItems.get(i); - changed |= proc.updateSize(context, mem[i], mSequence); + changed |= proc.updateSize(context, pss[i], mSequence); if (proc.mCurSeq == mSequence) { serviceProcessMemory += proc.mSize; } else if (proc.mRunningProcessInfo.importance >= diff --git a/src/com/android/settings/bluetooth/A2dpProfile.java b/src/com/android/settings/bluetooth/A2dpProfile.java index e8582f3..b7ba44d 100644 --- a/src/com/android/settings/bluetooth/A2dpProfile.java +++ b/src/com/android/settings/bluetooth/A2dpProfile.java @@ -138,14 +138,10 @@ final class A2dpProfile implements LocalBluetoothProfile { return ORDINAL; } - public int getNameResource() { + public int getNameResource(BluetoothDevice device) { return R.string.bluetooth_profile_a2dp; } - public int getDisconnectResource(BluetoothDevice device) { - return R.string.bluetooth_disconnect_a2dp_profile; - } - public int getSummaryResourceForDevice(BluetoothDevice device) { int state = mService.getConnectionState(device); switch (state) { diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceFilter.java b/src/com/android/settings/bluetooth/BluetoothDeviceFilter.java index 00e342c..e4f11a2 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceFilter.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceFilter.java @@ -42,6 +42,9 @@ final class BluetoothDeviceFilter { /** Bonded devices only filter (referenced directly). */ static final Filter BONDED_DEVICE_FILTER = new BondedDeviceFilter(); + /** Unbonded devices only filter (referenced directly). */ + static final Filter UNBONDED_DEVICE_FILTER = new UnbondedDeviceFilter(); + /** Table of singleton filter objects. */ private static final Filter[] FILTERS = { ALL_FILTER, // FILTER_TYPE_ALL @@ -85,6 +88,13 @@ final class BluetoothDeviceFilter { } } + /** Filter that matches only unbonded devices. */ + private static final class UnbondedDeviceFilter implements Filter { + public boolean matches(BluetoothDevice device) { + return device.getBondState() != BluetoothDevice.BOND_BONDED; + } + } + /** Parent class of filters based on UUID and/or Bluetooth class. */ private abstract static class ClassUuidFilter implements Filter { abstract boolean matches(ParcelUuid[] uuids, BluetoothClass btClass); diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 391c941..c659f70 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.DialogInterface; import android.graphics.drawable.Drawable; import android.preference.Preference; +import android.text.Html; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; @@ -49,8 +50,6 @@ public final class BluetoothDevicePreference extends Preference implements private final CachedBluetoothDevice mCachedDevice; - private ImageView mDeviceSettings; - private OnClickListener mOnSettingsClickListener; private AlertDialog mDisconnectDialog; @@ -66,7 +65,9 @@ public final class BluetoothDevicePreference extends Preference implements mCachedDevice = cachedDevice; - setWidgetLayoutResource(R.layout.preference_bluetooth); + if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { + setWidgetLayoutResource(R.layout.preference_bluetooth); + } mCachedDevice.registerCallback(this); @@ -99,7 +100,17 @@ public final class BluetoothDevicePreference extends Preference implements */ setTitle(mCachedDevice.getName()); - setSummary(getConnectionSummary()); + int summaryResId = getConnectionSummary(); + if (summaryResId != 0) { + setSummary(summaryResId); + } else { + setSummary(null); // empty summary for unpaired devices + } + + int iconResId = getBtClassDrawable(); + if (iconResId != 0) { + setIcon(iconResId); + } // Used to gray out the item setEnabled(!mCachedDevice.isBusy()); @@ -115,50 +126,26 @@ public final class BluetoothDevicePreference extends Preference implements setDependency("bt_checkbox"); } - super.onBindView(view); - - ImageView btClass = (ImageView) view.findViewById(android.R.id.icon); - btClass.setImageResource(getBtClassDrawable()); - btClass.setAlpha(isEnabled() ? 255 : sDimAlpha); - btClass.setVisibility(View.VISIBLE); - mDeviceSettings = (ImageView) view.findViewById(R.id.deviceDetails); - if (mOnSettingsClickListener != null) { - mDeviceSettings.setOnClickListener(this); - mDeviceSettings.setTag(mCachedDevice); - mDeviceSettings.setAlpha(isEnabled() ? 255 : sDimAlpha); - } else { // Hide the settings icon and divider - mDeviceSettings.setVisibility(View.GONE); - View divider = view.findViewById(R.id.divider); - if (divider != null) { - divider.setVisibility(View.GONE); + if (mCachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { + ImageView deviceDetails = (ImageView) view.findViewById(R.id.deviceDetails); + if (deviceDetails != null) { + deviceDetails.setOnClickListener(this); + deviceDetails.setTag(mCachedDevice); + deviceDetails.setAlpha(isEnabled() ? 255 : sDimAlpha); } } - LayoutInflater inflater = (LayoutInflater) - getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup profilesGroup = (ViewGroup) view.findViewById(R.id.profileIcons); - for (LocalBluetoothProfile profile : mCachedDevice.getProfiles()) { - int iconResource = profile.getDrawableResource(mCachedDevice.getBtClass()); - if (iconResource != 0) { - Drawable icon = getContext().getResources().getDrawable(iconResource); - inflater.inflate(R.layout.profile_icon_small, profilesGroup, true); - ImageView imageView = - (ImageView) profilesGroup.getChildAt(profilesGroup.getChildCount() - 1); - imageView.setImageDrawable(icon); - boolean profileEnabled = mCachedDevice.isConnectedProfile(profile); - imageView.setAlpha(profileEnabled ? 255 : sDimAlpha); - } - } + super.onBindView(view); } public void onClick(View v) { - if (v == mDeviceSettings) { - if (mOnSettingsClickListener != null) { - mOnSettingsClickListener.onClick(v); - } + // Should never be null by construction + if (mOnSettingsClickListener != null) { + mOnSettingsClickListener.onClick(v); } } + @Override public boolean equals(Object o) { if ((o == null) || !(o instanceof BluetoothDevicePreference)) { return false; @@ -167,6 +154,7 @@ public final class BluetoothDevicePreference extends Preference implements ((BluetoothDevicePreference) o).mCachedDevice); } + @Override public int hashCode() { return mCachedDevice.hashCode(); } @@ -174,8 +162,8 @@ public final class BluetoothDevicePreference extends Preference implements @Override public int compareTo(Preference another) { if (!(another instanceof BluetoothDevicePreference)) { - // Put other preference types above us - return 1; + // Rely on default sort + return super.compareTo(another); } return mCachedDevice @@ -201,7 +189,8 @@ public final class BluetoothDevicePreference extends Preference implements if (TextUtils.isEmpty(name)) { name = context.getString(R.string.bluetooth_device); } - String message = context.getString(R.string.bluetooth_disconnect_blank, name); + String message = context.getString(R.string.bluetooth_disconnect_all_profiles, name); + String title = context.getString(R.string.bluetooth_disconnect_title); DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -210,7 +199,7 @@ public final class BluetoothDevicePreference extends Preference implements }; mDisconnectDialog = Utils.showDisconnectDialog(context, - mDisconnectDialog, disconnectListener, name, message); + mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); } private void pair() { @@ -222,24 +211,53 @@ public final class BluetoothDevicePreference extends Preference implements private int getConnectionSummary() { final CachedBluetoothDevice cachedDevice = mCachedDevice; - final BluetoothDevice device = cachedDevice.getDevice(); - // if any profiles are connected or busy, return that status + boolean profileConnected = false; // at least one profile is connected + boolean a2dpNotConnected = false; // A2DP is preferred but not connected + boolean headsetNotConnected = false; // Headset is preferred but not connected + for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { int connectionStatus = cachedDevice.getProfileConnectionState(profile); - if (connectionStatus != BluetoothProfile.STATE_DISCONNECTED) { - return Utils.getConnectionStateSummary(connectionStatus); + switch (connectionStatus) { + case BluetoothProfile.STATE_CONNECTING: + case BluetoothProfile.STATE_DISCONNECTING: + return Utils.getConnectionStateSummary(connectionStatus); + + case BluetoothProfile.STATE_CONNECTED: + profileConnected = true; + break; + + case BluetoothProfile.STATE_DISCONNECTED: + if (profile.isProfileReady() && profile.isPreferred(cachedDevice.getDevice())) { + if (profile instanceof A2dpProfile) { + a2dpNotConnected = true; + } else if (profile instanceof HeadsetProfile) { + headsetNotConnected = true; + } + } + break; + } + } + + if (profileConnected) { + if (a2dpNotConnected && headsetNotConnected) { + return R.string.bluetooth_connected_no_headset_no_a2dp; + } else if (a2dpNotConnected) { + return R.string.bluetooth_connected_no_a2dp; + } else if (headsetNotConnected) { + return R.string.bluetooth_connected_no_headset; + } else { + return R.string.bluetooth_connected; } } switch (cachedDevice.getBondState()) { - case BluetoothDevice.BOND_BONDED: - return R.string.bluetooth_paired; case BluetoothDevice.BOND_BONDING: return R.string.bluetooth_pairing; + + case BluetoothDevice.BOND_BONDED: case BluetoothDevice.BOND_NONE: - return R.string.bluetooth_not_connected; default: return 0; } diff --git a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java index 40bf5bc..babf1e2 100644..100755 --- a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java @@ -21,11 +21,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.os.Handler; import android.os.SystemProperties; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; import android.preference.Preference; +import android.text.format.DateUtils; import com.android.settings.R; @@ -34,7 +34,7 @@ import com.android.settings.R; * checkbox. It sets/unsets discoverability and keeps track of how much time * until the the discoverability is automatically turned off. */ -final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChangeListener { +final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClickListener { private static final String SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT = "debug.bt.discoverable_time"; @@ -44,6 +44,10 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600; static final int DISCOVERABLE_TIMEOUT_NEVER = 0; + // Bluetooth advanced settings screen was replaced with action bar items. + // Use the same preference key for discoverable timeout as the old ListPreference. + private static final String KEY_DISCOVERABLE_TIMEOUT = "bt_discoverable_timeout"; + private static final String VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES = "twomin"; private static final String VALUE_DISCOVERABLE_TIMEOUT_FIVE_MINUTES = "fivemin"; private static final String VALUE_DISCOVERABLE_TIMEOUT_ONE_HOUR = "onehour"; @@ -53,11 +57,17 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang private final Context mContext; private final Handler mUiHandler; - private final CheckBoxPreference mCheckBoxPreference; - private final ListPreference mTimeoutListPreference; + private final Preference mDiscoveryPreference; private final LocalBluetoothAdapter mLocalAdapter; + private final SharedPreferences mSharedPreferences; + + private boolean mDiscoverable; + private int mNumberOfPairedDevices; + + private int mTimeoutSecs = -1; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -78,21 +88,13 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang }; BluetoothDiscoverableEnabler(Context context, LocalBluetoothAdapter adapter, - CheckBoxPreference checkBoxPreference, ListPreference timeoutListPreference) { + Preference discoveryPreference) { mContext = context; mUiHandler = new Handler(); - mCheckBoxPreference = checkBoxPreference; - mTimeoutListPreference = timeoutListPreference; - - checkBoxPreference.setPersistent(false); - // we actually want to persist this since can't infer from BT device state - mTimeoutListPreference.setPersistent(true); - mLocalAdapter = adapter; - if (adapter == null) { - // Bluetooth not supported - checkBoxPreference.setEnabled(false); - } + mDiscoveryPreference = discoveryPreference; + mSharedPreferences = discoveryPreference.getSharedPreferences(); + discoveryPreference.setPersistent(false); } public void resume() { @@ -102,8 +104,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); mContext.registerReceiver(mReceiver, filter); - mCheckBoxPreference.setOnPreferenceChangeListener(this); - mTimeoutListPreference.setOnPreferenceChangeListener(this); + mDiscoveryPreference.setOnPreferenceClickListener(this); handleModeChanged(mLocalAdapter.getScanMode()); } @@ -113,20 +114,14 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang } mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable); - mCheckBoxPreference.setOnPreferenceChangeListener(null); - mTimeoutListPreference.setOnPreferenceChangeListener(null); mContext.unregisterReceiver(mReceiver); + mDiscoveryPreference.setOnPreferenceClickListener(null); } - public boolean onPreferenceChange(Preference preference, Object value) { - if (preference == mCheckBoxPreference) { - // Turn on/off BT discoverability - setEnabled((Boolean) value); - } else if (preference == mTimeoutListPreference) { - mTimeoutListPreference.setValue((String) value); - setEnabled(true); - } - + public boolean onPreferenceClick(Preference preference) { + // toggle discoverability + mDiscoverable = !mDiscoverable; + setEnabled(mDiscoverable); return true; } @@ -138,9 +133,8 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang long endTimestamp = System.currentTimeMillis() + timeout * 1000L; LocalBluetoothPreferences.persistDiscoverableEndTimestamp(mContext, endTimestamp); - updateCountdownSummary(); - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout); + updateCountdownSummary(); } else { mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); } @@ -148,22 +142,63 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang private void updateTimerDisplay(int timeout) { if (getDiscoverableTimeout() == DISCOVERABLE_TIMEOUT_NEVER) { - mCheckBoxPreference.setSummaryOn( - mContext.getString(R.string.bluetooth_is_discoverable_always)); + mDiscoveryPreference.setSummary(R.string.bluetooth_is_discoverable_always); } else { - mCheckBoxPreference.setSummaryOn( - mContext.getString(R.string.bluetooth_is_discoverable, timeout)); + String textTimeout = formatTimeRemaining(timeout); + mDiscoveryPreference.setSummary(mContext.getString(R.string.bluetooth_is_discoverable, + textTimeout)); + } + } + + private static String formatTimeRemaining(int timeout) { + StringBuilder sb = new StringBuilder(6); // "mmm:ss" + int min = timeout / 60; + sb.append(min).append(':'); + int sec = timeout - (min * 60); + if (sec < 10) { + sb.append('0'); + } + sb.append(sec); + return sb.toString(); + } + + void setDiscoverableTimeout(int index) { + String timeoutValue; + switch (index) { + case 0: + default: + mTimeoutSecs = DISCOVERABLE_TIMEOUT_TWO_MINUTES; + timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES; + break; + + case 1: + mTimeoutSecs = DISCOVERABLE_TIMEOUT_FIVE_MINUTES; + timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_FIVE_MINUTES; + break; + + case 2: + mTimeoutSecs = DISCOVERABLE_TIMEOUT_ONE_HOUR; + timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_ONE_HOUR; + break; + + case 3: + mTimeoutSecs = DISCOVERABLE_TIMEOUT_NEVER; + timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_NEVER; + break; } + mSharedPreferences.edit().putString(KEY_DISCOVERABLE_TIMEOUT, timeoutValue).apply(); + setEnabled(true); // enable discovery and reset timer } private int getDiscoverableTimeout() { + if (mTimeoutSecs != -1) { + return mTimeoutSecs; + } + int timeout = SystemProperties.getInt(SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT, -1); if (timeout < 0) { - String timeoutValue = mTimeoutListPreference.getValue(); - if (timeoutValue == null) { - mTimeoutListPreference.setValue(VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES); - return DISCOVERABLE_TIMEOUT_TWO_MINUTES; - } + String timeoutValue = mSharedPreferences.getString(KEY_DISCOVERABLE_TIMEOUT, + VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES); if (timeoutValue.equals(VALUE_DISCOVERABLE_TIMEOUT_NEVER)) { timeout = DISCOVERABLE_TIMEOUT_NEVER; @@ -175,16 +210,48 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang timeout = DISCOVERABLE_TIMEOUT_TWO_MINUTES; } } - + mTimeoutSecs = timeout; return timeout; } - private void handleModeChanged(int mode) { + int getDiscoverableTimeoutIndex() { + int timeout = getDiscoverableTimeout(); + switch (timeout) { + case DISCOVERABLE_TIMEOUT_TWO_MINUTES: + default: + return 0; + + case DISCOVERABLE_TIMEOUT_FIVE_MINUTES: + return 1; + + case DISCOVERABLE_TIMEOUT_ONE_HOUR: + return 2; + + case DISCOVERABLE_TIMEOUT_NEVER: + return 3; + } + } + + void setNumberOfPairedDevices(int pairedDevices) { + mNumberOfPairedDevices = pairedDevices; + handleModeChanged(mLocalAdapter.getScanMode()); + } + + void handleModeChanged(int mode) { if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { - mCheckBoxPreference.setChecked(true); + mDiscoverable = true; updateCountdownSummary(); } else { - mCheckBoxPreference.setChecked(false); + mDiscoverable = false; + setSummaryNotDiscoverable(); + } + } + + private void setSummaryNotDiscoverable() { + if (mNumberOfPairedDevices != 0) { + mDiscoveryPreference.setSummary(R.string.bluetooth_only_visible_to_paired_devices); + } else { + mDiscoveryPreference.setSummary(R.string.bluetooth_not_visible_to_other_devices); } } @@ -199,7 +266,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChang if (currentTimestamp > endTimestamp) { // We're still in discoverable mode, but maybe there isn't a timeout. - mCheckBoxPreference.setSummaryOn(null); + updateTimerDisplay(0); return; } diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java index 79f23bb..f08e083 100644 --- a/src/com/android/settings/bluetooth/BluetoothEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -16,28 +16,27 @@ package com.android.settings.bluetooth; -import com.android.settings.R; -import com.android.settings.WirelessSettings; - import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.preference.Preference; -import android.preference.CheckBoxPreference; import android.provider.Settings; +import android.widget.CompoundButton; +import android.widget.Switch; import android.widget.Toast; +import com.android.settings.R; +import com.android.settings.WirelessSettings; + /** * BluetoothEnabler is a helper to manage the Bluetooth on/off checkbox * preference. It turns on/off Bluetooth and ensures the summary of the * preference reflects the current state. */ -public final class BluetoothEnabler implements Preference.OnPreferenceChangeListener { +public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeListener { private final Context mContext; - private final CheckBoxPreference mCheckBox; - private final CharSequence mOriginalSummary; + private Switch mSwitch; private final LocalBluetoothAdapter mLocalAdapter; private final IntentFilter mIntentFilter; @@ -50,17 +49,15 @@ public final class BluetoothEnabler implements Preference.OnPreferenceChangeList } }; - public BluetoothEnabler(Context context, CheckBoxPreference checkBox) { + public BluetoothEnabler(Context context, Switch switch_) { mContext = context; - mCheckBox = checkBox; - mOriginalSummary = checkBox.getSummary(); - checkBox.setPersistent(false); + mSwitch = switch_; LocalBluetoothManager manager = LocalBluetoothManager.getInstance(context); if (manager == null) { // Bluetooth is not supported mLocalAdapter = null; - checkBox.setEnabled(false); + mSwitch.setEnabled(false); } else { mLocalAdapter = manager.getBluetoothAdapter(); } @@ -69,6 +66,7 @@ public final class BluetoothEnabler implements Preference.OnPreferenceChangeList public void resume() { if (mLocalAdapter == null) { + mSwitch.setEnabled(false); return; } @@ -76,7 +74,7 @@ public final class BluetoothEnabler implements Preference.OnPreferenceChangeList handleStateChanged(mLocalAdapter.getBluetoothState()); mContext.registerReceiver(mReceiver, mIntentFilter); - mCheckBox.setOnPreferenceChangeListener(this); + mSwitch.setOnCheckedChangeListener(this); } public void pause() { @@ -85,51 +83,57 @@ public final class BluetoothEnabler implements Preference.OnPreferenceChangeList } mContext.unregisterReceiver(mReceiver); - mCheckBox.setOnPreferenceChangeListener(null); + mSwitch.setOnCheckedChangeListener(null); } - public boolean onPreferenceChange(Preference preference, Object value) { - boolean enable = (Boolean) value; + public void setSwitch(Switch switch_) { + if (mSwitch == switch_) return; + mSwitch.setOnCheckedChangeListener(null); + mSwitch = switch_; + mSwitch.setOnCheckedChangeListener(this); + + int bluetoothState = BluetoothAdapter.STATE_OFF; + if (mLocalAdapter != null) bluetoothState = mLocalAdapter.getBluetoothState(); + boolean isOn = bluetoothState == BluetoothAdapter.STATE_ON; + boolean isOff = bluetoothState == BluetoothAdapter.STATE_OFF; + mSwitch.setChecked(isOn); + mSwitch.setEnabled(isOn || isOff); + } + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Show toast message if Bluetooth is not allowed in airplane mode - if (enable && !WirelessSettings - .isRadioAllowed(mContext, Settings.System.RADIO_BLUETOOTH)) { - Toast.makeText(mContext, R.string.wifi_in_airplane_mode, - Toast.LENGTH_SHORT).show(); - return false; + if (isChecked && + !WirelessSettings.isRadioAllowed(mContext, Settings.System.RADIO_BLUETOOTH)) { + Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show(); + // Reset switch to off + buttonView.setChecked(false); } - mLocalAdapter.setBluetoothEnabled(enable); - mCheckBox.setEnabled(false); - - // Don't update UI to opposite state until we're sure - return false; + if (mLocalAdapter != null) { + mLocalAdapter.setBluetoothEnabled(isChecked); + } + mSwitch.setEnabled(false); } void handleStateChanged(int state) { switch (state) { case BluetoothAdapter.STATE_TURNING_ON: - mCheckBox.setSummary(R.string.wifi_starting); - mCheckBox.setEnabled(false); + mSwitch.setEnabled(false); break; case BluetoothAdapter.STATE_ON: - mCheckBox.setChecked(true); - mCheckBox.setSummary(null); - mCheckBox.setEnabled(true); + mSwitch.setChecked(true); + mSwitch.setEnabled(true); break; case BluetoothAdapter.STATE_TURNING_OFF: - mCheckBox.setSummary(R.string.wifi_stopping); - mCheckBox.setEnabled(false); + mSwitch.setEnabled(false); break; case BluetoothAdapter.STATE_OFF: - mCheckBox.setChecked(false); - mCheckBox.setSummary(mOriginalSummary); - mCheckBox.setEnabled(true); + mSwitch.setChecked(false); + mSwitch.setEnabled(true); break; default: - mCheckBox.setChecked(false); - mCheckBox.setSummary(R.string.wifi_error); - mCheckBox.setEnabled(true); + mSwitch.setChecked(false); + mSwitch.setEnabled(true); } } } diff --git a/src/com/android/settings/bluetooth/BluetoothEventManager.java b/src/com/android/settings/bluetooth/BluetoothEventManager.java index 6f83766..9140b25 100644 --- a/src/com/android/settings/bluetooth/BluetoothEventManager.java +++ b/src/com/android/settings/bluetooth/BluetoothEventManager.java @@ -225,7 +225,7 @@ final class BluetoothEventManager { Log.w(TAG, "received ACTION_DISAPPEARED for an unknown device: " + device); return; } - if (mDeviceManager.onDeviceDisappeared(cachedDevice)) { + if (CachedBluetoothDeviceManager.onDeviceDisappeared(cachedDevice)) { synchronized (mCallbacks) { for (BluetoothCallback callback : mCallbacks) { callback.onDeviceDeleted(cachedDevice); @@ -283,7 +283,7 @@ final class BluetoothEventManager { // if the device is undocked, remove it from the list as well if (!device.getAddress().equals(getDockedDeviceAddress(context))) { - mDeviceManager.onDeviceDisappeared(cachedDevice); + cachedDevice.setVisible(false); } } int reason = intent.getIntExtra(BluetoothDevice.EXTRA_REASON, @@ -361,7 +361,7 @@ final class BluetoothEventManager { if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) { CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); if (cachedDevice != null) { - mDeviceManager.onDeviceDisappeared(cachedDevice); + cachedDevice.setVisible(false); } } } diff --git a/src/com/android/settings/bluetooth/BluetoothFindNearby.java b/src/com/android/settings/bluetooth/BluetoothFindNearby.java deleted file mode 100644 index 066f4f6..0000000 --- a/src/com/android/settings/bluetooth/BluetoothFindNearby.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2011 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.bluetooth; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; - -import com.android.settings.R; - -/** - * Fragment to scan and show the discoverable devices. - */ -public final class BluetoothFindNearby extends DeviceListPreferenceFragment { - - @Override - void addPreferencesForActivity() { - addPreferencesFromResource(R.xml.device_picker); - } - - @Override - public void onResume() { - super.onResume(); - if (mSelectedDevice != null) { - CachedBluetoothDeviceManager manager = mLocalManager.getCachedDeviceManager(); - CachedBluetoothDevice device = manager.findDevice(mSelectedDevice); - if (device != null && device.getBondState() == BluetoothDevice.BOND_BONDED) { - // selected device was paired, so return from this screen - finish(); - return; - } - } - mLocalAdapter.startScanning(true); - } - - @Override - void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { - mLocalAdapter.stopScanning(); - super.onDevicePreferenceClick(btPreference); - } - - public void onDeviceBondStateChanged(CachedBluetoothDevice - cachedDevice, int bondState) { - if (bondState == BluetoothDevice.BOND_BONDED) { - // return from scan screen after successful auto-pairing - finish(); - } - } - - @Override - public void onBluetoothStateChanged(int bluetoothState) { - super.onBluetoothStateChanged(bluetoothState); - - if (bluetoothState == BluetoothAdapter.STATE_ON) { - mLocalAdapter.startScanning(false); - } - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java new file mode 100644 index 0000000..c00aff3 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2011 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.bluetooth; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; + +import com.android.settings.R; + +/** + * Dialog fragment for renaming the local Bluetooth device. + */ +final class BluetoothNameDialogFragment extends DialogFragment implements TextWatcher { + private static final int BLUETOOTH_NAME_MAX_LENGTH_BYTES = 248; + + private AlertDialog mAlertDialog; + private Button mOkButton; + + // accessed from inner class (not private to avoid thunks) + static final String TAG = "BluetoothNameDialogFragment"; + final LocalBluetoothAdapter mLocalAdapter; + EditText mDeviceNameView; + + // This flag is set when the name is updated by code, to distinguish from user changes + private boolean mDeviceNameUpdated; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { + updateDeviceName(); + } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED) && + (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) == + BluetoothAdapter.STATE_ON)) { + updateDeviceName(); + } + } + }; + + public BluetoothNameDialogFragment(LocalBluetoothAdapter adapter) { + mLocalAdapter = adapter; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mAlertDialog = new AlertDialog.Builder(getActivity()) + .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(R.string.bluetooth_rename_device) + .setView(createDialogView()) + .setPositiveButton(R.string.bluetooth_rename_button, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (mLocalAdapter != null) { + String deviceName = mDeviceNameView.getText().toString(); + Log.d(TAG, "Setting device name to " + deviceName); + mLocalAdapter.setName(deviceName); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + return mAlertDialog; + } + + private View createDialogView() { + final LayoutInflater layoutInflater = (LayoutInflater)getActivity() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = layoutInflater.inflate(R.layout.dialog_edittext, null); + mDeviceNameView = (EditText) view.findViewById(R.id.edittext); + mDeviceNameView.setFilters(new InputFilter[] { + new Utf8ByteLengthFilter(BLUETOOTH_NAME_MAX_LENGTH_BYTES) + }); + mDeviceNameView.addTextChangedListener(this); + return view; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mAlertDialog = null; + mDeviceNameView = null; + mOkButton = null; + } + + @Override + public void onResume() { + super.onResume(); + if (mOkButton == null) { + mOkButton = mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setEnabled(false); // Ok button is enabled when the user edits the name + } + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); + getActivity().registerReceiver(mReceiver, filter); + updateDeviceName(); + } + + @Override + public void onPause() { + super.onPause(); + getActivity().unregisterReceiver(mReceiver); + } + + void updateDeviceName() { + if (mLocalAdapter != null && mLocalAdapter.isEnabled()) { + mDeviceNameUpdated = true; + mDeviceNameView.setText(mLocalAdapter.getName()); + } + } + + public void afterTextChanged(Editable s) { + if (mDeviceNameUpdated) { + // Device name changed by code; disable Ok button until edited by user + mDeviceNameUpdated = false; + mOkButton.setEnabled(false); + } else { + mOkButton.setEnabled(s.length() != 0); + } + } + + /* Not used */ + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + /* Not used */ + public void onTextChanged(CharSequence s, int start, int before, int count) { + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothNamePreference.java b/src/com/android/settings/bluetooth/BluetoothNamePreference.java deleted file mode 100644 index f41689e..0000000 --- a/src/com/android/settings/bluetooth/BluetoothNamePreference.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2008 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.bluetooth; - -import android.app.AlertDialog; -import android.app.Dialog; - -import android.bluetooth.BluetoothAdapter; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.preference.EditTextPreference; -import android.text.Editable; -import android.text.InputFilter; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.widget.Button; -import android.widget.EditText; - -/** - * BluetoothNamePreference is the preference type for editing the device's - * Bluetooth name. It asks the user for a name, and persists it via the - * Bluetooth API. - */ -public final class BluetoothNamePreference extends EditTextPreference implements TextWatcher { -// private static final String TAG = "BluetoothNamePreference"; - private static final int BLUETOOTH_NAME_MAX_LENGTH_BYTES = 248; - - private final LocalBluetoothAdapter mLocalAdapter; - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { - setSummaryToName(); - } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED) && - (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) == - BluetoothAdapter.STATE_ON)) { - setSummaryToName(); - } - } - }; - - public BluetoothNamePreference(Context context, AttributeSet attrs) { - super(context, attrs); - - mLocalAdapter = LocalBluetoothManager.getInstance(context).getBluetoothAdapter(); - - setSummaryToName(); - } - - public void resume() { - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); - getContext().registerReceiver(mReceiver, filter); - - // Make sure the OK button is disabled (if necessary) after rotation - EditText et = getEditText(); - if (et != null) { - et.setFilters(new InputFilter[] { - new Utf8ByteLengthFilter(BLUETOOTH_NAME_MAX_LENGTH_BYTES) - }); - - et.addTextChangedListener(this); - Dialog d = getDialog(); - if (d instanceof AlertDialog) { - Button b = ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE); - b.setEnabled(et.getText().length() > 0); - } - } - } - - public void pause() { - EditText et = getEditText(); - if (et != null) { - et.removeTextChangedListener(this); - } - getContext().unregisterReceiver(mReceiver); - } - - private void setSummaryToName() { - if (mLocalAdapter != null && mLocalAdapter.isEnabled()) { - setSummary(mLocalAdapter.getName()); - } - } - - @Override - protected boolean persistString(String value) { - // Persist with Bluez instead of shared preferences - if (mLocalAdapter != null) { - mLocalAdapter.setName(value); - } - return true; - } - - @Override - protected void onClick() { - super.onClick(); - - // The dialog should be created by now - EditText et = getEditText(); - if (et != null && mLocalAdapter != null) { - et.setText(mLocalAdapter.getName()); - } - } - - // TextWatcher interface - public void afterTextChanged(Editable s) { - Dialog d = getDialog(); - if (d instanceof AlertDialog) { - ((AlertDialog) d).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(s.length() > 0); - } - } - - // TextWatcher interface - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // not used - } - - // TextWatcher interface - public void onTextChanged(CharSequence s, int start, int before, int count) { - // not used - } - -} diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java index 1b443c4..940d8d0 100644..100755 --- a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java @@ -24,26 +24,31 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.text.Editable; +import android.text.Html; import android.text.InputFilter; import android.text.InputType; +import android.text.Spanned; import android.text.TextWatcher; import android.text.InputFilter.LengthFilter; import android.util.Log; import android.view.View; import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.settings.R; +import android.view.KeyEvent; /** * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation * for pairing with a remote Bluetooth device. It is an activity that appears as a dialog. */ -public final class BluetoothPairingDialog extends AlertActivity implements DialogInterface.OnClickListener, - TextWatcher { +public final class BluetoothPairingDialog extends AlertActivity implements + CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener, TextWatcher { private static final String TAG = "BluetoothPairingDialog"; private static final int BLUETOOTH_PIN_MAX_LENGTH = 16; @@ -156,7 +161,7 @@ public final class BluetoothPairingDialog extends AlertActivity implements Dialo final AlertController.AlertParams p = mAlertParams; p.mIconId = android.R.drawable.ic_dialog_info; p.mTitle = getString(R.string.bluetooth_pairing_request); - p.mView = createView(deviceManager); + p.mView = createPinEntryView(deviceManager.getName(mDevice)); p.mPositiveButtonText = getString(android.R.string.ok); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(android.R.string.cancel); @@ -167,56 +172,78 @@ public final class BluetoothPairingDialog extends AlertActivity implements Dialo mOkButton.setEnabled(false); } - private View createView(CachedBluetoothDeviceManager deviceManager) { + private View createPinEntryView(String deviceName) { View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); - String name = deviceManager.getName(mDevice); TextView messageView = (TextView) view.findViewById(R.id.message); + TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin); + CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin); mPairingView = (EditText) view.findViewById(R.id.text); mPairingView.addTextChangedListener(this); + alphanumericPin.setOnCheckedChangeListener(this); + int messageId1; + int messageId2; + int maxLength; switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN: - messageView.setText(getString(R.string.bluetooth_enter_pin_msg, name)); - // Maximum of 16 characters in a PIN adb sync - mPairingView.setFilters(new InputFilter[] { - new LengthFilter(BLUETOOTH_PIN_MAX_LENGTH) }); + messageId1 = R.string.bluetooth_enter_pin_msg; + messageId2 = R.string.bluetooth_enter_pin_other_device; + // Maximum of 16 characters in a PIN + maxLength = BLUETOOTH_PIN_MAX_LENGTH; break; case BluetoothDevice.PAIRING_VARIANT_PASSKEY: - messageView.setText(getString(R.string.bluetooth_enter_passkey_msg, name)); + messageId1 = R.string.bluetooth_enter_passkey_msg; + messageId2 = R.string.bluetooth_enter_passkey_other_device; // Maximum of 6 digits for passkey - mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER | - InputType.TYPE_NUMBER_FLAG_SIGNED); - mPairingView.setFilters(new InputFilter[] { - new LengthFilter(BLUETOOTH_PASSKEY_MAX_LENGTH)}); + maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH; + alphanumericPin.setVisibility(View.GONE); break; + default: + Log.e(TAG, "Incorrect pairing type for createPinEntryView: " + mType); + return null; + } + + // Format the message string, then parse HTML style tags + String messageText = getString(messageId1, deviceName); + messageView.setText(Html.fromHtml(messageText)); + messageView2.setText(messageId2); + mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); + mPairingView.setFilters(new InputFilter[] { + new LengthFilter(maxLength) }); + + return view; + } + + private View createView(CachedBluetoothDeviceManager deviceManager) { + View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null); + String name = deviceManager.getName(mDevice); + TextView messageView = (TextView) view.findViewById(R.id.message); + + String messageText; // formatted string containing HTML style tags + switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: - mPairingView.setVisibility(View.GONE); - messageView.setText(getString(R.string.bluetooth_confirm_passkey_msg, name, - mPairingKey)); + messageText = getString(R.string.bluetooth_confirm_passkey_msg, + name, mPairingKey); break; case BluetoothDevice.PAIRING_VARIANT_CONSENT: - mPairingView.setVisibility(View.GONE); - messageView.setText(getString(R.string.bluetooth_incoming_pairing_msg, name)); + case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: + messageText = getString(R.string.bluetooth_incoming_pairing_msg, name); break; case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: - mPairingView.setVisibility(View.GONE); - messageView.setText(getString(R.string.bluetooth_display_passkey_pin_msg, name, - mPairingKey)); - break; - - case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: - mPairingView.setVisibility(View.GONE); - messageView.setText(getString(R.string.bluetooth_incoming_pairing_msg, name)); + messageText = getString(R.string.bluetooth_display_passkey_pin_msg, name, + mPairingKey); break; default: Log.e(TAG, "Incorrect pairing type received, not creating view"); + return null; } + messageView.setText(Html.fromHtml(messageText)); return view; } @@ -271,8 +298,8 @@ public final class BluetoothPairingDialog extends AlertActivity implements Dialo } public void afterTextChanged(Editable s) { - if (s.length() > 0) { - mOkButton.setEnabled(true); + if (mOkButton != null) { + mOkButton.setEnabled(s.length() > 0); } } @@ -314,10 +341,21 @@ public final class BluetoothPairingDialog extends AlertActivity implements Dialo mDevice.cancelPairingUserInput(); } + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + onCancel(); + } + return super.onKeyDown(keyCode,event); + } + public void onClick(DialogInterface dialog, int which) { switch (which) { case BUTTON_POSITIVE: - onPair(mPairingView.getText().toString()); + if (mPairingView != null) { + onPair(mPairingView.getText().toString()); + } else { + onPair(null); + } break; case BUTTON_NEGATIVE: @@ -335,4 +373,12 @@ public final class BluetoothPairingDialog extends AlertActivity implements Dialo public void onTextChanged(CharSequence s, int start, int before, int count) { } + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + // change input type for soft keyboard to numeric or alphanumeric + if (isChecked) { + mPairingView.setInputType(InputType.TYPE_CLASS_TEXT); + } else { + mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); + } + } } diff --git a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java index de96d71..838e7b1 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java @@ -82,7 +82,7 @@ public final class BluetoothPairingRequest extends BroadcastReceiver { String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME); if (TextUtils.isEmpty(name)) { - name = device != null ? device.getName() : + name = device != null ? device.getAliasName() : context.getString(android.R.string.unknownName); } diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java new file mode 100644 index 0000000..4d96140 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 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.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Button; +import android.widget.CompoundButton.OnCheckedChangeListener; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; + +import com.android.settings.R; + +/** + * BluetoothPermissionActivity shows a dialog for accepting incoming + * profile connection request from untrusted devices. + * It is also used to show a dialogue for accepting incoming phonebook + * read request. The request could be initiated by PBAP PCE or by HF AT+CPBR. + */ +public class BluetoothPermissionActivity extends AlertActivity implements + DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener { + private static final String TAG = "BluetoothPermissionActivity"; + private static final boolean DEBUG = Utils.D; + + private View mView; + private TextView messageView; + private Button mOkButton; + private BluetoothDevice mDevice; + + private CheckBox mAlwaysAllowed; + private boolean mAlwaysAllowedValue = true; + + private String mReturnPackage = null; + private String mReturnClass = null; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (mDevice.equals(device)) dismissDialog(); + } + } + }; + + private void dismissDialog() { + this.dismiss(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent i = getIntent(); + String action = i.getAction(); + mDevice = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + mReturnPackage = i.getStringExtra(BluetoothDevice.EXTRA_PACKAGE_NAME); + mReturnClass = i.getStringExtra(BluetoothDevice.EXTRA_CLASS_NAME); + + if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST)) { + mDevice = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (i.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, + BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) == + BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION) { + showConnectionDialog(); + } else { + showPbapDialog(); + } + } else { + Log.e(TAG, "Error: this activity may be started only with intent " + + "ACTION_CONNECTION_ACCESS_REQUEST"); + finish(); + } + registerReceiver(mReceiver, + new IntentFilter(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL)); + } + + private void showConnectionDialog() { + final AlertController.AlertParams p = mAlertParams; + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.bluetooth_connection_permission_request); + p.mView = createConnectionDialogView(); + p.mPositiveButtonText = getString(R.string.yes); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(R.string.no); + p.mNegativeButtonListener = this; + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + setupAlert(); + } + + private void showPbapDialog() { + final AlertController.AlertParams p = mAlertParams; + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.bluetooth_phonebook_request); + p.mView = createPbapDialogView(); + p.mPositiveButtonText = getString(android.R.string.yes); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.no); + p.mNegativeButtonListener = this; + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + setupAlert(); + } + + private String createConnectionDisplayText() { + String mRemoteName = mDevice != null ? mDevice.getAliasName() : null; + + if (mRemoteName == null) mRemoteName = getString(R.string.unknown); + String mMessage1 = getString(R.string.bluetooth_connection_dialog_text, + mRemoteName); + return mMessage1; + } + + private String createPbapDisplayText() { + String mRemoteName = mDevice != null ? mDevice.getAliasName() : null; + + if (mRemoteName == null) mRemoteName = getString(R.string.unknown); + String mMessage1 = getString(R.string.bluetooth_pb_acceptance_dialog_text, + mRemoteName, mRemoteName); + return mMessage1; + } + + private View createConnectionDialogView() { + mView = getLayoutInflater().inflate(R.layout.bluetooth_connection_access, null); + messageView = (TextView)mView.findViewById(R.id.message); + messageView.setText(createConnectionDisplayText()); + return mView; + } + + private View createPbapDialogView() { + mView = getLayoutInflater().inflate(R.layout.bluetooth_pb_access, null); + messageView = (TextView)mView.findViewById(R.id.message); + messageView.setText(createPbapDisplayText()); + mAlwaysAllowed = (CheckBox)mView.findViewById(R.id.bluetooth_pb_alwaysallowed); + mAlwaysAllowed.setChecked(true); + mAlwaysAllowed.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mAlwaysAllowedValue = true; + } else { + mAlwaysAllowedValue = false; + } + } + }); + return mView; + } + + private void onPositive() { + if (DEBUG) Log.d(TAG, "onPositive mAlwaysAllowedValue: " + mAlwaysAllowedValue); + sendIntentToReceiver(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY, true, + BluetoothDevice.EXTRA_ALWAYS_ALLOWED, mAlwaysAllowedValue); + finish(); + } + + private void onNegative() { + if (DEBUG) Log.d(TAG, "onNegative mAlwaysAllowedValue: " + mAlwaysAllowedValue); + sendIntentToReceiver(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY, false, + null, false // dummy value, no effect since last param is null + ); + finish(); + } + + private void sendIntentToReceiver(final String intentName, final boolean allowed, + final String extraName, final boolean extraValue) { + Intent intent = new Intent(intentName); + + if (mReturnPackage != null && mReturnClass != null) { + intent.setClassName(mReturnPackage, mReturnClass); + } + + intent.putExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, + allowed ? BluetoothDevice.CONNECTION_ACCESS_YES : + BluetoothDevice.CONNECTION_ACCESS_NO); + + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); + sendBroadcast(intent, android.Manifest.permission.BLUETOOTH_ADMIN); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + onPositive(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + onNegative(); + break; + default: + break; + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + return true; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java new file mode 100644 index 0000000..51055af --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2011 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.bluetooth; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.util.Log; + +import com.android.settings.R; + +/** + * BluetoothPermissionRequest is a receiver to receive Bluetooth connection + * access request. + */ +public final class BluetoothPermissionRequest extends BroadcastReceiver { + + private static final String TAG = "BluetoothPermissionRequest"; + private static final boolean DEBUG = Utils.V; + public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (DEBUG) Log.d(TAG, "onReceive"); + + if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST)) { + // convert broadcast intent into activity intent (same action string) + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, + BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION); + String returnPackage = intent.getStringExtra(BluetoothDevice.EXTRA_PACKAGE_NAME); + String returnClass = intent.getStringExtra(BluetoothDevice.EXTRA_CLASS_NAME); + + Intent connectionAccessIntent = new Intent(action); + connectionAccessIntent.setClass(context, BluetoothPermissionActivity.class); + connectionAccessIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, requestType); + connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_PACKAGE_NAME, returnPackage); + connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_CLASS_NAME, returnClass); + + String deviceAddress = device != null ? device.getAddress() : null; + + PowerManager powerManager = + (PowerManager) context.getSystemService(Context.POWER_SERVICE); + + if (powerManager.isScreenOn() && + LocalBluetoothPreferences.shouldShowDialogInForeground(context, deviceAddress) ) { + context.startActivity(connectionAccessIntent); + } else { + // Put up a notification that leads to the dialog + + // Create an intent triggered by clicking on the + // "Clear All Notifications" button + + Intent deleteIntent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); + deleteIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + deleteIntent.putExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, + BluetoothDevice.CONNECTION_ACCESS_NO); + + Notification notification = new Notification(android.R.drawable.stat_sys_data_bluetooth, + context.getString(R.string.bluetooth_connection_permission_request), + System.currentTimeMillis()); + String deviceName = device != null ? device.getAliasName() : null; + notification.setLatestEventInfo(context, + context.getString(R.string.bluetooth_connection_permission_request), + context.getString(R.string.bluetooth_connection_notif_message, deviceName), + PendingIntent.getActivity(context, 0, connectionAccessIntent, 0)); + notification.flags = Notification.FLAG_AUTO_CANCEL | + Notification.FLAG_ONLY_ALERT_ONCE; + notification.defaults = Notification.DEFAULT_SOUND; + notification.deleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL)) { + // Remove the notification + NotificationManager manager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(NOTIFICATION_ID); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothProfilePreference.java b/src/com/android/settings/bluetooth/BluetoothProfilePreference.java deleted file mode 100644 index 8f6d0a2..0000000 --- a/src/com/android/settings/bluetooth/BluetoothProfilePreference.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2010 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.bluetooth; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.preference.Preference; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageView; - -import com.android.settings.R; - -/** - * BluetoothProfilePreference is the preference type used to display each profile for a - * particular bluetooth device. - */ -final class BluetoothProfilePreference extends Preference implements OnClickListener { - -// private static final String TAG = "BluetoothProfilePreference"; - - private Drawable mProfileDrawable; - private boolean mExpanded; - private ImageView mProfileExpandView; - private final LocalBluetoothProfile mProfile; - - private OnClickListener mOnExpandClickListener; - - BluetoothProfilePreference(Context context, LocalBluetoothProfile profile) { - super(context); - - mProfile = profile; - - setWidgetLayoutResource(R.layout.preference_bluetooth_profile); - setExpanded(false); - } - - public void setOnExpandClickListener(OnClickListener listener) { - mOnExpandClickListener = listener; - } - - public void setExpanded(boolean expanded) { - mExpanded = expanded; - notifyChanged(); - } - - public boolean isExpanded() { - return mExpanded; - } - - public void setProfileDrawable(Drawable drawable) { - mProfileDrawable = drawable; - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - - ImageView btProfile = (ImageView) view.findViewById(android.R.id.icon); - btProfile.setImageDrawable(mProfileDrawable); - - mProfileExpandView = (ImageView) view.findViewById(R.id.profileExpand); - if (mProfile.isAutoConnectable()) { - mProfileExpandView.setOnClickListener(this); - mProfileExpandView.setTag(mProfile); - mProfileExpandView.setImageResource(mExpanded - ? com.android.internal.R.drawable.expander_close_holo_dark - : com.android.internal.R.drawable.expander_open_holo_dark); - } else { - mProfileExpandView.setVisibility(View.GONE); - } - } - - public void onClick(View v) { - if (v == mProfileExpandView) { - if (mOnExpandClickListener != null) { - setExpanded(!mExpanded); - mOnExpandClickListener.onClick(v); - } - } - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java index 5e4e130..af21149 100644 --- a/src/com/android/settings/bluetooth/BluetoothSettings.java +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -16,16 +16,32 @@ package com.android.settings.bluetooth; +import android.app.ActionBar; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; +import android.content.IntentFilter; +import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; +import android.widget.TextView; +import com.android.settings.ProgressCategory; import com.android.settings.R; /** @@ -35,107 +51,310 @@ import com.android.settings.R; public final class BluetoothSettings extends DeviceListPreferenceFragment { private static final String TAG = "BluetoothSettings"; - private static final String KEY_BT_CHECKBOX = "bt_checkbox"; - private static final String KEY_BT_DISCOVERABLE = "bt_discoverable"; - private static final String KEY_BT_DISCOVERABLE_TIMEOUT = "bt_discoverable_timeout"; - private static final String KEY_BT_NAME = "bt_name"; - private static final String KEY_BT_SHOW_RECEIVED = "bt_show_received_files"; - - private BluetoothEnabler mEnabler; - private BluetoothDiscoverableEnabler mDiscoverableEnabler; - private BluetoothNamePreference mNamePreference; + private static final int MENU_ID_SCAN = Menu.FIRST; + private static final int MENU_ID_RENAME_DEVICE = Menu.FIRST + 1; + private static final int MENU_ID_VISIBILITY_TIMEOUT = Menu.FIRST + 2; + private static final int MENU_ID_SHOW_RECEIVED = Menu.FIRST + 3; /* Private intent to show the list of received files */ private static final String BTOPP_ACTION_OPEN_RECEIVED_FILES = "android.btopp.intent.action.OPEN_RECEIVED_FILES"; - /** Initialize the filter to show bonded devices only. */ + private BluetoothEnabler mBluetoothEnabler; + + private BluetoothDiscoverableEnabler mDiscoverableEnabler; + + private PreferenceGroup mPairedDevicesCategory; + + private PreferenceGroup mAvailableDevicesCategory; + private boolean mAvailableDevicesCategoryIsPresent; + + private View mView; + private TextView mEmptyView; + + private final IntentFilter mIntentFilter; + + // accessed from inner class (not private to avoid thunks) + Preference mMyDevicePreference; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { + updateDeviceName(); + } + } + + private void updateDeviceName() { + if (mLocalAdapter.isEnabled() && mMyDevicePreference != null) { + mMyDevicePreference.setTitle(mLocalAdapter.getName()); + } + } + }; + public BluetoothSettings() { - super(BluetoothDeviceFilter.BONDED_DEVICE_FILTER); + mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mView = inflater.inflate(R.layout.custom_preference_list_fragment, container, false); + return mView; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mEmptyView = (TextView) mView.findViewById(R.id.empty); + getListView().setEmptyView(mEmptyView); } @Override void addPreferencesForActivity() { addPreferencesFromResource(R.xml.bluetooth_settings); - mEnabler = new BluetoothEnabler(getActivity(), - (CheckBoxPreference) findPreference(KEY_BT_CHECKBOX)); + Activity activity = getActivity(); + + Switch actionBarSwitch = new Switch(activity); - mDiscoverableEnabler = new BluetoothDiscoverableEnabler(getActivity(), - mLocalAdapter, - (CheckBoxPreference) findPreference(KEY_BT_DISCOVERABLE), - (ListPreference) findPreference(KEY_BT_DISCOVERABLE_TIMEOUT)); + if (activity instanceof PreferenceActivity) { + PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) { + final int padding = activity.getResources().getDimensionPixelSize( + R.dimen.action_bar_switch_padding); + actionBarSwitch.setPadding(0, 0, padding, 0); + activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, + ActionBar.DISPLAY_SHOW_CUSTOM); + activity.getActionBar().setCustomView(actionBarSwitch, new ActionBar.LayoutParams( + ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT)); + } + } + + mBluetoothEnabler = new BluetoothEnabler(activity, actionBarSwitch); - mNamePreference = (BluetoothNamePreference) findPreference(KEY_BT_NAME); + setHasOptionsMenu(true); } @Override public void onResume() { + // resume BluetoothEnabler before calling super.onResume() so we don't get + // any onDeviceAdded() callbacks before setting up view in updateContent() + mBluetoothEnabler.resume(); super.onResume(); - // Repopulate (which isn't too bad since it's cached in the settings - // bluetooth manager) - addDevices(); + if (mDiscoverableEnabler != null) { + mDiscoverableEnabler.resume(); + } + getActivity().registerReceiver(mReceiver, mIntentFilter); - mEnabler.resume(); - mDiscoverableEnabler.resume(); - mNamePreference.resume(); + updateContent(mLocalAdapter.getBluetoothState()); } @Override public void onPause() { super.onPause(); + mBluetoothEnabler.pause(); + getActivity().unregisterReceiver(mReceiver); + if (mDiscoverableEnabler != null) { + mDiscoverableEnabler.pause(); + } + } - mNamePreference.pause(); - mDiscoverableEnabler.pause(); - mEnabler.pause(); + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + boolean bluetoothIsEnabled = mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON; + boolean isDiscovering = mLocalAdapter.isDiscovering(); + int textId = isDiscovering ? R.string.bluetooth_searching_for_devices : + R.string.bluetooth_search_for_devices; + menu.add(Menu.NONE, MENU_ID_SCAN, 0, textId) + .setEnabled(bluetoothIsEnabled && !isDiscovering) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.add(Menu.NONE, MENU_ID_RENAME_DEVICE, 0, R.string.bluetooth_rename_device) + .setEnabled(bluetoothIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(Menu.NONE, MENU_ID_VISIBILITY_TIMEOUT, 0, R.string.bluetooth_visibility_timeout) + .setEnabled(bluetoothIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(Menu.NONE, MENU_ID_SHOW_RECEIVED, 0, R.string.bluetooth_show_received_files) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } - private final View.OnClickListener mListener = new View.OnClickListener() { - public void onClick(View v) { - // User clicked on advanced options icon for a device in the list - if (v.getTag() instanceof CachedBluetoothDevice) { - CachedBluetoothDevice - device = (CachedBluetoothDevice) v.getTag(); - - Preference pref = new Preference(getActivity()); - pref.setTitle(device.getName()); - pref.setFragment(DeviceProfilesSettings.class.getName()); - pref.getExtras().putParcelable(DeviceProfilesSettings.EXTRA_DEVICE, - device.getDevice()); - ((PreferenceActivity) getActivity()) - .onPreferenceStartFragment(BluetoothSettings.this, - pref); - } else { - Log.w(TAG, "onClick() called for other View: " + v); - } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_ID_SCAN: + if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) { + startScanning(); + } + return true; + + case MENU_ID_RENAME_DEVICE: + new BluetoothNameDialogFragment(mLocalAdapter).show( + getFragmentManager(), "rename device"); + return true; + + case MENU_ID_VISIBILITY_TIMEOUT: + new BluetoothVisibilityTimeoutFragment(mDiscoverableEnabler).show( + getFragmentManager(), "visibility timeout"); + return true; + + case MENU_ID_SHOW_RECEIVED: + Intent intent = new Intent(BTOPP_ACTION_OPEN_RECEIVED_FILES); + getActivity().sendBroadcast(intent); + return true; } - }; + return super.onOptionsItemSelected(item); + } + + private void startScanning() { + if (!mAvailableDevicesCategoryIsPresent) { + getPreferenceScreen().addPreference(mAvailableDevicesCategory); + } + mLocalAdapter.startScanning(true); + } @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { - if (KEY_BT_SHOW_RECEIVED.equals(preference.getKey())) { - Intent intent = new Intent(BTOPP_ACTION_OPEN_RECEIVED_FILES); - getActivity().sendBroadcast(intent); - return true; + void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { + mLocalAdapter.stopScanning(); + super.onDevicePreferenceClick(btPreference); + } + + private void addDeviceCategory(PreferenceGroup preferenceGroup, int titleId, + BluetoothDeviceFilter.Filter filter) { + preferenceGroup.setTitle(titleId); + getPreferenceScreen().addPreference(preferenceGroup); + setFilter(filter); + setDeviceListGroup(preferenceGroup); + addCachedDevices(); + preferenceGroup.setEnabled(true); + } + + private void updateContent(int bluetoothState) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + int messageId = 0; + + switch (bluetoothState) { + case BluetoothAdapter.STATE_ON: + preferenceScreen.removeAll(); + preferenceScreen.setOrderingAsAdded(true); + + // This device + if (mMyDevicePreference == null) { + mMyDevicePreference = new Preference(getActivity()); + } + mMyDevicePreference.setTitle(mLocalAdapter.getName()); + if (getResources().getBoolean(com.android.internal.R.bool.config_voice_capable)) { + mMyDevicePreference.setIcon(R.drawable.ic_bt_cellphone); // for phones + } else { + mMyDevicePreference.setIcon(R.drawable.ic_bt_laptop); // for tablets, etc. + } + mMyDevicePreference.setPersistent(false); + mMyDevicePreference.setEnabled(true); + preferenceScreen.addPreference(mMyDevicePreference); + + if (mDiscoverableEnabler == null) { + mDiscoverableEnabler = new BluetoothDiscoverableEnabler(getActivity(), + mLocalAdapter, mMyDevicePreference); + mDiscoverableEnabler.resume(); + } + + // Paired devices category + if (mPairedDevicesCategory == null) { + mPairedDevicesCategory = new PreferenceCategory(getActivity()); + } else { + mPairedDevicesCategory.removeAll(); + } + addDeviceCategory(mPairedDevicesCategory, + R.string.bluetooth_preference_paired_devices, + BluetoothDeviceFilter.BONDED_DEVICE_FILTER); + int numberOfPairedDevices = mPairedDevicesCategory.getPreferenceCount(); + + mDiscoverableEnabler.setNumberOfPairedDevices(numberOfPairedDevices); + + // Available devices category + if (mAvailableDevicesCategory == null) { + mAvailableDevicesCategory = new ProgressCategory(getActivity(), null); + } else { + mAvailableDevicesCategory.removeAll(); + } + addDeviceCategory(mAvailableDevicesCategory, + R.string.bluetooth_preference_found_devices, + BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER); + int numberOfAvailableDevices = mAvailableDevicesCategory.getPreferenceCount(); + mAvailableDevicesCategoryIsPresent = true; + + if (numberOfAvailableDevices == 0) { + preferenceScreen.removePreference(mAvailableDevicesCategory); + mAvailableDevicesCategoryIsPresent = false; + } + + if (numberOfPairedDevices == 0) { + preferenceScreen.removePreference(mPairedDevicesCategory); + startScanning(); + } + getActivity().invalidateOptionsMenu(); + return; // not break + + case BluetoothAdapter.STATE_TURNING_OFF: + messageId = R.string.bluetooth_turning_off; + break; + + case BluetoothAdapter.STATE_OFF: + messageId = R.string.bluetooth_empty_list_bluetooth_off; + break; + + case BluetoothAdapter.STATE_TURNING_ON: + messageId = R.string.bluetooth_turning_on; + break; } - return super.onPreferenceTreeClick(preferenceScreen, preference); + setDeviceListGroup(preferenceScreen); + removeAllDevices(); + mEmptyView.setText(messageId); + getActivity().invalidateOptionsMenu(); + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + super.onBluetoothStateChanged(bluetoothState); + updateContent(bluetoothState); + } + + @Override + public void onScanningStateChanged(boolean started) { + super.onScanningStateChanged(started); + // Update options' enabled state + getActivity().invalidateOptionsMenu(); + } + + public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { + setDeviceListGroup(getPreferenceScreen()); + removeAllDevices(); + updateContent(mLocalAdapter.getBluetoothState()); } - public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, - int bondState) { - if (bondState == BluetoothDevice.BOND_BONDED) { - // add to "Paired devices" list after remote-initiated pairing - if (mDevicePreferenceMap.get(cachedDevice) == null) { - createDevicePreference(cachedDevice); + private final View.OnClickListener mDeviceProfilesListener = new View.OnClickListener() { + public void onClick(View v) { + // User clicked on advanced options icon for a device in the list + if (v.getTag() instanceof CachedBluetoothDevice) { + CachedBluetoothDevice device = (CachedBluetoothDevice) v.getTag(); + + Bundle args = new Bundle(1); + args.putParcelable(DeviceProfilesSettings.EXTRA_DEVICE, device.getDevice()); + + ((PreferenceActivity) getActivity()).startPreferencePanel( + DeviceProfilesSettings.class.getName(), args, + R.string.bluetooth_device_advanced_title, null, null, 0); + } else { + Log.w(TAG, "onClick() called for other View: " + v); // TODO remove } - } else if (bondState == BluetoothDevice.BOND_NONE) { - // remove unpaired device from paired devices list - onDeviceDeleted(cachedDevice); } - } + }; /** * Add a listener, which enables the advanced settings icon. @@ -143,6 +362,10 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { */ @Override void initDevicePreference(BluetoothDevicePreference preference) { - preference.setOnSettingsClickListener(mListener); + CachedBluetoothDevice cachedDevice = preference.getCachedDevice(); + if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { + // Only paired device have an associated advanced settings screen + preference.setOnSettingsClickListener(mDeviceProfilesListener); + } } } diff --git a/src/com/android/settings/bluetooth/BluetoothVisibilityTimeoutFragment.java b/src/com/android/settings/bluetooth/BluetoothVisibilityTimeoutFragment.java new file mode 100644 index 0000000..7c518fb --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothVisibilityTimeoutFragment.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2011 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.bluetooth; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; + +import com.android.internal.app.AlertController; +import com.android.settings.R; + +/** + * Dialog fragment for setting the discoverability timeout. + */ +final class BluetoothVisibilityTimeoutFragment extends DialogFragment + implements DialogInterface.OnClickListener { + + private final BluetoothDiscoverableEnabler mDiscoverableEnabler; + + public BluetoothVisibilityTimeoutFragment(BluetoothDiscoverableEnabler enabler) { + mDiscoverableEnabler = enabler; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.bluetooth_visibility_timeout) + .setSingleChoiceItems(R.array.bluetooth_visibility_timeout_entries, + mDiscoverableEnabler.getDiscoverableTimeoutIndex(), this) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } + + public void onClick(DialogInterface dialog, int which) { + mDiscoverableEnabler.setDiscoverableTimeout(which); + dismiss(); + } +} diff --git a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java index 71a5c01..8082314 100644 --- a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java +++ b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java @@ -257,6 +257,14 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { return true; } + /** + * Return true if user initiated pairing on this device. The message text is + * slightly different for local vs. remote initiated pairing dialogs. + */ + boolean isUserInitiatedPairing() { + return mConnectAfterPairing; + } + void unpair() { disconnect(); @@ -318,8 +326,8 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { mName = mDevice.getAddress(); } else { mName = name; + mDevice.setAlias(name); } - // TODO: save custom device name in preferences dispatchAttributesChanged(); } } @@ -330,7 +338,7 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { } private void fetchName() { - mName = mDevice.getName(); + mName = mDevice.getAliasName(); if (TextUtils.isEmpty(mName)) { mName = mDevice.getAddress(); @@ -414,7 +422,7 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles); if (DEBUG) { - Log.e(TAG, "updating profiles for " + mDevice.getName()); + Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); diff --git a/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java b/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java index ab71ece..b511cb3 100644 --- a/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java +++ b/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java @@ -26,7 +26,6 @@ import java.util.List; * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices. */ final class CachedBluetoothDeviceManager { -// private static final String TAG = "CachedBluetoothDeviceManager"; private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); @@ -35,20 +34,9 @@ final class CachedBluetoothDeviceManager { return new ArrayList<CachedBluetoothDevice>(mCachedDevices); } - public boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) { + public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) { cachedDevice.setVisible(false); - return checkForDeviceRemoval(cachedDevice); - } - - private boolean checkForDeviceRemoval( - CachedBluetoothDevice cachedDevice) { - if (cachedDevice.getBondState() == BluetoothDevice.BOND_NONE && - !cachedDevice.isVisible()) { - // If device isn't paired, remove it altogether - mCachedDevices.remove(cachedDevice); - return true; // dispatch device deleted - } - return false; + return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE; } public void onDeviceNameUpdated(BluetoothDevice device) { @@ -104,7 +92,7 @@ final class CachedBluetoothDeviceManager { return cachedDevice.getName(); } - String name = device.getName(); + String name = device.getAliasName(); if (name != null) { return name; } @@ -120,7 +108,6 @@ final class CachedBluetoothDeviceManager { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); cachedDevice.setVisible(false); - checkForDeviceRemoval(cachedDevice); } } diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java index a978e23..061f2c9 100644 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothDevice; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceCategory; +import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.util.Log; @@ -36,7 +37,6 @@ import java.util.WeakHashMap; * * @see BluetoothSettings * @see DevicePickerFragment - * @see BluetoothFindNearby */ public abstract class DeviceListPreferenceFragment extends SettingsPreferenceFragment implements BluetoothCallback { @@ -53,7 +53,7 @@ public abstract class DeviceListPreferenceFragment extends LocalBluetoothAdapter mLocalAdapter; LocalBluetoothManager mLocalManager; - private PreferenceCategory mDeviceList; + private PreferenceGroup mDeviceListGroup; final WeakHashMap<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = new WeakHashMap<CachedBluetoothDevice, BluetoothDevicePreference>(); @@ -62,7 +62,7 @@ public abstract class DeviceListPreferenceFragment extends mFilter = BluetoothDeviceFilter.ALL_FILTER; } - DeviceListPreferenceFragment(BluetoothDeviceFilter.Filter filter) { + final void setFilter(BluetoothDeviceFilter.Filter filter) { mFilter = filter; } @@ -83,10 +83,11 @@ public abstract class DeviceListPreferenceFragment extends addPreferencesForActivity(); - mDeviceList = (PreferenceCategory) findPreference(KEY_BT_DEVICE_LIST); - if (mDeviceList == null) { - Log.e(TAG, "Could not find device list preference object!"); - } + mDeviceListGroup = (PreferenceCategory) findPreference(KEY_BT_DEVICE_LIST); + } + + void setDeviceListGroup(PreferenceGroup preferenceGroup) { + mDeviceListGroup = preferenceGroup; } /** Add preferences from the subclass. */ @@ -105,16 +106,18 @@ public abstract class DeviceListPreferenceFragment extends @Override public void onPause() { super.onPause(); - - mLocalAdapter.stopScanning(); + removeAllDevices(); mLocalManager.setForegroundActivity(null); mLocalManager.getEventManager().unregisterCallback(this); + } + void removeAllDevices() { + mLocalAdapter.stopScanning(); mDevicePreferenceMap.clear(); - mDeviceList.removeAll(); + mDeviceListGroup.removeAll(); } - void addDevices() { + void addCachedDevices() { Collection<CachedBluetoothDevice> cachedDevices = mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); for (CachedBluetoothDevice cachedDevice : cachedDevices) { @@ -125,14 +128,13 @@ public abstract class DeviceListPreferenceFragment extends @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (KEY_BT_SCAN.equals(preference.getKey())) { mLocalAdapter.startScanning(true); return true; } if (preference instanceof BluetoothDevicePreference) { - BluetoothDevicePreference btPreference = (BluetoothDevicePreference)preference; + BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference; CachedBluetoothDevice device = btPreference.getCachedDevice(); mSelectedDevice = device.getDevice(); onDevicePreferenceClick(btPreference); @@ -148,10 +150,12 @@ public abstract class DeviceListPreferenceFragment extends public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { if (mDevicePreferenceMap.get(cachedDevice) != null) { - Log.e(TAG, "Got onDeviceAdded, but cachedDevice already exists"); return; } + // Prevent updates while the list shows one of the state messages + if (mLocalAdapter.getBluetoothState() != BluetoothAdapter.STATE_ON) return; + if (mFilter.matches(cachedDevice.getDevice())) { createDevicePreference(cachedDevice); } @@ -162,7 +166,7 @@ public abstract class DeviceListPreferenceFragment extends getActivity(), cachedDevice); initDevicePreference(preference); - mDeviceList.addPreference(preference); + mDeviceListGroup.addPreference(preference); mDevicePreferenceMap.put(cachedDevice, preference); } @@ -170,13 +174,14 @@ public abstract class DeviceListPreferenceFragment extends * Overridden in {@link BluetoothSettings} to add a listener. * @param preference the newly added preference */ - void initDevicePreference(BluetoothDevicePreference preference) { } + void initDevicePreference(BluetoothDevicePreference preference) { + // Does nothing by default + } public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - BluetoothDevicePreference preference = mDevicePreferenceMap.remove( - cachedDevice); + BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice); if (preference != null) { - mDeviceList.removePreference(preference); + mDeviceListGroup.removePreference(preference); } } @@ -185,8 +190,8 @@ public abstract class DeviceListPreferenceFragment extends } private void updateProgressUi(boolean start) { - if (mDeviceList instanceof ProgressCategory) { - ((ProgressCategory) mDeviceList).setProgress(start); + if (mDeviceListGroup instanceof ProgressCategory) { + ((ProgressCategory) mDeviceListGroup).setProgress(start); } } diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java index 126df02..8b32941 100644 --- a/src/com/android/settings/bluetooth/DevicePickerFragment.java +++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java @@ -55,7 +55,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { @Override public void onResume() { super.onResume(); - addDevices(); + addCachedDevices(); mLocalAdapter.startScanning(true); } @@ -89,7 +89,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { super.onBluetoothStateChanged(bluetoothState); if (bluetoothState == BluetoothAdapter.STATE_ON) { - mLocalAdapter.startScanning(false); + mLocalAdapter.startScanning(false); } } diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java index 9db4baf..a840982 100644..100755 --- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java +++ b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2011 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. @@ -27,10 +27,15 @@ import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.text.Html; import android.text.TextUtils; import android.util.Log; import android.view.View; - +import android.widget.EditText; +import android.text.TextWatcher; +import android.app.Dialog; +import android.widget.Button; +import android.text.Editable; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -42,18 +47,15 @@ import java.util.HashMap; * (or disconnected). */ public final class DeviceProfilesSettings extends SettingsPreferenceFragment - implements CachedBluetoothDevice.Callback, Preference.OnPreferenceChangeListener, - View.OnClickListener { + implements CachedBluetoothDevice.Callback, Preference.OnPreferenceChangeListener { private static final String TAG = "DeviceProfilesSettings"; - private static final String KEY_TITLE = "title"; private static final String KEY_RENAME_DEVICE = "rename_device"; private static final String KEY_PROFILE_CONTAINER = "profile_container"; private static final String KEY_UNPAIR = "unpair"; - private static final String KEY_ALLOW_INCOMING = "allow_incoming"; public static final String EXTRA_DEVICE = "device"; - + private RenameEditTextPreference mRenameDeviceNamePref; private LocalBluetoothManager mManager; private CachedBluetoothDevice mCachedDevice; private LocalBluetoothProfileManager mProfileManager; @@ -65,6 +67,26 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment = new HashMap<LocalBluetoothProfile, CheckBoxPreference>(); private AlertDialog mDisconnectDialog; + private boolean mProfileGroupIsRemoved; + + private class RenameEditTextPreference implements TextWatcher{ + public void afterTextChanged(Editable s) { + Dialog d = mDeviceNamePref.getDialog(); + if (d instanceof AlertDialog) { + ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(s.length() > 0); + } + } + + // TextWatcher interface + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // not used + } + + // TextWatcher interface + public void onTextChanged(CharSequence s, int start, int before, int count) { + // not used + } + } @Override public void onCreate(Bundle savedInstanceState) { @@ -88,7 +110,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment finish(); return; // TODO: test this failure path } - + mRenameDeviceNamePref = new RenameEditTextPreference(); mManager = LocalBluetoothManager.getInstance(getActivity()); CachedBluetoothDeviceManager deviceManager = mManager.getCachedDeviceManager(); @@ -105,11 +127,6 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mDeviceNamePref.setText(deviceName); mDeviceNamePref.setOnPreferenceChangeListener(this); - // Set the title of the screen - findPreference(KEY_TITLE).setTitle( - getString(R.string.bluetooth_device_advanced_title, - deviceName)); - // Add a preference for each profile addPreferencesForProfiles(); } @@ -135,8 +152,18 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mManager.setForegroundActivity(getActivity()); mCachedDevice.registerCallback(this); - + if(mCachedDevice.getBondState() == BluetoothDevice.BOND_NONE) + finish(); refresh(); + EditText et = mDeviceNamePref.getEditText(); + if (et != null) { + et.addTextChangedListener(mRenameDeviceNamePref); + Dialog d = mDeviceNamePref.getDialog(); + if (d instanceof AlertDialog) { + Button b = ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE); + b.setEnabled(et.getText().length() > 0); + } + } } @Override @@ -152,6 +179,18 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment Preference pref = createProfilePreference(profile); mProfileContainer.addPreference(pref); } + showOrHideProfileGroup(); + } + + private void showOrHideProfileGroup() { + int numProfiles = mProfileContainer.getPreferenceCount(); + if (!mProfileGroupIsRemoved && numProfiles == 0) { + getPreferenceScreen().removePreference(mProfileContainer); + mProfileGroupIsRemoved = true; + } else if (mProfileGroupIsRemoved && numProfiles != 0) { + getPreferenceScreen().addPreference(mProfileContainer); + mProfileGroupIsRemoved = false; + } } /** @@ -162,18 +201,17 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment * @return A preference that allows the user to choose whether this profile * will be connected to. */ - private Preference createProfilePreference(LocalBluetoothProfile profile) { - BluetoothProfilePreference pref = new BluetoothProfilePreference(getActivity(), profile); + private CheckBoxPreference createProfilePreference(LocalBluetoothProfile profile) { + CheckBoxPreference pref = new CheckBoxPreference(getActivity()); pref.setKey(profile.toString()); - pref.setTitle(profile.getNameResource()); - pref.setExpanded(false); + pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); pref.setPersistent(false); pref.setOrder(getProfilePreferenceIndex(profile.getOrdinal())); - pref.setOnExpandClickListener(this); + pref.setOnPreferenceChangeListener(this); - int iconResource = profile.getDrawableResource(null); // FIXME: get BT class for this? + int iconResource = profile.getDrawableResource(mCachedDevice.getBtClass()); if (iconResource != 0) { - pref.setProfileDrawable(getResources().getDrawable(iconResource)); + pref.setIcon(getResources().getDrawable(iconResource)); } /** @@ -189,10 +227,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { String key = preference.getKey(); - if (preference instanceof BluetoothProfilePreference) { - onProfileClicked(mProfileManager.getProfileByName(key)); - return true; - } else if (key.equals(KEY_UNPAIR)) { + if (key.equals(KEY_UNPAIR)) { unpairDevice(); finish(); return true; @@ -205,11 +240,9 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment if (preference == mDeviceNamePref) { mCachedDevice.setName((String) newValue); } else if (preference instanceof CheckBoxPreference) { - boolean autoConnect = (Boolean) newValue; LocalBluetoothProfile prof = getProfileOf(preference); - prof.setPreferred(mCachedDevice.getDevice(), - autoConnect); - return true; + onProfileClicked(prof); + return false; // checkbox will update from onDeviceAttributesChanged() callback } else { return false; } @@ -227,6 +260,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment if (isConnected) { askDisconnect(getActivity(), profile); } else { + profile.setPreferred(device, true); mCachedDevice.connectProfile(profile); } } @@ -239,22 +273,23 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment if (TextUtils.isEmpty(name)) { name = context.getString(R.string.bluetooth_device); } - int disconnectMessage = profile.getDisconnectResource(device.getDevice()); - if (disconnectMessage == 0) { - Log.w(TAG, "askDisconnect: unexpected profile " + profile); - disconnectMessage = R.string.bluetooth_disconnect_blank; - } - String message = context.getString(disconnectMessage, name); + + String profileName = context.getString(profile.getNameResource(device.getDevice())); + + String title = context.getString(R.string.bluetooth_disable_profile_title); + String message = context.getString(R.string.bluetooth_disable_profile_message, + profileName, name); DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { device.disconnect(profile); + profile.setPreferred(device.getDevice(), false); } }; mDisconnectDialog = Utils.showDisconnectDialog(context, - mDisconnectDialog, disconnectListener, name, message); + mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); } public void onDeviceAttributesChanged() { @@ -263,15 +298,6 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment private void refresh() { String deviceName = mCachedDevice.getName(); - // TODO: figure out how to update "bread crumb" title in action bar -// FragmentTransaction transaction = getFragmentManager().openTransaction(); -// transaction.setBreadCrumbTitle(deviceName); -// transaction.commit(); - - findPreference(KEY_TITLE).setTitle(getString( - R.string.bluetooth_device_advanced_title, - deviceName)); - mDeviceNamePref = (EditTextPreference) findPreference(KEY_RENAME_DEVICE); mDeviceNamePref.setSummary(deviceName); mDeviceNamePref.setText(deviceName); @@ -280,7 +306,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment private void refreshProfiles() { for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { - Preference profilePref = findPreference(profile.toString()); + CheckBoxPreference profilePref = (CheckBoxPreference)findPreference(profile.toString()); if (profilePref == null) { profilePref = createProfilePreference(profile); mProfileContainer.addPreference(profilePref); @@ -295,15 +321,18 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mProfileContainer.removePreference(profilePref); } } + showOrHideProfileGroup(); } - private void refreshProfilePreference(Preference profilePref, LocalBluetoothProfile profile) { + private void refreshProfilePreference(CheckBoxPreference profilePref, + LocalBluetoothProfile profile) { BluetoothDevice device = mCachedDevice.getDevice(); /* * Gray out checkbox while connecting and disconnecting */ profilePref.setEnabled(!mCachedDevice.isBusy()); + profilePref.setChecked(mCachedDevice.isConnectedProfile(profile)); profilePref.setSummary(profile.getSummaryResourceForDevice(device)); } @@ -321,32 +350,6 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment } } - public void onClick(View v) { - if (v.getTag() instanceof LocalBluetoothProfile) { - LocalBluetoothProfile prof = (LocalBluetoothProfile) v.getTag(); - CheckBoxPreference autoConnectPref = mAutoConnectPrefs.get(prof); - if (autoConnectPref == null) { - autoConnectPref = new CheckBoxPreference(getActivity()); - autoConnectPref.setLayoutResource(com.android.internal.R.layout.preference_child); - autoConnectPref.setKey(prof.toString()); - autoConnectPref.setTitle(R.string.bluetooth_auto_connect); - autoConnectPref.setOrder(getProfilePreferenceIndex(prof.getOrdinal()) + 1); - autoConnectPref.setChecked(getAutoConnect(prof)); - autoConnectPref.setOnPreferenceChangeListener(this); - mAutoConnectPrefs.put(prof, autoConnectPref); - } - BluetoothProfilePreference profilePref = - (BluetoothProfilePreference) findPreference(prof.toString()); - if (profilePref != null) { - if (profilePref.isExpanded()) { - mProfileContainer.addPreference(autoConnectPref); - } else { - mProfileContainer.removePreference(autoConnectPref); - } - } - } - } - private int getProfilePreferenceIndex(int profIndex) { return mProfileContainer.getOrder() + profIndex * 10; } @@ -355,16 +358,6 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mCachedDevice.unpair(); } - private void setIncomingFileTransfersAllowed(boolean allow) { - // TODO: make an IPC call into BluetoothOpp to update - Log.d(TAG, "Set allow incoming = " + allow); - } - - private boolean isIncomingFileTransfersAllowed() { - // TODO: get this value from BluetoothOpp ??? - return true; - } - private boolean getAutoConnect(LocalBluetoothProfile prof) { return prof.isPreferred(mCachedDevice.getDevice()); } diff --git a/src/com/android/settings/bluetooth/DockEventReceiver.java b/src/com/android/settings/bluetooth/DockEventReceiver.java index 6ecbef6..d18348f 100644 --- a/src/com/android/settings/bluetooth/DockEventReceiver.java +++ b/src/com/android/settings/bluetooth/DockEventReceiver.java @@ -54,7 +54,7 @@ public final class DockEventReceiver extends BroadcastReceiver { if (DEBUG) { Log.d(TAG, "Action: " + intent.getAction() + " State:" + state + " Device: " - + (device == null ? "null" : device.getName())); + + (device == null ? "null" : device.getAliasName())); } if (Intent.ACTION_DOCK_EVENT.equals(intent.getAction()) diff --git a/src/com/android/settings/bluetooth/DockService.java b/src/com/android/settings/bluetooth/DockService.java index b457706..f0644c3 100644 --- a/src/com/android/settings/bluetooth/DockService.java +++ b/src/com/android/settings/bluetooth/DockService.java @@ -424,7 +424,7 @@ public final class DockService extends Service implements ServiceListener { if (DEBUG) { Log.d(TAG, "Action: " + intent.getAction() + " State:" + state - + " Device: " + (device == null ? "null" : device.getName())); + + " Device: " + (device == null ? "null" : device.getAliasName())); } if (device == null) { diff --git a/src/com/android/settings/bluetooth/HeadsetProfile.java b/src/com/android/settings/bluetooth/HeadsetProfile.java index 13dce33..99d070b 100644 --- a/src/com/android/settings/bluetooth/HeadsetProfile.java +++ b/src/com/android/settings/bluetooth/HeadsetProfile.java @@ -170,14 +170,10 @@ final class HeadsetProfile implements LocalBluetoothProfile { return ORDINAL; } - public int getNameResource() { + public int getNameResource(BluetoothDevice device) { return R.string.bluetooth_profile_headset; } - public int getDisconnectResource(BluetoothDevice device) { - return R.string.bluetooth_disconnect_headset_profile; - } - public int getSummaryResourceForDevice(BluetoothDevice device) { int state = mService.getConnectionState(device); switch (state) { diff --git a/src/com/android/settings/bluetooth/HidProfile.java b/src/com/android/settings/bluetooth/HidProfile.java index 13d3db9..920f4bb 100644 --- a/src/com/android/settings/bluetooth/HidProfile.java +++ b/src/com/android/settings/bluetooth/HidProfile.java @@ -112,14 +112,11 @@ final class HidProfile implements LocalBluetoothProfile { return ORDINAL; } - public int getNameResource() { + public int getNameResource(BluetoothDevice device) { + // TODO: distinguish between keyboard and mouse? return R.string.bluetooth_profile_hid; } - public int getDisconnectResource(BluetoothDevice device) { - return R.string.bluetooth_disconnect_hid_profile; - } - public int getSummaryResourceForDevice(BluetoothDevice device) { int state = mService.getConnectionState(device); switch (state) { diff --git a/src/com/android/settings/bluetooth/LocalBluetoothManager.java b/src/com/android/settings/bluetooth/LocalBluetoothManager.java index 63b8b7c..a1edca1 100644..100755 --- a/src/com/android/settings/bluetooth/LocalBluetoothManager.java +++ b/src/com/android/settings/bluetooth/LocalBluetoothManager.java @@ -79,6 +79,10 @@ public final class LocalBluetoothManager { return mContext; } + public Context getForegroundActivity() { + return mForegroundActivity; + } + boolean isForegroundActivity() { return mForegroundActivity != null; } diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfile.java b/src/com/android/settings/bluetooth/LocalBluetoothProfile.java index 878a032..8c0de95 100644 --- a/src/com/android/settings/bluetooth/LocalBluetoothProfile.java +++ b/src/com/android/settings/bluetooth/LocalBluetoothProfile.java @@ -54,15 +54,9 @@ interface LocalBluetoothProfile { /** * Returns the string resource ID for the localized name for this profile. + * @param device the Bluetooth device (to distinguish between PAN roles) */ - int getNameResource(); - - /** - * Returns the string resource ID for the disconnect confirmation text - * for this profile. - * @param device - */ - int getDisconnectResource(BluetoothDevice device); + int getNameResource(BluetoothDevice device); /** * Returns the string resource ID for the summary text for this profile diff --git a/src/com/android/settings/bluetooth/OppProfile.java b/src/com/android/settings/bluetooth/OppProfile.java index eb5900e..7ee2ad1 100644 --- a/src/com/android/settings/bluetooth/OppProfile.java +++ b/src/com/android/settings/bluetooth/OppProfile.java @@ -75,14 +75,10 @@ final class OppProfile implements LocalBluetoothProfile { return ORDINAL; } - public int getNameResource() { + public int getNameResource(BluetoothDevice device) { return R.string.bluetooth_profile_opp; } - public int getDisconnectResource(BluetoothDevice device) { - return 0; // user must use notification to disconnect OPP transfer. - } - public int getSummaryResourceForDevice(BluetoothDevice device) { return 0; // OPP profile not displayed in UI } diff --git a/src/com/android/settings/bluetooth/PanProfile.java b/src/com/android/settings/bluetooth/PanProfile.java index 6cb1991..3db4a2b 100644 --- a/src/com/android/settings/bluetooth/PanProfile.java +++ b/src/com/android/settings/bluetooth/PanProfile.java @@ -112,15 +112,11 @@ final class PanProfile implements LocalBluetoothProfile { return ORDINAL; } - public int getNameResource() { - return R.string.bluetooth_profile_pan; - } - - public int getDisconnectResource(BluetoothDevice device) { + public int getNameResource(BluetoothDevice device) { if (isLocalRoleNap(device)) { - return R.string.bluetooth_disconnect_pan_nap_profile; + return R.string.bluetooth_profile_pan_nap; } else { - return R.string.bluetooth_disconnect_pan_user_profile; + return R.string.bluetooth_profile_pan; } } diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index 7d38e17..01e72e0 100644..100755 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -89,11 +89,17 @@ final class Utils { static void showError(Context context, String name, int messageResId) { String message = context.getString(messageResId, name); - new AlertDialog.Builder(context) + LocalBluetoothManager manager = LocalBluetoothManager.getInstance(context); + Context activity = manager.getForegroundActivity(); + if(manager.isForegroundActivity()) { + new AlertDialog.Builder(activity) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.bluetooth_error_title) .setMessage(message) .setPositiveButton(android.R.string.ok, null) .show(); + } else { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } } } diff --git a/src/com/android/settings/deviceinfo/Memory.java b/src/com/android/settings/deviceinfo/Memory.java index db60c37..e257f86 100644 --- a/src/com/android/settings/deviceinfo/Memory.java +++ b/src/com/android/settings/deviceinfo/Memory.java @@ -34,8 +34,12 @@ import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.preference.Preference; +import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.widget.Toast; import com.android.settings.R; @@ -47,6 +51,8 @@ public class Memory extends SettingsPreferenceFragment { private static final int DLG_CONFIRM_UNMOUNT = 1; private static final int DLG_ERROR_UNMOUNT = 2; + private static final int MENU_ID_USB = Menu.FIRST; + private Resources mResources; // The mountToggle Preference that has last been clicked. @@ -60,6 +66,7 @@ public class Memory extends SettingsPreferenceFragment { private StorageManager mStorageManager = null; + private StorageVolumePreferenceCategory mInternalStorageVolumePreferenceCategory; private StorageVolumePreferenceCategory[] mStorageVolumePreferenceCategories; @Override @@ -75,18 +82,32 @@ public class Memory extends SettingsPreferenceFragment { mResources = getResources(); + if (!Environment.isExternalStorageEmulated()) { + // External storage is separate from internal storage; need to + // show internal storage as a separate item. + mInternalStorageVolumePreferenceCategory = new StorageVolumePreferenceCategory( + getActivity(), mResources, null, mStorageManager, false); + getPreferenceScreen().addPreference(mInternalStorageVolumePreferenceCategory); + mInternalStorageVolumePreferenceCategory.init(); + } + StorageVolume[] storageVolumes = mStorageManager.getVolumeList(); + // mass storage is enabled if primary volume supports it + boolean massStorageEnabled = (storageVolumes.length > 0 + && storageVolumes[0].allowMassStorage()); int length = storageVolumes.length; mStorageVolumePreferenceCategories = new StorageVolumePreferenceCategory[length]; for (int i = 0; i < length; i++) { StorageVolume storageVolume = storageVolumes[i]; - StorageVolumePreferenceCategory storagePreferenceCategory = - new StorageVolumePreferenceCategory(getActivity(), mResources, storageVolume, - mStorageManager, i == 0); // The first volume is the primary volume - mStorageVolumePreferenceCategories[i] = storagePreferenceCategory; - getPreferenceScreen().addPreference(storagePreferenceCategory); - storagePreferenceCategory.init(); + boolean isPrimary = i == 0; + mStorageVolumePreferenceCategories[i] = new StorageVolumePreferenceCategory( + getActivity(), mResources, storageVolume, mStorageManager, isPrimary); + getPreferenceScreen().addPreference(mStorageVolumePreferenceCategories[i]); + mStorageVolumePreferenceCategories[i].init(); } + + // only show options menu if we are not using the legacy USB mass storage support + setHasOptionsMenu(!massStorageEnabled); } @Override @@ -97,6 +118,9 @@ public class Memory extends SettingsPreferenceFragment { intentFilter.addDataScheme("file"); getActivity().registerReceiver(mMediaScannerReceiver, intentFilter); + if (mInternalStorageVolumePreferenceCategory != null) { + mInternalStorageVolumePreferenceCategory.onResume(); + } for (int i = 0; i < mStorageVolumePreferenceCategories.length; i++) { mStorageVolumePreferenceCategories[i].onResume(); } @@ -105,9 +129,8 @@ public class Memory extends SettingsPreferenceFragment { StorageEventListener mStorageListener = new StorageEventListener() { @Override public void onStorageStateChanged(String path, String oldState, String newState) { - Log.i(TAG, "Received storage state changed notification that " + - path + " changed state from " + oldState + - " to " + newState); + Log.i(TAG, "Received storage state changed notification that " + path + + " changed state from " + oldState + " to " + newState); for (int i = 0; i < mStorageVolumePreferenceCategories.length; i++) { StorageVolumePreferenceCategory svpc = mStorageVolumePreferenceCategories[i]; if (path.equals(svpc.getStorageVolume().getPath())) { @@ -122,6 +145,9 @@ public class Memory extends SettingsPreferenceFragment { public void onPause() { super.onPause(); getActivity().unregisterReceiver(mMediaScannerReceiver); + if (mInternalStorageVolumePreferenceCategory != null) { + mInternalStorageVolumePreferenceCategory.onPause(); + } for (int i = 0; i < mStorageVolumePreferenceCategories.length; i++) { mStorageVolumePreferenceCategories[i].onPause(); } @@ -135,6 +161,31 @@ public class Memory extends SettingsPreferenceFragment { super.onDestroy(); } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, MENU_ID_USB, 0, R.string.storage_menu_usb) + //.setIcon(com.android.internal.R.drawable.stat_sys_data_usb) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_ID_USB: + if (getActivity() instanceof PreferenceActivity) { + ((PreferenceActivity) getActivity()).startPreferencePanel( + UsbSettings.class.getCanonicalName(), + null, + R.string.storage_title_usb, null, + this, 0); + } else { + startFragment(this, UsbSettings.class.getCanonicalName(), -1, null); + } + return true; + } + return super.onOptionsItemSelected(item); + } + private synchronized IMountService getMountService() { if (mMountService == null) { IBinder service = ServiceManager.getService("mount"); @@ -178,6 +229,7 @@ public class Memory extends SettingsPreferenceFragment { private final BroadcastReceiver mMediaScannerReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + // mInternalStorageVolumePreferenceCategory is not affected by the media scanner for (int i = 0; i < mStorageVolumePreferenceCategories.length; i++) { mStorageVolumePreferenceCategories[i].onMediaScannerFinished(); } diff --git a/src/com/android/settings/deviceinfo/PercentageBarChart.java b/src/com/android/settings/deviceinfo/PercentageBarChart.java index 0c71c12..95973c4 100644 --- a/src/com/android/settings/deviceinfo/PercentageBarChart.java +++ b/src/com/android/settings/deviceinfo/PercentageBarChart.java @@ -71,20 +71,21 @@ public class PercentageBarChart extends View { final int width = right - left; - int lastX = left; + float lastX = left; if (mEntries != null) { for (final Entry e : mEntries) { - final int entryWidth; - if (e.percentage == 0f) { - entryWidth = 0; + final float entryWidth; + if (e.percentage == 0.0f) { + entryWidth = 0.0f; } else { - entryWidth = Math.max(mMinTickWidth, (int) (width * e.percentage)); + entryWidth = Math.max(mMinTickWidth, width * e.percentage); } - final int nextX = lastX + entryWidth; - if (nextX >= right) { - break; + final float nextX = lastX + entryWidth; + if (nextX > right) { + canvas.drawRect(lastX, top, right, bottom, e.paint); + return; } canvas.drawRect(lastX, top, nextX, bottom, e.paint); @@ -92,7 +93,7 @@ public class PercentageBarChart extends View { } } - canvas.drawRect(lastX, top, lastX + width, bottom, mEmptyPaint); + canvas.drawRect(lastX, top, right, bottom, mEmptyPaint); } /** diff --git a/src/com/android/settings/deviceinfo/Status.java b/src/com/android/settings/deviceinfo/Status.java index 456bc98..987fab8 100644 --- a/src/com/android/settings/deviceinfo/Status.java +++ b/src/com/android/settings/deviceinfo/Status.java @@ -157,33 +157,8 @@ public class Status extends PreferenceActivity { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { - - int level = intent.getIntExtra("level", 0); - int scale = intent.getIntExtra("scale", 100); - - mBatteryLevel.setSummary(String.valueOf(level * 100 / scale) + "%"); - - int plugType = intent.getIntExtra("plugged", 0); - int status = intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN); - String statusString; - if (status == BatteryManager.BATTERY_STATUS_CHARGING) { - statusString = getString(R.string.battery_info_status_charging); - if (plugType > 0) { - statusString = statusString + " " + getString( - (plugType == BatteryManager.BATTERY_PLUGGED_AC) - ? R.string.battery_info_status_charging_ac - : R.string.battery_info_status_charging_usb); - } - } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { - statusString = getString(R.string.battery_info_status_discharging); - } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { - statusString = getString(R.string.battery_info_status_not_charging); - } else if (status == BatteryManager.BATTERY_STATUS_FULL) { - statusString = getString(R.string.battery_info_status_full); - } else { - statusString = getString(R.string.battery_info_status_unknown); - } - mBatteryStatus.setSummary(statusString); + mBatteryLevel.setSummary(Utils.getBatteryPercentage(intent)); + mBatteryStatus.setSummary(Utils.getBatteryStatus(getResources(), intent)); } } }; diff --git a/src/com/android/settings/deviceinfo/StorageMeasurement.java b/src/com/android/settings/deviceinfo/StorageMeasurement.java index 7fb309c..b4004e9 100644 --- a/src/com/android/settings/deviceinfo/StorageMeasurement.java +++ b/src/com/android/settings/deviceinfo/StorageMeasurement.java @@ -88,6 +88,7 @@ public class StorageMeasurement { private static Map<StorageVolume, StorageMeasurement> sInstances = new ConcurrentHashMap<StorageVolume, StorageMeasurement>(); + private static StorageMeasurement sInternalInstance; private volatile WeakReference<MeasurementReceiver> mReceiver; @@ -100,6 +101,7 @@ public class StorageMeasurement { final private StorageVolume mStorageVolume; final private boolean mIsPrimary; + final private boolean mIsInternal; List<FileInfo> mFileInfoForMisc; @@ -110,7 +112,8 @@ public class StorageMeasurement { private StorageMeasurement(Context context, StorageVolume storageVolume, boolean isPrimary) { mStorageVolume = storageVolume; - mIsPrimary = isPrimary; + mIsInternal = storageVolume == null; + mIsPrimary = !mIsInternal && isPrimary; // Start the thread that will measure the disk usage. final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement"); @@ -126,6 +129,13 @@ public class StorageMeasurement { */ public static StorageMeasurement getInstance(Context context, StorageVolume storageVolume, boolean isPrimary) { + if (storageVolume == null) { + if (sInternalInstance == null) { + sInternalInstance = + new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary); + } + return sInternalInstance; + } if (sInstances.containsKey(storageVolume)) { return sInstances.get(storageVolume); } else { @@ -317,9 +327,18 @@ public class StorageMeasurement { } if (succeeded) { - mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize + - stats.externalCacheSize + stats.externalDataSize + - stats.externalMediaSize + stats.externalObbSize; + if (mIsInternal) { + mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize; + } else if (!Environment.isExternalStorageEmulated()) { + mAppsSizeForThisStatsObserver += stats.externalObbSize + + stats.externalCodeSize + stats.externalDataSize + + stats.externalCacheSize + stats.externalMediaSize; + } else { + mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize + + stats.externalCodeSize + stats.externalDataSize + + stats.externalCacheSize + stats.externalMediaSize + + stats.externalObbSize; + } } synchronized (mAppsList) { @@ -349,7 +368,8 @@ public class StorageMeasurement { } private void measureApproximateStorage() { - final StatFs stat = new StatFs(mStorageVolume.getPath()); + final StatFs stat = new StatFs(mStorageVolume != null + ? mStorageVolume.getPath() : Environment.getDataDirectory().getPath()); final long blockSize = stat.getBlockSize(); final long totalBlocks = stat.getBlockCount(); final long availableBlocks = stat.getAvailableBlocks(); @@ -434,7 +454,7 @@ public class StorageMeasurement { return; } final List<ApplicationInfo> apps; - if (mIsPrimary) { + if (mIsPrimary || mIsInternal) { apps = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS); } else { @@ -529,7 +549,7 @@ public class StorageMeasurement { /** * TODO remove this method, only used because external SD Card needs a special treatment. */ - boolean isPrimary() { - return mIsPrimary; + boolean isExternalSDCard() { + return !mIsPrimary && !mIsInternal; } } diff --git a/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java b/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java index 9dbff88..4d22548 100644 --- a/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java +++ b/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java @@ -162,12 +162,13 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory implemen mResources = resources; mStorageVolume = storageVolume; mStorageManager = storageManager; - setTitle(storageVolume.getDescription()); + setTitle(storageVolume != null ? storageVolume.getDescription() + : resources.getText(R.string.internal_storage)); mMeasurement = StorageMeasurement.getInstance(context, storageVolume, isPrimary); mMeasurement.setReceiver(this); // Cannot format emulated storage - mAllowFormat = !mStorageVolume.isEmulated(); + mAllowFormat = mStorageVolume != null && !mStorageVolume.isEmulated(); // For now we are disabling reformatting secondary external storage // until some interoperability problems with MTP are fixed if (!isPrimary) mAllowFormat = false; @@ -240,7 +241,9 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory implemen private void updatePreferencesFromState() { resetPreferences(); - String state = mStorageManager.getVolumeState(mStorageVolume.getPath()); + String state = mStorageVolume != null + ? mStorageManager.getVolumeState(mStorageVolume.getPath()) + : Environment.MEDIA_MOUNTED; String readOnly = ""; if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { @@ -251,11 +254,8 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory implemen } } - if (mFormatPreference != null) { - removePreference(mFormatPreference); - } - - if (!mStorageVolume.isRemovable() && !Environment.MEDIA_UNMOUNTED.equals(state)) { + if ((mStorageVolume == null || !mStorageVolume.isRemovable()) + && !Environment.MEDIA_UNMOUNTED.equals(state)) { // This device has built-in storage that is not removable. // There is no reason for the user to unmount it. removePreference(mMountTogglePreference); @@ -307,7 +307,7 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory implemen mPreferences[TOTAL_SIZE].setSummary(formatSize(totalSize)); - if (!mMeasurement.isPrimary()) { + if (mMeasurement.isExternalSDCard()) { // TODO FIXME: external SD card will not report any size. Show used space in bar graph final long usedSize = totalSize - availSize; mUsageBarPreference.addEntry(usedSize / (float) totalSize, android.graphics.Color.GRAY); diff --git a/src/com/android/settings/deviceinfo/UsageBarPreference.java b/src/com/android/settings/deviceinfo/UsageBarPreference.java index e9909f1..5aeaef5 100644 --- a/src/com/android/settings/deviceinfo/UsageBarPreference.java +++ b/src/com/android/settings/deviceinfo/UsageBarPreference.java @@ -36,17 +36,17 @@ public class UsageBarPreference extends Preference { public UsageBarPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setWidgetLayoutResource(R.layout.preference_memoryusage); + setLayoutResource(R.layout.preference_memoryusage); } public UsageBarPreference(Context context) { super(context); - setWidgetLayoutResource(R.layout.preference_memoryusage); + setLayoutResource(R.layout.preference_memoryusage); } public UsageBarPreference(Context context, AttributeSet attrs) { super(context, attrs); - setWidgetLayoutResource(R.layout.preference_memoryusage); + setLayoutResource(R.layout.preference_memoryusage); } public void addEntry(float percentage, int color) { diff --git a/src/com/android/settings/deviceinfo/UsbSettings.java b/src/com/android/settings/deviceinfo/UsbSettings.java new file mode 100644 index 0000000..538cde7 --- /dev/null +++ b/src/com/android/settings/deviceinfo/UsbSettings.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2011 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.deviceinfo; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbManager; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +/** + * USB storage settings. + */ +public class UsbSettings extends SettingsPreferenceFragment { + + private static final String TAG = "UsbSettings"; + + private static final String KEY_MTP = "usb_mtp"; + private static final String KEY_PTP = "usb_ptp"; + + private UsbManager mUsbManager; + private CheckBoxPreference mMtp; + private CheckBoxPreference mPtp; + + private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() { + public void onReceive(Context content, Intent intent) { + updateToggles(); + } + }; + + private PreferenceScreen createPreferenceHierarchy() { + PreferenceScreen root = getPreferenceScreen(); + if (root != null) { + root.removeAll(); + } + addPreferencesFromResource(R.xml.usb_settings); + root = getPreferenceScreen(); + + mMtp = (CheckBoxPreference)root.findPreference(KEY_MTP); + mPtp = (CheckBoxPreference)root.findPreference(KEY_PTP); + + return root; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE); + } + + @Override + public void onPause() { + super.onPause(); + getActivity().unregisterReceiver(mStateReceiver); + } + + @Override + public void onResume() { + super.onResume(); + + // Make sure we reload the preference hierarchy since some of these settings + // depend on others... + createPreferenceHierarchy(); + + // ACTION_USB_STATE is sticky so this will call updateToggles + getActivity().registerReceiver(mStateReceiver, + new IntentFilter(UsbManager.ACTION_USB_STATE)); + } + + private void updateToggles() { + String function = mUsbManager.getDefaultFunction(); + if (UsbManager.USB_FUNCTION_MTP.equals(function)) { + mMtp.setChecked(true); + mPtp.setChecked(false); + } else if (UsbManager.USB_FUNCTION_PTP.equals(function)) { + mMtp.setChecked(false); + mPtp.setChecked(true); + } else { + mMtp.setChecked(false); + mPtp.setChecked(false); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + + // temporary hack - using check boxes as radio buttons + // don't allow unchecking them + if (preference instanceof CheckBoxPreference) { + CheckBoxPreference checkBox = (CheckBoxPreference)preference; + if (!checkBox.isChecked()) { + checkBox.setChecked(true); + return true; + } + } + if (preference == mMtp) { + mUsbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP, true); + } else if (preference == mPtp) { + mUsbManager.setCurrentFunction(UsbManager.USB_FUNCTION_PTP, true); + } + updateToggles(); + return true; + } +} diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java b/src/com/android/settings/fuelgauge/BatteryHistoryChart.java index 97ebf43..13a962d 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryChart.java @@ -368,9 +368,9 @@ public class BatteryHistoryChart extends View { } if (rec.batteryLevel != lastLevel || pos == 1) { lastLevel = rec.batteryLevel; - lastInteresting = pos; - mHistEnd = rec.time; } + lastInteresting = pos; + mHistEnd = rec.time; aggrStates |= rec.states; } } @@ -438,7 +438,13 @@ public class BatteryHistoryChart extends View { 2, getResources().getDisplayMetrics()); if (h > (textHeight*6)) { mLargeMode = true; - mLineWidth = textHeight/2; + if (h > (textHeight*15)) { + // Plenty of room for the chart. + mLineWidth = textHeight/2; + } else { + // Compress lines to make more room for chart. + mLineWidth = textHeight/3; + } mLevelTop = textHeight + mLineWidth; mScreenOnPaint.setARGB(255, 32, 64, 255); mGpsOnPaint.setARGB(255, 32, 64, 255); @@ -472,7 +478,8 @@ public class BatteryHistoryChart extends View { mWifiRunningOffset = mWakeLockOffset + barOffset; mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? barOffset : 0); mPhoneSignalOffset = mGpsOnOffset + (mHaveGps ? barOffset : 0); - mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? barOffset : 0) + mLineWidth; + mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? barOffset : 0) + + ((mLineWidth*3)/2); if (mHavePhoneSignal) { mPhoneSignalChart.init(w); } @@ -670,8 +677,8 @@ public class BatteryHistoryChart extends View { if (!mBatCriticalPath.isEmpty()) { canvas.drawPath(mBatCriticalPath, mBatteryCriticalPaint); } - int top = height - (mHavePhoneSignal ? mPhoneSignalOffset - (mLineWidth/2) : 0); if (mHavePhoneSignal) { + int top = height-mPhoneSignalOffset - (mLineWidth/2); mPhoneSignalChart.draw(canvas, top, mLineWidth); } if (!mScreenOnPath.isEmpty()) { diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index fc903eb..f28ba93 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -16,18 +16,14 @@ package com.android.settings.fuelgauge; -import com.android.internal.app.IBatteryStats; -import com.android.internal.os.BatteryStatsImpl; -import com.android.internal.os.PowerProfile; -import com.android.settings.R; -import com.android.settings.applications.InstalledAppDetails; -import com.android.settings.fuelgauge.PowerUsageDetail.DrainType; - -import android.content.ContentResolver; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.hardware.SensorManager; import android.os.BatteryStats; +import android.os.BatteryStats.Uid; +import android.os.BatteryManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -36,7 +32,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.BatteryStats.Uid; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; @@ -49,6 +44,12 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.BatteryStatsImpl; +import com.android.internal.os.PowerProfile; +import com.android.settings.R; +import com.android.settings.fuelgauge.PowerUsageDetail.DrainType; + import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; @@ -67,6 +68,9 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { private static final String TAG = "PowerUsageSummary"; + private static final String KEY_APP_LIST = "app_list"; + private static final String KEY_BATTERY_STATUS = "battery_status"; + private static final int MENU_STATS_TYPE = Menu.FIRST; private static final int MENU_STATS_REFRESH = Menu.FIRST + 1; @@ -79,6 +83,7 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { private final List<BatterySipper> mBluetoothSippers = new ArrayList<BatterySipper>(); private PreferenceGroup mAppListGroup; + private Preference mBatteryStatusPref; private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; @@ -99,7 +104,23 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { private ArrayList<BatterySipper> mRequestQueue = new ArrayList<BatterySipper>(); private Thread mRequestThread; private boolean mAbort; - + + private BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + String batteryLevel = com.android.settings.Utils.getBatteryPercentage(intent); + String batteryStatus = com.android.settings.Utils.getBatteryStatus(getResources(), + intent); + String batterySummary = context.getResources().getString( + R.string.power_usage_level_and_status, batteryLevel, batteryStatus); + mBatteryStatusPref.setTitle(batterySummary); + } + } + }; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -111,7 +132,8 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { addPreferencesFromResource(R.xml.power_usage_summary); mBatteryInfo = IBatteryStats.Stub.asInterface( ServiceManager.getService("batteryinfo")); - mAppListGroup = (PreferenceGroup) findPreference("app_list"); + mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST); + mBatteryStatusPref = mAppListGroup.findPreference(KEY_BATTERY_STATUS); mPowerProfile = new PowerProfile(getActivity()); setHasOptionsMenu(true); } @@ -120,6 +142,8 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { public void onResume() { super.onResume(); mAbort = false; + getActivity().registerReceiver(mBatteryInfoReceiver, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); refreshStats(); } @@ -129,6 +153,7 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { mAbort = true; } mHandler.removeMessages(MSG_UPDATE_NAME_ICON); + getActivity().unregisterReceiver(mBatteryInfoReceiver); super.onPause(); } @@ -292,7 +317,8 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { MenuItem refresh = menu.add(0, MENU_STATS_REFRESH, 0, R.string.menu_stats_refresh) .setIcon(R.drawable.ic_menu_refresh_holo_dark) .setAlphabeticShortcut('r'); - refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | + MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @Override @@ -337,6 +363,8 @@ public class PowerUsageSummary extends PreferenceFragment implements Runnable { mBluetoothSippers.clear(); mAppListGroup.setOrderingAsAdded(false); + mBatteryStatusPref.setOrder(-2); + mAppListGroup.addPreference(mBatteryStatusPref); BatteryHistoryPreference hist = new BatteryHistoryPreference(getActivity(), mStats); hist.setOrder(-1); mAppListGroup.addPreference(hist); diff --git a/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java b/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java new file mode 100644 index 0000000..f983f59 --- /dev/null +++ b/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import android.content.Context; +import android.content.Intent; +import android.preference.CheckBoxPreference; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +public class CheckBoxAndSettingsPreference extends CheckBoxPreference { + private static final float DISABLED_ALPHA = 0.4f; + + private SettingsPreferenceFragment mFragment; + private TextView mTitleText; + private TextView mSummaryText; + private View mCheckBox; + private ImageView mSetingsButton; + private Intent mSettingsIntent; + + public CheckBoxAndSettingsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutResource(R.layout.preference_inputmethod); + setWidgetLayoutResource(R.layout.preference_inputmethod_widget); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mCheckBox = view.findViewById(R.id.inputmethod_pref); + mCheckBox.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + onCheckBoxClicked(arg0); + } + }); + mSetingsButton = (ImageView)view.findViewById(R.id.inputmethod_settings); + mTitleText = (TextView)view.findViewById(android.R.id.title); + mSummaryText = (TextView)view.findViewById(android.R.id.summary); + mSetingsButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + onSettingsButtonClicked(arg0); + } + }); + enableSettingsButton(); + } + + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + enableSettingsButton(); + } + + public void setFragmentIntent(SettingsPreferenceFragment fragment, Intent intent) { + mFragment = fragment; + mSettingsIntent = intent; + } + + protected void onCheckBoxClicked(View view) { + if (isChecked()) { + setChecked(false); + } else { + setChecked(true); + } + } + + protected void onSettingsButtonClicked(View arg0) { + if (mFragment != null && mSettingsIntent != null) { + mFragment.startActivity(mSettingsIntent); + } + } + + private void enableSettingsButton() { + if (mSetingsButton != null) { + if (mSettingsIntent == null) { + mSetingsButton.setVisibility(View.GONE); + } else { + final boolean checked = isChecked(); + mSetingsButton.setEnabled(checked); + mSetingsButton.setClickable(checked); + mSetingsButton.setFocusable(checked); + if (!checked) { + mSetingsButton.setAlpha(DISABLED_ALPHA); + } + } + } + if (mTitleText != null) { + mTitleText.setEnabled(true); + } + if (mSummaryText != null) { + mSummaryText.setEnabled(true); + } + } +} diff --git a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java index a4808b0..699a4a6 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java +++ b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java @@ -17,29 +17,63 @@ package com.android.settings.inputmethod; import com.android.settings.R; +import com.android.settings.Settings.SpellCheckersSettingsActivity; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.UserDictionarySettings; import com.android.settings.Utils; import com.android.settings.VoiceInputOutputSettings; +import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.provider.Settings; +import android.provider.Settings.System; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener{ private static final String KEY_PHONE_LANGUAGE = "phone_language"; private static final String KEY_CURRENT_INPUT_METHOD = "current_input_method"; private static final String KEY_INPUT_METHOD_SELECTOR = "input_method_selector"; + private static final String KEY_USER_DICTIONARY_SETTINGS = "key_user_dictionary_settings"; + // false: on ICS or later + private static final boolean SHOW_INPUT_METHOD_SWITCHER_SETTINGS = false; + + private static final String[] sSystemSettingNames = { + System.TEXT_AUTO_REPLACE, System.TEXT_AUTO_CAPS, System.TEXT_AUTO_PUNCTUATE, + }; + + private static final String[] sHardKeyboardKeys = { + "auto_replace", "auto_caps", "auto_punctuate", + }; private int mDefaultInputMethodSelectorVisibility = 0; private ListPreference mShowInputMethodSelectorPref; private Preference mLanguagePref; + private ArrayList<InputMethodPreference> mInputMethodPreferenceList = + new ArrayList<InputMethodPreference>(); + private boolean mHaveHardKeyboard; + private PreferenceCategory mHardKeyboardCategory; + private InputMethodManager mImm; + private List<InputMethodInfo> mImis; + private boolean mIsOnlyImeSettings; @Override public void onCreate(Bundle icicle) { @@ -59,13 +93,34 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } else { mLanguagePref = findPreference(KEY_PHONE_LANGUAGE); } - mShowInputMethodSelectorPref = (ListPreference)findPreference( - KEY_INPUT_METHOD_SELECTOR); - mShowInputMethodSelectorPref.setOnPreferenceChangeListener(this); - // TODO: Update current input method name on summary - updateInputMethodSelectorSummary(loadInputMethodSelectorVisibility()); + if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { + mShowInputMethodSelectorPref = (ListPreference)findPreference( + KEY_INPUT_METHOD_SELECTOR); + mShowInputMethodSelectorPref.setOnPreferenceChangeListener(this); + // TODO: Update current input method name on summary + updateInputMethodSelectorSummary(loadInputMethodSelectorVisibility()); + } new VoiceInputOutputSettings(this).onCreate(); + + // Hard keyboard + final Configuration config = getResources().getConfiguration(); + mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY); + + // IME + mIsOnlyImeSettings = Settings.ACTION_INPUT_METHOD_SETTINGS.equals( + getActivity().getIntent().getAction()); + mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + mImis = mImm.getInputMethodList(); + createImePreferenceHierarchy((PreferenceGroup)findPreference("keyboard_settings_category")); + + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(getActivity(), SpellCheckersSettingsActivity.class); + final SpellCheckersPreference scp = ((SpellCheckersPreference)findPreference( + "spellcheckers_settings")); + if (scp != null) { + scp.setFragmentIntent(this, intent); + } } private void updateInputMethodSelectorSummary(int value) { @@ -77,25 +132,76 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } } + private void updateUserDictionaryPreference(Preference userDictionaryPreference) { + final Activity activity = getActivity(); + final Set<String> localeList = UserDictionaryList.getUserDictionaryLocalesList(activity); + if (null == localeList) { + // The locale list is null if and only if the user dictionary service is + // not present or disabled. In this case we need to remove the preference. + ((PreferenceGroup)findPreference("language_settings_category")).removePreference( + userDictionaryPreference); + } else if (localeList.size() <= 1) { + userDictionaryPreference.setTitle(R.string.user_dict_single_settings_title); + userDictionaryPreference.setFragment(UserDictionarySettings.class.getName()); + // If the size of localeList is 0, we don't set the locale parameter in the + // extras. This will be interpreted by the UserDictionarySettings class as + // meaning "the current locale". + // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesList() + // the locale list always has at least one element, since it always includes the current + // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesList(). + if (localeList.size() == 1) { + final String locale = (String)localeList.toArray()[0]; + userDictionaryPreference.getExtras().putString("locale", locale); + } + } else { + userDictionaryPreference.setTitle(R.string.user_dict_multiple_settings_title); + userDictionaryPreference.setFragment(UserDictionaryList.class.getName()); + } + } + @Override public void onResume() { super.onResume(); - if (mLanguagePref != null) { - Configuration conf = getResources().getConfiguration(); - String locale = conf.locale.getDisplayName(conf.locale); - if (locale != null && locale.length() > 1) { - locale = Character.toUpperCase(locale.charAt(0)) + locale.substring(1); - mLanguagePref.setSummary(locale); + if (!mIsOnlyImeSettings) { + if (mLanguagePref != null) { + Configuration conf = getResources().getConfiguration(); + String locale = conf.locale.getDisplayName(conf.locale); + if (locale != null && locale.length() > 1) { + locale = Character.toUpperCase(locale.charAt(0)) + locale.substring(1); + mLanguagePref.setSummary(locale); + } + } + + updateUserDictionaryPreference(findPreference(KEY_USER_DICTIONARY_SETTINGS)); + if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { + mShowInputMethodSelectorPref.setOnPreferenceChangeListener(this); + } + } + + // Hard keyboard + if (mHaveHardKeyboard) { + for (int i = 0; i < sHardKeyboardKeys.length; ++i) { + InputMethodPreference chkPref = (InputMethodPreference) + mHardKeyboardCategory.findPreference(sHardKeyboardKeys[i]); + chkPref.setChecked( + System.getInt(getContentResolver(), sSystemSettingNames[i], 1) > 0); } } - mShowInputMethodSelectorPref.setOnPreferenceChangeListener(this); + // IME + InputMethodAndSubtypeUtil.loadInputMethodSubtypeList( + this, getContentResolver(), mImis, null); + updateActiveInputMethodsSummary(); } @Override public void onPause() { super.onPause(); - mShowInputMethodSelectorPref.setOnPreferenceChangeListener(null); + if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { + mShowInputMethodSelectorPref.setOnPreferenceChangeListener(null); + } + InputMethodAndSubtypeUtil.saveInputMethodSubtypeList( + this, getContentResolver(), mImis, mHaveHardKeyboard); } @Override @@ -112,6 +218,17 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment getSystemService(Context.INPUT_METHOD_SERVICE); imm.showInputMethodPicker(); } + } else if (preference instanceof CheckBoxPreference) { + final CheckBoxPreference chkPref = (CheckBoxPreference) preference; + if (mHaveHardKeyboard) { + for (int i = 0; i < sHardKeyboardKeys.length; ++i) { + if (chkPref == mHardKeyboardCategory.findPreference(sHardKeyboardKeys[i])) { + System.putInt(getContentResolver(), sSystemSettingNames[i], + chkPref.isChecked() ? 1 : 0); + return true; + } + } + } } return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -134,12 +251,84 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceChange(Preference preference, Object value) { - if (preference == mShowInputMethodSelectorPref) { - if (value instanceof String) { - saveInputMethodSelectorVisibility((String)value); + if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { + if (preference == mShowInputMethodSelectorPref) { + if (value instanceof String) { + saveInputMethodSelectorVisibility((String)value); + } } } return false; } + private void updateActiveInputMethodsSummary() { + for (Preference pref : mInputMethodPreferenceList) { + if (pref instanceof InputMethodPreference) { + ((InputMethodPreference)pref).updateSummary(); + } + } + } + + private InputMethodPreference getInputMethodPreference(InputMethodInfo imi, int imiSize) { + final PackageManager pm = getPackageManager(); + final CharSequence label = imi.loadLabel(pm); + // IME settings + final Intent intent; + final String settingsActivity = imi.getSettingsActivity(); + if (!TextUtils.isEmpty(settingsActivity)) { + intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(imi.getPackageName(), settingsActivity); + } else { + intent = null; + } + + // Add a check box for enabling/disabling IME + InputMethodPreference pref = new InputMethodPreference(this, intent, mImm, imi, imiSize); + pref.setKey(imi.getId()); + pref.setTitle(label); + return pref; + } + + private void createImePreferenceHierarchy(PreferenceGroup root) { + final Preference hardKeyPref = findPreference("hard_keyboard"); + if (mIsOnlyImeSettings) { + getPreferenceScreen().removeAll(); + if (hardKeyPref != null && mHaveHardKeyboard) { + getPreferenceScreen().addPreference(hardKeyPref); + } + if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { + getPreferenceScreen().addPreference(mShowInputMethodSelectorPref); + } + getPreferenceScreen().addPreference(root); + } + if (hardKeyPref != null) { + if (mHaveHardKeyboard) { + mHardKeyboardCategory = (PreferenceCategory) hardKeyPref; + } else { + getPreferenceScreen().removePreference(hardKeyPref); + } + } + root.removeAll(); + mInputMethodPreferenceList.clear(); + + if (!mIsOnlyImeSettings) { + // Current IME selection + final PreferenceScreen currentIme = new PreferenceScreen(getActivity(), null); + currentIme.setKey(KEY_CURRENT_INPUT_METHOD); + currentIme.setTitle(getResources().getString(R.string.current_input_method)); + root.addPreference(currentIme); + } + + final int N = (mImis == null ? 0 : mImis.size()); + for (int i = 0; i < N; ++i) { + final InputMethodInfo imi = mImis.get(i); + final InputMethodPreference pref = getInputMethodPreference(imi, N); + mInputMethodPreferenceList.add(pref); + } + + Collections.sort(mInputMethodPreferenceList); + for (int i = 0; i < N; ++i) { + root.addPreference(mInputMethodPreferenceList.get(i)); + } + } } diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java index 6e1d4d1..e5ce987 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java @@ -22,6 +22,7 @@ import com.android.settings.SettingsPreferenceFragment; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Bundle; @@ -48,6 +49,7 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { private InputMethodManager mImm; private List<InputMethodInfo> mInputMethodProperties; private String mInputMethodId; + private String mTitle; @Override public void onCreate(Bundle icicle) { @@ -56,25 +58,41 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { Configuration config = getResources().getConfiguration(); mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY); + final Bundle arguments = getArguments(); // Input method id should be available from an Intent when this preference is launched as a // single Activity (see InputMethodAndSubtypeEnablerActivity). It should be available // from a preference argument when the preference is launched as a part of the other // Activity (like a right pane of 2-pane Settings app) mInputMethodId = getActivity().getIntent().getStringExtra( android.provider.Settings.EXTRA_INPUT_METHOD_ID); - if (mInputMethodId == null && (getArguments() != null)) { + if (mInputMethodId == null && (arguments != null)) { final String inputMethodId = - getArguments().getString(android.provider.Settings.EXTRA_INPUT_METHOD_ID); + arguments.getString(android.provider.Settings.EXTRA_INPUT_METHOD_ID); if (inputMethodId != null) { mInputMethodId = inputMethodId; } } + mTitle = getActivity().getIntent().getStringExtra(Intent.EXTRA_TITLE); + if (mTitle == null && (arguments != null)) { + final String title = arguments.getString(Intent.EXTRA_TITLE); + if (title != null) { + mTitle = title; + } + } onCreateIMM(); setPreferenceScreen(createPreferenceHierarchy()); } @Override + public void onActivityCreated(Bundle icicle) { + super.onActivityCreated(icicle); + if (!TextUtils.isEmpty(mTitle)) { + getActivity().setTitle(mTitle); + } + } + + @Override public void onResume() { super.onResume(); InputMethodAndSubtypeUtil.loadInputMethodSubtypeList( @@ -136,6 +154,7 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { .setCancelable(true) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { chkPref.setChecked(true); InputMethodAndSubtypeUtil.setSubtypesPreferenceEnabled( @@ -146,6 +165,7 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { } @@ -188,7 +208,8 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { private PreferenceScreen createPreferenceHierarchy() { // Root - PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity()); + final PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity()); + final Context context = getActivity(); int N = (mInputMethodProperties == null ? 0 : mInputMethodProperties.size()); @@ -202,7 +223,7 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { if (!TextUtils.isEmpty(mInputMethodId) && !mInputMethodId.equals(imiId)) { continue; } - PreferenceCategory keyboardSettingsCategory = new PreferenceCategory(getActivity()); + PreferenceCategory keyboardSettingsCategory = new PreferenceCategory(context); root.addPreference(keyboardSettingsCategory); PackageManager pm = getPackageManager(); CharSequence label = imi.loadLabel(pm); @@ -210,31 +231,22 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { keyboardSettingsCategory.setTitle(label); keyboardSettingsCategory.setKey(imiId); // TODO: Use toggle Preference if images are ready. - CheckBoxPreference autoCB = new CheckBoxPreference(getActivity()); + CheckBoxPreference autoCB = new CheckBoxPreference(context); autoCB.setTitle(R.string.use_system_language_to_select_input_method_subtypes); mSubtypeAutoSelectionCBMap.put(imiId, autoCB); keyboardSettingsCategory.addPreference(autoCB); - PreferenceCategory activeInputMethodsCategory = new PreferenceCategory(getActivity()); + PreferenceCategory activeInputMethodsCategory = new PreferenceCategory(context); activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes); root.addPreference(activeInputMethodsCategory); ArrayList<Preference> subtypePreferences = new ArrayList<Preference>(); if (subtypeCount > 0) { for (int j = 0; j < subtypeCount; ++j) { - InputMethodSubtype subtype = imi.getSubtypeAt(j); - CharSequence subtypeLabel; - int nameResId = subtype.getNameResId(); - if (nameResId != 0) { - subtypeLabel = pm.getText(imi.getPackageName(), nameResId, - imi.getServiceInfo().applicationInfo); - } else { - String mode = subtype.getMode(); - CharSequence language = subtype.getLocale(); - subtypeLabel = (mode == null ? "" : mode) + "," - + (language == null ? "" : language); - } - CheckBoxPreference chkbxPref = new CheckBoxPreference(getActivity()); + final InputMethodSubtype subtype = imi.getSubtypeAt(j); + final CharSequence subtypeLabel = subtype.getDisplayName(context, + imi.getPackageName(), imi.getServiceInfo().applicationInfo); + final CheckBoxPreference chkbxPref = new CheckBoxPreference(context); chkbxPref.setKey(imiId + subtype.hashCode()); chkbxPref.setTitle(subtypeLabel); activeInputMethodsCategory.addPreference(chkbxPref); diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java index 362fbb5..f62edc4 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java @@ -169,9 +169,10 @@ public class InputMethodAndSubtypeUtil { final boolean isImeChecked = (pref instanceof CheckBoxPreference) ? ((CheckBoxPreference) pref).isChecked() : enabledIMEAndSubtypesMap.containsKey(imiId); - boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); - boolean systemIme = isSystemIme(imi); - if (((onlyOneIME || systemIme) && !hasHardKeyboard) || isImeChecked) { + final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); + final boolean auxIme = isAuxiliaryIme(imi); + final boolean systemIme = isSystemIme(imi); + if (((onlyOneIME || (systemIme && !auxIme)) && !hasHardKeyboard) || isImeChecked) { if (!enabledIMEAndSubtypesMap.containsKey(imiId)) { // imiId has just been enabled enabledIMEAndSubtypesMap.put(imiId, new HashSet<String>()); @@ -276,7 +277,7 @@ public class InputMethodAndSubtypeUtil { List<InputMethodInfo> inputMethodInfos, final Map<String, List<Preference>> inputMethodPrefsMap) { HashMap<String, HashSet<String>> enabledSubtypes = - getEnabledInputMethodsAndSubtypeList(resolver); + getEnabledInputMethodsAndSubtypeList(resolver); for (InputMethodInfo imi : inputMethodInfos) { final String imiId = imi.getId(); @@ -342,4 +343,19 @@ public class InputMethodAndSubtypeUtil { return (property.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } + + public static boolean isAuxiliaryIme(InputMethodInfo imi) { + final int subtypeCount = imi.getSubtypeCount(); + if (subtypeCount == 0) { + return false; + } else { + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (!subtype.isAuxiliary()) { + return false; + } + } + } + return true; + } } diff --git a/src/com/android/settings/inputmethod/InputMethodConfig.java b/src/com/android/settings/inputmethod/InputMethodConfig.java deleted file mode 100644 index 2cfe35d..0000000 --- a/src/com/android/settings/inputmethod/InputMethodConfig.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2010 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.inputmethod; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - -import android.app.AlertDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceCategory; -import android.preference.PreferenceScreen; -import android.provider.Settings; -import android.provider.Settings.System; -import android.text.TextUtils; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class InputMethodConfig extends SettingsPreferenceFragment { - - private static final String[] sSystemSettingNames = { - System.TEXT_AUTO_REPLACE, System.TEXT_AUTO_CAPS, System.TEXT_AUTO_PUNCTUATE, - }; - - private static final String[] sHardKeyboardKeys = { - "auto_replace", "auto_caps", "auto_punctuate", - }; - - private AlertDialog mDialog = null; - private boolean mHaveHardKeyboard; - private PreferenceCategory mHardKeyboardCategory; - // Map of imi and its preferences - final private HashMap<String, List<Preference>> mInputMethodPrefsMap = - new HashMap<String, List<Preference>>(); - final private HashMap<InputMethodInfo, Preference> mActiveInputMethodsPrefMap = - new HashMap<InputMethodInfo, Preference>(); - private List<InputMethodInfo> mInputMethodProperties; - - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - Configuration config = getResources().getConfiguration(); - mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY); - InputMethodManager imm = (InputMethodManager) getSystemService( - Context.INPUT_METHOD_SERVICE); - - // TODO: Change mInputMethodProperties to Map - mInputMethodProperties = imm.getInputMethodList(); - setPreferenceScreen(createPreferenceHierarchy()); - } - - @Override - public void onResume() { - super.onResume(); - - ContentResolver resolver = getContentResolver(); - if (mHaveHardKeyboard) { - for (int i = 0; i < sHardKeyboardKeys.length; ++i) { - CheckBoxPreference chkPref = (CheckBoxPreference) - mHardKeyboardCategory.findPreference(sHardKeyboardKeys[i]); - chkPref.setChecked(System.getInt(resolver, sSystemSettingNames[i], 1) > 0); - } - } - - InputMethodAndSubtypeUtil.loadInputMethodSubtypeList( - this, resolver, mInputMethodProperties, mInputMethodPrefsMap); - updateActiveInputMethodsSummary(); - } - - @Override - public void onPause() { - super.onPause(); - InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(this, getContentResolver(), - mInputMethodProperties, mHaveHardKeyboard); - } - - private void showSecurityWarnDialog(InputMethodInfo imi, final CheckBoxPreference chkPref, - final String imiId) { - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - mDialog = (new AlertDialog.Builder(getActivity())) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - chkPref.setChecked(true); - for (Preference pref: mInputMethodPrefsMap.get(imiId)) { - pref.setEnabled(true); - } - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - } - }) - .create(); - mDialog.setMessage(getResources().getString(R.string.ime_security_warning, - imi.getServiceInfo().applicationInfo.loadLabel(getPackageManager()))); - mDialog.show(); - } - - private InputMethodInfo getInputMethodInfoFromImiId(String imiId) { - final int N = mInputMethodProperties.size(); - for (int i = 0; i < N; ++i) { - InputMethodInfo imi = mInputMethodProperties.get(i); - if (imiId.equals(imi.getId())) { - return imi; - } - } - return null; - } - - @Override - public boolean onPreferenceTreeClick( - PreferenceScreen preferenceScreen, Preference preference) { - - if (preference instanceof CheckBoxPreference) { - final CheckBoxPreference chkPref = (CheckBoxPreference) preference; - - if (mHaveHardKeyboard) { - for (int i = 0; i < sHardKeyboardKeys.length; ++i) { - if (chkPref == mHardKeyboardCategory.findPreference(sHardKeyboardKeys[i])) { - System.putInt(getContentResolver(), sSystemSettingNames[i], - chkPref.isChecked() ? 1 : 0); - return true; - } - } - } - - final String imiId = chkPref.getKey(); - if (chkPref.isChecked()) { - InputMethodInfo selImi = getInputMethodInfoFromImiId(imiId); - if (selImi != null) { - if (InputMethodAndSubtypeUtil.isSystemIme(selImi)) { - // This is a built-in IME, so no need to warn. - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - } else { - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - chkPref.setChecked(false); - showSecurityWarnDialog(selImi, chkPref, imiId); - } else { - for (Preference pref: mInputMethodPrefsMap.get(imiId)) { - pref.setEnabled(false); - } - } - } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mDialog != null) { - mDialog.dismiss(); - mDialog = null; - } - } - - private void addInputMethodPreference(PreferenceScreen root, InputMethodInfo imi, - final int imiSize) { - PreferenceCategory keyboardSettingsCategory = new PreferenceCategory(getActivity()); - root.addPreference(keyboardSettingsCategory); - final String imiId = imi.getId(); - mInputMethodPrefsMap.put(imiId, new ArrayList<Preference>()); - - PackageManager pm = getPackageManager(); - CharSequence label = imi.loadLabel(pm); - keyboardSettingsCategory.setTitle(label); - - final boolean isSystemIME = InputMethodAndSubtypeUtil.isSystemIme(imi); - // Add a check box for enabling/disabling IME - CheckBoxPreference chkbxPref = new CheckBoxPreference(getActivity()); - chkbxPref.setKey(imiId); - chkbxPref.setTitle(label); - keyboardSettingsCategory.addPreference(chkbxPref); - // Disable the toggle if it's the only keyboard in the system, or it's a system IME. - if (imiSize <= 1 || isSystemIME) { - chkbxPref.setEnabled(false); - } - - Intent intent; - // Add subtype settings when this IME has two or more subtypes. - PreferenceScreen prefScreen = new PreferenceScreen(getActivity(), null); - prefScreen.setTitle(R.string.active_input_method_subtypes); - if (imi.getSubtypeCount() > 1) { - prefScreen.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference){ - final Bundle bundle = new Bundle(); - bundle.putString(Settings.EXTRA_INPUT_METHOD_ID, imiId); - startFragment(InputMethodConfig.this, - InputMethodAndSubtypeEnabler.class.getName(), - 0, bundle); - return true; - } - }); - keyboardSettingsCategory.addPreference(prefScreen); - mActiveInputMethodsPrefMap.put(imi, prefScreen); - mInputMethodPrefsMap.get(imiId).add(prefScreen); - } - - // Add IME settings - String settingsActivity = imi.getSettingsActivity(); - if (!TextUtils.isEmpty(settingsActivity)) { - prefScreen = new PreferenceScreen(getActivity(), null); - prefScreen.setTitle(R.string.input_method_settings); - intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(imi.getPackageName(), settingsActivity); - prefScreen.setIntent(intent); - keyboardSettingsCategory.addPreference(prefScreen); - mInputMethodPrefsMap.get(imiId).add(prefScreen); - } - } - - private PreferenceScreen createPreferenceHierarchy() { - addPreferencesFromResource(R.xml.hard_keyboard_settings); - PreferenceScreen root = getPreferenceScreen(); - - if (mHaveHardKeyboard) { - mHardKeyboardCategory = (PreferenceCategory) findPreference("hard_keyboard"); - } else { - root.removeAll(); - } - - final int N = (mInputMethodProperties == null ? 0 : mInputMethodProperties.size()); - for (int i = 0; i < N; ++i) { - addInputMethodPreference(root, mInputMethodProperties.get(i), N); - } - return root; - } - - private void updateActiveInputMethodsSummary() { - final InputMethodManager imm = - (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - final PackageManager pm = getPackageManager(); - for (InputMethodInfo imi: mActiveInputMethodsPrefMap.keySet()) { - Preference pref = mActiveInputMethodsPrefMap.get(imi); - List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(imi, true); - StringBuilder summary = new StringBuilder(); - boolean subtypeAdded = false; - for (InputMethodSubtype subtype: subtypes) { - if (subtypeAdded) { - summary.append(", "); - } - summary.append(pm.getText(imi.getPackageName(), subtype.getNameResId(), - imi.getServiceInfo().applicationInfo)); - subtypeAdded = true; - } - pref.setSummary(summary.toString()); - } - } -} diff --git a/src/com/android/settings/inputmethod/InputMethodDialogReceiver.java b/src/com/android/settings/inputmethod/InputMethodDialogReceiver.java new file mode 100644 index 0000000..46be132 --- /dev/null +++ b/src/com/android/settings/inputmethod/InputMethodDialogReceiver.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.view.inputmethod.InputMethodManager; + +public class InputMethodDialogReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Settings.ACTION_SHOW_INPUT_METHOD_PICKER.equals(intent.getAction())) { + ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)) + .showInputMethodPicker(); + } + } +} diff --git a/src/com/android/settings/inputmethod/InputMethodPreference.java b/src/com/android/settings/inputmethod/InputMethodPreference.java new file mode 100644 index 0000000..f490fd2 --- /dev/null +++ b/src/com/android/settings/inputmethod/InputMethodPreference.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.PreferenceActivity; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.Comparator; +import java.util.List; + +public class InputMethodPreference extends CheckBoxPreference + implements Comparator<InputMethodPreference> { + private static final String TAG = InputMethodPreference.class.getSimpleName(); + private static final float DISABLED_ALPHA = 0.4f; + private final SettingsPreferenceFragment mFragment; + private final InputMethodInfo mImi; + private final InputMethodManager mImm; + private final Intent mSettingsIntent; + private final boolean mIsSystemIme; + + private AlertDialog mDialog = null; + private ImageView mInputMethodSettingsButton; + private TextView mTitleText; + private TextView mSummaryText; + private View mInputMethodPref; + + public InputMethodPreference(SettingsPreferenceFragment fragment, Intent settingsIntent, + InputMethodManager imm, InputMethodInfo imi, int imiCount) { + super(fragment.getActivity(), null, R.style.InputMethodPreferenceStyle); + setLayoutResource(R.layout.preference_inputmethod); + setWidgetLayoutResource(R.layout.preference_inputmethod_widget); + mFragment = fragment; + mSettingsIntent = settingsIntent; + mImm = imm; + mImi = imi; + updateSummary(); + mIsSystemIme = InputMethodAndSubtypeUtil.isSystemIme(imi); + final boolean isAuxIme = InputMethodAndSubtypeUtil.isAuxiliaryIme(imi); + if (imiCount <= 1 || (mIsSystemIme && !isAuxIme)) { + setEnabled(false); + } + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mInputMethodPref = view.findViewById(R.id.inputmethod_pref); + mInputMethodPref.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + if (isChecked()) { + setChecked(false); + } else { + if (mIsSystemIme) { + setChecked(true); + } else { + showSecurityWarnDialog(mImi, InputMethodPreference.this); + } + } + } + }); + mInputMethodSettingsButton = (ImageView)view.findViewById(R.id.inputmethod_settings); + mTitleText = (TextView)view.findViewById(android.R.id.title); + mSummaryText = (TextView)view.findViewById(android.R.id.summary); + if (mSettingsIntent != null) { + mInputMethodSettingsButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + try { + mFragment.startActivity(mSettingsIntent); + } catch (ActivityNotFoundException e) { + Log.d(TAG, "IME's Settings Activity Not Found: " + e); + // If the IME's settings activity does not exist, we can just + // do nothing... + } + } + }); + } + final boolean hasSubtypes = mImi.getSubtypeCount() > 1; + final String imiId = mImi.getId(); + if (hasSubtypes) { + final OnLongClickListener listener = new OnLongClickListener() { + @Override + public boolean onLongClick(View arg0) { + final Bundle bundle = new Bundle(); + bundle.putString(Settings.EXTRA_INPUT_METHOD_ID, imiId); + startFragment(mFragment, InputMethodAndSubtypeEnabler.class.getName(), + 0, bundle); + return true; + } + }; + mInputMethodSettingsButton.setOnLongClickListener(listener); + } + if (mSettingsIntent == null) { + mInputMethodSettingsButton.setVisibility(View.GONE); + } else { + enableSettingsButton(); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + enableSettingsButton(); + } + + private void enableSettingsButton() { + if (mInputMethodSettingsButton != null) { + final boolean checked = isChecked(); + mInputMethodSettingsButton.setEnabled(checked); + mInputMethodSettingsButton.setClickable(checked); + mInputMethodSettingsButton.setFocusable(checked); + if (!checked) { + mInputMethodSettingsButton.setAlpha(DISABLED_ALPHA); + } + } + if (mTitleText != null) { + mTitleText.setEnabled(true); + } + if (mSummaryText != null) { + mSummaryText.setEnabled(true); + } + } + + public static boolean startFragment( + Fragment fragment, String fragmentClass, int requestCode, Bundle extras) { + if (fragment.getActivity() instanceof PreferenceActivity) { + PreferenceActivity preferenceActivity = (PreferenceActivity)fragment.getActivity(); + preferenceActivity.startPreferencePanel(fragmentClass, extras, 0, null, fragment, + requestCode); + return true; + } else { + Log.w(TAG, "Parent isn't PreferenceActivity, thus there's no way to launch the " + + "given Fragment (name: " + fragmentClass + ", requestCode: " + requestCode + + ")"); + return false; + } + } + + public String getSummaryString() { + final StringBuilder builder = new StringBuilder(); + final List<InputMethodSubtype> subtypes = mImm.getEnabledInputMethodSubtypeList(mImi, true); + for (InputMethodSubtype subtype : subtypes) { + if (builder.length() > 0) { + builder.append(", "); + } + final CharSequence subtypeLabel = subtype.getDisplayName(mFragment.getActivity(), + mImi.getPackageName(), mImi.getServiceInfo().applicationInfo); + builder.append(subtypeLabel); + } + return builder.toString(); + } + + public void updateSummary() { + final String summary = getSummaryString(); + if (TextUtils.isEmpty(summary)) { + return; + } + setSummary(summary); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + saveImeSettings(); + } + + private void showSecurityWarnDialog(InputMethodInfo imi, final CheckBoxPreference chkPref) { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + mDialog = (new AlertDialog.Builder(mFragment.getActivity())) + .setTitle(android.R.string.dialog_alert_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + chkPref.setChecked(true); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .create(); + mDialog.setMessage(mFragment.getResources().getString(R.string.ime_security_warning, + imi.getServiceInfo().applicationInfo.loadLabel( + mFragment.getActivity().getPackageManager()))); + mDialog.show(); + } + + @Override + public int compare(InputMethodPreference arg0, InputMethodPreference arg1) { + if (arg0.isEnabled() == arg0.isEnabled()) { + return arg0.mImi.getId().compareTo(arg1.mImi.getId()); + } else { + // Prefer system IMEs + return arg0.isEnabled() ? 1 : -1; + } + } + + private void saveImeSettings() { + InputMethodAndSubtypeUtil.saveInputMethodSubtypeList( + mFragment, mFragment.getActivity().getContentResolver(), mImm.getInputMethodList(), + mFragment.getResources().getConfiguration().keyboard + == Configuration.KEYBOARD_QWERTY); + } +} diff --git a/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java b/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java new file mode 100644 index 0000000..98ca3af --- /dev/null +++ b/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import android.content.Intent; + +import android.preference.Preference; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.textservice.SpellCheckerInfo; +import android.widget.ImageView; +import android.widget.TextView; + +public class SingleSpellCheckerPreference extends Preference { + private static final float DISABLED_ALPHA = 0.4f; + + private final SpellCheckerInfo mSpellCheckerInfo; + + private SettingsPreferenceFragment mFragment; + private TextView mTitleText; + private TextView mSummaryText; + private View mPrefAll; + private View mPrefLeftButton; + private ImageView mSetingsButton; + private Intent mSettingsIntent; + private boolean mSelected; + + public SingleSpellCheckerPreference(SettingsPreferenceFragment fragment, Intent settingsIntent, + SpellCheckerInfo sci) { + super(fragment.getActivity(), null, 0); + setLayoutResource(R.layout.preference_spellchecker); + mSpellCheckerInfo = sci; + mSelected = false; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mPrefAll = view.findViewById(R.id.pref_all); + mPrefLeftButton = view.findViewById(R.id.pref_left_button); + mPrefLeftButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + onLeftButtonClicked(arg0); + } + }); + mSetingsButton = (ImageView)view.findViewById(R.id.pref_right_button); + mTitleText = (TextView)view.findViewById(android.R.id.title); + mSummaryText = (TextView)view.findViewById(android.R.id.summary); + mSetingsButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View arg0) { + onSettingsButtonClicked(arg0); + } + }); + updateSelectedState(mSelected); + } + + private void onLeftButtonClicked(View arg0) { + final OnPreferenceClickListener listener = getOnPreferenceClickListener(); + if (listener != null) { + listener.onPreferenceClick(this); + } + } + + public SpellCheckerInfo getSpellCheckerInfo() { + return mSpellCheckerInfo; + } + + public void updateSelectedState(boolean selected) { + if (mPrefAll != null) { + if (selected) { + // TODO: Use a color defined by the design guideline. + mPrefAll.setBackgroundColor(0x88006666); + } else { + mPrefAll.setBackgroundColor(0); + } + enableSettingsButton(selected); + } + } + + public void setSelected(boolean selected) { + mSelected = selected; + } + + protected void onSettingsButtonClicked(View arg0) { + if (mFragment != null && mSettingsIntent != null) { + mFragment.startActivity(mSettingsIntent); + } + } + + private void enableSettingsButton(boolean enabled) { + if (mSetingsButton != null) { + if (mSettingsIntent == null) { + mSetingsButton.setVisibility(View.GONE); + } else { + mSetingsButton.setEnabled(enabled); + mSetingsButton.setClickable(enabled); + mSetingsButton.setFocusable(enabled); + if (!enabled) { + mSetingsButton.setAlpha(DISABLED_ALPHA); + } + } + } + } +} diff --git a/src/com/android/settings/inputmethod/SpellCheckerUtils.java b/src/com/android/settings/inputmethod/SpellCheckerUtils.java new file mode 100644 index 0000000..fe761a6 --- /dev/null +++ b/src/com/android/settings/inputmethod/SpellCheckerUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import android.util.Log; +import android.view.textservice.SpellCheckerInfo; +import android.view.textservice.TextServicesManager; + +public class SpellCheckerUtils { + private static final String TAG = SpellCheckerUtils.class.getSimpleName(); + private static final boolean DBG = false; + public static void setSpellCheckersEnabled(TextServicesManager tsm, boolean enable) { + } + public static boolean getSpellCheckersEnabled(TextServicesManager tsm) { + return true; + } + public static void setCurrentSpellChecker(TextServicesManager tsm, SpellCheckerInfo info) { + } + public static SpellCheckerInfo getCurrentSpellChecker(TextServicesManager tsm) { + final SpellCheckerInfo retval = tsm.getCurrentSpellChecker(); + if (DBG) { + Log.d(TAG, "getCurrentSpellChecker: " + retval); + } + return retval; + } + public static SpellCheckerInfo[] getEnabledSpellCheckers(TextServicesManager tsm) { + final SpellCheckerInfo[] retval = tsm.getEnabledSpellCheckers(); + if (DBG) { + Log.d(TAG, "get spell checkers: " + retval.length); + } + return retval; + } +} diff --git a/src/com/android/settings/inputmethod/SpellCheckersPreference.java b/src/com/android/settings/inputmethod/SpellCheckersPreference.java new file mode 100644 index 0000000..7d2eec8 --- /dev/null +++ b/src/com/android/settings/inputmethod/SpellCheckersPreference.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import android.content.Context; +import android.util.AttributeSet; + +public class SpellCheckersPreference extends CheckBoxAndSettingsPreference { + + public SpellCheckersPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } +} diff --git a/src/com/android/settings/inputmethod/SpellCheckersSettings.java b/src/com/android/settings/inputmethod/SpellCheckersSettings.java new file mode 100644 index 0000000..d6c0b1c --- /dev/null +++ b/src/com/android/settings/inputmethod/SpellCheckersSettings.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import android.content.Context; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.view.textservice.SpellCheckerInfo; +import android.view.textservice.TextServicesManager; + +import java.util.ArrayList; + +public class SpellCheckersSettings extends SettingsPreferenceFragment + implements Preference.OnPreferenceClickListener { + + private SpellCheckerInfo mCurrentSci; + private SpellCheckerInfo[] mEnabledScis; + private TextServicesManager mTsm; + private final ArrayList<SingleSpellCheckerPreference> mSpellCheckers = + new ArrayList<SingleSpellCheckerPreference>(); + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mTsm = (TextServicesManager) getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); + addPreferencesFromResource(R.xml.spellchecker_prefs); + updateScreen(); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + return false; + } + + @Override + public void onResume() { + super.onResume(); + updateScreen(); + } + + @Override + public void onPause() { + super.onPause(); + saveState(); + } + + private void saveState() { + SpellCheckerUtils.setCurrentSpellChecker(mTsm, mCurrentSci); + } + + private void updateScreen() { + getPreferenceScreen().removeAll(); + updateEnabledSpellCheckers(); + } + + private void updateEnabledSpellCheckers() { + mCurrentSci = SpellCheckerUtils.getCurrentSpellChecker(mTsm); + mEnabledScis = SpellCheckerUtils.getEnabledSpellCheckers(mTsm); + if (mCurrentSci == null || mEnabledScis == null) { + return; + } + mSpellCheckers.clear(); + for (int i = 0; i < mEnabledScis.length; ++i) { + final SpellCheckerInfo sci = mEnabledScis[i]; + final SingleSpellCheckerPreference scPref = new SingleSpellCheckerPreference( + this, null, sci); + mSpellCheckers.add(scPref); + scPref.setTitle(sci.getId()); + scPref.setSelected(mCurrentSci != null && mCurrentSci.getId().equals(sci.getId())); + getPreferenceScreen().addPreference(scPref); + } + } + + @Override + public boolean onPreferenceClick(Preference arg0) { + for (SingleSpellCheckerPreference scp : mSpellCheckers) { + if (arg0.equals(scp)) { + scp.setSelected(true); + mTsm.setCurrentSpellChecker(scp.getSpellCheckerInfo()); + } else { + scp.setSelected(false); + } + } + return true; + } +} diff --git a/src/com/android/settings/inputmethod/UserDictionaryList.java b/src/com/android/settings/inputmethod/UserDictionaryList.java new file mode 100644 index 0000000..232a6db --- /dev/null +++ b/src/com/android/settings/inputmethod/UserDictionaryList.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011 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.inputmethod; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.UserDictionarySettings; +import com.android.settings.Utils; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.provider.UserDictionary; + +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; + +public class UserDictionaryList extends SettingsPreferenceFragment { + + private static final String USER_DICTIONARY_SETTINGS_INTENT_ACTION = + "android.settings.USER_DICTIONARY_SETTINGS"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity())); + } + + static Set<String> getUserDictionaryLocalesList(Activity activity) { + @SuppressWarnings("deprecation") + final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI, + new String[] { UserDictionary.Words.LOCALE }, + null, null, null); + final Set<String> localeList = new TreeSet<String>(); + if (null == cursor) { + // The user dictionary service is not present or disabled. Return null. + return null; + } else if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + String locale = cursor.getString(columnIndex); + localeList.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + localeList.add(Locale.getDefault().toString()); + return localeList; + } + + /** + * Creates the entries that allow the user to go into the user dictionary for each locale. + * @param userDictGroup The group to put the settings in. + */ + protected void createUserDictSettings(PreferenceGroup userDictGroup) { + final Activity activity = getActivity(); + userDictGroup.removeAll(); + final Set<String> localeList = UserDictionaryList.getUserDictionaryLocalesList(activity); + + if (localeList.isEmpty()) { + userDictGroup.addPreference(createUserDictionaryPreference(null, activity)); + } else { + for (String locale : localeList) { + userDictGroup.addPreference(createUserDictionaryPreference(locale, activity)); + } + } + } + + /** + * Create a single User Dictionary Preference object, with its parameters set. + * @param locale The locale for which this user dictionary is for. + * @return The corresponding preference. + */ + protected Preference createUserDictionaryPreference(String locale, Activity activity) { + final Preference newPref = new Preference(getActivity()); + final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION); + if (null == locale) { + newPref.setTitle(Locale.getDefault().getDisplayName()); + } else { + if ("".equals(locale)) + newPref.setTitle(getString(R.string.user_dict_settings_all_languages)); + else + newPref.setTitle(Utils.createLocaleFromString(locale).getDisplayName()); + intent.putExtra("locale", locale); + newPref.getExtras().putString("locale", locale); + } + newPref.setIntent(intent); + newPref.setFragment(UserDictionarySettings.class.getName()); + return newPref; + } + + @Override + public void onResume() { + super.onResume(); + createUserDictSettings(getPreferenceScreen()); + } +} diff --git a/src/com/android/settings/net/NetworkPolicyEditor.java b/src/com/android/settings/net/NetworkPolicyEditor.java new file mode 100644 index 0000000..81cf78e --- /dev/null +++ b/src/com/android/settings/net/NetworkPolicyEditor.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2011 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.net; + +import static android.net.NetworkPolicy.LIMIT_DISABLED; +import static android.net.NetworkPolicy.SNOOZE_NEVER; +import static android.net.NetworkPolicy.WARNING_DISABLED; +import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER; +import static android.net.NetworkTemplate.MATCH_MOBILE_4G; +import static android.net.NetworkTemplate.buildTemplateMobile3gLower; +import static android.net.NetworkTemplate.buildTemplateMobile4g; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.net.INetworkPolicyManager; +import android.net.NetworkPolicy; +import android.net.NetworkTemplate; +import android.os.AsyncTask; +import android.os.RemoteException; + +import com.android.internal.util.Objects; +import com.google.android.collect.Lists; + +import java.util.ArrayList; + +/** + * Utility class to modify list of {@link NetworkPolicy}. Specifically knows + * about which policies can coexist. + */ +public class NetworkPolicyEditor { + // TODO: be more robust when missing policies from service + + private INetworkPolicyManager mPolicyService; + private ArrayList<NetworkPolicy> mPolicies = Lists.newArrayList(); + + public NetworkPolicyEditor(INetworkPolicyManager policyService) { + mPolicyService = checkNotNull(policyService); + } + + public void read() { + try { + final NetworkPolicy[] policies = mPolicyService.getNetworkPolicies(); + mPolicies.clear(); + for (NetworkPolicy policy : policies) { + // TODO: find better place to clamp these + if (policy.limitBytes < -1) { + policy.limitBytes = LIMIT_DISABLED; + } + if (policy.warningBytes < -1) { + policy.warningBytes = WARNING_DISABLED; + } + + mPolicies.add(policy); + } + } catch (RemoteException e) { + throw new RuntimeException("problem reading policies", e); + } + } + + public void writeAsync() { + // TODO: consider making more robust by passing through service + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + write(); + return null; + } + }.execute(); + } + + public void write() { + try { + final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]); + mPolicyService.setNetworkPolicies(policies); + } catch (RemoteException e) { + throw new RuntimeException("problem reading policies", e); + } + } + + public NetworkPolicy getPolicy(NetworkTemplate template) { + for (NetworkPolicy policy : mPolicies) { + if (policy.template.equals(template)) { + return policy; + } + } + return null; + } + + public void setPolicyCycleDay(NetworkTemplate template, int cycleDay) { + final NetworkPolicy policy = getPolicy(template); + policy.cycleDay = cycleDay; + policy.lastSnooze = SNOOZE_NEVER; + writeAsync(); + } + + public void setPolicyWarningBytes(NetworkTemplate template, long warningBytes) { + final NetworkPolicy policy = getPolicy(template); + policy.warningBytes = warningBytes; + policy.lastSnooze = SNOOZE_NEVER; + writeAsync(); + } + + public void setPolicyLimitBytes(NetworkTemplate template, long limitBytes) { + final NetworkPolicy policy = getPolicy(template); + policy.limitBytes = limitBytes; + policy.lastSnooze = SNOOZE_NEVER; + writeAsync(); + } + + public boolean isMobilePolicySplit(String subscriberId) { + boolean has3g = false; + boolean has4g = false; + for (NetworkPolicy policy : mPolicies) { + final NetworkTemplate template = policy.template; + if (Objects.equal(subscriberId, template.getSubscriberId())) { + switch (template.getMatchRule()) { + case MATCH_MOBILE_3G_LOWER: + has3g = true; + break; + case MATCH_MOBILE_4G: + has4g = true; + break; + } + } + } + return has3g && has4g; + } + + public void setMobilePolicySplit(String subscriberId, boolean split) { + final boolean beforeSplit = isMobilePolicySplit(subscriberId); + + final NetworkTemplate template3g = buildTemplateMobile3gLower(subscriberId); + final NetworkTemplate template4g = buildTemplateMobile4g(subscriberId); + final NetworkTemplate templateAll = buildTemplateMobileAll(subscriberId); + + if (split == beforeSplit) { + // already in requested state; skip + return; + + } else if (beforeSplit && !split) { + // combine, picking most restrictive policy + final NetworkPolicy policy3g = getPolicy(template3g); + final NetworkPolicy policy4g = getPolicy(template4g); + + final NetworkPolicy restrictive = policy3g.compareTo(policy4g) < 0 ? policy3g + : policy4g; + mPolicies.remove(policy3g); + mPolicies.remove(policy4g); + mPolicies.add( + new NetworkPolicy(templateAll, restrictive.cycleDay, restrictive.warningBytes, + restrictive.limitBytes, SNOOZE_NEVER)); + writeAsync(); + + } else if (!beforeSplit && split) { + // duplicate existing policy into two rules + final NetworkPolicy policyAll = getPolicy(templateAll); + mPolicies.remove(policyAll); + mPolicies.add( + new NetworkPolicy(template3g, policyAll.cycleDay, policyAll.warningBytes, + policyAll.limitBytes, SNOOZE_NEVER)); + mPolicies.add( + new NetworkPolicy(template4g, policyAll.cycleDay, policyAll.warningBytes, + policyAll.limitBytes, SNOOZE_NEVER)); + writeAsync(); + + } + } + +} diff --git a/src/com/android/settings/net/SummaryForAllUidLoader.java b/src/com/android/settings/net/SummaryForAllUidLoader.java new file mode 100644 index 0000000..c01de45 --- /dev/null +++ b/src/com/android/settings/net/SummaryForAllUidLoader.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 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.net; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.net.INetworkStatsService; +import android.net.NetworkStats; +import android.net.NetworkTemplate; +import android.os.Bundle; +import android.os.RemoteException; + +public class SummaryForAllUidLoader extends AsyncTaskLoader<NetworkStats> { + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_START = "start"; + private static final String KEY_END = "end"; + + private final INetworkStatsService mStatsService; + private final Bundle mArgs; + + public static Bundle buildArgs(NetworkTemplate template, long start, long end) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putLong(KEY_START, start); + args.putLong(KEY_END, end); + return args; + } + + public SummaryForAllUidLoader( + Context context, INetworkStatsService statsService, Bundle args) { + super(context); + mStatsService = statsService; + mArgs = args; + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public NetworkStats loadInBackground() { + final NetworkTemplate template = mArgs.getParcelable(KEY_TEMPLATE); + final long start = mArgs.getLong(KEY_START); + final long end = mArgs.getLong(KEY_END); + + try { + return mStatsService.getSummaryForAllUid(template, start, end, false); + } catch (RemoteException e) { + return null; + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } +} diff --git a/src/com/android/settings/nfc/NfcEnabler.java b/src/com/android/settings/nfc/NfcEnabler.java index d1cec13..722787d 100644 --- a/src/com/android/settings/nfc/NfcEnabler.java +++ b/src/com/android/settings/nfc/NfcEnabler.java @@ -16,8 +16,6 @@ package com.android.settings.nfc; -import com.android.settings.R; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -26,9 +24,11 @@ import android.nfc.NfcAdapter; import android.os.Handler; import android.preference.CheckBoxPreference; import android.preference.Preference; -import android.provider.Settings; +import android.preference.PreferenceScreen; import android.util.Log; +import com.android.settings.R; + /** * NfcEnabler is a helper to manage the Nfc on/off checkbox preference. It is * turns on/off Nfc and ensures the summary of the preference reflects the @@ -39,6 +39,7 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { private final Context mContext; private final CheckBoxPreference mCheckbox; + private final PreferenceScreen mZeroClick; private final NfcAdapter mNfcAdapter; private final IntentFilter mIntentFilter; private final Handler mHandler = new Handler(); @@ -47,38 +48,37 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (NfcAdapter.ACTION_ADAPTER_STATE_CHANGE.equals(action)) { - handleNfcStateChanged(intent.getBooleanExtra( - NfcAdapter.EXTRA_NEW_BOOLEAN_STATE, - false)); + if (NfcAdapter.ACTION_ADAPTER_STATE_CHANGED.equals(action)) { + handleNfcStateChanged(intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, + NfcAdapter.STATE_OFF)); } } }; - private boolean mNfcState; - - public NfcEnabler(Context context, CheckBoxPreference checkBoxPreference) { + public NfcEnabler(Context context, CheckBoxPreference checkBoxPreference, + PreferenceScreen zeroclick) { mContext = context; mCheckbox = checkBoxPreference; + mZeroClick = zeroclick; mNfcAdapter = NfcAdapter.getDefaultAdapter(context); if (mNfcAdapter == null) { // NFC is not supported mCheckbox.setEnabled(false); + mZeroClick.setEnabled(false); + mIntentFilter = null; + return; } - - mIntentFilter = new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGE); - + mIntentFilter = new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); } public void resume() { if (mNfcAdapter == null) { return; } + handleNfcStateChanged(mNfcAdapter.getAdapterState()); mContext.registerReceiver(mReceiver, mIntentFilter); mCheckbox.setOnPreferenceChangeListener(this); - mNfcState = mNfcAdapter.isEnabled(); - mCheckbox.setChecked(mNfcState); } public void pause() { @@ -95,41 +95,43 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { final boolean desiredState = (Boolean) value; mCheckbox.setEnabled(false); - // Start async update of the NFC adapter state, as the API is - // unfortunately blocking... - new Thread("toggleNFC") { - public void run() { - Log.d(TAG, "Setting NFC enabled state to: " + desiredState); - boolean success = false; - if (desiredState) { - success = mNfcAdapter.enable(); - } else { - success = mNfcAdapter.disable(); - } - if (success) { - Log.d(TAG, "Successfully changed NFC enabled state to " + desiredState); - mHandler.post(new Runnable() { - public void run() { - handleNfcStateChanged(desiredState); - } - }); - } else { - Log.w(TAG, "Error setting NFC enabled state to " + desiredState); - mHandler.post(new Runnable() { - public void run() { - mCheckbox.setEnabled(true); - mCheckbox.setSummary(R.string.nfc_toggle_error); - } - }); - } - } - }.start(); + if (desiredState) { + mNfcAdapter.enable(); + } else { + mNfcAdapter.disable(); + } + return false; } - private void handleNfcStateChanged(boolean newState) { - mCheckbox.setChecked(newState); - mCheckbox.setEnabled(true); - mCheckbox.setSummary(R.string.nfc_quick_toggle_summary); + private void handleNfcStateChanged(int newState) { + switch (newState) { + case NfcAdapter.STATE_OFF: + mCheckbox.setChecked(false); + mCheckbox.setEnabled(true); + mZeroClick.setEnabled(false); + mZeroClick.setSummary(R.string.zeroclick_off_summary); + break; + case NfcAdapter.STATE_ON: + mCheckbox.setChecked(true); + mCheckbox.setEnabled(true); + mZeroClick.setEnabled(true); + if (mNfcAdapter.isZeroClickEnabled()) { + mZeroClick.setSummary(R.string.zeroclick_on_summary); + } else { + mZeroClick.setSummary(R.string.zeroclick_off_summary); + } + break; + case NfcAdapter.STATE_TURNING_ON: + mCheckbox.setChecked(true); + mCheckbox.setEnabled(false); + mZeroClick.setEnabled(false); + break; + case NfcAdapter.STATE_TURNING_OFF: + mCheckbox.setChecked(false); + mCheckbox.setEnabled(false); + mZeroClick.setEnabled(false); + break; + } } } diff --git a/src/com/android/settings/nfc/ZeroClick.java b/src/com/android/settings/nfc/ZeroClick.java new file mode 100644 index 0000000..1b59b52 --- /dev/null +++ b/src/com/android/settings/nfc/ZeroClick.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2011 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.nfc; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.Fragment; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; +import com.android.settings.R; + +public class ZeroClick extends Fragment + implements CompoundButton.OnCheckedChangeListener { + private View mView; + private NfcAdapter mNfcAdapter; + private Switch mActionBarSwitch; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Activity activity = getActivity(); + + mActionBarSwitch = new Switch(activity); + + if (activity instanceof PreferenceActivity) { + PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) { + final int padding = activity.getResources().getDimensionPixelSize( + R.dimen.action_bar_switch_padding); + mActionBarSwitch.setPadding(0, 0, padding, 0); + activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, + ActionBar.DISPLAY_SHOW_CUSTOM); + activity.getActionBar().setCustomView(mActionBarSwitch, new ActionBar.LayoutParams( + ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT)); + activity.getActionBar().setTitle(R.string.zeroclick_settings_title); + } + } + + mActionBarSwitch.setOnCheckedChangeListener(this); + + mNfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + mActionBarSwitch.setChecked(mNfcAdapter.isZeroClickEnabled()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mView = inflater.inflate(R.layout.zeroclick, container, false); + initView(mView); + return mView; + } + + private void initView(View view) { + mNfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + mActionBarSwitch.setOnCheckedChangeListener(this); + mActionBarSwitch.setChecked(mNfcAdapter.isZeroClickEnabled()); + } + + @Override + public void onPause() { + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) { + boolean success = false; + mActionBarSwitch.setEnabled(false); + if (desiredState) { + success = mNfcAdapter.enableZeroClick(); + } else { + success = mNfcAdapter.disableZeroClick(); + } + if (success) { + mActionBarSwitch.setChecked(desiredState); + } + mActionBarSwitch.setEnabled(true); + } +} diff --git a/src/com/android/settings/vpn/AuthenticationActor.java b/src/com/android/settings/vpn/AuthenticationActor.java deleted file mode 100644 index f8401d6..0000000 --- a/src/com/android/settings/vpn/AuthenticationActor.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.app.Dialog; -import android.content.Context; -import android.net.vpn.VpnManager; -import android.net.vpn.VpnProfile; -import android.net.vpn.VpnState; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; - -import java.io.IOException; - -/** - * A {@link VpnProfileActor} that provides an authentication view for users to - * input username and password before connecting to the VPN server. - */ -public class AuthenticationActor implements VpnProfileActor { - private static final String TAG = AuthenticationActor.class.getName(); - - private Context mContext; - private VpnProfile mProfile; - private VpnManager mVpnManager; - - public AuthenticationActor(Context context, VpnProfile p) { - mContext = context; - mProfile = p; - mVpnManager = new VpnManager(context); - } - - //@Override - public VpnProfile getProfile() { - return mProfile; - } - - //@Override - public boolean isConnectDialogNeeded() { - return true; - } - - //@Override - public String validateInputs(Dialog d) { - TextView usernameView = (TextView) d.findViewById(R.id.username_value); - TextView passwordView = (TextView) d.findViewById(R.id.password_value); - Context c = mContext; - if (TextUtils.isEmpty(usernameView.getText().toString())) { - return c.getString(R.string.vpn_a_username); - } else if (TextUtils.isEmpty(passwordView.getText().toString())) { - return c.getString(R.string.vpn_a_password); - } else { - return null; - } - } - - //@Override - public void connect(Dialog d) { - TextView usernameView = (TextView) d.findViewById(R.id.username_value); - TextView passwordView = (TextView) d.findViewById(R.id.password_value); - CheckBox saveUsername = (CheckBox) d.findViewById(R.id.save_username); - - try { - setSavedUsername(saveUsername.isChecked() - ? usernameView.getText().toString() - : ""); - } catch (IOException e) { - Log.e(TAG, "setSavedUsername()", e); - } - - connect(usernameView.getText().toString(), - passwordView.getText().toString()); - passwordView.setText(""); - } - - //@Override - public View createConnectView() { - View v = View.inflate(mContext, R.layout.vpn_connect_dialog_view, null); - TextView usernameView = (TextView) v.findViewById(R.id.username_value); - TextView passwordView = (TextView) v.findViewById(R.id.password_value); - CheckBox saveUsername = (CheckBox) v.findViewById(R.id.save_username); - - String username = mProfile.getSavedUsername(); - if (!TextUtils.isEmpty(username)) { - usernameView.setText(username); - saveUsername.setChecked(true); - passwordView.requestFocus(); - } - return v; - } - - protected Context getContext() { - return mContext; - } - - private void connect(String username, String password) { - mVpnManager.connect(mProfile, username, password); - } - - //@Override - public void disconnect() { - mVpnManager.disconnect(); - } - - private void setSavedUsername(String name) throws IOException { - if (!name.equals(mProfile.getSavedUsername())) { - mProfile.setSavedUsername(name); - VpnSettings.saveProfileToStorage(mProfile); - } - } -} diff --git a/src/com/android/settings/vpn/L2tpEditor.java b/src/com/android/settings/vpn/L2tpEditor.java deleted file mode 100644 index 05d51d6..0000000 --- a/src/com/android/settings/vpn/L2tpEditor.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.content.Context; -import android.net.vpn.L2tpProfile; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceGroup; - -/** - * The class for editing {@link L2tpProfile}. - */ -class L2tpEditor extends VpnProfileEditor { - private CheckBoxPreference mSecret; - private SecretHandler mSecretHandler; - - public L2tpEditor(L2tpProfile p) { - super(p); - } - - @Override - protected void loadExtraPreferencesTo(PreferenceGroup subpanel) { - Context c = subpanel.getContext(); - subpanel.addPreference(createSecretPreference(c)); - subpanel.addPreference(createSecretStringPreference(c)); - - L2tpProfile profile = (L2tpProfile) getProfile(); - } - - @Override - public String validate() { - String result = super.validate(); - if (!mSecret.isChecked()) return result; - - return ((result != null) ? result : mSecretHandler.validate()); - } - - private Preference createSecretPreference(Context c) { - final L2tpProfile profile = (L2tpProfile) getProfile(); - CheckBoxPreference secret = mSecret = new CheckBoxPreference(c); - boolean enabled = profile.isSecretEnabled(); - setCheckBoxTitle(secret, R.string.vpn_l2tp_secret); - secret.setChecked(enabled); - setSecretSummary(secret, enabled); - secret.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - boolean enabled = (Boolean) newValue; - profile.setSecretEnabled(enabled); - mSecretHandler.getPreference().setEnabled(enabled); - setSecretSummary(mSecret, enabled); - return true; - } - }); - return secret; - } - - private Preference createSecretStringPreference(Context c) { - SecretHandler sHandler = mSecretHandler = new SecretHandler(c, - R.string.vpn_l2tp_secret_string_title, - R.string.vpn_l2tp_secret) { - @Override - protected String getSecretFromProfile() { - return ((L2tpProfile) getProfile()).getSecretString(); - } - - @Override - protected void saveSecretToProfile(String secret) { - ((L2tpProfile) getProfile()).setSecretString(secret); - } - }; - Preference pref = sHandler.getPreference(); - pref.setEnabled(mSecret.isChecked()); - return pref; - } - - private void setSecretSummary(CheckBoxPreference secret, boolean enabled) { - Context c = secret.getContext(); - String formatString = c.getString(enabled - ? R.string.vpn_is_enabled - : R.string.vpn_is_disabled); - secret.setSummary(String.format( - formatString, c.getString(R.string.vpn_l2tp_secret))); - } -} diff --git a/src/com/android/settings/vpn/L2tpIpsecEditor.java b/src/com/android/settings/vpn/L2tpIpsecEditor.java deleted file mode 100644 index 276ee2f..0000000 --- a/src/com/android/settings/vpn/L2tpIpsecEditor.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.content.Context; -import android.net.vpn.L2tpIpsecProfile; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceGroup; -import android.security.Credentials; -import android.security.KeyStore; -import android.text.TextUtils; - -/** - * The class for editing {@link L2tpIpsecProfile}. - */ -class L2tpIpsecEditor extends L2tpEditor { - private static final String TAG = L2tpIpsecEditor.class.getSimpleName(); - - private KeyStore mKeyStore = KeyStore.getInstance(); - - private ListPreference mUserCertificate; - private ListPreference mCaCertificate; - - private L2tpIpsecProfile mProfile; - - public L2tpIpsecEditor(L2tpIpsecProfile p) { - super(p); - mProfile = p; - } - - @Override - protected void loadExtraPreferencesTo(PreferenceGroup subpanel) { - super.loadExtraPreferencesTo(subpanel); - Context c = subpanel.getContext(); - subpanel.addPreference(createUserCertificatePreference(c)); - subpanel.addPreference(createCaCertificatePreference(c)); - } - - @Override - public String validate() { - String result = super.validate(); - if (result == null) { - result = validate(mUserCertificate, R.string.vpn_a_user_certificate); - } - if (result == null) { - result = validate(mCaCertificate, R.string.vpn_a_ca_certificate); - } - return result; - } - - private Preference createUserCertificatePreference(Context c) { - mUserCertificate = createListPreference(c, - R.string.vpn_user_certificate_title, - mProfile.getUserCertificate(), - mKeyStore.saw(Credentials.USER_CERTIFICATE), - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - mProfile.setUserCertificate((String) newValue); - setSummary(pref, R.string.vpn_user_certificate, - (String) newValue); - return true; - } - }); - setSummary(mUserCertificate, R.string.vpn_user_certificate, - mProfile.getUserCertificate()); - return mUserCertificate; - } - - private Preference createCaCertificatePreference(Context c) { - mCaCertificate = createListPreference(c, - R.string.vpn_ca_certificate_title, - mProfile.getCaCertificate(), - mKeyStore.saw(Credentials.CA_CERTIFICATE), - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - mProfile.setCaCertificate((String) newValue); - setSummary(pref, R.string.vpn_ca_certificate, - (String) newValue); - return true; - } - }); - setSummary(mCaCertificate, R.string.vpn_ca_certificate, - mProfile.getCaCertificate()); - return mCaCertificate; - } - - private ListPreference createListPreference(Context c, int titleResId, - String text, String[] keys, - Preference.OnPreferenceChangeListener listener) { - ListPreference pref = new ListPreference(c); - pref.setTitle(titleResId); - pref.setDialogTitle(titleResId); - pref.setPersistent(true); - pref.setEntries(keys); - pref.setEntryValues(keys); - pref.setValue(text); - pref.setOnPreferenceChangeListener(listener); - return pref; - } -} diff --git a/src/com/android/settings/vpn/L2tpIpsecPskEditor.java b/src/com/android/settings/vpn/L2tpIpsecPskEditor.java deleted file mode 100644 index 1277c28..0000000 --- a/src/com/android/settings/vpn/L2tpIpsecPskEditor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.content.Context; -import android.net.vpn.L2tpIpsecPskProfile; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceGroup; - -/** - * The class for editing {@link L2tpIpsecPskProfile}. - */ -class L2tpIpsecPskEditor extends L2tpEditor { - private EditTextPreference mPresharedKey; - private SecretHandler mPskHandler; - - public L2tpIpsecPskEditor(L2tpIpsecPskProfile p) { - super(p); - } - - @Override - protected void loadExtraPreferencesTo(PreferenceGroup subpanel) { - Context c = subpanel.getContext(); - subpanel.addPreference(createPresharedKeyPreference(c)); - super.loadExtraPreferencesTo(subpanel); - } - - @Override - public String validate() { - String result = super.validate(); - - return ((result != null) ? result : mPskHandler.validate()); - } - - private Preference createPresharedKeyPreference(Context c) { - SecretHandler pskHandler = mPskHandler = new SecretHandler(c, - R.string.vpn_ipsec_presharedkey_title, - R.string.vpn_ipsec_presharedkey) { - @Override - protected String getSecretFromProfile() { - return ((L2tpIpsecPskProfile) getProfile()).getPresharedKey(); - } - - @Override - protected void saveSecretToProfile(String secret) { - ((L2tpIpsecPskProfile) getProfile()).setPresharedKey(secret); - } - }; - return pskHandler.getPreference(); - } -} diff --git a/src/com/android/settings/vpn/PptpEditor.java b/src/com/android/settings/vpn/PptpEditor.java deleted file mode 100644 index cfb3fa3..0000000 --- a/src/com/android/settings/vpn/PptpEditor.java +++ /dev/null @@ -1,73 +0,0 @@ -/* * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.content.Context; -import android.net.vpn.PptpProfile; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.PreferenceGroup; - -/** - * The class for editing {@link PptpProfile}. - */ -class PptpEditor extends VpnProfileEditor { - private CheckBoxPreference mEncryption; - - public PptpEditor(PptpProfile p) { - super(p); - } - - @Override - protected void loadExtraPreferencesTo(PreferenceGroup subpanel) { - Context c = subpanel.getContext(); - subpanel.addPreference(createEncryptionPreference(c)); - - PptpProfile profile = (PptpProfile) getProfile(); - } - - private Preference createEncryptionPreference(Context c) { - final PptpProfile profile = (PptpProfile) getProfile(); - CheckBoxPreference encryption = mEncryption = new CheckBoxPreference(c); - boolean enabled = profile.isEncryptionEnabled(); - setCheckBoxTitle(encryption, R.string.vpn_pptp_encryption_title); - encryption.setChecked(enabled); - setEncryptionSummary(encryption, enabled); - encryption.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - boolean enabled = (Boolean) newValue; - profile.setEncryptionEnabled(enabled); - setEncryptionSummary(mEncryption, enabled); - return true; - } - }); - return encryption; - } - - private void setEncryptionSummary(CheckBoxPreference encryption, - boolean enabled) { - Context c = encryption.getContext(); - String formatString = c.getString(enabled - ? R.string.vpn_is_enabled - : R.string.vpn_is_disabled); - encryption.setSummary(String.format( - formatString, c.getString(R.string.vpn_pptp_encryption))); - } -} diff --git a/src/com/android/settings/vpn/Util.java b/src/com/android/settings/vpn/Util.java deleted file mode 100644 index a37049d..0000000 --- a/src/com/android/settings/vpn/Util.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.widget.Toast; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -class Util { - - static void showShortToastMessage(Context context, String message) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); - } - - static void showShortToastMessage(Context context, int messageId) { - Toast.makeText(context, messageId, Toast.LENGTH_SHORT).show(); - } - - static void showLongToastMessage(Context context, String message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } - - static void showLongToastMessage(Context context, int messageId) { - Toast.makeText(context, messageId, Toast.LENGTH_LONG).show(); - } - - static void showErrorMessage(Context c, String message) { - createErrorDialog(c, message, null).show(); - } - - static void showErrorMessage(Context c, String message, - DialogInterface.OnClickListener listener) { - createErrorDialog(c, message, listener).show(); - } - - static void deleteFile(String path) { - deleteFile(new File(path)); - } - - static void deleteFile(String path, boolean toDeleteSelf) { - deleteFile(new File(path), toDeleteSelf); - } - - static void deleteFile(File f) { - deleteFile(f, true); - } - - static void deleteFile(File f, boolean toDeleteSelf) { - if (f.isDirectory()) { - for (File child : f.listFiles()) deleteFile(child, true); - } - if (toDeleteSelf) f.delete(); - } - - static boolean isFileOrEmptyDirectory(String path) { - File f = new File(path); - if (!f.isDirectory()) return true; - - String[] list = f.list(); - return ((list == null) || (list.length == 0)); - } - - static boolean copyFiles(String sourcePath , String targetPath) - throws IOException { - return copyFiles(new File(sourcePath), new File(targetPath)); - } - - // returns false if sourceLocation is the same as the targetLocation - static boolean copyFiles(File sourceLocation , File targetLocation) - throws IOException { - if (sourceLocation.equals(targetLocation)) return false; - - if (sourceLocation.isDirectory()) { - if (!targetLocation.exists()) { - targetLocation.mkdir(); - } - String[] children = sourceLocation.list(); - for (int i=0; i<children.length; i++) { - copyFiles(new File(sourceLocation, children[i]), - new File(targetLocation, children[i])); - } - } else if (sourceLocation.exists()) { - InputStream in = new FileInputStream(sourceLocation); - OutputStream out = new FileOutputStream(targetLocation); - - // Copy the bits from instream to outstream - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - in.close(); - out.close(); - } - return true; - } - - private static AlertDialog createErrorDialog(Context c, String message, - DialogInterface.OnClickListener okListener) { - AlertDialog.Builder b = new AlertDialog.Builder(c) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(message); - if (okListener != null) { - b.setPositiveButton(R.string.vpn_back_button, okListener); - } else { - b.setPositiveButton(android.R.string.ok, null); - } - return b.create(); - } - - private Util() { - } -} diff --git a/src/com/android/settings/vpn/VpnEditor.java b/src/com/android/settings/vpn/VpnEditor.java deleted file mode 100644 index d362793..0000000 --- a/src/com/android/settings/vpn/VpnEditor.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.vpn.L2tpIpsecProfile; -import android.net.vpn.L2tpIpsecPskProfile; -import android.net.vpn.L2tpProfile; -import android.net.vpn.PptpProfile; -import android.net.vpn.VpnManager; -import android.net.vpn.VpnProfile; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; -import android.preference.PreferenceActivity; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.Toast; - -/** - * The activity class for editing a new or existing VPN profile. - */ -public class VpnEditor extends SettingsPreferenceFragment { - private static final int MENU_SAVE = Menu.FIRST; - private static final int MENU_CANCEL = Menu.FIRST + 1; - private static final int CONFIRM_DIALOG_ID = 0; - private static final String KEY_PROFILE = "profile"; - private static final String KEY_ORIGINAL_PROFILE_NAME = "orig_profile_name"; - - private VpnManager mVpnManager; - private VpnProfileEditor mProfileEditor; - private boolean mAddingProfile; - private byte[] mOriginalProfileData; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Loads the XML preferences file - addPreferencesFromResource(R.xml.vpn_edit); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mVpnManager = new VpnManager(getActivity()); - - VpnProfile p; - if (savedInstanceState != null) { - p = (VpnProfile)savedInstanceState.getParcelable(KEY_PROFILE); - } else { - p = (VpnProfile)getArguments().getParcelable(VpnSettings.KEY_VPN_PROFILE); - if (p == null) { - p = getActivity().getIntent().getParcelableExtra(VpnSettings.KEY_VPN_PROFILE); - } - } - - Parcel parcel = Parcel.obtain(); - p.writeToParcel(parcel, 0); - mOriginalProfileData = parcel.marshall(); - parcel.setDataPosition(0); - VpnProfile profile = (VpnProfile) VpnProfile.CREATOR.createFromParcel(parcel); - - mProfileEditor = getEditor(profile); - mAddingProfile = TextUtils.isEmpty(profile.getName()); - - initViewFor(profile); - - registerForContextMenu(getListView()); - setHasOptionsMenu(true); - } - - @Override - public synchronized void onSaveInstanceState(Bundle outState) { - if (mProfileEditor == null) return; - - outState.putParcelable(KEY_PROFILE, getProfile()); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.add(0, MENU_SAVE, 0, R.string.vpn_menu_done) - .setIcon(android.R.drawable.ic_menu_save); - menu.add(0, MENU_CANCEL, 0, - mAddingProfile ? R.string.vpn_menu_cancel - : R.string.vpn_menu_revert) - .setIcon(android.R.drawable.ic_menu_close_clear_cancel); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MENU_SAVE: - Intent resultIntent = validateAndGetResult(); - if (resultIntent != null) { - PreferenceActivity activity = - (PreferenceActivity) getActivity(); - if (!mVpnManager.isIdle()) { - Toast.makeText(activity, R.string.service_busy, - Toast.LENGTH_SHORT).show(); - } else { - activity.finishPreferencePanel(this, - Activity.RESULT_OK, resultIntent); - } - } - return true; - - case MENU_CANCEL: - if (profileChanged()) { - showDialog(CONFIRM_DIALOG_ID); - } else { - finishFragment(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - /* - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - if (validateAndSetResult()) finish(); - return true; - } - return super.onKeyDown(keyCode, event); - }*/ - - private void initViewFor(VpnProfile profile) { - mProfileEditor.loadPreferencesTo(getPreferenceScreen()); - } - - /* package */static String getTitle(Context context, VpnProfile profile, boolean adding) { - String formatString = adding - ? context.getString(R.string.vpn_edit_title_add) - : context.getString(R.string.vpn_edit_title_edit); - return String.format(formatString, - profile.getType().getDisplayName()); - } - - /** - * Checks the validity of the inputs and set the profile as result if valid. - * @return true if the result is successfully set - */ - private Intent validateAndGetResult() { - String errorMsg = mProfileEditor.validate(); - - if (errorMsg != null) { - Util.showErrorMessage(getActivity(), errorMsg); - return null; - } - - if (profileChanged()) { - return getResult(getProfile()); - } - return null; - } - - private Intent getResult(VpnProfile p) { - Intent intent = new Intent(getActivity(), VpnSettings.class); - intent.putExtra(VpnSettings.KEY_VPN_PROFILE, (Parcelable) p); - return intent; - } - - private VpnProfileEditor getEditor(VpnProfile p) { - switch (p.getType()) { - case L2TP_IPSEC: - return new L2tpIpsecEditor((L2tpIpsecProfile) p); - - case L2TP_IPSEC_PSK: - return new L2tpIpsecPskEditor((L2tpIpsecPskProfile) p); - - case L2TP: - return new L2tpEditor((L2tpProfile) p); - - case PPTP: - return new PptpEditor((PptpProfile) p); - - default: - return new VpnProfileEditor(p); - } - } - - - @Override - public Dialog onCreateDialog(int id) { - if (id == CONFIRM_DIALOG_ID) { - return new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(mAddingProfile - ? R.string.vpn_confirm_add_profile_cancellation - : R.string.vpn_confirm_edit_profile_cancellation) - .setPositiveButton(R.string.vpn_yes_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - finishFragment(); - } - }) - .setNegativeButton(R.string.vpn_mistake_button, null) - .create(); - } - - return super.onCreateDialog(id); - } - - /* - @Override - public void onPrepareDialog(int id, Dialog dialog) { - super.onPrepareDialog(id, dialog); - - if (id == CONFIRM_DIALOG_ID) { - ((AlertDialog)dialog).setMessage(mAddingProfile - ? getString(R.string.vpn_confirm_add_profile_cancellation) - : getString(R.string.vpn_confirm_edit_profile_cancellation)); - } - }*/ - - private VpnProfile getProfile() { - return mProfileEditor.getProfile(); - } - - private boolean profileChanged() { - Parcel newParcel = Parcel.obtain(); - getProfile().writeToParcel(newParcel, 0); - byte[] newData = newParcel.marshall(); - if (mOriginalProfileData.length == newData.length) { - for (int i = 0, n = mOriginalProfileData.length; i < n; i++) { - if (mOriginalProfileData[i] != newData[i]) return true; - } - return false; - } - return true; - } -} diff --git a/src/com/android/settings/vpn/VpnProfileActor.java b/src/com/android/settings/vpn/VpnProfileActor.java deleted file mode 100644 index 4c7b014..0000000 --- a/src/com/android/settings/vpn/VpnProfileActor.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import android.app.Dialog; -import android.net.vpn.VpnProfile; -import android.view.View; - -/** - * The interface to act on a {@link VpnProfile}. - */ -public interface VpnProfileActor { - VpnProfile getProfile(); - - /** - * Returns true if a connect dialog is needed before establishing a - * connection. - */ - boolean isConnectDialogNeeded(); - - /** - * Creates the view in the connect dialog. - */ - View createConnectView(); - - /** - * Validates the inputs in the dialog. - * @param dialog the connect dialog - * @return an error message if the inputs are not valid - */ - String validateInputs(Dialog dialog); - - /** - * Establishes a VPN connection. - * @param dialog the connect dialog - */ - void connect(Dialog dialog); - - /** - * Tears down the connection. - */ - void disconnect(); -} diff --git a/src/com/android/settings/vpn/VpnProfileEditor.java b/src/com/android/settings/vpn/VpnProfileEditor.java deleted file mode 100644 index 100b78e..0000000 --- a/src/com/android/settings/vpn/VpnProfileEditor.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; - -import android.content.Context; -import android.net.vpn.VpnProfile; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceGroup; -import android.text.InputType; -import android.text.TextUtils; -import android.text.method.PasswordTransformationMethod; - -/** - * The common class for editing {@link VpnProfile}. - */ -class VpnProfileEditor { - private static final String KEY_VPN_NAME = "vpn_name"; - - private EditTextPreference mName; - private EditTextPreference mServerName; - private EditTextPreference mDomainSuffices; - private VpnProfile mProfile; - - public VpnProfileEditor(VpnProfile p) { - mProfile = p; - } - - //@Override - public VpnProfile getProfile() { - return mProfile; - } - - /** - * Adds the preferences to the panel. Subclasses should override - * {@link #loadExtraPreferencesTo(PreferenceGroup)} instead of this method. - */ - public void loadPreferencesTo(PreferenceGroup subpanel) { - Context c = subpanel.getContext(); - - mName = (EditTextPreference) subpanel.findPreference(KEY_VPN_NAME); - mName.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - setName((String) newValue); - return true; - } - }); - setName(getProfile().getName()); - mName.getEditText().setInputType(InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); - - subpanel.addPreference(createServerNamePreference(c)); - loadExtraPreferencesTo(subpanel); - subpanel.addPreference(createDomainSufficesPreference(c)); - } - - /** - * Adds the extra preferences to the panel. Subclasses should add - * additional preferences in this method. - */ - protected void loadExtraPreferencesTo(PreferenceGroup subpanel) { - } - - /** - * Validates the inputs in the preferences. - * - * @return an error message that is ready to be displayed in a dialog; or - * null if all the inputs are valid - */ - public String validate() { - String result = validate(mName, R.string.vpn_a_name); - return ((result != null) - ? result - : validate(mServerName, R.string.vpn_a_vpn_server)); - } - - /** - * Creates a preference for users to input domain suffices. - */ - protected EditTextPreference createDomainSufficesPreference(Context c) { - EditTextPreference pref = mDomainSuffices = createEditTextPreference(c, - R.string.vpn_dns_search_list_title, - R.string.vpn_dns_search_list, - mProfile.getDomainSuffices(), - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - String v = ((String) newValue).trim(); - mProfile.setDomainSuffices(v); - setSummary(pref, R.string.vpn_dns_search_list, v, false); - return true; - } - }); - pref.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); - return pref; - } - - private Preference createServerNamePreference(Context c) { - EditTextPreference pref = mServerName = createEditTextPreference(c, - R.string.vpn_vpn_server_title, - R.string.vpn_vpn_server, - mProfile.getServerName(), - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - String v = ((String) newValue).trim(); - mProfile.setServerName(v); - setSummary(pref, R.string.vpn_vpn_server, v); - return true; - } - }); - pref.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); - return pref; - } - - protected EditTextPreference createEditTextPreference(Context c, int titleId, - int prefNameId, String value, - Preference.OnPreferenceChangeListener listener) { - EditTextPreference pref = new EditTextPreference(c); - pref.setTitle(titleId); - pref.setDialogTitle(titleId); - setSummary(pref, prefNameId, value); - pref.setText(value); - pref.setPersistent(true); - pref.setOnPreferenceChangeListener(listener); - return pref; - } - - protected String validate(Preference pref, int fieldNameId) { - Context c = pref.getContext(); - String value = (pref instanceof EditTextPreference) - ? ((EditTextPreference) pref).getText() - : ((ListPreference) pref).getValue(); - String formatString = (pref instanceof EditTextPreference) - ? c.getString(R.string.vpn_error_miss_entering) - : c.getString(R.string.vpn_error_miss_selecting); - return (TextUtils.isEmpty(value) - ? String.format(formatString, c.getString(fieldNameId)) - : null); - } - - protected void setSummary(Preference pref, int fieldNameId, String v) { - setSummary(pref, fieldNameId, v, true); - } - - protected void setSummary(Preference pref, int fieldNameId, String v, - boolean required) { - Context c = pref.getContext(); - String formatString = required - ? c.getString(R.string.vpn_field_not_set) - : c.getString(R.string.vpn_field_not_set_optional); - pref.setSummary(TextUtils.isEmpty(v) - ? String.format(formatString, c.getString(fieldNameId)) - : v); - } - - protected void setCheckBoxTitle(CheckBoxPreference pref, int fieldNameId) { - Context c = pref.getContext(); - String formatString = c.getString(R.string.vpn_enable_field); - pref.setTitle(String.format(formatString, c.getString(fieldNameId))); - } - - private void setName(String newName) { - newName = (newName == null) ? "" : newName.trim(); - mName.setText(newName); - getProfile().setName(newName); - setSummary(mName, R.string.vpn_name, newName); - } - - // Secret is tricky to handle because empty field may mean "not set" or - // "unchanged". This class hides that logic from callers. - protected static abstract class SecretHandler { - private EditTextPreference mPref; - private int mFieldNameId; - private boolean mHadSecret; - - protected SecretHandler(Context c, int titleId, int fieldNameId) { - String value = getSecretFromProfile(); - mHadSecret = !TextUtils.isEmpty(value); - mFieldNameId = fieldNameId; - - EditTextPreference pref = mPref = new EditTextPreference(c); - pref.setTitle(titleId); - pref.setDialogTitle(titleId); - pref.getEditText().setInputType( - InputType.TYPE_TEXT_VARIATION_PASSWORD); - pref.getEditText().setTransformationMethod( - new PasswordTransformationMethod()); - pref.setText(""); - pref.getEditText().setHint(mHadSecret - ? R.string.vpn_secret_unchanged - : R.string.vpn_secret_not_set); - setSecretSummary(value); - pref.setPersistent(true); - saveSecretToProfile(""); - pref.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange( - Preference pref, Object newValue) { - saveSecretToProfile((String) newValue); - setSecretSummary((String) newValue); - return true; - } - }); - } - - protected EditTextPreference getPreference() { - return mPref; - } - - protected String validate() { - Context c = mPref.getContext(); - String value = mPref.getText(); - return ((TextUtils.isEmpty(value) && !mHadSecret) - ? String.format( - c.getString(R.string.vpn_error_miss_entering), - c.getString(mFieldNameId)) - : null); - } - - private void setSecretSummary(String value) { - EditTextPreference pref = mPref; - Context c = pref.getContext(); - String formatString = (TextUtils.isEmpty(value) && !mHadSecret) - ? c.getString(R.string.vpn_field_not_set) - : c.getString(R.string.vpn_field_is_set); - pref.setSummary( - String.format(formatString, c.getString(mFieldNameId))); - } - - protected abstract String getSecretFromProfile(); - protected abstract void saveSecretToProfile(String secret); - } -} diff --git a/src/com/android/settings/vpn/VpnSettings.java b/src/com/android/settings/vpn/VpnSettings.java deleted file mode 100644 index 5d75b6a..0000000 --- a/src/com/android/settings/vpn/VpnSettings.java +++ /dev/null @@ -1,1108 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.vpn.L2tpIpsecProfile; -import android.net.vpn.L2tpIpsecPskProfile; -import android.net.vpn.L2tpProfile; -import android.net.vpn.VpnManager; -import android.net.vpn.VpnProfile; -import android.net.vpn.VpnState; -import android.net.vpn.VpnType; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceCategory; -import android.preference.PreferenceScreen; -import android.preference.Preference.OnPreferenceClickListener; -import android.security.Credentials; -import android.security.KeyStore; -import android.text.TextUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.MenuItem; -import android.view.View; -import android.view.ContextMenu.ContextMenuInfo; -import android.widget.AdapterView.AdapterContextMenuInfo; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * The preference activity for configuring VPN settings. - */ -public class VpnSettings extends SettingsPreferenceFragment - implements DialogInterface.OnClickListener { - - private static final boolean DEBUG = false; - - // Key to the field exchanged for profile editing. - static final String KEY_VPN_PROFILE = "vpn_profile"; - - // Key to the field exchanged for VPN type selection. - static final String KEY_VPN_TYPE = "vpn_type"; - - private static final String TAG = VpnSettings.class.getSimpleName(); - - private static final String PREF_ADD_VPN = "add_new_vpn"; - private static final String PREF_VPN_LIST = "vpn_list"; - - private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/"; - private static final String PROFILE_OBJ_FILE = ".pobj"; - - private static final String KEY_ACTIVE_PROFILE = "ActiveProfile"; - private static final String KEY_PROFILE_CONNECTING = "ProfileConnecting"; - private static final String KEY_CONNECT_DIALOG_SHOWING = "ConnectDialogShowing"; - - private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1; - static final int REQUEST_SELECT_VPN_TYPE = 2; - - private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0; - private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1; - private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2; - private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3; - - private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE; - private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE; - - private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1; - private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1; - - private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR; - - private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i'; - private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l'; - - private static List<VpnProfile> sVpnProfileList = new ArrayList<VpnProfile>(); - - private PreferenceScreen mAddVpn; - private PreferenceCategory mVpnListContainer; - - // profile name --> VpnPreference - private Map<String, VpnPreference> mVpnPreferenceMap; - - // profile engaged in a connection - private VpnProfile mActiveProfile; - - // actor engaged in connecting - private VpnProfileActor mConnectingActor; - - // states saved for unlocking keystore - private Runnable mUnlockAction; - - private KeyStore mKeyStore = KeyStore.getInstance(); - - private VpnManager mVpnManager; - - private ConnectivityReceiver mConnectivityReceiver = - new ConnectivityReceiver(); - - private int mConnectingErrorCode = NO_ERROR; - - private Dialog mShowingDialog; - - private boolean mConnectDialogShowing = false; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.vpn_settings); - - mVpnManager = new VpnManager(getActivity()); - // restore VpnProfile list and construct VpnPreference map - mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST); - - // set up the "add vpn" preference - mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN); - mAddVpn.setOnPreferenceClickListener( - new OnPreferenceClickListener() { - public boolean onPreferenceClick(Preference preference) { - startVpnTypeSelection(); - return true; - } - }); - - retrieveVpnListFromStorage(); - restoreInstanceState(savedInstanceState); - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - if (mActiveProfile != null) { - savedInstanceState.putString(KEY_ACTIVE_PROFILE, - mActiveProfile.getId()); - savedInstanceState.putBoolean(KEY_PROFILE_CONNECTING, - (mConnectingActor != null)); - savedInstanceState.putBoolean(KEY_CONNECT_DIALOG_SHOWING, - mConnectDialogShowing); - } - super.onSaveInstanceState(savedInstanceState); - } - - private void restoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState == null) return; - String profileId = savedInstanceState.getString(KEY_ACTIVE_PROFILE); - if (profileId != null) { - mActiveProfile = getProfile(getProfileIndexFromId(profileId)); - if (savedInstanceState.getBoolean(KEY_PROFILE_CONNECTING)) { - mConnectingActor = getActor(mActiveProfile); - } - mConnectDialogShowing = savedInstanceState.getBoolean(KEY_CONNECT_DIALOG_SHOWING); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // for long-press gesture on a profile preference - registerForContextMenu(getListView()); - } - - @Override - public void onPause() { - // ignore vpn connectivity event - mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver); - if ((mShowingDialog != null) && mShowingDialog.isShowing()) { - mShowingDialog.dismiss(); - mShowingDialog = null; - } - super.onPause(); - } - - @Override - public void onResume() { - super.onResume(); - updatePreferenceMap(); - - if (DEBUG) Log.d(TAG, "onResume"); - - // listen to vpn connectivity event - mVpnManager.registerConnectivityReceiver(mConnectivityReceiver); - - if ((mUnlockAction != null) && isKeyStoreUnlocked()) { - Runnable action = mUnlockAction; - mUnlockAction = null; - getActivity().runOnUiThread(action); - } - - if (!mConnectDialogShowing) { - // If mActiveProfile is not null but it's in IDLE state, then a - // retry dialog must be showing now as the previous connection - // attempt failed. In this case, don't call checkVpnConnectionStatus() - // as it will clean up mActiveProfile due to the IDLE state. - if ((mActiveProfile == null) - || (mActiveProfile.getState() != VpnState.IDLE)) { - checkVpnConnectionStatus(); - } - } else { - // Dismiss the connect dialog in case there is another instance - // trying to operate a vpn connection. - if (!mVpnManager.isIdle() || (mActiveProfile == null)) { - removeDialog(DIALOG_CONNECT); - checkVpnConnectionStatus(); - } - } - } - - @Override - public void onDestroyView() { - unregisterForContextMenu(getListView()); - // This should be called after the procedure above as ListView inside this Fragment - // will be deleted here. - super.onDestroyView(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Remove any onClick listeners - if (mVpnListContainer != null) { - for (int i = 0; i < mVpnListContainer.getPreferenceCount(); i++) { - mVpnListContainer.getPreference(i).setOnPreferenceClickListener(null); - } - } - } - - @Override - public Dialog onCreateDialog (int id) { - setOnCancelListener(new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - if (mActiveProfile != null) { - changeState(mActiveProfile, VpnState.IDLE); - } - // Make sure onIdle() is called as the above changeState() - // may not be effective if the state is already IDLE. - // XXX: VpnService should broadcast non-IDLE state, say UNUSABLE, - // when an error occurs. - onIdle(); - } - }); - - switch (id) { - case DIALOG_CONNECT: - mConnectDialogShowing = true; - setOnDismissListener(new DialogInterface.OnDismissListener() { - public void onDismiss(DialogInterface dialog) { - mConnectDialogShowing = false; - } - }); - return createConnectDialog(); - - case DIALOG_SECRET_NOT_SET: - return createSecretNotSetDialog(); - - case VpnManager.VPN_ERROR_CHALLENGE: - case VpnManager.VPN_ERROR_UNKNOWN_SERVER: - case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED: - return createEditDialog(id); - - default: - Log.d(TAG, "create reconnect dialog for event " + id); - return createReconnectDialog(id); - } - } - - private Dialog createConnectDialog() { - final Activity activity = getActivity(); - return new AlertDialog.Builder(activity) - .setView(mConnectingActor.createConnectView()) - .setTitle(String.format(activity.getString(R.string.vpn_connect_to), - mConnectingActor.getProfile().getName())) - .setPositiveButton(activity.getString(R.string.vpn_connect_button), - this) - .setNegativeButton(activity.getString(android.R.string.cancel), - this) - .create(); - } - - private Dialog createReconnectDialog(int id) { - int msgId; - switch (id) { - case VpnManager.VPN_ERROR_AUTH: - msgId = R.string.vpn_auth_error_dialog_msg; - break; - - case VpnManager.VPN_ERROR_REMOTE_HUNG_UP: - msgId = R.string.vpn_remote_hung_up_error_dialog_msg; - break; - - case VpnManager.VPN_ERROR_CONNECTION_LOST: - msgId = R.string.vpn_reconnect_from_lost; - break; - - case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP: - msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg; - break; - - default: - msgId = R.string.vpn_confirm_reconnect; - } - return createCommonDialogBuilder().setMessage(msgId).create(); - } - - private Dialog createEditDialog(int id) { - int msgId; - switch (id) { - case VpnManager.VPN_ERROR_CHALLENGE: - msgId = R.string.vpn_challenge_error_dialog_msg; - break; - - case VpnManager.VPN_ERROR_UNKNOWN_SERVER: - msgId = R.string.vpn_unknown_server_dialog_msg; - break; - - case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED: - msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg; - break; - - default: - return null; - } - return createCommonEditDialogBuilder().setMessage(msgId).create(); - } - - private Dialog createSecretNotSetDialog() { - return createCommonDialogBuilder() - .setMessage(R.string.vpn_secret_not_set_dialog_msg) - .setPositiveButton(R.string.vpn_yes_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - startVpnEditor(mActiveProfile, false); - } - }) - .create(); - } - - private AlertDialog.Builder createCommonEditDialogBuilder() { - return createCommonDialogBuilder() - .setPositiveButton(R.string.vpn_yes_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - VpnProfile p = mActiveProfile; - onIdle(); - startVpnEditor(p, false); - } - }); - } - - private AlertDialog.Builder createCommonDialogBuilder() { - return new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setPositiveButton(R.string.vpn_yes_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - connectOrDisconnect(mActiveProfile); - } - }) - .setNegativeButton(R.string.vpn_no_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - onIdle(); - } - }); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - - VpnProfile p = getProfile(getProfilePositionFrom( - (AdapterContextMenuInfo) menuInfo)); - if (p != null) { - VpnState state = p.getState(); - menu.setHeaderTitle(p.getName()); - - boolean isIdle = (state == VpnState.IDLE); - boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING) - || (state == VpnState.CANCELLED)); - menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect) - .setEnabled(isIdle && (mActiveProfile == null)); - menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0, - R.string.vpn_menu_disconnect) - .setEnabled(state == VpnState.CONNECTED); - menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit) - .setEnabled(isNotConnect); - menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete) - .setEnabled(isNotConnect); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - int position = getProfilePositionFrom( - (AdapterContextMenuInfo) item.getMenuInfo()); - VpnProfile p = getProfile(position); - - switch(item.getItemId()) { - case CONTEXT_MENU_CONNECT_ID: - case CONTEXT_MENU_DISCONNECT_ID: - connectOrDisconnect(p); - return true; - - case CONTEXT_MENU_EDIT_ID: - startVpnEditor(p, false); - return true; - - case CONTEXT_MENU_DELETE_ID: - deleteProfile(position); - return true; - } - - return super.onContextItemSelected(item); - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, - final Intent data) { - - if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data); - if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) { - Log.d(TAG, "no result returned by editor"); - return; - } - - if (requestCode == REQUEST_SELECT_VPN_TYPE) { - final String typeName = data.getStringExtra(KEY_VPN_TYPE); - startVpnEditor(createVpnProfile(typeName), true); - } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) { - VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE); - if (p == null) { - Log.e(TAG, "null object returned by editor"); - return; - } - - final Activity activity = getActivity(); - int index = getProfileIndexFromId(p.getId()); - if (checkDuplicateName(p, index)) { - final VpnProfile profile = p; - Util.showErrorMessage(activity, String.format( - activity.getString(R.string.vpn_error_duplicate_name), - p.getName()), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - startVpnEditor(profile, false); - } - }); - return; - } - - if (needKeyStoreToSave(p)) { - Runnable action = new Runnable() { - public void run() { - onActivityResult(requestCode, resultCode, data); - } - }; - if (!unlockKeyStore(p, action)) return; - } - - try { - if (index < 0) { - addProfile(p); - Util.showShortToastMessage(activity, String.format( - activity.getString(R.string.vpn_profile_added), p.getName())); - } else { - replaceProfile(index, p); - Util.showShortToastMessage(activity, String.format( - activity.getString(R.string.vpn_profile_replaced), - p.getName())); - } - } catch (IOException e) { - final VpnProfile profile = p; - Util.showErrorMessage(activity, e + ": " + e.getMessage(), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int w) { - startVpnEditor(profile, false); - } - }); - } - - // Remove cached VpnEditor as it is needless anymore. - } else { - throw new RuntimeException("unknown request code: " + requestCode); - } - } - - // Called when the buttons on the connect dialog are clicked. - @Override - public synchronized void onClick(DialogInterface dialog, int which) { - if (which == CONNECT_BUTTON) { - Dialog d = (Dialog) dialog; - String error = mConnectingActor.validateInputs(d); - if (error == null) { - mConnectingActor.connect(d); - } else { - // show error dialog - final Activity activity = getActivity(); - mShowingDialog = new AlertDialog.Builder(activity) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(String.format(activity.getString( - R.string.vpn_error_miss_entering), error)) - .setPositiveButton(R.string.vpn_back_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, - int which) { - showDialog(DIALOG_CONNECT); - } - }) - .create(); - // The profile state is "connecting". If we allow the dialog to - // be cancelable, then we need to clear the state in the - // onCancel handler. - mShowingDialog.setCancelable(false); - mShowingDialog.show(); - } - } else { - changeState(mActiveProfile, VpnState.IDLE); - } - } - - private int getProfileIndexFromId(String id) { - int index = 0; - for (VpnProfile p : sVpnProfileList) { - if (p.getId().equals(id)) { - return index; - } else { - index++; - } - } - return -1; - } - - // Replaces the profile at index in sVpnProfileList with p. - // Returns true if p's name is a duplicate. - private boolean checkDuplicateName(VpnProfile p, int index) { - List<VpnProfile> list = sVpnProfileList; - VpnPreference pref = mVpnPreferenceMap.get(p.getName()); - if ((pref != null) && (index >= 0) && (index < list.size())) { - // not a duplicate if p is to replace the profile at index - if (pref.mProfile == list.get(index)) pref = null; - } - return (pref != null); - } - - private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) { - // excludes mVpnListContainer and the preferences above it - return menuInfo.position - mVpnListContainer.getOrder() - 1; - } - - // position: position in sVpnProfileList - private VpnProfile getProfile(int position) { - return ((position >= 0) ? sVpnProfileList.get(position) : null); - } - - // position: position in sVpnProfileList - private void deleteProfile(final int position) { - if ((position < 0) || (position >= sVpnProfileList.size())) return; - final VpnProfile target = sVpnProfileList.get(position); - DialogInterface.OnClickListener onClickListener = - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Double check if the target is still the one we want - // to remove. - VpnProfile p = sVpnProfileList.get(position); - if (p != target) return; - if (which == OK_BUTTON) { - sVpnProfileList.remove(position); - VpnPreference pref = - mVpnPreferenceMap.remove(p.getName()); - mVpnListContainer.removePreference(pref); - removeProfileFromStorage(p); - } - } - }; - mShowingDialog = new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(R.string.vpn_confirm_profile_deletion) - .setPositiveButton(android.R.string.ok, onClickListener) - .setNegativeButton(R.string.vpn_no_button, onClickListener) - .create(); - mShowingDialog.show(); - } - - // Randomly generates an ID for the profile. - // The ID is unique and only set once when the profile is created. - private void setProfileId(VpnProfile profile) { - String id; - - while (true) { - id = String.valueOf(Math.abs( - Double.doubleToLongBits(Math.random()))); - if (id.length() >= 8) break; - } - for (VpnProfile p : sVpnProfileList) { - if (p.getId().equals(id)) { - setProfileId(profile); - return; - } - } - profile.setId(id); - } - - private void addProfile(VpnProfile p) throws IOException { - setProfileId(p); - processSecrets(p); - saveProfileToStorage(p); - - sVpnProfileList.add(p); - addPreferenceFor(p, true); - disableProfilePreferencesIfOneActive(); - } - - // Adds a preference in mVpnListContainer - private VpnPreference addPreferenceFor( - VpnProfile p, boolean addToContainer) { - VpnPreference pref = new VpnPreference(getActivity(), p); - mVpnPreferenceMap.put(p.getName(), pref); - if (addToContainer) mVpnListContainer.addPreference(pref); - - pref.setOnPreferenceClickListener( - new Preference.OnPreferenceClickListener() { - public boolean onPreferenceClick(Preference pref) { - connectOrDisconnect(((VpnPreference) pref).mProfile); - return true; - } - }); - return pref; - } - - // index: index to sVpnProfileList - private void replaceProfile(int index, VpnProfile p) throws IOException { - Map<String, VpnPreference> map = mVpnPreferenceMap; - VpnProfile oldProfile = sVpnProfileList.set(index, p); - VpnPreference pref = map.remove(oldProfile.getName()); - if (pref.mProfile != oldProfile) { - throw new RuntimeException("inconsistent state!"); - } - - p.setId(oldProfile.getId()); - - processSecrets(p); - - // TODO: remove copyFiles once the setId() code propagates. - // Copy config files and remove the old ones if they are in different - // directories. - if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) { - removeProfileFromStorage(oldProfile); - } - saveProfileToStorage(p); - - pref.setProfile(p); - map.put(p.getName(), pref); - } - - private void startVpnTypeSelection() { - if ((getActivity() == null) || isRemoving()) return; - - ((PreferenceActivity) getActivity()).startPreferencePanel( - VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null, - this, REQUEST_SELECT_VPN_TYPE); - } - - private boolean isKeyStoreUnlocked() { - return mKeyStore.test() == KeyStore.NO_ERROR; - } - - // Returns true if the profile needs to access keystore - private boolean needKeyStoreToSave(VpnProfile p) { - switch (p.getType()) { - case L2TP_IPSEC_PSK: - L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; - String presharedKey = pskProfile.getPresharedKey(); - if (!TextUtils.isEmpty(presharedKey)) return true; - // $FALL-THROUGH$ - case L2TP: - L2tpProfile l2tpProfile = (L2tpProfile) p; - if (l2tpProfile.isSecretEnabled() && - !TextUtils.isEmpty(l2tpProfile.getSecretString())) { - return true; - } - // $FALL-THROUGH$ - default: - return false; - } - } - - // Returns true if the profile needs to access keystore - private boolean needKeyStoreToConnect(VpnProfile p) { - switch (p.getType()) { - case L2TP_IPSEC: - case L2TP_IPSEC_PSK: - return true; - - case L2TP: - return ((L2tpProfile) p).isSecretEnabled(); - - default: - return false; - } - } - - // Returns true if keystore is unlocked or keystore is not a concern - private boolean unlockKeyStore(VpnProfile p, Runnable action) { - if (isKeyStoreUnlocked()) return true; - mUnlockAction = action; - Credentials.getInstance().unlock(getActivity()); - return false; - } - - private void startVpnEditor(final VpnProfile profile, boolean add) { - if ((getActivity() == null) || isRemoving()) return; - - Bundle args = new Bundle(); - args.putParcelable(KEY_VPN_PROFILE, profile); - // TODO: Show different titles for add and edit. - ((PreferenceActivity)getActivity()).startPreferencePanel( - VpnEditor.class.getCanonicalName(), args, - 0, VpnEditor.getTitle(getActivity(), profile, add), - this, REQUEST_ADD_OR_EDIT_PROFILE); - } - - private synchronized void connect(final VpnProfile p) { - if (needKeyStoreToConnect(p)) { - Runnable action = new Runnable() { - public void run() { - connect(p); - } - }; - if (!unlockKeyStore(p, action)) return; - } - - if (!checkSecrets(p)) return; - changeState(p, VpnState.CONNECTING); - if (mConnectingActor.isConnectDialogNeeded()) { - showDialog(DIALOG_CONNECT); - } else { - mConnectingActor.connect(null); - } - } - - // Do connect or disconnect based on the current state. - private synchronized void connectOrDisconnect(VpnProfile p) { - VpnPreference pref = mVpnPreferenceMap.get(p.getName()); - switch (p.getState()) { - case IDLE: - connect(p); - break; - - case CONNECTING: - // do nothing - break; - - case CONNECTED: - case DISCONNECTING: - changeState(p, VpnState.DISCONNECTING); - getActor(p).disconnect(); - break; - } - } - - private void changeState(VpnProfile p, VpnState state) { - VpnState oldState = p.getState(); - p.setState(state); - mVpnPreferenceMap.get(p.getName()).setSummary( - getProfileSummaryString(p)); - - switch (state) { - case CONNECTED: - mConnectingActor = null; - mActiveProfile = p; - disableProfilePreferencesIfOneActive(); - break; - - case CONNECTING: - if (mConnectingActor == null) { - mConnectingActor = getActor(p); - } - // $FALL-THROUGH$ - case DISCONNECTING: - mActiveProfile = p; - disableProfilePreferencesIfOneActive(); - break; - - case CANCELLED: - changeState(p, VpnState.IDLE); - break; - - case IDLE: - assert(mActiveProfile == p); - - if (mConnectingErrorCode == NO_ERROR) { - onIdle(); - } else { - showDialog(mConnectingErrorCode); - mConnectingErrorCode = NO_ERROR; - } - break; - } - } - - private void onIdle() { - if (DEBUG) Log.d(TAG, " onIdle()"); - mActiveProfile = null; - mConnectingActor = null; - enableProfilePreferences(); - } - - private void disableProfilePreferencesIfOneActive() { - if (mActiveProfile == null) return; - - for (VpnProfile p : sVpnProfileList) { - switch (p.getState()) { - case CONNECTING: - case DISCONNECTING: - case IDLE: - mVpnPreferenceMap.get(p.getName()).setEnabled(false); - break; - - default: - mVpnPreferenceMap.get(p.getName()).setEnabled(true); - } - } - } - - private void enableProfilePreferences() { - for (VpnProfile p : sVpnProfileList) { - mVpnPreferenceMap.get(p.getName()).setEnabled(true); - } - } - - static String getProfileDir(VpnProfile p) { - return PROFILES_ROOT + p.getId(); - } - - static void saveProfileToStorage(VpnProfile p) throws IOException { - File f = new File(getProfileDir(p)); - if (!f.exists()) f.mkdirs(); - ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream( - new File(f, PROFILE_OBJ_FILE))); - oos.writeObject(p); - oos.close(); - } - - private void removeProfileFromStorage(VpnProfile p) { - Util.deleteFile(getProfileDir(p)); - } - - private void updatePreferenceMap() { - mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>(); - mVpnListContainer.removeAll(); - for (VpnProfile p : sVpnProfileList) { - addPreferenceFor(p, true); - } - // reset the mActiveProfile if the profile has been removed from the - // other instance. - if ((mActiveProfile != null) - && !mVpnPreferenceMap.containsKey(mActiveProfile.getName())) { - onIdle(); - } - } - - private void retrieveVpnListFromStorage() { - // skip the loop if the profile is loaded already. - if (sVpnProfileList.size() > 0) return; - File root = new File(PROFILES_ROOT); - String[] dirs = root.list(); - if (dirs == null) return; - for (String dir : dirs) { - File f = new File(new File(root, dir), PROFILE_OBJ_FILE); - if (!f.exists()) continue; - try { - VpnProfile p = deserialize(f); - if (p == null) continue; - if (!checkIdConsistency(dir, p)) continue; - - sVpnProfileList.add(p); - } catch (IOException e) { - Log.e(TAG, "retrieveVpnListFromStorage()", e); - } - } - Collections.sort(sVpnProfileList, new Comparator<VpnProfile>() { - public int compare(VpnProfile p1, VpnProfile p2) { - return p1.getName().compareTo(p2.getName()); - } - }); - disableProfilePreferencesIfOneActive(); - } - - private void checkVpnConnectionStatus() { - for (VpnProfile p : sVpnProfileList) { - changeState(p, mVpnManager.getState(p)); - } - } - - // A sanity check. Returns true if the profile directory name and profile ID - // are consistent. - private boolean checkIdConsistency(String dirName, VpnProfile p) { - if (!dirName.equals(p.getId())) { - Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId()); - return false; - } else { - return true; - } - } - - private VpnProfile deserialize(File profileObjectFile) throws IOException { - try { - ObjectInputStream ois = new ObjectInputStream(new FileInputStream( - profileObjectFile)); - VpnProfile p = (VpnProfile) ois.readObject(); - ois.close(); - return p; - } catch (ClassNotFoundException e) { - Log.d(TAG, "deserialize a profile", e); - return null; - } - } - - private String getProfileSummaryString(VpnProfile p) { - final Activity activity = getActivity(); - switch (p.getState()) { - case CONNECTING: - return activity.getString(R.string.vpn_connecting); - case DISCONNECTING: - return activity.getString(R.string.vpn_disconnecting); - case CONNECTED: - return activity.getString(R.string.vpn_connected); - default: - return activity.getString(R.string.vpn_connect_hint); - } - } - - private VpnProfileActor getActor(VpnProfile p) { - return new AuthenticationActor(getActivity(), p); - } - - private VpnProfile createVpnProfile(String type) { - return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type)); - } - - private boolean checkSecrets(VpnProfile p) { - boolean secretMissing = false; - - if (p instanceof L2tpIpsecProfile) { - L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p; - - String cert = certProfile.getCaCertificate(); - if (TextUtils.isEmpty(cert) || - !mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) { - certProfile.setCaCertificate(null); - secretMissing = true; - } - - cert = certProfile.getUserCertificate(); - if (TextUtils.isEmpty(cert) || - !mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) { - certProfile.setUserCertificate(null); - secretMissing = true; - } - } - - if (p instanceof L2tpIpsecPskProfile) { - L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; - String presharedKey = pskProfile.getPresharedKey(); - String key = KEY_PREFIX_IPSEC_PSK + p.getId(); - if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) { - pskProfile.setPresharedKey(null); - secretMissing = true; - } - } - - if (p instanceof L2tpProfile) { - L2tpProfile l2tpProfile = (L2tpProfile) p; - if (l2tpProfile.isSecretEnabled()) { - String secret = l2tpProfile.getSecretString(); - String key = KEY_PREFIX_L2TP_SECRET + p.getId(); - if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) { - l2tpProfile.setSecretString(null); - secretMissing = true; - } - } - } - - if (secretMissing) { - mActiveProfile = p; - showDialog(DIALOG_SECRET_NOT_SET); - return false; - } else { - return true; - } - } - - private void processSecrets(VpnProfile p) { - switch (p.getType()) { - case L2TP_IPSEC_PSK: - L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; - String presharedKey = pskProfile.getPresharedKey(); - String key = KEY_PREFIX_IPSEC_PSK + p.getId(); - if (!TextUtils.isEmpty(presharedKey) && - !mKeyStore.put(key, presharedKey)) { - Log.e(TAG, "keystore write failed: key=" + key); - } - pskProfile.setPresharedKey(key); - // $FALL-THROUGH$ - case L2TP_IPSEC: - case L2TP: - L2tpProfile l2tpProfile = (L2tpProfile) p; - key = KEY_PREFIX_L2TP_SECRET + p.getId(); - if (l2tpProfile.isSecretEnabled()) { - String secret = l2tpProfile.getSecretString(); - if (!TextUtils.isEmpty(secret) && - !mKeyStore.put(key, secret)) { - Log.e(TAG, "keystore write failed: key=" + key); - } - l2tpProfile.setSecretString(key); - } else { - mKeyStore.delete(key); - } - break; - } - } - - private class VpnPreference extends Preference { - VpnProfile mProfile; - VpnPreference(Context c, VpnProfile p) { - super(c); - setProfile(p); - } - - void setProfile(VpnProfile p) { - mProfile = p; - setTitle(p.getName()); - setSummary(getProfileSummaryString(p)); - } - } - - // to receive vpn connectivity events broadcast by VpnService - private class ConnectivityReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - String profileName = intent.getStringExtra( - VpnManager.BROADCAST_PROFILE_NAME); - if (profileName == null) return; - - VpnState s = (VpnState) intent.getSerializableExtra( - VpnManager.BROADCAST_CONNECTION_STATE); - - if (s == null) { - Log.e(TAG, "received null connectivity state"); - return; - } - - mConnectingErrorCode = intent.getIntExtra( - VpnManager.BROADCAST_ERROR_CODE, NO_ERROR); - - VpnPreference pref = mVpnPreferenceMap.get(profileName); - if (pref != null) { - Log.d(TAG, "received connectivity: " + profileName - + ": connected? " + s - + " err=" + mConnectingErrorCode); - // XXX: VpnService should broadcast non-IDLE state, say UNUSABLE, - // when an error occurs. - changeState(pref.mProfile, s); - } else { - Log.e(TAG, "received connectivity: " + profileName - + ": connected? " + s + ", but profile does not exist;" - + " just ignore it"); - } - } - } -} diff --git a/src/com/android/settings/vpn/VpnTypeSelection.java b/src/com/android/settings/vpn/VpnTypeSelection.java deleted file mode 100644 index 45e33b9..0000000 --- a/src/com/android/settings/vpn/VpnTypeSelection.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2009 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.vpn; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - -import android.app.Activity; -import android.content.Intent; -import android.net.vpn.VpnManager; -import android.net.vpn.VpnType; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceScreen; - -import java.util.HashMap; -import java.util.Map; - -/** - * The activity to select a VPN type. - */ -public class VpnTypeSelection extends SettingsPreferenceFragment { - private Map<String, VpnType> mTypeMap = new HashMap<String, VpnType>(); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.vpn_type); - initTypeList(); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen ps, Preference pref) { - ((PreferenceActivity)getActivity()) - .finishPreferencePanel(this, Activity.RESULT_OK, - getResultIntent(mTypeMap.get(pref.getTitle().toString()))); - return true; - } - - private void initTypeList() { - PreferenceScreen root = getPreferenceScreen(); - final Activity activity = getActivity(); - for (VpnType t : VpnManager.getSupportedVpnTypes()) { - String displayName = t.getDisplayName(); - String message = String.format( - activity.getString(R.string.vpn_edit_title_add), displayName); - mTypeMap.put(message, t); - - Preference pref = new Preference(activity); - pref.setTitle(message); - pref.setSummary(t.getDescriptionId()); - root.addPreference(pref); - } - } - - private Intent getResultIntent(VpnType type) { - Intent intent = new Intent(getActivity(), VpnSettings.class); - intent.putExtra(VpnSettings.KEY_VPN_TYPE, type.toString()); - return intent; - } -} diff --git a/src/com/android/settings/vpn2/VpnDialog.java b/src/com/android/settings/vpn2/VpnDialog.java new file mode 100644 index 0000000..cce5e2d --- /dev/null +++ b/src/com/android/settings/vpn2/VpnDialog.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2011 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.vpn2; + +import com.android.settings.R; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.security.Credentials; +import android.security.KeyStore; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.TextView; + +import java.net.InetAddress; + +class VpnDialog extends AlertDialog implements TextWatcher, + View.OnClickListener, AdapterView.OnItemSelectedListener { + private final KeyStore mKeyStore = KeyStore.getInstance(); + private final DialogInterface.OnClickListener mListener; + private final VpnProfile mProfile; + + private boolean mEditing; + + private View mView; + + private TextView mName; + private Spinner mType; + private TextView mServer; + private TextView mUsername; + private TextView mPassword; + private TextView mSearchDomains; + private TextView mDnsServers; + private TextView mRoutes; + private CheckBox mMppe; + private TextView mL2tpSecret; + private TextView mIpsecIdentifier; + private TextView mIpsecSecret; + private Spinner mIpsecUserCert; + private Spinner mIpsecCaCert; + private CheckBox mSaveLogin; + + VpnDialog(Context context, DialogInterface.OnClickListener listener, + VpnProfile profile, boolean editing) { + super(context); + mListener = listener; + mProfile = profile; + mEditing = editing; + } + + @Override + protected void onCreate(Bundle savedState) { + mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); + setView(mView); + setInverseBackgroundForced(true); + + Context context = getContext(); + + // First, find out all the fields. + mName = (TextView) mView.findViewById(R.id.name); + mType = (Spinner) mView.findViewById(R.id.type); + mServer = (TextView) mView.findViewById(R.id.server); + mUsername = (TextView) mView.findViewById(R.id.username); + mPassword = (TextView) mView.findViewById(R.id.password); + mSearchDomains = (TextView) mView.findViewById(R.id.search_domains); + mDnsServers = (TextView) mView.findViewById(R.id.dns_servers); + mRoutes = (TextView) mView.findViewById(R.id.routes); + mMppe = (CheckBox) mView.findViewById(R.id.mppe); + mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret); + mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); + mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); + mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); + mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); + mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login); + + // Second, copy values from the profile. + mName.setText(mProfile.name); + mType.setSelection(mProfile.type); + mServer.setText(mProfile.server); + mUsername.setText(mProfile.username); + mPassword.setText(mProfile.password); + mSearchDomains.setText(mProfile.searchDomains); + mDnsServers.setText(mProfile.dnsServers); + mRoutes.setText(mProfile.routes); + mMppe.setChecked(mProfile.mppe); + mL2tpSecret.setText(mProfile.l2tpSecret); + mIpsecIdentifier.setText(mProfile.ipsecIdentifier); + mIpsecSecret.setText(mProfile.ipsecSecret); + loadCertificates(mIpsecUserCert, Credentials.USER_CERTIFICATE, + 0, mProfile.ipsecUserCert); + loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE, + R.string.vpn_no_ca_cert, mProfile.ipsecCaCert); + mSaveLogin.setChecked(mProfile.saveLogin); + + // Third, add listeners to required fields. + mName.addTextChangedListener(this); + mType.setOnItemSelectedListener(this); + mServer.addTextChangedListener(this); + mUsername.addTextChangedListener(this); + mPassword.addTextChangedListener(this); + mDnsServers.addTextChangedListener(this); + mRoutes.addTextChangedListener(this); + mIpsecSecret.addTextChangedListener(this); + mIpsecUserCert.setOnItemSelectedListener(this); + + // Forth, determine to do editing or connecting. + boolean valid = validate(true); + mEditing = mEditing || !valid; + + if (mEditing) { + setTitle(R.string.vpn_edit); + + // Show common fields. + mView.findViewById(R.id.editor).setVisibility(View.VISIBLE); + + // Show type-specific fields. + changeType(mProfile.type); + + // Show advanced options directly if any of them is set. + View showOptions = mView.findViewById(R.id.show_options); + if (mProfile.searchDomains.isEmpty() && mProfile.dnsServers.isEmpty() && + mProfile.routes.isEmpty()) { + showOptions.setOnClickListener(this); + } else { + onClick(showOptions); + } + + // Create a button to save the profile. + setButton(DialogInterface.BUTTON_POSITIVE, + context.getString(R.string.vpn_save), mListener); + } else { + setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); + + // Not editing, just show username and password. + mView.findViewById(R.id.login).setVisibility(View.VISIBLE); + + // Create a button to connect the network. + setButton(DialogInterface.BUTTON_POSITIVE, + context.getString(R.string.vpn_connect), mListener); + } + + // Always provide a cancel button. + setButton(DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.vpn_cancel), mListener); + + // Let AlertDialog create everything. + super.onCreate(null); + + // Disable the action button if necessary. + getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(mEditing ? valid : validate(false)); + + // Workaround to resize the dialog for the input method. + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | + WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + + @Override + public void afterTextChanged(Editable field) { + getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void onClick(View showOptions) { + showOptions.setVisibility(View.GONE); + mView.findViewById(R.id.options).setVisibility(View.VISIBLE); + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (parent == mType) { + changeType(position); + } + getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + private void changeType(int type) { + // First, hide everything. + mMppe.setVisibility(View.GONE); + mView.findViewById(R.id.l2tp).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_ca).setVisibility(View.GONE); + + // Then, unhide type-specific fields. + switch (type) { + case VpnProfile.TYPE_PPTP: + mMppe.setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + mView.findViewById(R.id.ipsec_ca).setVisibility(View.VISIBLE); + break; + } + } + + private boolean validate(boolean editing) { + if (!editing) { + return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; + } + if (mName.getText().length() == 0 || mServer.getText().length() == 0 || + !validateAddresses(mDnsServers.getText().toString(), false) || + !validateAddresses(mRoutes.getText().toString(), true)) { + return false; + } + switch (mType.getSelectedItemPosition()) { + case VpnProfile.TYPE_PPTP: + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + return true; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + return mIpsecSecret.getText().length() != 0; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + return mIpsecUserCert.getSelectedItemPosition() != 0; + } + return false; + } + + private boolean validateAddresses(String addresses, boolean cidr) { + try { + for (String address : addresses.split(" ")) { + if (address.isEmpty()) { + continue; + } + // Legacy VPN currently only supports IPv4. + int prefixLength = 32; + if (cidr) { + String[] parts = address.split("/", 2); + address = parts[0]; + prefixLength = Integer.parseInt(parts[1]); + } + byte[] bytes = InetAddress.parseNumericAddress(address).getAddress(); + int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 | + (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24; + if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 || + (prefixLength < 32 && (integer << prefixLength) != 0)) { + return false; + } + } + } catch (Exception e) { + return false; + } + return true; + } + + private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) { + Context context = getContext(); + String first = (firstId == 0) ? "" : context.getString(firstId); + String[] certificates = mKeyStore.saw(prefix); + + if (certificates == null || certificates.length == 0) { + certificates = new String[] {first}; + } else { + String[] array = new String[certificates.length + 1]; + array[0] = first; + System.arraycopy(certificates, 0, array, 1, certificates.length); + certificates = array; + } + + ArrayAdapter<String> adapter = new ArrayAdapter<String>( + context, android.R.layout.simple_spinner_item, certificates); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + + for (int i = 1; i < certificates.length; ++i) { + if (certificates[i].equals(selected)) { + spinner.setSelection(i); + break; + } + } + } + + boolean isEditing() { + return mEditing; + } + + VpnProfile getProfile() { + // First, save common fields. + VpnProfile profile = new VpnProfile(mProfile.key); + profile.name = mName.getText().toString(); + profile.type = mType.getSelectedItemPosition(); + profile.server = mServer.getText().toString().trim(); + profile.username = mUsername.getText().toString(); + profile.password = mPassword.getText().toString(); + profile.searchDomains = mSearchDomains.getText().toString().trim(); + profile.dnsServers = mDnsServers.getText().toString().trim(); + profile.routes = mRoutes.getText().toString().trim(); + + // Then, save type-specific fields. + switch (profile.type) { + case VpnProfile.TYPE_PPTP: + profile.mppe = mMppe.isChecked(); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + profile.l2tpSecret = mL2tpSecret.getText().toString(); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); + profile.ipsecSecret = mIpsecSecret.getText().toString(); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + profile.l2tpSecret = mL2tpSecret.getText().toString(); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + if (mIpsecUserCert.getSelectedItemPosition() != 0) { + profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem(); + } + // fall through + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + if (mIpsecCaCert.getSelectedItemPosition() != 0) { + profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem(); + } + break; + } + + profile.saveLogin = mSaveLogin.isChecked(); + return profile; + } +} diff --git a/src/com/android/settings/vpn2/VpnProfile.java b/src/com/android/settings/vpn2/VpnProfile.java new file mode 100644 index 0000000..3ee3af0 --- /dev/null +++ b/src/com/android/settings/vpn2/VpnProfile.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 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.vpn2; + +import java.nio.charset.Charsets; + +/** + * Parcel-like entity class for VPN profiles. To keep things simple, all + * fields are package private. Methods are provided for serialization, so + * storage can be implemented easily. Two rules are set for this class. + * First, all fields must be kept non-null. Second, always make a copy + * using clone() before modifying. + */ +class VpnProfile implements Cloneable { + // Match these constants with R.array.vpn_types. + static final int TYPE_PPTP = 0; + static final int TYPE_L2TP_IPSEC_PSK = 1; + static final int TYPE_L2TP_IPSEC_RSA = 2; + static final int TYPE_IPSEC_XAUTH_PSK = 3; + static final int TYPE_IPSEC_XAUTH_RSA = 4; + static final int TYPE_IPSEC_HYBRID_RSA = 5; + static final int TYPE_MAX = 5; + + // Entity fields. + final String key; // -1 + String name = ""; // 0 + int type = TYPE_PPTP; // 1 + String server = ""; // 2 + String username = ""; // 3 + String password = ""; // 4 + String dnsServers = ""; // 5 + String searchDomains = ""; // 6 + String routes = ""; // 7 + boolean mppe = true; // 8 + String l2tpSecret = ""; // 9 + String ipsecIdentifier = "";// 10 + String ipsecSecret = ""; // 11 + String ipsecUserCert = ""; // 12 + String ipsecCaCert = ""; // 13 + + // Helper fields. + boolean saveLogin = false; + + VpnProfile(String key) { + this.key = key; + } + + static VpnProfile decode(String key, byte[] value) { + try { + if (key == null) { + return null; + } + + String[] values = new String(value, Charsets.UTF_8).split("\0", -1); + // Currently it always has 14 fields. + if (values.length < 14) { + return null; + } + + VpnProfile profile = new VpnProfile(key); + profile.name = values[0]; + profile.type = Integer.valueOf(values[1]); + if (profile.type < 0 || profile.type > TYPE_MAX) { + return null; + } + profile.server = values[2]; + profile.username = values[3]; + profile.password = values[4]; + profile.dnsServers = values[5]; + profile.searchDomains = values[6]; + profile.routes = values[7]; + profile.mppe = Boolean.valueOf(values[8]); + profile.l2tpSecret = values[9]; + profile.ipsecIdentifier = values[10]; + profile.ipsecSecret = values[11]; + profile.ipsecUserCert = values[12]; + profile.ipsecCaCert = values[13]; + + profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty(); + return profile; + } catch (Exception e) { + // ignore + } + return null; + } + + byte[] encode() { + StringBuilder builder = new StringBuilder(name); + builder.append('\0').append(type); + builder.append('\0').append(server); + builder.append('\0').append(saveLogin ? username : ""); + builder.append('\0').append(saveLogin ? password : ""); + builder.append('\0').append(dnsServers); + builder.append('\0').append(searchDomains); + builder.append('\0').append(routes); + builder.append('\0').append(mppe); + builder.append('\0').append(l2tpSecret); + builder.append('\0').append(ipsecIdentifier); + builder.append('\0').append(ipsecSecret); + builder.append('\0').append(ipsecUserCert); + builder.append('\0').append(ipsecCaCert); + return builder.toString().getBytes(Charsets.UTF_8); + } +} diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java new file mode 100644 index 0000000..0197333 --- /dev/null +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2011 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.vpn2; + +import com.android.settings.R; + +import android.content.Context; +import android.content.DialogInterface; +import android.net.IConnectivityManager; +import android.net.LinkProperties; +import android.net.RouteInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.ServiceManager; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.security.Credentials; +import android.security.KeyStore; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView.AdapterContextMenuInfo; + +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.settings.SettingsPreferenceFragment; + +import java.net.Inet4Address; +import java.nio.charset.Charsets; +import java.util.Arrays; +import java.util.HashMap; + +public class VpnSettings extends SettingsPreferenceFragment implements + Handler.Callback, Preference.OnPreferenceClickListener, + DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + + private static final String TAG = "VpnSettings"; + + private final IConnectivityManager mService = IConnectivityManager.Stub + .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); + private final KeyStore mKeyStore = KeyStore.getInstance(); + private boolean mUnlocking = false; + + private HashMap<String, VpnPreference> mPreferences; + private VpnDialog mDialog; + + private Handler mUpdater; + private LegacyVpnInfo mInfo; + + // The key of the profile for the current ContextMenu. + private String mSelectedKey; + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + addPreferencesFromResource(R.xml.vpn_settings2); + getPreferenceScreen().setOrderingAsAdded(false); + + if (savedState != null) { + VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), + savedState.getByteArray("VpnProfile")); + if (profile != null) { + mDialog = new VpnDialog(getActivity(), this, profile, + savedState.getBoolean("VpnEditing")); + } + } + } + + @Override + public void onSaveInstanceState(Bundle savedState) { + // We do not save view hierarchy, as they are just profiles. + if (mDialog != null) { + VpnProfile profile = mDialog.getProfile(); + savedState.putString("VpnKey", profile.key); + savedState.putByteArray("VpnProfile", profile.encode()); + savedState.putBoolean("VpnEditing", mDialog.isEditing()); + } + // else? + } + + @Override + public void onResume() { + super.onResume(); + + // Check KeyStore here, so others do not need to deal with it. + if (mKeyStore.state() != KeyStore.State.UNLOCKED) { + if (!mUnlocking) { + // Let us unlock KeyStore. See you later! + Credentials.getInstance().unlock(getActivity()); + } else { + // We already tried, but it is still not working! + finishFragment(); + } + mUnlocking = !mUnlocking; + return; + } + + // Now KeyStore is always unlocked. Reset the flag. + mUnlocking = false; + + // Currently we are the only user of profiles in KeyStore. + // Assuming KeyStore and KeyGuard do the right thing, we can + // safely cache profiles in the memory. + if (mPreferences == null) { + mPreferences = new HashMap<String, VpnPreference>(); + PreferenceGroup group = getPreferenceScreen(); + + String[] keys = mKeyStore.saw(Credentials.VPN); + if (keys != null && keys.length > 0) { + Context context = getActivity(); + + for (String key : keys) { + VpnProfile profile = VpnProfile.decode(key, + mKeyStore.get(Credentials.VPN + key)); + if (profile == null) { + Log.w(TAG, "bad profile: key = " + key); + mKeyStore.delete(Credentials.VPN + key); + } else { + VpnPreference preference = new VpnPreference(context, profile); + mPreferences.put(key, preference); + group.addPreference(preference); + } + } + } + group.findPreference("add_network").setOnPreferenceClickListener(this); + } + + // Show the dialog if there is one. + if (mDialog != null) { + mDialog.setOnDismissListener(this); + mDialog.show(); + } + + // Start monitoring. + if (mUpdater == null) { + mUpdater = new Handler(this); + } + mUpdater.sendEmptyMessage(0); + + // Register for context menu. Hmmm, getListView() is hidden? + registerForContextMenu(getListView()); + } + + @Override + public void onPause() { + super.onPause(); + + // Hide the dialog if there is one. + if (mDialog != null) { + mDialog.setOnDismissListener(null); + mDialog.dismiss(); + } + + // Unregister for context menu. + if (getView() != null) { + unregisterForContextMenu(getListView()); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + // Here is the exit of a dialog. + mDialog = null; + } + + @Override + public void onClick(DialogInterface dialog, int button) { + if (button == DialogInterface.BUTTON_POSITIVE) { + // Always save the profile. + VpnProfile profile = mDialog.getProfile(); + mKeyStore.put(Credentials.VPN + profile.key, profile.encode()); + + // Update the preference. + VpnPreference preference = mPreferences.get(profile.key); + if (preference != null) { + disconnect(profile.key); + preference.update(profile); + } else { + preference = new VpnPreference(getActivity(), profile); + mPreferences.put(profile.key, preference); + getPreferenceScreen().addPreference(preference); + } + + // If we are not editing, connect! + if (!mDialog.isEditing()) { + try { + connect(profile); + } catch (Exception e) { + Log.e(TAG, "connect", e); + } + } + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { + if (mDialog != null) { + Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); + return; + } + + if (info instanceof AdapterContextMenuInfo) { + Preference preference = (Preference) getListView().getItemAtPosition( + ((AdapterContextMenuInfo) info).position); + if (preference instanceof VpnPreference) { + VpnProfile profile = ((VpnPreference) preference).getProfile(); + mSelectedKey = profile.key; + menu.setHeaderTitle(profile.name); + menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); + menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mDialog != null) { + Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); + return false; + } + + VpnPreference preference = mPreferences.get(mSelectedKey); + if (preference == null) { + Log.v(TAG, "onContextItemSelected() is called but no preference is found"); + return false; + } + + switch (item.getItemId()) { + case R.string.vpn_menu_edit: + mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); + mDialog.setOnDismissListener(this); + mDialog.show(); + return true; + case R.string.vpn_menu_delete: + disconnect(mSelectedKey); + getPreferenceScreen().removePreference(preference); + mPreferences.remove(mSelectedKey); + mKeyStore.delete(Credentials.VPN + mSelectedKey); + return true; + } + return false; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mDialog != null) { + Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); + return true; + } + + if (preference instanceof VpnPreference) { + VpnProfile profile = ((VpnPreference) preference).getProfile(); + if (mInfo != null && profile.key.equals(mInfo.key) && + mInfo.state == LegacyVpnInfo.STATE_CONNECTED) { + try { + mInfo.intent.send(); + return true; + } catch (Exception e) { + // ignore + } + } + mDialog = new VpnDialog(getActivity(), this, profile, false); + } else { + // Generate a new key. Here we just use the current time. + long millis = System.currentTimeMillis(); + while (mPreferences.containsKey(Long.toHexString(millis))) { + ++millis; + } + mDialog = new VpnDialog(getActivity(), this, + new VpnProfile(Long.toHexString(millis)), true); + } + mDialog.setOnDismissListener(this); + mDialog.show(); + return true; + } + + @Override + public boolean handleMessage(Message message) { + mUpdater.removeMessages(0); + + if (isResumed()) { + try { + LegacyVpnInfo info = mService.getLegacyVpnInfo(); + if (mInfo != null) { + VpnPreference preference = mPreferences.get(mInfo.key); + if (preference != null) { + preference.update(-1); + } + mInfo = null; + } + if (info != null) { + VpnPreference preference = mPreferences.get(info.key); + if (preference != null) { + preference.update(info.state); + mInfo = info; + } + } + } catch (Exception e) { + // ignore + } + mUpdater.sendEmptyMessageDelayed(0, 1000); + } + return true; + } + + private String[] getDefaultNetwork() throws Exception { + LinkProperties network = mService.getActiveLinkProperties(); + if (network == null) { + throw new IllegalStateException("Network is not available"); + } + String interfaze = network.getInterfaceName(); + if (interfaze == null) { + throw new IllegalStateException("Cannot get the default interface"); + } + String gateway = null; + for (RouteInfo route : network.getRoutes()) { + // Currently legacy VPN only works on IPv4. + if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) { + gateway = route.getGateway().getHostAddress(); + break; + } + } + if (gateway == null) { + throw new IllegalStateException("Cannot get the default gateway"); + } + return new String[] {interfaze, gateway}; + } + + private void connect(VpnProfile profile) throws Exception { + // Get the default interface and the default gateway. + String[] network = getDefaultNetwork(); + String interfaze = network[0]; + String gateway = network[1]; + + // Load certificates. + String privateKey = ""; + String userCert = ""; + String caCert = ""; + if (!profile.ipsecUserCert.isEmpty()) { + byte[] value = mKeyStore.get(Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert); + privateKey = (value == null) ? null : new String(value, Charsets.UTF_8); + value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); + userCert = (value == null) ? null : new String(value, Charsets.UTF_8); + } + if (!profile.ipsecCaCert.isEmpty()) { + byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); + caCert = (value == null) ? null : new String(value, Charsets.UTF_8); + } + if (privateKey == null || userCert == null || caCert == null) { + // TODO: find out a proper way to handle this. Delete these keys? + throw new IllegalStateException("Cannot load credentials"); + } + + // Prepare arguments for racoon. + String[] racoon = null; + switch (profile.type) { + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + racoon = new String[] { + interfaze, profile.server, "udppsk", profile.ipsecIdentifier, + profile.ipsecSecret, "1701", + }; + break; + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + racoon = new String[] { + interfaze, profile.server, "udprsa", privateKey, userCert, caCert, "1701", + }; + break; + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + racoon = new String[] { + interfaze, profile.server, "xauthpsk", profile.ipsecIdentifier, + profile.ipsecSecret, profile.username, profile.password, "", gateway, + }; + break; + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + racoon = new String[] { + interfaze, profile.server, "xauthrsa", privateKey, userCert, caCert, + profile.username, profile.password, "", gateway, + }; + break; + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + racoon = new String[] { + interfaze, profile.server, "hybridrsa", caCert, + profile.username, profile.password, "", gateway, + }; + break; + } + + // Prepare arguments for mtpd. + String[] mtpd = null; + switch (profile.type) { + case VpnProfile.TYPE_PPTP: + mtpd = new String[] { + interfaze, "pptp", profile.server, "1723", + "name", profile.username, "password", profile.password, + "linkname", "vpn", "refuse-eap", "nodefaultroute", + "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", + (profile.mppe ? "+mppe" : "nomppe"), + }; + break; + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + mtpd = new String[] { + interfaze, "l2tp", profile.server, "1701", profile.l2tpSecret, + "name", profile.username, "password", profile.password, + "linkname", "vpn", "refuse-eap", "nodefaultroute", + "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", + }; + break; + } + + VpnConfig config = new VpnConfig(); + config.user = profile.key; + config.interfaze = interfaze; + config.session = profile.name; + config.routes = profile.routes; + if (!profile.dnsServers.isEmpty()) { + config.dnsServers = Arrays.asList(profile.dnsServers.split(" +")); + } + if (!profile.searchDomains.isEmpty()) { + config.searchDomains = Arrays.asList(profile.searchDomains.split(" +")); + } + + mService.startLegacyVpn(config, racoon, mtpd); + } + + private void disconnect(String key) { + if (mInfo != null && key.equals(mInfo.key)) { + try { + mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); + } catch (Exception e) { + // ignore + } + } + } + + private class VpnPreference extends Preference { + private VpnProfile mProfile; + private int mState = -1; + + VpnPreference(Context context, VpnProfile profile) { + super(context); + setPersistent(false); + setOrder(0); + setOnPreferenceClickListener(VpnSettings.this); + + mProfile = profile; + update(); + } + + VpnProfile getProfile() { + return mProfile; + } + + void update(VpnProfile profile) { + mProfile = profile; + update(); + } + + void update(int state) { + mState = state; + update(); + } + + void update() { + if (mState < 0) { + String[] types = getContext().getResources() + .getStringArray(R.array.vpn_types_long); + setSummary(types[mProfile.type]); + } else { + String[] states = getContext().getResources() + .getStringArray(R.array.vpn_states); + setSummary(states[mState]); + } + setTitle(mProfile.name); + notifyHierarchyChanged(); + } + + @Override + public int compareTo(Preference preference) { + int result = -1; + if (preference instanceof VpnPreference) { + VpnPreference another = (VpnPreference) preference; + if ((result = another.mState - mState) == 0 && + (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && + (result = mProfile.type - another.mProfile.type) == 0) { + result = mProfile.key.compareTo(another.mProfile.key); + } + } + return result; + } + } +} diff --git a/src/com/android/settings/widget/ChartAxis.java b/src/com/android/settings/widget/ChartAxis.java new file mode 100644 index 0000000..4e0da1d --- /dev/null +++ b/src/com/android/settings/widget/ChartAxis.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.res.Resources; +import android.text.SpannableStringBuilder; + +/** + * Axis along a {@link ChartView} that knows how to convert between raw point + * and screen coordinate systems. + */ +public interface ChartAxis { + + /** Set range of raw values this axis should cover. */ + public void setBounds(long min, long max); + /** Set range of screen points this axis should cover. */ + public void setSize(float size); + + /** Convert raw value into screen point. */ + public float convertToPoint(long value); + /** Convert screen point into raw value. */ + public long convertToValue(float point); + + /** Build label that describes given raw value. */ + public void buildLabel(Resources res, SpannableStringBuilder builder, long value); + + /** Return list of tick points for drawing a grid. */ + public float[] getTickPoints(); + + /** + * Test if given raw value should cause the axis to grow or shrink; + * returning positive value to grow and negative to shrink. + */ + public int shouldAdjustAxis(long value); + +} diff --git a/src/com/android/settings/widget/ChartGridView.java b/src/com/android/settings/widget/ChartGridView.java new file mode 100644 index 0000000..c2702e4 --- /dev/null +++ b/src/com/android/settings/widget/ChartGridView.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.android.settings.DataUsageSummary; +import com.android.settings.R; +import com.google.common.base.Preconditions; + +/** + * Background of {@link ChartView} that renders grid lines as requested by + * {@link ChartAxis#getTickPoints()}. + */ +public class ChartGridView extends View { + + private ChartAxis mHoriz; + private ChartAxis mVert; + + private Drawable mPrimary; + private Drawable mSecondary; + private Drawable mBorder; + private int mLabelColor; + + private Layout mLayoutStart; + private Layout mLayoutEnd; + + public ChartGridView(Context context) { + this(context, null, 0); + } + + public ChartGridView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ChartGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setWillNotDraw(false); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ChartGridView, defStyle, 0); + + mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable); + mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable); + mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable); + mLabelColor = a.getColor(R.styleable.ChartGridView_labelColor, Color.RED); + + a.recycle(); + } + + void init(ChartAxis horiz, ChartAxis vert) { + mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); + mVert = Preconditions.checkNotNull(vert, "missing vert"); + } + + void setBounds(long start, long end) { + final Context context = getContext(); + mLayoutStart = makeLayout(DataUsageSummary.formatDateRange(context, start, start, true)); + mLayoutEnd = makeLayout(DataUsageSummary.formatDateRange(context, end, end, true)); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int width = getWidth(); + final int height = getHeight(); + + final Drawable secondary = mSecondary; + final int secondaryHeight = mSecondary.getIntrinsicHeight(); + + final float[] vertTicks = mVert.getTickPoints(); + for (float y : vertTicks) { + final int bottom = (int) Math.min(y + secondaryHeight, height); + secondary.setBounds(0, (int) y, width, bottom); + secondary.draw(canvas); + } + + final Drawable primary = mPrimary; + final int primaryWidth = mPrimary.getIntrinsicWidth(); + final int primaryHeight = mPrimary.getIntrinsicHeight(); + + final float[] horizTicks = mHoriz.getTickPoints(); + for (float x : horizTicks) { + final int right = (int) Math.min(x + primaryWidth, width); + primary.setBounds((int) x, 0, right, height); + primary.draw(canvas); + } + + mBorder.setBounds(0, 0, width, height); + mBorder.draw(canvas); + + final int padding = mLayoutStart.getHeight() / 8; + + final Layout start = mLayoutStart; + if (start != null) { + canvas.save(); + canvas.translate(0, height + padding); + start.draw(canvas); + canvas.restore(); + } + + final Layout end = mLayoutEnd; + if (end != null) { + canvas.save(); + canvas.translate(width - end.getWidth(), height + padding); + end.draw(canvas); + canvas.restore(); + } + } + + private Layout makeLayout(CharSequence text) { + final Resources res = getResources(); + final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + paint.density = res.getDisplayMetrics().density; + paint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); + paint.setColor(mLabelColor); + paint.setTextSize( + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, res.getDisplayMetrics())); + + return new StaticLayout(text, paint, + (int) Math.ceil(Layout.getDesiredWidth(text, paint)), + Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); + } + +} diff --git a/src/com/android/settings/widget/ChartNetworkSeriesView.java b/src/com/android/settings/widget/ChartNetworkSeriesView.java new file mode 100644 index 0000000..481f7cc --- /dev/null +++ b/src/com/android/settings/widget/ChartNetworkSeriesView.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2011 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.widget; + +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.RectF; +import android.net.NetworkStatsHistory; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.settings.R; +import com.google.common.base.Preconditions; + +/** + * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, + * using {@link ChartAxis} to map into screen coordinates. + */ +public class ChartNetworkSeriesView extends View { + private static final String TAG = "ChartNetworkSeriesView"; + private static final boolean LOGD = false; + + private ChartAxis mHoriz; + private ChartAxis mVert; + + private Paint mPaintStroke; + private Paint mPaintFill; + private Paint mPaintFillSecondary; + private Paint mPaintEstimate; + + private NetworkStatsHistory mStats; + + private Path mPathStroke; + private Path mPathFill; + private Path mPathEstimate; + + private long mPrimaryLeft; + private long mPrimaryRight; + + /** Series will be extended to reach this end time. */ + private long mEndTime = Long.MIN_VALUE; + + private boolean mEstimateVisible = false; + + private long mMax; + private long mMaxEstimate; + + public ChartNetworkSeriesView(Context context) { + this(context, null, 0); + } + + public ChartNetworkSeriesView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0); + + final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED); + final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); + final int fillSecondary = a.getColor( + R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); + + setChartColor(stroke, fill, fillSecondary); + setWillNotDraw(false); + + a.recycle(); + + mPathStroke = new Path(); + mPathFill = new Path(); + mPathEstimate = new Path(); + } + + void init(ChartAxis horiz, ChartAxis vert) { + mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); + mVert = Preconditions.checkNotNull(vert, "missing vert"); + } + + public void setChartColor(int stroke, int fill, int fillSecondary) { + mPaintStroke = new Paint(); + mPaintStroke.setStrokeWidth(6.0f); + mPaintStroke.setColor(stroke); + mPaintStroke.setStyle(Style.STROKE); + mPaintStroke.setAntiAlias(true); + + mPaintFill = new Paint(); + mPaintFill.setColor(fill); + mPaintFill.setStyle(Style.FILL); + mPaintFill.setAntiAlias(true); + + mPaintFillSecondary = new Paint(); + mPaintFillSecondary.setColor(fillSecondary); + mPaintFillSecondary.setStyle(Style.FILL); + mPaintFillSecondary.setAntiAlias(true); + + mPaintEstimate = new Paint(); + mPaintEstimate.setStrokeWidth(3.0f); + mPaintEstimate.setColor(fillSecondary); + mPaintEstimate.setStyle(Style.STROKE); + mPaintEstimate.setAntiAlias(true); + mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); + } + + public void bindNetworkStats(NetworkStatsHistory stats) { + mStats = stats; + + mPathStroke.reset(); + mPathFill.reset(); + mPathEstimate.reset(); + invalidate(); + } + + /** + * Set the range to paint with {@link #mPaintFill}, leaving the remaining + * area to be painted with {@link #mPaintFillSecondary}. + */ + public void setPrimaryRange(long left, long right) { + mPrimaryLeft = left; + mPrimaryRight = right; + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + generatePath(); + } + + /** + * Erase any existing {@link Path} and generate series outline based on + * currently bound {@link NetworkStatsHistory} data. + */ + public void generatePath() { + if (LOGD) Log.d(TAG, "generatePath()"); + + mMax = 0; + mPathStroke.reset(); + mPathFill.reset(); + mPathEstimate.reset(); + + // bail when not enough stats to render + if (mStats == null || mStats.size() < 2) return; + + final int width = getWidth(); + final int height = getHeight(); + + boolean started = false; + float firstX = 0; + float lastX = 0; + float lastY = 0; + long lastTime = Long.MIN_VALUE; + + // TODO: count fractional data from first bucket crossing start; + // currently it only accepts first full bucket. + + long totalData = 0; + + NetworkStatsHistory.Entry entry = null; + for (int i = 0; i < mStats.size(); i++) { + entry = mStats.getValues(i, entry); + + lastTime = entry.bucketStart + entry.bucketDuration; + final float x = mHoriz.convertToPoint(lastTime); + final float y = mVert.convertToPoint(totalData); + + // skip until we find first stats on screen + if (i > 0 && !started && x > 0) { + mPathStroke.moveTo(lastX, lastY); + mPathFill.moveTo(lastX, lastY); + started = true; + firstX = x; + } + + if (started) { + mPathStroke.lineTo(x, y); + mPathFill.lineTo(x, y); + totalData += entry.rxBytes + entry.txBytes; + } + + // skip if beyond view + if (x > width) break; + + lastX = x; + lastY = y; + } + + // when data falls short, extend to requested end time + if (lastTime < mEndTime) { + lastX = mHoriz.convertToPoint(mEndTime); + + if (started) { + mPathStroke.lineTo(lastX, lastY); + mPathFill.lineTo(lastX, lastY); + } + } + + if (LOGD) { + final RectF bounds = new RectF(); + mPathFill.computeBounds(bounds, true); + Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" + + totalData); + } + + // drop to bottom of graph from current location + mPathFill.lineTo(lastX, height); + mPathFill.lineTo(firstX, height); + + mMax = totalData; + + // build estimated data + mPathEstimate.moveTo(lastX, lastY); + + final long now = System.currentTimeMillis(); + final long bucketDuration = mStats.getBucketDuration(); + + // long window is average over two weeks + entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); + final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration + / entry.bucketDuration; + + long futureTime = 0; + while (lastX < width) { + futureTime += bucketDuration; + + // short window is day average last week + final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); + entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); + final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration + / entry.bucketDuration; + + totalData += (longWindow * 7 + shortWindow * 3) / 10; + + lastX = mHoriz.convertToPoint(lastTime + futureTime); + lastY = mVert.convertToPoint(totalData); + + mPathEstimate.lineTo(lastX, lastY); + } + + mMaxEstimate = totalData; + } + + public void setEndTime(long endTime) { + mEndTime = endTime; + } + + public void setEstimateVisible(boolean estimateVisible) { + mEstimateVisible = estimateVisible; + invalidate(); + } + + public long getMaxEstimate() { + return mMaxEstimate; + } + + public long getMaxVisible() { + return mEstimateVisible ? mMaxEstimate : mMax; + } + + @Override + protected void onDraw(Canvas canvas) { + int save; + + final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft); + final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight); + + if (mEstimateVisible) { + save = canvas.save(); + canvas.clipRect(0, 0, getWidth(), getHeight()); + canvas.drawPath(mPathEstimate, mPaintEstimate); + canvas.restoreToCount(save); + } + + save = canvas.save(); + canvas.clipRect(0, 0, primaryLeftPoint, getHeight()); + canvas.drawPath(mPathFill, mPaintFillSecondary); + canvas.restoreToCount(save); + + save = canvas.save(); + canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight()); + canvas.drawPath(mPathFill, mPaintFillSecondary); + canvas.restoreToCount(save); + + save = canvas.save(); + canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight()); + canvas.drawPath(mPathFill, mPaintFill); + canvas.drawPath(mPathStroke, mPaintStroke); + canvas.restoreToCount(save); + + } +} diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java new file mode 100644 index 0000000..d5e8de8 --- /dev/null +++ b/src/com/android/settings/widget/ChartSweepView.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.DynamicLayout; +import android.text.Layout.Alignment; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.MathUtils; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.settings.R; +import com.google.common.base.Preconditions; + +/** + * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which + * a user can drag. + */ +public class ChartSweepView extends FrameLayout { + + private Drawable mSweep; + private Rect mSweepPadding = new Rect(); + private Point mSweepOffset = new Point(); + + private Rect mMargins = new Rect(); + private float mNeighborMargin; + + private int mFollowAxis; + + private int mLabelSize; + private int mLabelTemplateRes; + private int mLabelColor; + + private SpannableStringBuilder mLabelTemplate; + private DynamicLayout mLabelLayout; + + private ChartAxis mAxis; + private long mValue; + + private long mValidAfter; + private long mValidBefore; + private ChartSweepView mValidAfterDynamic; + private ChartSweepView mValidBeforeDynamic; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public interface OnSweepListener { + public void onSweep(ChartSweepView sweep, boolean sweepDone); + } + + private OnSweepListener mListener; + private MotionEvent mTracking; + + public ChartSweepView(Context context) { + this(context, null, 0); + } + + public ChartSweepView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ChartSweepView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ChartSweepView, defStyle, 0); + + setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); + setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); + setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0)); + + setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); + setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); + setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE)); + + a.recycle(); + + setClipToPadding(false); + setClipChildren(false); + setWillNotDraw(false); + } + + void init(ChartAxis axis) { + mAxis = Preconditions.checkNotNull(axis, "missing axis"); + } + + public int getFollowAxis() { + return mFollowAxis; + } + + public Rect getMargins() { + return mMargins; + } + + /** + * Return the number of pixels that the "target" area is inset from the + * {@link View} edge, along the current {@link #setFollowAxis(int)}. + */ + private float getTargetInset() { + if (mFollowAxis == VERTICAL) { + final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top + - mSweepPadding.bottom; + return mSweepPadding.top + (targetHeight / 2); + } else { + final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left + - mSweepPadding.right; + return mSweepPadding.left + (targetWidth / 2); + } + } + + public void addOnSweepListener(OnSweepListener listener) { + mListener = listener; + } + + private void dispatchOnSweep(boolean sweepDone) { + if (mListener != null) { + mListener.onSweep(this, sweepDone); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + requestLayout(); + } + + public void setSweepDrawable(Drawable sweep) { + if (mSweep != null) { + mSweep.setCallback(null); + unscheduleDrawable(mSweep); + } + + if (sweep != null) { + sweep.setCallback(this); + if (sweep.isStateful()) { + sweep.setState(getDrawableState()); + } + sweep.setVisible(getVisibility() == VISIBLE, false); + mSweep = sweep; + sweep.getPadding(mSweepPadding); + } else { + mSweep = null; + } + + invalidate(); + } + + public void setFollowAxis(int followAxis) { + mFollowAxis = followAxis; + } + + public void setLabelSize(int size) { + mLabelSize = size; + invalidateLabelTemplate(); + } + + public void setLabelTemplate(int resId) { + mLabelTemplateRes = resId; + invalidateLabelTemplate(); + } + + public void setLabelColor(int color) { + mLabelColor = color; + invalidateLabelTemplate(); + } + + private void invalidateLabelTemplate() { + if (mLabelTemplateRes != 0) { + final CharSequence template = getResources().getText(mLabelTemplateRes); + + final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + paint.density = getResources().getDisplayMetrics().density; + paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); + paint.setColor(mLabelColor); + + mLabelTemplate = new SpannableStringBuilder(template); + mLabelLayout = new DynamicLayout( + mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false); + invalidateLabel(); + + } else { + mLabelTemplate = null; + mLabelLayout = null; + } + + invalidate(); + requestLayout(); + } + + private void invalidateLabel() { + if (mLabelTemplate != null && mAxis != null) { + mAxis.buildLabel(getResources(), mLabelTemplate, mValue); + invalidate(); + } + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mSweep != null) { + mSweep.jumpToCurrentState(); + } + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (mSweep != null) { + mSweep.setVisible(visibility == VISIBLE, false); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mSweep || super.verifyDrawable(who); + } + + public ChartAxis getAxis() { + return mAxis; + } + + public void setValue(long value) { + mValue = value; + invalidateLabel(); + } + + public long getValue() { + return mValue; + } + + public float getPoint() { + if (isEnabled()) { + return mAxis.convertToPoint(mValue); + } else { + // when disabled, show along top edge + return 0; + } + } + + /** + * Set valid range this sweep can move within, in {@link #mAxis} values. The + * most restrictive combination of all valid ranges is used. + */ + public void setValidRange(long validAfter, long validBefore) { + mValidAfter = validAfter; + mValidBefore = validBefore; + } + + public void setNeighborMargin(float neighborMargin) { + mNeighborMargin = neighborMargin; + } + + /** + * Set valid range this sweep can move within, defined by the given + * {@link ChartSweepView}. The most restrictive combination of all valid + * ranges is used. + */ + public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) { + mValidAfterDynamic = validAfter; + mValidBeforeDynamic = validBefore; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + final View parent = (View) getParent(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + + // only start tracking when in sweet spot + final boolean accept; + if (mFollowAxis == VERTICAL) { + accept = event.getX() > getWidth() - (mSweepPadding.right * 2); + } else { + accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2); + } + + if (accept) { + mTracking = event.copy(); + + // starting drag should activate entire chart + if (!parent.isActivated()) { + parent.setActivated(true); + } + + return true; + } else { + return false; + } + } + case MotionEvent.ACTION_MOVE: { + getParent().requestDisallowInterceptTouchEvent(true); + + // content area of parent + final Rect parentContent = getParentContentRect(); + final Rect clampRect = computeClampRect(parentContent); + + if (mFollowAxis == VERTICAL) { + final float currentTargetY = getTop() - mMargins.top; + final float requestedTargetY = currentTargetY + + (event.getRawY() - mTracking.getRawY()); + final float clampedTargetY = MathUtils.constrain( + requestedTargetY, clampRect.top, clampRect.bottom); + setTranslationY(clampedTargetY - currentTargetY); + + setValue(mAxis.convertToValue(clampedTargetY - parentContent.top)); + } else { + final float currentTargetX = getLeft() - mMargins.left; + final float requestedTargetX = currentTargetX + + (event.getRawX() - mTracking.getRawX()); + final float clampedTargetX = MathUtils.constrain( + requestedTargetX, clampRect.left, clampRect.right); + setTranslationX(clampedTargetX - currentTargetX); + + setValue(mAxis.convertToValue(clampedTargetX - parentContent.left)); + } + + dispatchOnSweep(false); + return true; + } + case MotionEvent.ACTION_UP: { + mTracking = null; + dispatchOnSweep(true); + setTranslationX(0); + setTranslationY(0); + requestLayout(); + return true; + } + default: { + return false; + } + } + } + + /** + * Update {@link #mValue} based on current position, including any + * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when + * {@link ChartAxis} changes during sweep adjustment. + */ + public void updateValueFromPosition() { + final Rect parentContent = getParentContentRect(); + if (mFollowAxis == VERTICAL) { + final float effectiveY = getY() - mMargins.top - parentContent.top; + setValue(mAxis.convertToValue(effectiveY)); + } else { + final float effectiveX = getX() - mMargins.left - parentContent.left; + setValue(mAxis.convertToValue(effectiveX)); + } + } + + public int shouldAdjustAxis() { + return mAxis.shouldAdjustAxis(getValue()); + } + + private Rect getParentContentRect() { + final View parent = (View) getParent(); + return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), + parent.getWidth() - parent.getPaddingRight(), + parent.getHeight() - parent.getPaddingBottom()); + } + + @Override + public void addOnLayoutChangeListener(OnLayoutChangeListener listener) { + // ignored to keep LayoutTransition from animating us + } + + @Override + public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) { + // ignored to keep LayoutTransition from animating us + } + + private long getValidAfterDynamic() { + final ChartSweepView dynamic = mValidAfterDynamic; + return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE; + } + + private long getValidBeforeDynamic() { + final ChartSweepView dynamic = mValidBeforeDynamic; + return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE; + } + + /** + * Compute {@link Rect} in {@link #getParent()} coordinates that we should + * be clamped inside of, usually from {@link #setValidRange(long, long)} + * style rules. + */ + private Rect computeClampRect(Rect parentContent) { + // create two rectangles, and pick most restrictive combination + final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f); + final Rect dynamicRect = buildClampRect( + parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin); + + rect.intersect(dynamicRect); + return rect; + } + + private Rect buildClampRect( + Rect parentContent, long afterValue, long beforeValue, float margin) { + if (mAxis instanceof InvertedChartAxis) { + long temp = beforeValue; + beforeValue = afterValue; + afterValue = temp; + } + + final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE; + final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE; + + final float afterPoint = mAxis.convertToPoint(afterValue) + margin; + final float beforePoint = mAxis.convertToPoint(beforeValue) - margin; + + final Rect clampRect = new Rect(parentContent); + if (mFollowAxis == VERTICAL) { + if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint; + if (afterValid) clampRect.top += afterPoint; + } else { + if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint; + if (afterValid) clampRect.left += afterPoint; + } + return clampRect; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mSweep.isStateful()) { + mSweep.setState(getDrawableState()); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + // TODO: handle vertical labels + if (isEnabled() && mLabelLayout != null) { + final int sweepHeight = mSweep.getIntrinsicHeight(); + final int templateHeight = mLabelLayout.getHeight(); + + mSweepOffset.x = 0; + mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset()); + setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight)); + + } else { + mSweepOffset.x = 0; + mSweepOffset.y = 0; + setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight()); + } + + if (mFollowAxis == VERTICAL) { + final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top + - mSweepPadding.bottom; + mMargins.top = -(mSweepPadding.top + (targetHeight / 2)); + mMargins.bottom = 0; + mMargins.left = -mSweepPadding.left; + mMargins.right = mSweepPadding.right; + } else { + final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left + - mSweepPadding.right; + mMargins.left = -(mSweepPadding.left + (targetWidth / 2)); + mMargins.right = 0; + mMargins.top = -mSweepPadding.top; + mMargins.bottom = mSweepPadding.bottom; + } + + mMargins.offset(-mSweepOffset.x, -mSweepOffset.y); + } + + @Override + protected void onDraw(Canvas canvas) { + final int width = getWidth(); + final int height = getHeight(); + + final int labelSize; + if (isEnabled() && mLabelLayout != null) { + mLabelLayout.draw(canvas); + labelSize = mLabelSize; + } else { + labelSize = 0; + } + + if (mFollowAxis == VERTICAL) { + mSweep.setBounds(labelSize, mSweepOffset.y, width, + mSweepOffset.y + mSweep.getIntrinsicHeight()); + } else { + mSweep.setBounds(mSweepOffset.x, labelSize, + mSweepOffset.x + mSweep.getIntrinsicWidth(), height); + } + + mSweep.draw(canvas); + } + +} diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java new file mode 100644 index 0000000..a5b8b09 --- /dev/null +++ b/src/com/android/settings/widget/ChartView.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 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.widget; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +/** + * Container for two-dimensional chart, drawn with a combination of + * {@link ChartGridView}, {@link ChartNetworkSeriesView} and {@link ChartSweepView} + * children. The entire chart uses {@link ChartAxis} to map between raw values + * and screen coordinates. + */ +public class ChartView extends FrameLayout { + private static final String TAG = "ChartView"; + + // TODO: extend something that supports two-dimensional scrolling + + private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.LEFT; + + ChartAxis mHoriz; + ChartAxis mVert; + + private Rect mContent = new Rect(); + + public ChartView(Context context) { + this(context, null, 0); + } + + public ChartView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ChartView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setClipToPadding(false); + setClipChildren(false); + } + + void init(ChartAxis horiz, ChartAxis vert) { + mHoriz = checkNotNull(horiz, "missing horiz"); + mVert = checkNotNull(vert, "missing vert"); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(), + b - t - getPaddingBottom()); + final int width = mContent.width(); + final int height = mContent.height(); + + // no scrolling yet, so tell dimensions to fill exactly + mHoriz.setSize(width); + mVert.setSize(height); + + final Rect parentRect = new Rect(); + final Rect childRect = new Rect(); + + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams params = (LayoutParams) child.getLayoutParams(); + + parentRect.set(mContent); + + if (child instanceof ChartNetworkSeriesView || child instanceof ChartGridView) { + // series are always laid out to fill entire graph area + // TODO: handle scrolling for series larger than content area + Gravity.apply(params.gravity, width, height, parentRect, childRect); + child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); + + } else if (child instanceof ChartSweepView) { + // sweep is always placed along specific dimension + final ChartSweepView sweep = (ChartSweepView) child; + final Rect sweepMargins = sweep.getMargins(); + + if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) { + parentRect.top += sweepMargins.top + (int) sweep.getPoint(); + parentRect.bottom = parentRect.top; + parentRect.left += sweepMargins.left; + parentRect.right += sweepMargins.right; + Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(), + parentRect, childRect); + + } else { + parentRect.left += sweepMargins.left + (int) sweep.getPoint(); + parentRect.right = parentRect.left; + parentRect.top += sweepMargins.top; + parentRect.bottom += sweepMargins.bottom; + Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(), + parentRect, childRect); + } + } + + child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); + } + } + +} diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java new file mode 100644 index 0000000..a1c92e1 --- /dev/null +++ b/src/com/android/settings/widget/DataUsageChartView.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.net.NetworkPolicy; +import android.net.NetworkStatsHistory; +import android.os.Handler; +import android.os.Message; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.android.settings.R; +import com.android.settings.widget.ChartSweepView.OnSweepListener; + +/** + * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along + * with {@link ChartSweepView} for inspection ranges and warning/limits. + */ +public class DataUsageChartView extends ChartView { + + private static final long KB_IN_BYTES = 1024; + private static final long MB_IN_BYTES = KB_IN_BYTES * 1024; + private static final long GB_IN_BYTES = MB_IN_BYTES * 1024; + + private static final int MSG_UPDATE_AXIS = 100; + private static final long DELAY_MILLIS = 250; + + private ChartGridView mGrid; + private ChartNetworkSeriesView mSeries; + private ChartNetworkSeriesView mDetailSeries; + + private NetworkStatsHistory mHistory; + + private ChartSweepView mSweepLeft; + private ChartSweepView mSweepRight; + private ChartSweepView mSweepWarning; + private ChartSweepView mSweepLimit; + + private Handler mHandler; + + /** Current maximum value of {@link #mVert}. */ + private long mVertMax; + + public interface DataUsageChartListener { + public void onInspectRangeChanged(); + public void onWarningChanged(); + public void onLimitChanged(); + } + + private DataUsageChartListener mListener; + + public DataUsageChartView(Context context) { + this(context, null, 0); + } + + public DataUsageChartView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DataUsageChartView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(new TimeAxis(), new InvertedChartAxis(new DataAxis())); + + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + final ChartSweepView sweep = (ChartSweepView) msg.obj; + updateVertAxisBounds(sweep); + updateEstimateVisible(); + + // we keep dispatching repeating updates until sweep is dropped + sendUpdateAxisDelayed(sweep, true); + } + }; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mGrid = (ChartGridView) findViewById(R.id.grid); + mSeries = (ChartNetworkSeriesView) findViewById(R.id.series); + mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series); + mDetailSeries.setVisibility(View.GONE); + + mSweepLeft = (ChartSweepView) findViewById(R.id.sweep_left); + mSweepRight = (ChartSweepView) findViewById(R.id.sweep_right); + mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit); + mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning); + + // prevent sweeps from crossing each other + mSweepLeft.setValidRangeDynamic(null, mSweepRight); + mSweepRight.setValidRangeDynamic(mSweepLeft, null); + mSweepWarning.setValidRangeDynamic(null, mSweepLimit); + mSweepLimit.setValidRangeDynamic(mSweepWarning, null); + + mSweepLeft.addOnSweepListener(mHorizListener); + mSweepRight.addOnSweepListener(mHorizListener); + mSweepWarning.addOnSweepListener(mVertListener); + mSweepLimit.addOnSweepListener(mVertListener); + + // tell everyone about our axis + mGrid.init(mHoriz, mVert); + mSeries.init(mHoriz, mVert); + mDetailSeries.init(mHoriz, mVert); + mSweepLeft.init(mHoriz); + mSweepRight.init(mHoriz); + mSweepWarning.init(mVert); + mSweepLimit.init(mVert); + + setActivated(false); + } + + public void setListener(DataUsageChartListener listener) { + mListener = listener; + } + + public void bindNetworkStats(NetworkStatsHistory stats) { + mSeries.bindNetworkStats(stats); + mHistory = stats; + updateVertAxisBounds(null); + updateEstimateVisible(); + updatePrimaryRange(); + requestLayout(); + } + + public void bindDetailNetworkStats(NetworkStatsHistory stats) { + mDetailSeries.bindNetworkStats(stats); + mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE); + if (mHistory != null) { + mDetailSeries.setEndTime(mHistory.getEnd()); + } + updateVertAxisBounds(null); + updateEstimateVisible(); + updatePrimaryRange(); + requestLayout(); + } + + public void bindNetworkPolicy(NetworkPolicy policy) { + if (policy == null) { + mSweepLimit.setVisibility(View.INVISIBLE); + mSweepLimit.setValue(-1); + mSweepWarning.setVisibility(View.INVISIBLE); + mSweepWarning.setValue(-1); + return; + } + + if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) { + mSweepLimit.setVisibility(View.VISIBLE); + mSweepLimit.setEnabled(true); + mSweepLimit.setValue(policy.limitBytes); + } else { + mSweepLimit.setVisibility(View.VISIBLE); + mSweepLimit.setEnabled(false); + mSweepLimit.setValue(-1); + } + + if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) { + mSweepWarning.setVisibility(View.VISIBLE); + mSweepWarning.setValue(policy.warningBytes); + } else { + mSweepWarning.setVisibility(View.INVISIBLE); + mSweepWarning.setValue(-1); + } + + updateVertAxisBounds(null); + requestLayout(); + } + + /** + * Update {@link #mVert} to both show data from {@link NetworkStatsHistory} + * and controls from {@link NetworkPolicy}. + */ + private void updateVertAxisBounds(ChartSweepView activeSweep) { + final long max = mVertMax; + final long newMax; + if (activeSweep != null) { + final int adjustAxis = activeSweep.shouldAdjustAxis(); + if (adjustAxis > 0) { + // hovering around upper edge, grow axis + newMax = max * 11 / 10; + } else if (adjustAxis < 0) { + // hovering around lower edge, shrink axis + newMax = max * 9 / 10; + } else { + newMax = max; + } + + } else { + // try showing all known data and policy + final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue()); + final long maxVisible = Math.max(mSeries.getMaxVisible(), maxSweep) * 12 / 10; + newMax = Math.max(maxVisible, 2 * GB_IN_BYTES); + } + + // only invalidate when vertMax actually changed + if (newMax != mVertMax) { + mVertMax = newMax; + + mVert.setBounds(0L, newMax); + mSweepWarning.setValidRange(0L, newMax); + mSweepLimit.setValidRange(0L, newMax); + + mSeries.generatePath(); + mDetailSeries.generatePath(); + + mGrid.invalidate(); + mSeries.invalidate(); + mDetailSeries.invalidate(); + + // since we just changed axis, make sweep recalculate its value + if (activeSweep != null) { + activeSweep.updateValueFromPosition(); + } + } + } + + /** + * Control {@link ChartNetworkSeriesView#setEstimateVisible(boolean)} based + * on how close estimate comes to {@link #mSweepWarning}. + */ + private void updateEstimateVisible() { + final long maxEstimate = mSeries.getMaxEstimate(); + + // show estimate when near warning/limit + long interestLine = Long.MAX_VALUE; + if (mSweepWarning.isEnabled()) { + interestLine = mSweepWarning.getValue(); + } else if (mSweepLimit.isEnabled()) { + interestLine = mSweepLimit.getValue(); + } + + final boolean estimateVisible = (maxEstimate >= interestLine * 7 / 10); + mSeries.setEstimateVisible(estimateVisible); + } + + private OnSweepListener mHorizListener = new OnSweepListener() { + public void onSweep(ChartSweepView sweep, boolean sweepDone) { + updatePrimaryRange(); + + // update detail list only when done sweeping + if (sweepDone && mListener != null) { + mListener.onInspectRangeChanged(); + } + } + }; + + private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) { + if (force || !mHandler.hasMessages(MSG_UPDATE_AXIS, sweep)) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_UPDATE_AXIS, sweep), DELAY_MILLIS); + } + } + + private void clearUpdateAxisDelayed(ChartSweepView sweep) { + mHandler.removeMessages(MSG_UPDATE_AXIS, sweep); + } + + private OnSweepListener mVertListener = new OnSweepListener() { + public void onSweep(ChartSweepView sweep, boolean sweepDone) { + if (sweepDone) { + clearUpdateAxisDelayed(sweep); + updateEstimateVisible(); + + if (sweep == mSweepWarning && mListener != null) { + mListener.onWarningChanged(); + } else if (sweep == mSweepLimit && mListener != null) { + mListener.onLimitChanged(); + } + } else { + // while moving, kick off delayed grow/shrink axis updates + sendUpdateAxisDelayed(sweep, false); + } + } + }; + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isActivated()) return false; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + return true; + } + case MotionEvent.ACTION_UP: { + setActivated(true); + return true; + } + default: { + return false; + } + } + } + + public long getInspectStart() { + return mSweepLeft.getValue(); + } + + public long getInspectEnd() { + return mSweepRight.getValue(); + } + + public long getWarningBytes() { + return mSweepWarning.getValue(); + } + + public long getLimitBytes() { + return mSweepLimit.getValue(); + } + + private long getStatsStart() { + return mHistory != null ? mHistory.getStart() : Long.MIN_VALUE; + } + + private long getStatsEnd() { + return mHistory != null ? mHistory.getEnd() : Long.MAX_VALUE; + } + + /** + * Set the exact time range that should be displayed, updating how + * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the + * last "week" of available data, without triggering listener events. + */ + public void setVisibleRange(long visibleStart, long visibleEnd) { + mHoriz.setBounds(visibleStart, visibleEnd); + mGrid.setBounds(visibleStart, visibleEnd); + + final long validStart = Math.max(visibleStart, getStatsStart()); + final long validEnd = Math.min(visibleEnd, getStatsEnd()); + + // prevent time sweeps from leaving valid data + mSweepLeft.setValidRange(validStart, validEnd); + mSweepRight.setValidRange(validStart, validEnd); + + // default sweeps to last week of data + final long halfRange = (visibleEnd + visibleStart) / 2; + final long sweepMax = validEnd; + final long sweepMin = Math.max(visibleStart, (sweepMax - DateUtils.WEEK_IN_MILLIS)); + + mSweepLeft.setValue(sweepMin); + mSweepRight.setValue(sweepMax); + + requestLayout(); + mSeries.generatePath(); + mSeries.invalidate(); + + updateVertAxisBounds(null); + updateEstimateVisible(); + updatePrimaryRange(); + } + + private void updatePrimaryRange() { + final long left = mSweepLeft.getValue(); + final long right = mSweepRight.getValue(); + + // prefer showing primary range on detail series, when available + if (mDetailSeries.getVisibility() == View.VISIBLE) { + mDetailSeries.setPrimaryRange(left, right); + mSeries.setPrimaryRange(0, 0); + } else { + mSeries.setPrimaryRange(left, right); + } + } + + public static class TimeAxis implements ChartAxis { + private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7; + + private long mMin; + private long mMax; + private float mSize; + + public TimeAxis() { + final long currentTime = System.currentTimeMillis(); + setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime); + } + + /** {@inheritDoc} */ + public void setBounds(long min, long max) { + mMin = min; + mMax = max; + } + + /** {@inheritDoc} */ + public void setSize(float size) { + this.mSize = size; + } + + /** {@inheritDoc} */ + public float convertToPoint(long value) { + return (mSize * (value - mMin)) / (mMax - mMin); + } + + /** {@inheritDoc} */ + public long convertToValue(float point) { + return (long) (mMin + ((point * (mMax - mMin)) / mSize)); + } + + /** {@inheritDoc} */ + public void buildLabel(Resources res, SpannableStringBuilder builder, long value) { + // TODO: convert to better string + builder.replace(0, builder.length(), Long.toString(value)); + } + + /** {@inheritDoc} */ + public float[] getTickPoints() { + // tick mark for every week + final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL); + final float[] tickPoints = new float[tickCount]; + for (int i = 0; i < tickCount; i++) { + tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * i)); + } + return tickPoints; + } + + /** {@inheritDoc} */ + public int shouldAdjustAxis(long value) { + // time axis never adjusts + return 0; + } + } + + public static class DataAxis implements ChartAxis { + private long mMin; + private long mMax; + private float mSize; + + /** {@inheritDoc} */ + public void setBounds(long min, long max) { + mMin = min; + mMax = max; + } + + /** {@inheritDoc} */ + public void setSize(float size) { + mSize = size; + } + + /** {@inheritDoc} */ + public float convertToPoint(long value) { + // derived polynomial fit to make lower values more visible + final double normalized = ((double) value - mMin) / (mMax - mMin); + final double fraction = Math.pow( + 10, 0.36884343106175121463 * Math.log10(normalized) + -0.04328199452018252624); + return (float) (fraction * mSize); + } + + /** {@inheritDoc} */ + public long convertToValue(float point) { + final double normalized = point / mSize; + final double fraction = 1.3102228476089056629 + * Math.pow(normalized, 2.7111774693164631640); + return (long) (mMin + (fraction * (mMax - mMin))); + } + + private static final Object sSpanSize = new Object(); + private static final Object sSpanUnit = new Object(); + + /** {@inheritDoc} */ + public void buildLabel(Resources res, SpannableStringBuilder builder, long value) { + + float result = value; + final CharSequence unit; + if (result <= 100 * MB_IN_BYTES) { + unit = res.getText(com.android.internal.R.string.megabyteShort); + result /= MB_IN_BYTES; + } else { + unit = res.getText(com.android.internal.R.string.gigabyteShort); + result /= GB_IN_BYTES; + } + + final CharSequence size; + if (result < 10) { + size = String.format("%.1f", result); + } else { + size = String.format("%.0f", result); + } + + final int[] sizeBounds = findOrCreateSpan(builder, sSpanSize, "^1"); + builder.replace(sizeBounds[0], sizeBounds[1], size); + final int[] unitBounds = findOrCreateSpan(builder, sSpanUnit, "^2"); + builder.replace(unitBounds[0], unitBounds[1], unit); + } + + /** {@inheritDoc} */ + public float[] getTickPoints() { + final long range = mMax - mMin; + final long tickJump = 256 * MB_IN_BYTES; + + final int tickCount = (int) (range / tickJump); + final float[] tickPoints = new float[tickCount]; + long value = mMin; + for (int i = 0; i < tickPoints.length; i++) { + tickPoints[i] = convertToPoint(value); + value += tickJump; + } + + return tickPoints; + } + + /** {@inheritDoc} */ + public int shouldAdjustAxis(long value) { + final float point = convertToPoint(value); + if (point < mSize * 0.1) { + return -1; + } else if (point > mSize * 0.85) { + return 1; + } else { + return 0; + } + } + } + + private static int[] findOrCreateSpan( + SpannableStringBuilder builder, Object key, CharSequence bootstrap) { + int start = builder.getSpanStart(key); + int end = builder.getSpanEnd(key); + if (start == -1) { + start = TextUtils.indexOf(builder, bootstrap); + end = start + bootstrap.length(); + builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + return new int[] { start, end }; + } + +} diff --git a/src/com/android/settings/widget/InvertedChartAxis.java b/src/com/android/settings/widget/InvertedChartAxis.java new file mode 100644 index 0000000..96aec7b --- /dev/null +++ b/src/com/android/settings/widget/InvertedChartAxis.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.res.Resources; +import android.text.SpannableStringBuilder; + +/** + * Utility to invert another {@link ChartAxis}. + */ +public class InvertedChartAxis implements ChartAxis { + private final ChartAxis mWrapped; + private float mSize; + + public InvertedChartAxis(ChartAxis wrapped) { + mWrapped = wrapped; + } + + /** {@inheritDoc} */ + public void setBounds(long min, long max) { + mWrapped.setBounds(min, max); + } + + /** {@inheritDoc} */ + public void setSize(float size) { + mSize = size; + mWrapped.setSize(size); + } + + /** {@inheritDoc} */ + public float convertToPoint(long value) { + return mSize - mWrapped.convertToPoint(value); + } + + /** {@inheritDoc} */ + public long convertToValue(float point) { + return mWrapped.convertToValue(mSize - point); + } + + /** {@inheritDoc} */ + public void buildLabel(Resources res, SpannableStringBuilder builder, long value) { + mWrapped.buildLabel(res, builder, value); + } + + /** {@inheritDoc} */ + public float[] getTickPoints() { + final float[] points = mWrapped.getTickPoints(); + for (int i = 0; i < points.length; i++) { + points[i] = mSize - points[i]; + } + return points; + } + + /** {@inheritDoc} */ + public int shouldAdjustAxis(long value) { + return mWrapped.shouldAdjustAxis(value); + } +} diff --git a/src/com/android/settings/wifi/AccessPoint.java b/src/com/android/settings/wifi/AccessPoint.java index 774ac58..8181746 100644 --- a/src/com/android/settings/wifi/AccessPoint.java +++ b/src/com/android/settings/wifi/AccessPoint.java @@ -25,6 +25,7 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.KeyMgmt; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.Bundle; import android.preference.Preference; import android.view.View; import android.widget.ImageView; @@ -32,6 +33,12 @@ import android.widget.ImageView; import java.util.Comparator; class AccessPoint extends Preference { + + private static final String KEY_DETAILEDSTATE = "key_detailedstate"; + private static final String KEY_WIFIINFO = "key_wifiinfo"; + private static final String KEY_SCANRESULT = "key_scanresult"; + private static final String KEY_CONFIG = "key_config"; + private static final int[] STATE_SECURED = {R.attr.state_encrypted}; private static final int[] STATE_NONE = {}; @@ -41,13 +48,15 @@ class AccessPoint extends Preference { static final int SECURITY_PSK = 2; static final int SECURITY_EAP = 3; - final String ssid; - final String bssid; - final int security; - final int networkId; + String ssid; + String bssid; + int security; + int networkId; boolean wpsAvailable = false; private WifiConfiguration mConfig; + /*package*/ScanResult mScanResult; + private int mRssi; private WifiInfo mInfo; private DetailedState mState; @@ -78,24 +87,60 @@ class AccessPoint extends Preference { AccessPoint(Context context, WifiConfiguration config) { super(context); setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + loadConfig(config); + } + + AccessPoint(Context context, ScanResult result) { + super(context); + setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + loadResult(result); + } + + AccessPoint(Context context, Bundle savedState) { + super(context); + setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + + mConfig = savedState.getParcelable(KEY_CONFIG); + if (mConfig != null) { + loadConfig(mConfig); + } + mScanResult = (ScanResult) savedState.getParcelable(KEY_SCANRESULT); + if (mScanResult != null) { + loadResult(mScanResult); + } + mInfo = (WifiInfo) savedState.getParcelable(KEY_WIFIINFO); + if (savedState.containsKey(KEY_DETAILEDSTATE)) { + mState = DetailedState.valueOf(savedState.getString(KEY_DETAILEDSTATE)); + } + update(mInfo, mState); + } + + public void saveWifiState(Bundle savedState) { + savedState.putParcelable(KEY_CONFIG, mConfig); + savedState.putParcelable(KEY_SCANRESULT, mScanResult); + savedState.putParcelable(KEY_WIFIINFO, mInfo); + if (mState != null) { + savedState.putString(KEY_DETAILEDSTATE, mState.toString()); + } + } + + private void loadConfig(WifiConfiguration config) { ssid = (config.SSID == null ? "" : removeDoubleQuotes(config.SSID)); bssid = config.BSSID; security = getSecurity(config); networkId = config.networkId; - mConfig = config; mRssi = Integer.MAX_VALUE; + mConfig = config; } - AccessPoint(Context context, ScanResult result) { - super(context); - setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + private void loadResult(ScanResult result) { ssid = result.SSID; bssid = result.BSSID; security = getSecurity(result); - wpsAvailable = security != SECURITY_EAP && - result.capabilities.contains("WPS"); + wpsAvailable = security != SECURITY_EAP && result.capabilities.contains("WPS"); networkId = -1; mRssi = result.level; + mScanResult = result; } @Override diff --git a/src/com/android/settings/wifi/AdvancedSettings.java b/src/com/android/settings/wifi/AdvancedSettings.java deleted file mode 100644 index faea9f2..0000000 --- a/src/com/android/settings/wifi/AdvancedSettings.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2007 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.wifi; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; - -import android.app.Activity; -import android.content.Context; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import android.os.SystemProperties; -import android.preference.ListPreference; -import android.preference.Preference; -import android.text.TextUtils; -import android.widget.Toast; -import android.util.Log; - -public class AdvancedSettings extends SettingsPreferenceFragment - implements Preference.OnPreferenceChangeListener { - - private static final String TAG = "AdvancedSettings"; - private static final String KEY_MAC_ADDRESS = "mac_address"; - private static final String KEY_CURRENT_IP_ADDRESS = "current_ip_address"; - private static final String KEY_FREQUENCY_BAND = "frequency_band"; - - private WifiManager mWifiManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.wifi_advanced_settings); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); - } - - @Override - public void onResume() { - super.onResume(); - initPreferences(); - refreshWifiInfo(); - } - - private void initPreferences() { - - ListPreference pref = (ListPreference) findPreference(KEY_FREQUENCY_BAND); - - if (mWifiManager.isDualBandSupported()) { - pref.setOnPreferenceChangeListener(this); - int value = mWifiManager.getFrequencyBand(); - if (value != -1) { - pref.setValue(String.valueOf(value)); - } else { - Log.e(TAG, "Failed to fetch frequency band"); - } - } else { - getPreferenceScreen().removePreference(pref); - } - } - - public boolean onPreferenceChange(Preference preference, Object newValue) { - String key = preference.getKey(); - if (key == null) return true; - - if (key.equals(KEY_FREQUENCY_BAND)) { - try { - mWifiManager.setFrequencyBand(Integer.parseInt(((String) newValue)), true); - } catch (NumberFormatException e) { - Toast.makeText(getActivity(), R.string.wifi_setting_frequency_band_error, - Toast.LENGTH_SHORT).show(); - return false; - } - } - - return true; - } - - private void refreshWifiInfo() { - WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); - - Preference wifiMacAddressPref = findPreference(KEY_MAC_ADDRESS); - String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); - wifiMacAddressPref.setSummary(!TextUtils.isEmpty(macAddress) ? macAddress - : getActivity().getString(R.string.status_unavailable)); - - Preference wifiIpAddressPref = findPreference(KEY_CURRENT_IP_ADDRESS); - String ipAddress = Utils.getWifiIpAddresses(getActivity()); - wifiIpAddressPref.setSummary(ipAddress == null ? - getActivity().getString(R.string.status_unavailable) : ipAddress); - } - -} diff --git a/src/com/android/settings/wifi/AdvancedWifiSettings.java b/src/com/android/settings/wifi/AdvancedWifiSettings.java new file mode 100644 index 0000000..6c983fd --- /dev/null +++ b/src/com/android/settings/wifi/AdvancedWifiSettings.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2011 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.wifi; + +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; + +public class AdvancedWifiSettings extends SettingsPreferenceFragment + implements Preference.OnPreferenceChangeListener { + + private static final String TAG = "AdvancedWifiSettings"; + private static final String KEY_MAC_ADDRESS = "mac_address"; + private static final String KEY_CURRENT_IP_ADDRESS = "current_ip_address"; + private static final String KEY_FREQUENCY_BAND = "frequency_band"; + private static final String KEY_NOTIFY_OPEN_NETWORKS = "notify_open_networks"; + private static final String KEY_SLEEP_POLICY = "sleep_policy"; + private static final String KEY_ENABLE_WIFI_WATCHDOG = "wifi_enable_watchdog_service"; + + private WifiManager mWifiManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.wifi_advanced_settings); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + } + + @Override + public void onResume() { + super.onResume(); + initPreferences(); + refreshWifiInfo(); + } + + private void initPreferences() { + CheckBoxPreference notifyOpenNetworks = + (CheckBoxPreference) findPreference(KEY_NOTIFY_OPEN_NETWORKS); + notifyOpenNetworks.setChecked(Secure.getInt(getContentResolver(), + Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 0) == 1); + notifyOpenNetworks.setEnabled(mWifiManager.isWifiEnabled()); + + CheckBoxPreference watchdogEnabled = + (CheckBoxPreference) findPreference(KEY_ENABLE_WIFI_WATCHDOG); + watchdogEnabled.setChecked(Secure.getInt(getContentResolver(), + Secure.WIFI_WATCHDOG_ON, 1) == 1); + + watchdogEnabled.setEnabled(mWifiManager.isWifiEnabled()); + + ListPreference frequencyPref = (ListPreference) findPreference(KEY_FREQUENCY_BAND); + + if (mWifiManager.isDualBandSupported()) { + frequencyPref.setOnPreferenceChangeListener(this); + int value = mWifiManager.getFrequencyBand(); + if (value != -1) { + frequencyPref.setValue(String.valueOf(value)); + } else { + Log.e(TAG, "Failed to fetch frequency band"); + } + } else { + if (frequencyPref != null) { + // null if it has already been removed before resume + getPreferenceScreen().removePreference(frequencyPref); + } + } + + ListPreference sleepPolicyPref = (ListPreference) findPreference(KEY_SLEEP_POLICY); + if (sleepPolicyPref != null) { + if (Utils.isWifiOnly()) { + sleepPolicyPref.setEntries(R.array.wifi_sleep_policy_entries_wifi_only); + sleepPolicyPref.setSummary(R.string.wifi_setting_sleep_policy_summary_wifi_only); + } + sleepPolicyPref.setOnPreferenceChangeListener(this); + int value = Settings.System.getInt(getContentResolver(), + Settings.System.WIFI_SLEEP_POLICY, + Settings.System.WIFI_SLEEP_POLICY_NEVER); + sleepPolicyPref.setValue(String.valueOf(value)); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + String key = preference.getKey(); + + if (KEY_NOTIFY_OPEN_NETWORKS.equals(key)) { + Secure.putInt(getContentResolver(), + Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, + ((CheckBoxPreference) preference).isChecked() ? 1 : 0); + } else if (KEY_ENABLE_WIFI_WATCHDOG.equals(key)) { + Secure.putInt(getContentResolver(), + Secure.WIFI_WATCHDOG_ON, + ((CheckBoxPreference) preference).isChecked() ? 1 : 0); + } else { + return super.onPreferenceTreeClick(screen, preference); + } + return true; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + + if (KEY_FREQUENCY_BAND.equals(key)) { + try { + mWifiManager.setFrequencyBand(Integer.parseInt(((String) newValue)), true); + } catch (NumberFormatException e) { + Toast.makeText(getActivity(), R.string.wifi_setting_frequency_band_error, + Toast.LENGTH_SHORT).show(); + return false; + } + } + + if (KEY_SLEEP_POLICY.equals(key)) { + try { + Settings.System.putInt(getContentResolver(), + Settings.System.WIFI_SLEEP_POLICY, Integer.parseInt(((String) newValue))); + } catch (NumberFormatException e) { + Toast.makeText(getActivity(), R.string.wifi_setting_sleep_policy_error, + Toast.LENGTH_SHORT).show(); + return false; + } + } + + return true; + } + + private void refreshWifiInfo() { + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + + Preference wifiMacAddressPref = findPreference(KEY_MAC_ADDRESS); + String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); + wifiMacAddressPref.setSummary(!TextUtils.isEmpty(macAddress) ? macAddress + : getActivity().getString(R.string.status_unavailable)); + + Preference wifiIpAddressPref = findPreference(KEY_CURRENT_IP_ADDRESS); + String ipAddress = Utils.getWifiIpAddresses(getActivity()); + wifiIpAddressPref.setSummary(ipAddress == null ? + getActivity().getString(R.string.status_unavailable) : ipAddress); + } + +} diff --git a/src/com/android/settings/wifi/WifiConfigController.java b/src/com/android/settings/wifi/WifiConfigController.java index 91f4110..876fd99 100644 --- a/src/com/android/settings/wifi/WifiConfigController.java +++ b/src/com/android/settings/wifi/WifiConfigController.java @@ -16,33 +16,29 @@ package com.android.settings.wifi; +import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID; + import android.content.Context; -import android.content.DialogInterface; import android.content.res.Resources; -import android.net.DhcpInfo; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.NetworkInfo.DetailedState; import android.net.NetworkUtils; -import android.net.Proxy; import android.net.ProxyProperties; import android.net.RouteInfo; import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiConfiguration.IpAssignment; import android.net.wifi.WifiConfiguration.AuthAlgorithm; +import android.net.wifi.WifiConfiguration.IpAssignment; import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WpsConfiguration; -import android.net.wifi.WpsConfiguration.Setup; - -import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID; import android.net.wifi.WifiConfiguration.ProxySettings; import android.net.wifi.WifiInfo; +import android.net.wifi.WpsConfiguration; +import android.net.wifi.WpsConfiguration.Setup; import android.security.Credentials; import android.security.KeyStore; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; -import android.text.format.Formatter; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -57,12 +53,10 @@ import com.android.settings.ProxySelector; import com.android.settings.R; import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.util.Iterator; /** - * The class for allowing UIs like {@link WifiDialog} and {@link WifiConfigPreference} to + * The class for allowing UIs like {@link WifiDialog} and {@link WifiConfigUiBase} to * share the logic for controlling buttons, text fields, etc. */ public class WifiConfigController implements TextWatcher, @@ -96,8 +90,8 @@ public class WifiConfigController implements TextWatcher, /* These values come from "wifi_network_setup" resource array */ public static final int MANUAL = 0; public static final int WPS_PBC = 1; - public static final int WPS_PIN_FROM_ACCESS_POINT = 2; - public static final int WPS_PIN_FROM_DEVICE = 3; + public static final int WPS_KEYPAD = 2; + public static final int WPS_DISPLAY = 3; /* These values come from "wifi_proxy_settings" resource array */ public static final int PROXY_NONE = 0; @@ -118,8 +112,8 @@ public class WifiConfigController implements TextWatcher, private TextView mProxyPortView; private TextView mProxyExclusionListView; - private IpAssignment mIpAssignment; - private ProxySettings mProxySettings; + private IpAssignment mIpAssignment = IpAssignment.UNASSIGNED; + private ProxySettings mProxySettings = ProxySettings.UNASSIGNED; private LinkProperties mLinkProperties = new LinkProperties(); // True when this instance is used in SetupWizard XL context. @@ -429,7 +423,9 @@ public class WifiConfigController implements TextWatcher, int networkPrefixLength = -1; try { networkPrefixLength = Integer.parseInt(mNetworkPrefixLengthView.getText().toString()); - } catch (NumberFormatException e) { } + } catch (NumberFormatException e) { + // Use -1 + } if (networkPrefixLength < 0 || networkPrefixLength > 32) { return R.string.wifi_ip_settings_invalid_network_prefix_length; } @@ -477,11 +473,11 @@ public class WifiConfigController implements TextWatcher, case WPS_PBC: config.setup = Setup.PBC; break; - case WPS_PIN_FROM_ACCESS_POINT: - config.setup = Setup.PIN_FROM_ACCESS_POINT; + case WPS_KEYPAD: + config.setup = Setup.KEYPAD; break; - case WPS_PIN_FROM_DEVICE: - config.setup = Setup.PIN_FROM_DEVICE; + case WPS_DISPLAY: + config.setup = Setup.DISPLAY; break; default: config.setup = Setup.INVALID; @@ -563,14 +559,14 @@ public class WifiConfigController implements TextWatcher, int pos = mNetworkSetupSpinner.getSelectedItemPosition(); /* Show pin text input if needed */ - if (pos == WPS_PIN_FROM_ACCESS_POINT) { + if (pos == WPS_DISPLAY) { mView.findViewById(R.id.wps_fields).setVisibility(View.VISIBLE); } else { mView.findViewById(R.id.wps_fields).setVisibility(View.GONE); } /* show/hide manual security fields appropriately */ - if ((pos == WPS_PIN_FROM_ACCESS_POINT) || (pos == WPS_PIN_FROM_DEVICE) + if ((pos == WPS_DISPLAY) || (pos == WPS_KEYPAD) || (pos == WPS_PBC)) { mView.findViewById(R.id.security_fields).setVisibility(View.GONE); } else { @@ -698,6 +694,7 @@ public class WifiConfigController implements TextWatcher, private void setSelection(Spinner spinner, String value) { if (value != null) { + @SuppressWarnings("unchecked") ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinner.getAdapter(); for (int i = adapter.getCount() - 1; i >= 0; --i) { if (value.equals(adapter.getItem(i))) { @@ -719,10 +716,12 @@ public class WifiConfigController implements TextWatcher, @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // work done in afterTextChanged } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + // work done in afterTextChanged } @Override @@ -750,5 +749,6 @@ public class WifiConfigController implements TextWatcher, @Override public void onNothingSelected(AdapterView<?> parent) { + // } } diff --git a/src/com/android/settings/wifi/WifiEnabler.java b/src/com/android/settings/wifi/WifiEnabler.java index 7f1221e..223022d 100644 --- a/src/com/android/settings/wifi/WifiEnabler.java +++ b/src/com/android/settings/wifi/WifiEnabler.java @@ -16,9 +16,6 @@ package com.android.settings.wifi; -import com.android.settings.R; -import com.android.settings.WirelessSettings; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,19 +24,19 @@ import android.net.NetworkInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.preference.Preference; -import android.preference.CheckBoxPreference; import android.provider.Settings; -import android.text.TextUtils; +import android.widget.CompoundButton; +import android.widget.Switch; import android.widget.Toast; +import com.android.settings.R; +import com.android.settings.WirelessSettings; + import java.util.concurrent.atomic.AtomicBoolean; -public class WifiEnabler implements Preference.OnPreferenceChangeListener { +public class WifiEnabler implements CompoundButton.OnCheckedChangeListener { private final Context mContext; - private final CheckBoxPreference mCheckBox; - private final CharSequence mOriginalSummary; - + private Switch mSwitch; private AtomicBoolean mConnected = new AtomicBoolean(false); private final WifiManager mWifiManager; @@ -65,11 +62,9 @@ public class WifiEnabler implements Preference.OnPreferenceChangeListener { } }; - public WifiEnabler(Context context, CheckBoxPreference checkBox) { + public WifiEnabler(Context context, Switch switch_) { mContext = context; - mCheckBox = checkBox; - mOriginalSummary = checkBox.getSummary(); - checkBox.setPersistent(false); + mSwitch = switch_; mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mIntentFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); @@ -81,78 +76,86 @@ public class WifiEnabler implements Preference.OnPreferenceChangeListener { public void resume() { // Wi-Fi state is sticky, so just let the receiver update UI mContext.registerReceiver(mReceiver, mIntentFilter); - mCheckBox.setOnPreferenceChangeListener(this); + mSwitch.setOnCheckedChangeListener(this); } public void pause() { mContext.unregisterReceiver(mReceiver); - mCheckBox.setOnPreferenceChangeListener(null); + mSwitch.setOnCheckedChangeListener(null); } - public boolean onPreferenceChange(Preference preference, Object value) { - boolean enable = (Boolean) value; + public void setSwitch(Switch switch_) { + if (mSwitch == switch_) return; + mSwitch.setOnCheckedChangeListener(null); + mSwitch = switch_; + mSwitch.setOnCheckedChangeListener(this); + + final int wifiState = mWifiManager.getWifiState(); + boolean isEnabled = wifiState == WifiManager.WIFI_STATE_ENABLED; + boolean isDisabled = wifiState == WifiManager.WIFI_STATE_DISABLED; + mSwitch.setChecked(isEnabled); + mSwitch.setEnabled(isEnabled || isDisabled); + } + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Show toast message if Wi-Fi is not allowed in airplane mode - if (enable && !WirelessSettings - .isRadioAllowed(mContext, Settings.System.RADIO_WIFI)) { - Toast.makeText(mContext, R.string.wifi_in_airplane_mode, - Toast.LENGTH_SHORT).show(); - return false; + if (isChecked && !WirelessSettings.isRadioAllowed(mContext, Settings.System.RADIO_WIFI)) { + Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show(); + // Reset switch to off. No infinite check/listenenr loop. + buttonView.setChecked(false); } - /** - * Disable tethering if enabling Wifi - */ + // Disable tethering if enabling Wifi int wifiApState = mWifiManager.getWifiApState(); - if (enable && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || + if (isChecked && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { mWifiManager.setWifiApEnabled(null, false); } - if (mWifiManager.setWifiEnabled(enable)) { - mCheckBox.setEnabled(false); + + if (mWifiManager.setWifiEnabled(isChecked)) { + // Intent has been taken into account, disable until new state is active + mSwitch.setEnabled(false); } else { - mCheckBox.setSummary(R.string.wifi_error); + // Error + Toast.makeText(mContext, R.string.wifi_error, Toast.LENGTH_SHORT).show(); } - - // Don't update UI to opposite state until we're sure - return false; } private void handleWifiStateChanged(int state) { switch (state) { case WifiManager.WIFI_STATE_ENABLING: - mCheckBox.setSummary(R.string.wifi_starting); - mCheckBox.setEnabled(false); + mSwitch.setEnabled(false); break; case WifiManager.WIFI_STATE_ENABLED: - mCheckBox.setChecked(true); - mCheckBox.setSummary(null); - mCheckBox.setEnabled(true); + mSwitch.setChecked(true); + mSwitch.setEnabled(true); break; case WifiManager.WIFI_STATE_DISABLING: - mCheckBox.setSummary(R.string.wifi_stopping); - mCheckBox.setEnabled(false); + mSwitch.setEnabled(false); break; case WifiManager.WIFI_STATE_DISABLED: - mCheckBox.setChecked(false); - mCheckBox.setSummary(mOriginalSummary); - mCheckBox.setEnabled(true); + mSwitch.setChecked(false); + mSwitch.setEnabled(true); break; default: - mCheckBox.setChecked(false); - mCheckBox.setSummary(R.string.wifi_error); - mCheckBox.setEnabled(true); + mSwitch.setChecked(false); + mSwitch.setEnabled(true); } } - private void handleStateChanged(NetworkInfo.DetailedState state) { + private void handleStateChanged(@SuppressWarnings("unused") NetworkInfo.DetailedState state) { + // After the refactoring from a CheckBoxPreference to a Switch, this method is useless since + // there is nowhere to display a summary. + // This code is kept in case a future change re-introduces an associated text. + /* // WifiInfo is valid if and only if Wi-Fi is enabled. - // Here we use the state of the check box as an optimization. - if (state != null && mCheckBox.isChecked()) { + // Here we use the state of the switch as an optimization. + if (state != null && mSwitch.isChecked()) { WifiInfo info = mWifiManager.getConnectionInfo(); if (info != null) { - mCheckBox.setSummary(Summary.get(mContext, info.getSSID(), state)); + //setSummary(Summary.get(mContext, info.getSSID(), state)); } } + */ } } diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java index ab5e686..b3259e0 100644 --- a/src/com/android/settings/wifi/WifiSettings.java +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -18,8 +18,10 @@ package com.android.settings.wifi; import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID; +import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -31,40 +33,36 @@ import android.net.NetworkInfo.DetailedState; import android.net.wifi.ScanResult; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.KeyMgmt; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WpsResult; -import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WpsConfiguration; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.preference.CheckBoxPreference; import android.preference.Preference; -import android.preference.ListPreference; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; -import android.provider.Settings.Secure; -import android.provider.Settings; import android.security.Credentials; import android.security.KeyStore; import android.util.Log; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ContextMenu.ContextMenuInfo; -import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; import com.android.internal.util.AsyncChannel; -import com.android.settings.ProgressCategoryBase; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; import java.util.ArrayList; import java.util.Collection; @@ -83,14 +81,20 @@ import java.util.concurrent.atomic.AtomicBoolean; * other decorations specific to that screen. */ public class WifiSettings extends SettingsPreferenceFragment - implements DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener { + implements DialogInterface.OnClickListener { private static final String TAG = "WifiSettings"; private static final int MENU_ID_SCAN = Menu.FIRST; - private static final int MENU_ID_ADVANCED = Menu.FIRST + 1; - private static final int MENU_ID_CONNECT = Menu.FIRST + 2; - private static final int MENU_ID_FORGET = Menu.FIRST + 3; - private static final int MENU_ID_MODIFY = Menu.FIRST + 4; - private static final String KEY_SLEEP_POLICY = "sleep_policy"; + private static final int MENU_ID_ADD_NETWORK = Menu.FIRST + 1; + private static final int MENU_ID_ADVANCED = Menu.FIRST + 2; + private static final int MENU_ID_CONNECT = Menu.FIRST + 3; + private static final int MENU_ID_FORGET = Menu.FIRST + 4; + private static final int MENU_ID_MODIFY = Menu.FIRST + 5; + + private static final int WIFI_DIALOG_ID = 1; + + // Instance state keys + private static final String SAVE_DIALOG_EDIT_MODE = "edit_mode"; + private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; private final IntentFilter mFilter; private final BroadcastReceiver mReceiver; @@ -98,12 +102,8 @@ public class WifiSettings extends SettingsPreferenceFragment private WifiManager mWifiManager; private WifiEnabler mWifiEnabler; - private CheckBoxPreference mNotifyOpenNetworks; - private ProgressCategoryBase mAccessPoints; - private Preference mAddNetwork; // An access point being editted is stored here. private AccessPoint mSelectedAccessPoint; - private boolean mEdit; private DetailedState mLastState; private WifiInfo mLastInfo; @@ -114,6 +114,9 @@ public class WifiSettings extends SettingsPreferenceFragment private WifiDialog mDialog; + private View mView; + private TextView mEmptyView; + /* Used in Wifi Setup context */ // this boolean extra specifies whether to disable the Next button when not connected @@ -123,6 +126,11 @@ public class WifiSettings extends SettingsPreferenceFragment private boolean mEnableNextOnConnection; private boolean mInXlSetupWizard; + // Save the dialog details + private boolean mDlgEdit; + private AccessPoint mDlgAccessPoint; + private Bundle mAccessPointSavedState; + /* End of "used in Wifi Setup context" */ public WifiSettings() { @@ -157,11 +165,8 @@ public class WifiSettings extends SettingsPreferenceFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (mInXlSetupWizard) { - return inflater.inflate(R.layout.custom_preference_list_fragment, container, false); - } else { - return super.onCreateView(inflater, container, savedInstanceState); - } + mView = inflater.inflate(R.layout.custom_preference_list_fragment, container, false); + return mView; } @Override @@ -172,6 +177,11 @@ public class WifiSettings extends SettingsPreferenceFragment mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); mWifiManager.asyncConnect(getActivity(), new WifiServiceHandler()); + if (savedInstanceState != null + && savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { + mDlgEdit = savedInstanceState.getBoolean(SAVE_DIALOG_EDIT_MODE); + mAccessPointSavedState = savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); + } final Activity activity = getActivity(); final Intent intent = activity.getIntent(); @@ -180,60 +190,51 @@ public class WifiSettings extends SettingsPreferenceFragment // state, start it off in the right state mEnableNextOnConnection = intent.getBooleanExtra(EXTRA_ENABLE_NEXT_ON_CONNECT, false); - // Avoid re-adding on returning from an overlapping activity/fragment. - if (getPreferenceScreen() == null || getPreferenceScreen().getPreferenceCount() < 2) { - if (mEnableNextOnConnection) { - if (hasNextButton()) { - final ConnectivityManager connectivity = (ConnectivityManager) - getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity != null) { - NetworkInfo info = connectivity.getNetworkInfo( - ConnectivityManager.TYPE_WIFI); - changeNextButtonState(info.isConnected()); - } + if (mEnableNextOnConnection) { + if (hasNextButton()) { + final ConnectivityManager connectivity = (ConnectivityManager) + getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity != null) { + NetworkInfo info = connectivity.getNetworkInfo( + ConnectivityManager.TYPE_WIFI); + changeNextButtonState(info.isConnected()); } } + } - if (mInXlSetupWizard) { - addPreferencesFromResource(R.xml.wifi_access_points_for_wifi_setup_xl); - } else if (intent.getBooleanExtra("only_access_points", false)) { - addPreferencesFromResource(R.xml.wifi_access_points); - } else { - addPreferencesFromResource(R.xml.wifi_settings); - mWifiEnabler = new WifiEnabler(activity, - (CheckBoxPreference) findPreference("enable_wifi")); - mNotifyOpenNetworks = - (CheckBoxPreference) findPreference("notify_open_networks"); - mNotifyOpenNetworks.setChecked(Secure.getInt(getContentResolver(), - Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 0) == 1); - } - // This may be either ProgressCategory or AccessPointCategoryForXL. - final ProgressCategoryBase preference = - (ProgressCategoryBase) findPreference("access_points"); - mAccessPoints = preference; - mAccessPoints.setOrderingAsAdded(false); - mAddNetwork = findPreference("add_network"); - - ListPreference pref = (ListPreference) findPreference(KEY_SLEEP_POLICY); - if (pref != null) { - if (Utils.isWifiOnly()) { - pref.setEntries(R.array.wifi_sleep_policy_entries_wifi_only); - pref.setSummary(R.string.wifi_setting_sleep_policy_summary_wifi_only); + if (mInXlSetupWizard) { + addPreferencesFromResource(R.xml.wifi_access_points_for_wifi_setup_xl); + } else { + addPreferencesFromResource(R.xml.wifi_settings); + + Switch actionBarSwitch = new Switch(activity); + + if (activity instanceof PreferenceActivity) { + PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) { + final int padding = activity.getResources().getDimensionPixelSize( + R.dimen.action_bar_switch_padding); + actionBarSwitch.setPadding(0, 0, padding, 0); + activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, + ActionBar.DISPLAY_SHOW_CUSTOM); + activity.getActionBar().setCustomView(actionBarSwitch, new ActionBar.LayoutParams( + ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT)); } - pref.setOnPreferenceChangeListener(this); - int value = Settings.System.getInt(getContentResolver(), - Settings.System.WIFI_SLEEP_POLICY, - Settings.System.WIFI_SLEEP_POLICY_NEVER); - pref.setValue(String.valueOf(value)); } - registerForContextMenu(getListView()); - setHasOptionsMenu(true); + mWifiEnabler = new WifiEnabler(activity, actionBarSwitch); } + mEmptyView = (TextView) mView.findViewById(R.id.empty); + getListView().setEmptyView(mEmptyView); + + registerForContextMenu(getListView()); + setHasOptionsMenu(true); + // After confirming PreferenceScreen is available, we call super. super.onActivityCreated(savedInstanceState); - } @Override @@ -245,10 +246,11 @@ public class WifiSettings extends SettingsPreferenceFragment getActivity().registerReceiver(mReceiver, mFilter); if (mKeyStoreNetworkId != INVALID_NETWORK_ID && - KeyStore.getInstance().test() == KeyStore.NO_ERROR) { + KeyStore.getInstance().state() == KeyStore.State.UNLOCKED) { mWifiManager.connectNetwork(mKeyStoreNetworkId); } mKeyStoreNetworkId = INVALID_NETWORK_ID; + updateAccessPoints(); } @@ -260,25 +262,43 @@ public class WifiSettings extends SettingsPreferenceFragment } getActivity().unregisterReceiver(mReceiver); mScanner.pause(); - if (mDialog != null) { - mDialog.dismiss(); - mDialog = null; - } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // We don't want menus in Setup Wizard XL. if (!mInXlSetupWizard) { + final boolean wifiIsEnabled = mWifiManager.isWifiEnabled(); menu.add(Menu.NONE, MENU_ID_SCAN, 0, R.string.wifi_menu_scan) - .setIcon(R.drawable.ic_menu_scan_network); + //.setIcon(R.drawable.ic_menu_scan_network) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.add(Menu.NONE, MENU_ID_ADVANCED, 0, R.string.wifi_menu_advanced) - .setIcon(android.R.drawable.ic_menu_manage); + //.setIcon(android.R.drawable.ic_menu_manage) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } super.onCreateOptionsMenu(menu, inflater); } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // If the dialog is showing, save its state. + if (mDialog != null && mDialog.isShowing()) { + outState.putBoolean(SAVE_DIALOG_EDIT_MODE, mDlgEdit); + if (mDlgAccessPoint != null) { + mAccessPointSavedState = new Bundle(); + mDlgAccessPoint.saveWifiState(mAccessPointSavedState); + outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState); + } + } + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_ID_SCAN: @@ -286,15 +306,20 @@ public class WifiSettings extends SettingsPreferenceFragment mScanner.forceScan(); } return true; + case MENU_ID_ADD_NETWORK: + if (mWifiManager.isWifiEnabled()) { + onAddNetworkPressed(); + } + return true; case MENU_ID_ADVANCED: if (getActivity() instanceof PreferenceActivity) { ((PreferenceActivity) getActivity()).startPreferencePanel( - AdvancedSettings.class.getCanonicalName(), + AdvancedWifiSettings.class.getCanonicalName(), null, R.string.wifi_advanced_titlebar, null, this, 0); } else { - startFragment(this, AdvancedSettings.class.getCanonicalName(), -1, null); + startFragment(this, AdvancedWifiSettings.class.getCanonicalName(), -1, null); } return true; } @@ -363,43 +388,17 @@ public class WifiSettings extends SettingsPreferenceFragment if (preference instanceof AccessPoint) { mSelectedAccessPoint = (AccessPoint) preference; showConfigUi(mSelectedAccessPoint, false); - } else if (preference == mAddNetwork) { - onAddNetworkPressed(); - } else if (preference == mNotifyOpenNetworks) { - Secure.putInt(getContentResolver(), - Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, - mNotifyOpenNetworks.isChecked() ? 1 : 0); } else { return super.onPreferenceTreeClick(screen, preference); } return true; } - public boolean onPreferenceChange(Preference preference, Object newValue) { - String key = preference.getKey(); - if (key == null) return true; - - if (key.equals(KEY_SLEEP_POLICY)) { - try { - Settings.System.putInt(getContentResolver(), - Settings.System.WIFI_SLEEP_POLICY, Integer.parseInt(((String) newValue))); - } catch (NumberFormatException e) { - Toast.makeText(getActivity(), R.string.wifi_setting_sleep_policy_error, - Toast.LENGTH_SHORT).show(); - return false; - } - } - - return true; - } - - /** * Shows an appropriate Wifi configuration component. * Called when a user clicks "Add network" preference or one of available networks is selected. */ private void showConfigUi(AccessPoint accessPoint, boolean edit) { - mEdit = edit; if (mInXlSetupWizard) { ((WifiSettingsForSetupWizardXL)getActivity()).showConfigUi(accessPoint, edit); } else { @@ -409,15 +408,36 @@ public class WifiSettings extends SettingsPreferenceFragment private void showDialog(AccessPoint accessPoint, boolean edit) { if (mDialog != null) { - mDialog.dismiss(); + removeDialog(WIFI_DIALOG_ID); + mDialog = null; } - mDialog = new WifiDialog(getActivity(), this, accessPoint, edit); - mDialog.show(); + + // Save the access point and edit mode + mDlgAccessPoint = accessPoint; + mDlgEdit = edit; + + showDialog(WIFI_DIALOG_ID); + } + + @Override + public Dialog onCreateDialog(int dialogId) { + AccessPoint ap = mDlgAccessPoint; // For manual launch + if (ap == null) { // For re-launch from saved state + if (mAccessPointSavedState != null) { + ap = new AccessPoint(getActivity(), mAccessPointSavedState); + // For repeated orientation changes + mDlgAccessPoint = ap; + } + } + // If it's still null, fine, it's for Add Network + mSelectedAccessPoint = ap; + mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit); + return mDialog; } private boolean requireKeyStore(WifiConfiguration config) { if (WifiConfigController.requireKeyStore(config) && - KeyStore.getInstance().test() != KeyStore.NO_ERROR) { + KeyStore.getInstance().state() != KeyStore.State.UNLOCKED) { mKeyStoreNetworkId = config.networkId; Credentials.getInstance().unlock(getActivity()); return true; @@ -430,20 +450,42 @@ public class WifiSettings extends SettingsPreferenceFragment * the strength of network and the security for it. */ private void updateAccessPoints() { - mAccessPoints.removeAll(); + final int wifiState = mWifiManager.getWifiState(); + + switch (wifiState) { + case WifiManager.WIFI_STATE_ENABLED: + getPreferenceScreen().removeAll(); + // AccessPoints are automatically sorted with TreeSet. + final Collection<AccessPoint> accessPoints = constructAccessPoints(); + if (mInXlSetupWizard) { + ((WifiSettingsForSetupWizardXL)getActivity()).onAccessPointsUpdated( + getPreferenceScreen(), accessPoints); + } else { + for (AccessPoint accessPoint : accessPoints) { + getPreferenceScreen().addPreference(accessPoint); + } + } + break; - // AccessPoints are automatically sorted with TreeSet. - final Collection<AccessPoint> accessPoints = constructAccessPoints(); - if (mInXlSetupWizard) { - ((WifiSettingsForSetupWizardXL)getActivity()).onAccessPointsUpdated( - mAccessPoints, accessPoints); - } else { - for (AccessPoint accessPoint : accessPoints) { - mAccessPoints.addPreference(accessPoint); - } + case WifiManager.WIFI_STATE_ENABLING: + getPreferenceScreen().removeAll(); + break; + + case WifiManager.WIFI_STATE_DISABLING: + addMessagePreference(R.string.wifi_stopping); + break; + + case WifiManager.WIFI_STATE_DISABLED: + addMessagePreference(R.string.wifi_empty_list_wifi_off); + break; } } + private void addMessagePreference(int messageId) { + if (mEmptyView != null) mEmptyView.setText(messageId); + getPreferenceScreen().removeAll(); + } + private Collection<AccessPoint> constructAccessPoints() { Collection<AccessPoint> accessPoints = new ArrayList<AccessPoint>(); @@ -542,9 +584,9 @@ public class WifiSettings extends SettingsPreferenceFragment mLastState = state; } - for (int i = mAccessPoints.getPreferenceCount() - 1; i >= 0; --i) { + for (int i = getPreferenceScreen().getPreferenceCount() - 1; i >= 0; --i) { // Maybe there's a WifiConfigPreference - Preference preference = mAccessPoints.getPreference(i); + Preference preference = getPreferenceScreen().getPreference(i); if (preference instanceof AccessPoint) { final AccessPoint accessPoint = (AccessPoint) preference; accessPoint.update(mLastInfo, mLastState); @@ -557,12 +599,25 @@ public class WifiSettings extends SettingsPreferenceFragment } private void updateWifiState(int state) { - if (state == WifiManager.WIFI_STATE_ENABLED) { - mScanner.resume(); - } else { - mScanner.pause(); - mAccessPoints.removeAll(); + getActivity().invalidateOptionsMenu(); + + switch (state) { + case WifiManager.WIFI_STATE_ENABLED: + mScanner.resume(); + return; // not break, to avoid the call to pause() below + + case WifiManager.WIFI_STATE_ENABLING: + addMessagePreference(R.string.wifi_starting); + break; + + case WifiManager.WIFI_STATE_DISABLED: + addMessagePreference(R.string.wifi_empty_list_wifi_off); + break; } + + mLastInfo = null; + mLastState = null; + mScanner.pause(); } private class Scanner extends Handler { @@ -580,7 +635,6 @@ public class WifiSettings extends SettingsPreferenceFragment void pause() { mRetry = 0; - mAccessPoints.setProgress(false); removeMessages(0); } @@ -594,7 +648,6 @@ public class WifiSettings extends SettingsPreferenceFragment Toast.LENGTH_LONG).show(); return; } - mAccessPoints.setProgress(mRetry != 0); // Combo scans can take 5-6s to complete. Increase interval to 10s. sendEmptyMessageDelayed(0, 10000); } @@ -636,6 +689,7 @@ public class WifiSettings extends SettingsPreferenceFragment } break; } + break; //TODO: more connectivity feedback default: //Ignore @@ -679,8 +733,8 @@ public class WifiSettings extends SettingsPreferenceFragment int networkSetup = configController.chosenNetworkSetupMethod(); switch(networkSetup) { case WifiConfigController.WPS_PBC: - case WifiConfigController.WPS_PIN_FROM_ACCESS_POINT: - case WifiConfigController.WPS_PIN_FROM_DEVICE: + case WifiConfigController.WPS_DISPLAY: + case WifiConfigController.WPS_KEYPAD: mWifiManager.startWps(configController.getWpsConfig()); break; case WifiConfigController.MANUAL: @@ -740,7 +794,7 @@ public class WifiSettings extends SettingsPreferenceFragment mScanner.resume(); } - mAccessPoints.removeAll(); + getPreferenceScreen().removeAll(); } /** @@ -753,8 +807,9 @@ public class WifiSettings extends SettingsPreferenceFragment } /* package */ int getAccessPointsCount() { - if (mAccessPoints != null) { - return mAccessPoints.getPreferenceCount(); + final boolean wifiIsEnabled = mWifiManager.isWifiEnabled(); + if (wifiIsEnabled) { + return getPreferenceScreen().getPreferenceCount(); } else { return 0; } diff --git a/src/com/android/settings/wifi/WifiSettingsForSetupWizardXL.java b/src/com/android/settings/wifi/WifiSettingsForSetupWizardXL.java index a73f96c..a3f1764 100644 --- a/src/com/android/settings/wifi/WifiSettingsForSetupWizardXL.java +++ b/src/com/android/settings/wifi/WifiSettingsForSetupWizardXL.java @@ -16,8 +16,6 @@ package com.android.settings.wifi; -import com.android.settings.R; - import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -28,11 +26,9 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; import android.text.TextUtils; import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -43,6 +39,7 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.android.internal.util.AsyncChannel; +import com.android.settings.R; import java.util.Collection; import java.util.EnumMap; @@ -75,12 +72,6 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis sNetworkStateMap.put(DetailedState.FAILED, DetailedState.FAILED); } - /** - * Used with {@link Button#setTag(Object)} to remember "Connect" button is pressed in - * with "add network" flow. - */ - private static final int CONNECT_BUTTON_TAG_ADD_NETWORK = 1; - private WifiSettings mWifiSettings; private WifiManager mWifiManager; @@ -166,7 +157,7 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis // At first, Wifi module doesn't return SCANNING state (it's too early), so we manually // show it. - showScanningProgressBar(); + showScanningState(); } private void initViews() { @@ -291,17 +282,16 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis switch (state) { case SCANNING: { - // Let users know the device is working correctly though currently there's - // no visible network on the list. - if (mWifiSettings.getAccessPointsCount() == 0) { - showScanningState(); - } else { - // Users already see available networks. - showDisconnectedProgressBar(); - if (mScreenState == SCREEN_STATE_DISCONNECTED) { + if (mScreenState == SCREEN_STATE_DISCONNECTED) { + if (mWifiSettings.getAccessPointsCount() == 0) { + showScanningState(); + } else { + showDisconnectedProgressBar(); mWifiSettingsFragmentLayout.setVisibility(View.VISIBLE); mBottomPadding.setVisibility(View.GONE); } + } else { + showDisconnectedProgressBar(); } break; } @@ -316,7 +306,8 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis break; } default: // DISCONNECTED, FAILED - if (mScreenState != SCREEN_STATE_CONNECTED) { + if (mScreenState != SCREEN_STATE_CONNECTED && + mWifiSettings.getAccessPointsCount() > 0) { showDisconnectedState(Summary.get(this, state)); } break; @@ -326,7 +317,8 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis private void showDisconnectedState(String stateString) { showDisconnectedProgressBar(); - if (mScreenState == SCREEN_STATE_DISCONNECTED) { + if (mScreenState == SCREEN_STATE_DISCONNECTED && + mWifiSettings.getAccessPointsCount() > 0) { mWifiSettingsFragmentLayout.setVisibility(View.VISIBLE); mBottomPadding.setVisibility(View.GONE); } @@ -474,13 +466,11 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis parent.removeAllViews(); mWifiConfig = new WifiConfigUiForSetupWizardXL(this, parent, selectedAccessPoint, edit); - // Tag will be updated in this method when needed. - mConnectButton.setTag(null); if (selectedAccessPoint == null) { // "Add network" flow showAddNetworkTitle(); mConnectButton.setVisibility(View.VISIBLE); - mConnectButton.setTag(CONNECT_BUTTON_TAG_ADD_NETWORK); + showDisconnectedProgressBar(); showEditingButtonState(); } else if (selectedAccessPoint.security == AccessPoint.SECURITY_NONE) { mNetworkName = selectedAccessPoint.getTitle().toString(); @@ -490,6 +480,7 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis } else { mNetworkName = selectedAccessPoint.getTitle().toString(); showEditingTitle(); + showDisconnectedProgressBar(); showEditingButtonState(); if (selectedAccessPoint.security == AccessPoint.SECURITY_EAP) { onEapNetworkSelected(); @@ -645,8 +636,9 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis mAddNetworkButton.setEnabled(true); mRefreshButton.setEnabled(true); mSkipOrNextButton.setEnabled(true); - mWifiSettingsFragmentLayout.setVisibility(View.VISIBLE); showDisconnectedProgressBar(); + mWifiSettingsFragmentLayout.setVisibility(View.VISIBLE); + mBottomPadding.setVisibility(View.GONE); } setPaddingVisibility(View.VISIBLE); @@ -671,9 +663,10 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis /** * Called when the list of AccessPoints are modified and this Activity needs to refresh * the list. + * @param preferenceScreen */ /* package */ void onAccessPointsUpdated( - PreferenceCategory holder, Collection<AccessPoint> accessPoints) { + PreferenceScreen preferenceScreen, Collection<AccessPoint> accessPoints) { // If we already show some of access points but the bar still shows "scanning" state, it // should be stopped. if (mProgressBar.isIndeterminate() && accessPoints.size() > 0) { @@ -688,20 +681,12 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis for (AccessPoint accessPoint : accessPoints) { accessPoint.setLayoutResource(R.layout.custom_preference); - holder.addPreference(accessPoint); + preferenceScreen.addPreference(accessPoint); } } private void refreshAccessPoints(boolean disconnectNetwork) { - final Object tag = mConnectButton.getTag(); - if (tag != null && (tag instanceof Integer) && - ((Integer)tag == CONNECT_BUTTON_TAG_ADD_NETWORK)) { - // In "Add network" flow, we won't get DetaledState available for changing ProgressBar - // state. Instead we manually show previous status here. - showDisconnectedState(Summary.get(this, mPreviousNetworkState)); - } else { - showScanningState(); - } + showScanningState(); if (disconnectNetwork) { mWifiManager.disconnect(); @@ -801,11 +786,6 @@ public class WifiSettingsForSetupWizardXL extends Activity implements OnClickLis mWifiManager.connectNetwork(config); } - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, view, menuInfo); - } - /** * Replace the current background with a new background whose id is resId if needed. */ diff --git a/src/com/android/settings/wifi/p2p/WifiP2pDialog.java b/src/com/android/settings/wifi/p2p/WifiP2pDialog.java new file mode 100644 index 0000000..380fa13 --- /dev/null +++ b/src/com/android/settings/wifi/p2p/WifiP2pDialog.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2011 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.wifi.p2p; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.wifi.WpsConfiguration; +import android.net.wifi.WpsConfiguration.Setup; +import android.net.wifi.p2p.WifiP2pConfig; +import android.net.wifi.p2p.WifiP2pDevice; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.settings.R; + +/** + * Dialog to setup a p2p connection + */ +public class WifiP2pDialog extends AlertDialog implements AdapterView.OnItemSelectedListener { + + static final int BUTTON_SUBMIT = DialogInterface.BUTTON_POSITIVE; + + private final DialogInterface.OnClickListener mListener; + + private View mView; + private TextView mDeviceName; + private TextView mDeviceAddress; + + /* These values come from "wifi_p2p_wps_setup" resource array */ + private static final int WPS_PBC = 0; + private static final int WPS_KEYPAD = 1; + private static final int WPS_DISPLAY = 2; + + private int mWpsSetupIndex = WPS_PBC; //default is pbc + + WifiP2pDevice mDevice; + + public WifiP2pDialog(Context context, DialogInterface.OnClickListener listener, + WifiP2pDevice device) { + super(context); + mListener = listener; + mDevice = device; + } + + public WifiP2pConfig getConfig() { + WifiP2pConfig config = new WifiP2pConfig(); + config.deviceAddress = mDeviceAddress.getText().toString(); + config.deviceName = mDeviceName.getText().toString(); + config.wpsConfig = new WpsConfiguration(); + switch (mWpsSetupIndex) { + case WPS_PBC: + config.wpsConfig.setup = Setup.PBC; + break; + case WPS_KEYPAD: + config.wpsConfig.setup = Setup.KEYPAD; + config.wpsConfig.pin = ((TextView) mView.findViewById(R.id.wps_pin)). + getText().toString(); + break; + case WPS_DISPLAY: + config.wpsConfig.setup = Setup.DISPLAY; + break; + default: + config.wpsConfig.setup = Setup.PBC; + break; + } + if (mDevice.isGroupOwner()) { + config.joinExistingGroup = true; + } + return config; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + mView = getLayoutInflater().inflate(R.layout.wifi_p2p_dialog, null); + Spinner mWpsSetup = ((Spinner) mView.findViewById(R.id.wps_setup)); + + setView(mView); + setInverseBackgroundForced(true); + + Context context = getContext(); + + setTitle(R.string.wifi_p2p_settings_title); + mDeviceName = (TextView) mView.findViewById(R.id.device_name); + mDeviceAddress = (TextView) mView.findViewById(R.id.device_address); + + setButton(BUTTON_SUBMIT, context.getString(R.string.wifi_connect), mListener); + setButton(DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.wifi_cancel), mListener); + + if (mDevice != null) { + mDeviceName.setText(mDevice.deviceName); + mDeviceAddress.setText(mDevice.deviceAddress); + mWpsSetup.setSelection(mWpsSetupIndex); //keep pbc as default + } + + mWpsSetup.setOnItemSelectedListener(this); + + super.onCreate(savedInstanceState); + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + mWpsSetupIndex = position; + + if (mWpsSetupIndex == WPS_KEYPAD) { + mView.findViewById(R.id.wps_pin_entry).setVisibility(View.VISIBLE); + } else { + mView.findViewById(R.id.wps_pin_entry).setVisibility(View.GONE); + } + return; + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + +} diff --git a/src/com/android/settings/wifi/p2p/WifiP2pEnabler.java b/src/com/android/settings/wifi/p2p/WifiP2pEnabler.java new file mode 100644 index 0000000..fd79a58 --- /dev/null +++ b/src/com/android/settings/wifi/p2p/WifiP2pEnabler.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2011 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.wifi.p2p; + +import com.android.settings.R; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.p2p.WifiP2pManager; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.provider.Settings; +import android.util.Log; +import android.widget.CompoundButton; +import android.widget.Switch; + +/** + * WifiP2pEnabler is a helper to manage the Wifi p2p on/off + */ +public class WifiP2pEnabler implements CompoundButton.OnCheckedChangeListener { + private static final String TAG = "WifiP2pEnabler"; + + private final Context mContext; + private Switch mSwitch; + private int mWifiP2pState; + private final IntentFilter mIntentFilter; + private final Handler mHandler = new WifiP2pHandler(); + private WifiP2pManager mWifiP2pManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { + handleP2pStateChanged(intent.getIntExtra( + WifiP2pManager.EXTRA_WIFI_STATE, WifiP2pManager.WIFI_P2P_STATE_DISABLED)); + } + } + }; + + public WifiP2pEnabler(Context context, Switch switch_) { + mContext = context; + mSwitch = switch_; + + mWifiP2pManager = (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE); + if (!mWifiP2pManager.connectHandler(mContext, mHandler)) { + //Failure to set up connection + Log.e(TAG, "Failed to set up connection with wifi p2p service"); + mWifiP2pManager = null; + mSwitch.setEnabled(false); + } + mIntentFilter = new IntentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); + + } + + public void resume() { + if (mWifiP2pManager == null) return; + mContext.registerReceiver(mReceiver, mIntentFilter); + mSwitch.setOnCheckedChangeListener(this); + } + + public void pause() { + if (mWifiP2pManager == null) return; + mContext.unregisterReceiver(mReceiver); + mSwitch.setOnCheckedChangeListener(null); + } + + public void setSwitch(Switch switch_) { + if (mSwitch == switch_) return; + mSwitch.setOnCheckedChangeListener(null); + mSwitch = switch_; + mSwitch.setOnCheckedChangeListener(this); + + mSwitch.setChecked(mWifiP2pState == WifiP2pManager.WIFI_P2P_STATE_ENABLED); + } + + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (mWifiP2pManager == null) return; + + if (isChecked) { + mWifiP2pManager.enableP2p(); + } else { + mWifiP2pManager.disableP2p(); + } + } + + private void handleP2pStateChanged(int state) { + mSwitch.setEnabled(true); + switch (state) { + case WifiP2pManager.WIFI_P2P_STATE_ENABLED: + mWifiP2pState = WifiP2pManager.WIFI_P2P_STATE_ENABLED; + mSwitch.setChecked(true); + break; + case WifiP2pManager.WIFI_P2P_STATE_DISABLED: + mWifiP2pState = WifiP2pManager.WIFI_P2P_STATE_DISABLED; + mSwitch.setChecked(false); + break; + default: + mWifiP2pState = WifiP2pManager.WIFI_P2P_STATE_DISABLED; + Log.e(TAG,"Unhandled wifi state " + state); + break; + } + } + + private class WifiP2pHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case WifiP2pManager.HANDLER_DISCONNECTION: + //Failure to set up connection + Log.e(TAG, "Lost connection with wifi p2p service"); + mWifiP2pManager = null; + mSwitch.setEnabled(false); + break; + case WifiP2pManager.ENABLE_P2P_FAILED: + mSwitch.setEnabled(true); + break; + default: + //Ignore + break; + } + } + } + +} diff --git a/src/com/android/settings/wifi/p2p/WifiP2pPeer.java b/src/com/android/settings/wifi/p2p/WifiP2pPeer.java new file mode 100644 index 0000000..35ae15a --- /dev/null +++ b/src/com/android/settings/wifi/p2p/WifiP2pPeer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.wifi.p2p; + +import com.android.settings.R; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.net.wifi.p2p.WifiP2pManager; +import android.net.wifi.p2p.WifiP2pDevice; +import android.net.wifi.p2p.WifiP2pDevice.Status; +import android.preference.Preference; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; + +import java.util.Comparator; + +public class WifiP2pPeer extends Preference { + + private static final int[] STATE_SECURED = {R.attr.state_encrypted}; + public WifiP2pDevice device; + + private int mRssi; + private ImageView mSignal; + + private static final int SIGNAL_LEVELS = 4; + + public WifiP2pPeer(Context context, WifiP2pDevice dev) { + super(context); + device = dev; + setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + mRssi = 60; //TODO: fix + } + + @Override + protected void onBindView(View view) { + if (TextUtils.isEmpty(device.deviceName)) { + setTitle(device.deviceAddress); + } else { + setTitle(device.deviceName); + } + mSignal = (ImageView) view.findViewById(R.id.signal); + if (mRssi == Integer.MAX_VALUE) { + mSignal.setImageDrawable(null); + } else { + mSignal.setImageResource(R.drawable.wifi_signal); + mSignal.setImageState(STATE_SECURED, true); + } + refresh(); + super.onBindView(view); + } + + @Override + public int compareTo(Preference preference) { + if (!(preference instanceof WifiP2pPeer)) { + return 1; + } + WifiP2pPeer other = (WifiP2pPeer) preference; + + // devices go in the order of the status + if (device.status != other.device.status) { + return device.status.ordinal() < other.device.status.ordinal() ? -1 : 1; + } + + // Sort by name/address + if (device.deviceName != null) { + return device.deviceName.compareToIgnoreCase(other.device.deviceName); + } + + return device.deviceAddress.compareToIgnoreCase(other.device.deviceAddress); + } + + int getLevel() { + if (mRssi == Integer.MAX_VALUE) { + return -1; + } + return WifiManager.calculateSignalLevel(mRssi, SIGNAL_LEVELS); + } + + private void refresh() { + if (mSignal == null) { + return; + } + Context context = getContext(); + mSignal.setImageLevel(getLevel()); + String[] statusArray = context.getResources().getStringArray(R.array.wifi_p2p_status); + setSummary(statusArray[device.status.ordinal()]); + } +} diff --git a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java new file mode 100644 index 0000000..6ec3a1a --- /dev/null +++ b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2011 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.wifi.p2p; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.p2p.WifiP2pConfig; +import android.net.wifi.p2p.WifiP2pDevice; +import android.net.wifi.p2p.WifiP2pDeviceList; +import android.net.wifi.p2p.WifiP2pManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.util.Log; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Switch; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import java.util.Arrays; +import java.util.List; +import java.util.Collection; + +/* + * Displays Wi-fi p2p settings UI + */ +public class WifiP2pSettings extends SettingsPreferenceFragment { + + private static final String TAG = "WifiP2pSettings"; + private static final int MENU_ID_SEARCH = Menu.FIRST; + private static final int MENU_ID_CREATE_GROUP = Menu.FIRST + 1; + private static final int MENU_ID_ADVANCED = Menu.FIRST +2; + + + private final IntentFilter mIntentFilter = new IntentFilter(); + private final Handler mHandler = new WifiP2pHandler(); + private WifiP2pManager mWifiP2pManager; + private WifiP2pEnabler mWifiP2pEnabler; + private WifiP2pDialog mConnectDialog; + private OnClickListener mConnectListener; + private OnClickListener mDisconnectListener; + private WifiP2pPeer mSelectedWifiPeer; + + private static final int DIALOG_CONNECT = 1; + private static final int DIALOG_DISCONNECT = 2; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { + //TODO: nothing right now + } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { + if (mWifiP2pManager != null) mWifiP2pManager.requestPeers(); + } + } + }; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.wifi_p2p_settings); + + mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); + + final Activity activity = getActivity(); + mWifiP2pManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE); + if (!mWifiP2pManager.connectHandler(activity, mHandler)) { + //Failure to set up connection + Log.e(TAG, "Failed to set up connection with wifi p2p service"); + mWifiP2pManager = null; + } + + Switch actionBarSwitch = new Switch(activity); + + if (activity instanceof PreferenceActivity) { + PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) { + final int padding = activity.getResources().getDimensionPixelSize( + R.dimen.action_bar_switch_padding); + actionBarSwitch.setPadding(0, 0, padding, 0); + activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, + ActionBar.DISPLAY_SHOW_CUSTOM); + activity.getActionBar().setCustomView(actionBarSwitch, new ActionBar.LayoutParams( + ActionBar.LayoutParams.WRAP_CONTENT, + ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_VERTICAL | Gravity.RIGHT)); + } + } + + mWifiP2pEnabler = new WifiP2pEnabler(activity, actionBarSwitch); + + //connect dialog listener + mConnectListener = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + WifiP2pConfig config = mConnectDialog.getConfig(); + if (mWifiP2pManager != null) { + mWifiP2pManager.connect(config); + } + } + } + }; + + //disconnect dialog listener + mDisconnectListener = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + if (mWifiP2pManager != null) { + mWifiP2pManager.disconnect(); + } + } + } + }; + setHasOptionsMenu(true); + } + + @Override + public void onResume() { + super.onResume(); + getActivity().registerReceiver(mReceiver, mIntentFilter); + if (mWifiP2pEnabler != null) { + mWifiP2pEnabler.resume(); + } + if (mWifiP2pManager != null) mWifiP2pManager.discoverPeers(); + } + + @Override + public void onPause() { + super.onPause(); + if (mWifiP2pEnabler != null) { + mWifiP2pEnabler.pause(); + } + getActivity().unregisterReceiver(mReceiver); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, MENU_ID_SEARCH, 0, R.string.wifi_p2p_menu_search) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.add(Menu.NONE, MENU_ID_CREATE_GROUP, 0, R.string.wifi_p2p_menu_create_group) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.add(Menu.NONE, MENU_ID_ADVANCED, 0, R.string.wifi_p2p_menu_advanced) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_ID_SEARCH: + mWifiP2pManager.discoverPeers(); + return true; + case MENU_ID_CREATE_GROUP: + mWifiP2pManager.createGroup(); + return true; + case MENU_ID_ADVANCED: + //TODO: add advanced settings for p2p + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (preference instanceof WifiP2pPeer) { + mSelectedWifiPeer = (WifiP2pPeer) preference; + if (mSelectedWifiPeer.device.status == WifiP2pDevice.Status.CONNECTED) { + showDialog(DIALOG_DISCONNECT); + } else { + showDialog(DIALOG_CONNECT); + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + @Override + public Dialog onCreateDialog(int id) { + if (id == DIALOG_CONNECT) { + mConnectDialog = new WifiP2pDialog(getActivity(), mConnectListener, + mSelectedWifiPeer.device); + return mConnectDialog; + } else if (id == DIALOG_DISCONNECT) { + AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setTitle("Disconnect ?") + .setMessage("Do you want to disconnect ?") + .setPositiveButton(getActivity().getString(R.string.dlg_ok), mDisconnectListener) + .setNegativeButton(getActivity().getString(R.string.dlg_cancel), null) + .create(); + return dialog; + } + return null; + } + + private void updatePeers(WifiP2pDeviceList peers) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + preferenceScreen.removeAll(); + + for (WifiP2pDevice peer: peers.getDeviceList()) { + preferenceScreen.addPreference(new WifiP2pPeer(getActivity(), peer)); + } + } + + private class WifiP2pHandler extends Handler { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case WifiP2pManager.HANDLER_DISCONNECTION: + //Failure to set up connection + Log.e(TAG, "Lost connection with wifi p2p service"); + mWifiP2pManager = null; + break; + case WifiP2pManager.RESPONSE_PEERS: + updatePeers(mWifiP2pManager.peersInResponse(message)); + break; + default: + //Ignore + break; + } + } + } + +} |