diff options
Diffstat (limited to 'src/com')
262 files changed, 28131 insertions, 12011 deletions
diff --git a/src/com/android/settings/AccountPreference.java b/src/com/android/settings/AccountPreference.java index 2cc013c..7547721 100644 --- a/src/com/android/settings/AccountPreference.java +++ b/src/com/android/settings/AccountPreference.java @@ -140,6 +140,8 @@ public class AccountPreference extends Preference { return getContext().getString(R.string.accessibility_sync_disabled); case SYNC_ERROR: return getContext().getString(R.string.accessibility_sync_error); + case SYNC_IN_PROGRESS: + return getContext().getString(R.string.accessibility_sync_in_progress); default: Log.e(TAG, "Unknown sync status: " + status); return getContext().getString(R.string.accessibility_sync_error); diff --git a/src/com/android/settings/ActiveNetworkScorerDialog.java b/src/com/android/settings/ActiveNetworkScorerDialog.java new file mode 100644 index 0000000..e47f8e3 --- /dev/null +++ b/src/com/android/settings/ActiveNetworkScorerDialog.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 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.DialogInterface; +import android.content.Intent; +import android.net.NetworkScoreManager; +import android.net.NetworkScorerAppManager; +import android.net.NetworkScorerAppManager.NetworkScorerAppData; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; + +/** + * Dialog to allow a user to select a new network scorer. + * + * <p>Finishes with {@link #RESULT_CANCELED} in all circumstances unless the scorer is successfully + * changed or was already set to the new value (in which case it finishes with {@link #RESULT_OK}). + */ +public final class ActiveNetworkScorerDialog extends AlertActivity implements + DialogInterface.OnClickListener { + private static final String TAG = "ActiveNetScorerDlg"; + + private String mNewPackageName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + mNewPackageName = intent.getStringExtra(NetworkScoreManager.EXTRA_PACKAGE_NAME); + + if (!buildDialog()) { + finish(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case BUTTON_POSITIVE: + NetworkScoreManager nsm = + (NetworkScoreManager) getSystemService(Context.NETWORK_SCORE_SERVICE); + if (nsm.setActiveScorer(mNewPackageName)) { + setResult(RESULT_OK); + } + break; + case BUTTON_NEGATIVE: + break; + } + } + + private boolean buildDialog() { + NetworkScorerAppData newScorer = NetworkScorerAppManager.getScorer(this, mNewPackageName); + if (newScorer == null) { + Log.e(TAG, "New package " + mNewPackageName + " is not a valid scorer."); + return false; + } + + NetworkScorerAppData oldScorer = NetworkScorerAppManager.getActiveScorer(this); + if (oldScorer != null && TextUtils.equals(oldScorer.mPackageName, mNewPackageName)) { + Log.i(TAG, "New package " + mNewPackageName + " is already the active scorer."); + // Set RESULT_OK to indicate to the caller that the "switch" was successful. + setResult(RESULT_OK); + return false; + } + + // Compose dialog. + CharSequence newName = newScorer.mScorerName; + final AlertController.AlertParams p = mAlertParams; + p.mTitle = getString(R.string.network_scorer_change_active_dialog_title); + if (oldScorer != null) { + p.mMessage = getString(R.string.network_scorer_change_active_dialog_text, newName, + oldScorer.mScorerName); + } else { + p.mMessage = getString(R.string.network_scorer_change_active_no_previous_dialog_text, + newName); + } + p.mPositiveButtonText = getString(R.string.yes); + p.mNegativeButtonText = getString(R.string.no); + p.mPositiveButtonListener = this; + p.mNegativeButtonListener = this; + setupAlert(); + + return true; + } +} diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index d1c591e..4ce5198 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -25,6 +25,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.SwitchPreference; import android.provider.Settings; import com.android.internal.telephony.PhoneStateIntentReceiver; @@ -36,7 +37,7 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene private PhoneStateIntentReceiver mPhoneStateReceiver; - private final CheckBoxPreference mCheckBoxPref; + private final SwitchPreference mSwitchPref; private static final int EVENT_SERVICE_STATE_CHANGED = 3; @@ -58,10 +59,10 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene } }; - public AirplaneModeEnabler(Context context, CheckBoxPreference airplaneModeCheckBoxPreference) { + public AirplaneModeEnabler(Context context, SwitchPreference airplaneModeCheckBoxPreference) { mContext = context; - mCheckBoxPref = airplaneModeCheckBoxPreference; + mSwitchPref = airplaneModeCheckBoxPreference; airplaneModeCheckBoxPreference.setPersistent(false); @@ -71,10 +72,10 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene public void resume() { - mCheckBoxPref.setChecked(isAirplaneModeOn(mContext)); + mSwitchPref.setChecked(isAirplaneModeOn(mContext)); mPhoneStateReceiver.registerIntent(); - mCheckBoxPref.setOnPreferenceChangeListener(this); + mSwitchPref.setOnPreferenceChangeListener(this); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true, mAirplaneModeObserver); @@ -82,7 +83,7 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene public void pause() { mPhoneStateReceiver.unregisterIntent(); - mCheckBoxPref.setOnPreferenceChangeListener(null); + mSwitchPref.setOnPreferenceChangeListener(null); mContext.getContentResolver().unregisterContentObserver(mAirplaneModeObserver); } @@ -96,7 +97,7 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, enabling ? 1 : 0); // Update the UI to reflect system setting - mCheckBoxPref.setChecked(enabling); + mSwitchPref.setChecked(enabling); // Post the intent Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); @@ -113,7 +114,7 @@ public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListene * - mobile does not send failure notification, fail on timeout. */ private void onAirplaneModeChanged() { - mCheckBoxPref.setChecked(isAirplaneModeOn(mContext)); + mSwitchPref.setChecked(isAirplaneModeOn(mContext)); } /** diff --git a/src/com/android/settings/AirplaneModeVoiceActivity.java b/src/com/android/settings/AirplaneModeVoiceActivity.java new file mode 100644 index 0000000..3ab0c37 --- /dev/null +++ b/src/com/android/settings/AirplaneModeVoiceActivity.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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.Intent; +import android.provider.Settings; +import android.util.Log; + +/** + * Activity for modifying the {@link Settings.Global#AIRPLANE_MODE_ON AIRPLANE_MODE_ON} + * setting using the Voice Interaction API. + */ +public class AirplaneModeVoiceActivity extends VoiceSettingsActivity { + private static final String TAG = "AirplaneModeVoiceActivity"; + + protected void onVoiceSettingInteraction(Intent intent) { + if (intent.hasExtra(Settings.EXTRA_AIRPLANE_MODE_ENABLED)) { + boolean enabled = + intent.getBooleanExtra(Settings.EXTRA_AIRPLANE_MODE_ENABLED, false); + Settings.Global.putInt(getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, enabled ? 1 : 0); + } else { + Log.v(TAG, "Missing airplane mode extra"); + } + } +} diff --git a/src/com/android/settings/AllowBindAppWidgetActivity.java b/src/com/android/settings/AllowBindAppWidgetActivity.java index f05c1c2..c3bf78a 100644 --- a/src/com/android/settings/AllowBindAppWidgetActivity.java +++ b/src/com/android/settings/AllowBindAppWidgetActivity.java @@ -25,7 +25,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; -import android.util.DisplayMetrics; +import android.os.UserHandle; import android.util.Log; import android.view.LayoutInflater; import android.widget.CheckBox; @@ -42,6 +42,7 @@ public class AllowBindAppWidgetActivity extends AlertActivity implements private CheckBox mAlwaysUse; private int mAppWidgetId; + private UserHandle mProfile; private ComponentName mComponentName; private String mCallingPackage; private AppWidgetManager mAppWidgetManager; @@ -55,27 +56,32 @@ public class AllowBindAppWidgetActivity extends AlertActivity implements setResult(RESULT_CANCELED); if (mAppWidgetId != -1 && mComponentName != null && mCallingPackage != null) { try { - mAppWidgetManager.bindAppWidgetId(mAppWidgetId, mComponentName); - Intent result = new Intent(); - result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - setResult(RESULT_OK, result); + final boolean bound = mAppWidgetManager.bindAppWidgetIdIfAllowed(mAppWidgetId, + mProfile, mComponentName, null); + if (bound) { + Intent result = new Intent(); + result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + setResult(RESULT_OK, result); + } } catch (Exception e) { Log.v("BIND_APPWIDGET", "Error binding widget with id " + mAppWidgetId + " and component " + mComponentName); } - } - boolean alwaysAllowBind = mAlwaysUse.isChecked(); - if (alwaysAllowBind != mAppWidgetManager.hasBindAppWidgetPermission(mCallingPackage)) { - mAppWidgetManager.setBindAppWidgetPermission(mCallingPackage, alwaysAllowBind); + + final boolean alwaysAllowBind = mAlwaysUse.isChecked(); + if (alwaysAllowBind != mAppWidgetManager.hasBindAppWidgetPermission( + mCallingPackage)) { + mAppWidgetManager.setBindAppWidgetPermission(mCallingPackage, + alwaysAllowBind); + } } } finish(); } - protected void onDestroy() { - if (!mClicked) { + protected void onPause() { + if (isDestroyed() && !mClicked) { setResult(RESULT_CANCELED); - finish(); } super.onDestroy(); } @@ -87,7 +93,12 @@ public class AllowBindAppWidgetActivity extends AlertActivity implements if (intent != null) { try { mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); - mComponentName = (ComponentName) + mProfile = intent.getParcelableExtra( + AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE); + if (mProfile == null) { + mProfile = android.os.Process.myUserHandle(); + } + mComponentName = intent.getParcelableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER); mCallingPackage = getCallingPackage(); PackageManager pm = getPackageManager(); @@ -123,7 +134,8 @@ public class AllowBindAppWidgetActivity extends AlertActivity implements getResources().getDimension(R.dimen.bind_app_widget_dialog_checkbox_bottom_padding))); mAppWidgetManager = AppWidgetManager.getInstance(this); - mAlwaysUse.setChecked(mAppWidgetManager.hasBindAppWidgetPermission(mCallingPackage)); + mAlwaysUse.setChecked(mAppWidgetManager.hasBindAppWidgetPermission(mCallingPackage, + mProfile.getIdentifier())); setupAlert(); } diff --git a/src/com/android/settings/ApnEditor.java b/src/com/android/settings/ApnEditor.java index 2da2d76..8cfee92 100644 --- a/src/com/android/settings/ApnEditor.java +++ b/src/com/android/settings/ApnEditor.java @@ -327,6 +327,13 @@ public class ApnEditor extends PreferenceActivity mMvnoType.setSummary( checkNull(mvnoDescription(mMvnoType.getValue()))); mMvnoMatchData.setSummary(checkNull(mMvnoMatchData.getText())); + // allow user to edit carrier_enabled for some APN + boolean ceEditable = getResources().getBoolean(R.bool.config_allow_edit_carrier_enabled); + if (ceEditable) { + mCarrierEnabled.setEnabled(true); + } else { + mCarrierEnabled.setEnabled(false); + } } /** @@ -446,7 +453,7 @@ public class ApnEditor extends PreferenceActivity // If it's a new APN, then cancel will delete the new entry in onPause if (!mNewApn) { menu.add(0, MENU_DELETE, 0, R.string.menu_delete) - .setIcon(R.drawable.ic_menu_delete_holo_dark); + .setIcon(R.drawable.ic_menu_delete); } menu.add(0, MENU_SAVE, 0, R.string.menu_save) .setIcon(android.R.drawable.ic_menu_save); @@ -571,6 +578,7 @@ public class ApnEditor extends PreferenceActivity values.put(Telephony.Carriers.MVNO_TYPE, checkNotSet(mMvnoType.getValue())); values.put(Telephony.Carriers.MVNO_MATCH_DATA, checkNotSet(mMvnoMatchData.getText())); + values.put(Telephony.Carriers.CARRIER_ENABLED, mCarrierEnabled.isChecked() ? 1 : 0); getContentResolver().update(mUri, values, null, null); return true; @@ -664,6 +672,8 @@ public class ApnEditor extends PreferenceActivity if (pref != null) { if (pref.equals(mPassword)){ pref.setSummary(starify(sharedPreferences.getString(key, ""))); + } else if (pref.equals(mCarrierEnabled)) { + // do nothing } else { pref.setSummary(checkNull(sharedPreferences.getString(key, ""))); } diff --git a/src/com/android/settings/ApnPreference.java b/src/com/android/settings/ApnPreference.java index addb695..1e29d22 100644 --- a/src/com/android/settings/ApnPreference.java +++ b/src/com/android/settings/ApnPreference.java @@ -71,6 +71,7 @@ public class ApnPreference extends Preference implements mProtectFromCheckedChange = true; rb.setChecked(isChecked); mProtectFromCheckedChange = false; + rb.setVisibility(View.VISIBLE); } else { rb.setVisibility(View.GONE); } diff --git a/src/com/android/settings/ApnSettings.java b/src/com/android/settings/ApnSettings.java index 3fbb5e3..0c0e53c 100644 --- a/src/com/android/settings/ApnSettings.java +++ b/src/com/android/settings/ApnSettings.java @@ -16,6 +16,7 @@ package com.android.settings; +import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; @@ -32,14 +33,19 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.provider.Telephony; 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.widget.TextView; import android.widget.Toast; import com.android.internal.telephony.Phone; @@ -49,7 +55,7 @@ import com.android.internal.telephony.TelephonyProperties; import java.util.ArrayList; -public class ApnSettings extends PreferenceActivity implements +public class ApnSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener { static final String TAG = "ApnSettings"; @@ -83,10 +89,14 @@ public class ApnSettings extends PreferenceActivity implements private RestoreApnProcessHandler mRestoreApnProcessHandler; private HandlerThread mRestoreDefaultApnThread; + private UserManager mUm; + private String mSelectedKey; private IntentFilter mMobileStateFilter; + private boolean mUnavailable; + private final BroadcastReceiver mMobileStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -116,38 +126,68 @@ public class ApnSettings extends PreferenceActivity implements } @Override - protected void onCreate(Bundle icicle) { + public void onCreate(Bundle icicle) { super.onCreate(icicle); - addPreferencesFromResource(R.xml.apn_settings); - getListView().setItemsCanFocus(true); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); mMobileStateFilter = new IntentFilter( TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + + if (!mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { + setHasOptionsMenu(true); + } } @Override - protected void onResume() { + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + TextView empty = (TextView) getView().findViewById(android.R.id.empty); + if (empty != null) { + empty.setText(R.string.apn_settings_not_available); + getListView().setEmptyView(empty); + } + + if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { + mUnavailable = true; + setPreferenceScreen(new PreferenceScreen(getActivity(), null)); + return; + } + + addPreferencesFromResource(R.xml.apn_settings); + + getListView().setItemsCanFocus(true); + } + + @Override + public void onResume() { super.onResume(); - registerReceiver(mMobileStateReceiver, mMobileStateFilter); + if (mUnavailable) { + return; + } + + getActivity().registerReceiver(mMobileStateReceiver, mMobileStateFilter); if (!mRestoreDefaultApnMode) { fillList(); - } else { - showDialog(DIALOG_RESTORE_DEFAULTAPN); } } @Override - protected void onPause() { + public void onPause() { super.onPause(); - unregisterReceiver(mMobileStateReceiver); + if (mUnavailable) { + return; + } + + getActivity().unregisterReceiver(mMobileStateReceiver); } @Override - protected void onDestroy() { + public void onDestroy() { super.onDestroy(); if (mRestoreDefaultApnThread != null) { @@ -178,7 +218,7 @@ public class ApnSettings extends PreferenceActivity implements String key = cursor.getString(ID_INDEX); String type = cursor.getString(TYPES_INDEX); - ApnPreference pref = new ApnPreference(this); + ApnPreference pref = new ApnPreference(getActivity()); pref.setKey(key); pref.setTitle(name); @@ -207,16 +247,18 @@ public class ApnSettings extends PreferenceActivity implements } @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - menu.add(0, MENU_NEW, 0, - getResources().getString(R.string.menu_new)) - .setIcon(android.R.drawable.ic_menu_add) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.add(0, MENU_RESTORE, 0, - getResources().getString(R.string.menu_restore)) - .setIcon(android.R.drawable.ic_menu_upload); - return true; + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!mUnavailable) { + menu.add(0, MENU_NEW, 0, + getResources().getString(R.string.menu_new)) + .setIcon(android.R.drawable.ic_menu_add) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.add(0, MENU_RESTORE, 0, + getResources().getString(R.string.menu_restore)) + .setIcon(android.R.drawable.ic_menu_upload); + } + + super.onCreateOptionsMenu(menu, inflater); } @Override @@ -305,12 +347,17 @@ public class ApnSettings extends PreferenceActivity implements public void handleMessage(Message msg) { switch (msg.what) { case EVENT_RESTORE_DEFAULTAPN_COMPLETE: + Activity activity = getActivity(); + if (activity == null) { + mRestoreDefaultApnMode = false; + return; + } fillList(); getPreferenceScreen().setEnabled(true); mRestoreDefaultApnMode = false; - dismissDialog(DIALOG_RESTORE_DEFAULTAPN); + removeDialog(DIALOG_RESTORE_DEFAULTAPN); Toast.makeText( - ApnSettings.this, + activity, getResources().getString( R.string.restore_default_apn_completed), Toast.LENGTH_LONG).show(); @@ -341,20 +388,13 @@ public class ApnSettings extends PreferenceActivity implements } @Override - protected Dialog onCreateDialog(int id) { + public Dialog onCreateDialog(int id) { if (id == DIALOG_RESTORE_DEFAULTAPN) { - ProgressDialog dialog = new ProgressDialog(this); + ProgressDialog dialog = new ProgressDialog(getActivity()); dialog.setMessage(getResources().getString(R.string.restore_default_apn)); dialog.setCancelable(false); return dialog; } return null; } - - @Override - protected void onPrepareDialog(int id, Dialog dialog) { - if (id == DIALOG_RESTORE_DEFAULTAPN) { - getPreferenceScreen().setEnabled(false); - } - } } diff --git a/src/com/android/settings/AppListPreference.java b/src/com/android/settings/AppListPreference.java new file mode 100644 index 0000000..2180983 --- /dev/null +++ b/src/com/android/settings/AppListPreference.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2013 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.AlertDialog.Builder; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; +import android.widget.ImageView; +import android.widget.ListAdapter; + +/** + * Extends ListPreference to allow us to show the icons for a given list of applications. We do this + * because the names of applications are very similar and the user may not be able to determine what + * app they are selecting without an icon. + */ +public class AppListPreference extends ListPreference { + private Drawable[] mEntryDrawables; + + public class AppArrayAdapter extends ArrayAdapter<CharSequence> { + private Drawable[] mImageDrawables = null; + private int mSelectedIndex = 0; + + public AppArrayAdapter(Context context, int textViewResourceId, + CharSequence[] objects, Drawable[] imageDrawables, int selectedIndex) { + super(context, textViewResourceId, objects); + mSelectedIndex = selectedIndex; + mImageDrawables = imageDrawables; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = ((Activity)getContext()).getLayoutInflater(); + View view = inflater.inflate(R.layout.app_preference_item, parent, false); + CheckedTextView checkedTextView = (CheckedTextView)view.findViewById(R.id.app_label); + checkedTextView.setText(getItem(position)); + if (position == mSelectedIndex) { + checkedTextView.setChecked(true); + } + ImageView imageView = (ImageView)view.findViewById(R.id.app_image); + imageView.setImageDrawable(mImageDrawables[position]); + return view; + } + } + + public AppListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setPackageNames(String[] packageNames, String defaultPackageName) { + // Look up all package names in PackageManager. Skip ones we can't find. + int foundPackages = 0; + PackageManager pm = getContext().getPackageManager(); + ApplicationInfo[] appInfos = new ApplicationInfo[packageNames.length]; + for (int i = 0; i < packageNames.length; i++) { + try { + appInfos[i] = pm.getApplicationInfo(packageNames[i], 0); + foundPackages++; + } catch (NameNotFoundException e) { + // Leave appInfos[i] uninitialized; it will be skipped in the list. + } + } + + // Show the label and icon for each application package. + CharSequence[] applicationNames = new CharSequence[foundPackages]; + mEntryDrawables = new Drawable[foundPackages]; + int index = 0; + int selectedIndex = -1; + for (ApplicationInfo appInfo : appInfos) { + if (appInfo != null) { + applicationNames[index] = appInfo.loadLabel(pm); + mEntryDrawables[index] = appInfo.loadIcon(pm); + if (defaultPackageName != null && + appInfo.packageName.contentEquals(defaultPackageName)) { + selectedIndex = index; + } + index++; + } + } + setEntries(applicationNames); + setEntryValues(packageNames); + if (selectedIndex != -1) { + setValueIndex(selectedIndex); + } + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + int selectedIndex = findIndexOfValue(getValue()); + ListAdapter adapter = new AppArrayAdapter(getContext(), + R.layout.app_preference_item, getEntries(), mEntryDrawables, selectedIndex); + builder.setAdapter(adapter, this); + super.onPrepareDialogBuilder(builder); + } +} diff --git a/src/com/android/settings/BandMode.java b/src/com/android/settings/BandMode.java index 0a0f77f..81e8b49 100644 --- a/src/com/android/settings/BandMode.java +++ b/src/com/android/settings/BandMode.java @@ -58,7 +58,7 @@ public class BandMode extends Activity { super.onCreate(icicle); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - + setContentView(R.layout.band_mode); setTitle(getString(R.string.band_mode_title)); @@ -73,8 +73,6 @@ public class BandMode extends Activity { mBandList.setAdapter(mBandListAdapter); mBandList.setOnItemClickListener(mBandSelectionHandler); - - loadBandList(); } @@ -109,6 +107,7 @@ public class BandMode extends Activity { } public String toString() { + if (mBandMode >= BAND_NAMES.length) return "Band mode " + mBandMode; return BAND_NAMES[mBandMode]; } } diff --git a/src/com/android/settings/BrightnessPreference.java b/src/com/android/settings/BrightnessPreference.java index 6d3b81b..7d60e7f 100644 --- a/src/com/android/settings/BrightnessPreference.java +++ b/src/com/android/settings/BrightnessPreference.java @@ -30,7 +30,7 @@ public class BrightnessPreference extends Preference { @Override protected void onClick() { - Intent intent = new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG); - getContext().sendBroadcastAsUser(intent, UserHandle.CURRENT_OR_SELF); + getContext().startActivityAsUser(new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG), + UserHandle.CURRENT_OR_SELF); } } diff --git a/src/com/android/settings/ButtonBarHandler.java b/src/com/android/settings/ButtonBarHandler.java index d61da13..85e39d1 100644 --- a/src/com/android/settings/ButtonBarHandler.java +++ b/src/com/android/settings/ButtonBarHandler.java @@ -19,7 +19,7 @@ import android.widget.Button; /** * Interface letting {@link SettingsPreferenceFragment} access to bottom bar inside - * {@link android.preference.PreferenceActivity}. + * {@link SettingsActivity}. */ public interface ButtonBarHandler { public boolean hasNextButton(); diff --git a/src/com/android/settings/ChooseLockGeneric.java b/src/com/android/settings/ChooseLockGeneric.java index 49de366..e3a9932 100644 --- a/src/com/android/settings/ChooseLockGeneric.java +++ b/src/com/android/settings/ChooseLockGeneric.java @@ -16,8 +16,9 @@ package com.android.settings; +import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; -import android.app.Fragment; +import android.app.ActivityManagerNative; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -25,32 +26,31 @@ import android.content.Intent; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.Process; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; import android.security.KeyStore; import android.util.EventLog; +import android.util.MutableBoolean; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; import android.widget.ListView; import com.android.internal.widget.LockPatternUtils; -import com.android.settings.ConfirmLockPattern.ConfirmLockPatternFragment; import java.util.List; -import libcore.util.MutableBoolean; - -public class ChooseLockGeneric extends PreferenceActivity { +public class ChooseLockGeneric extends SettingsActivity { + public static final String CONFIRM_CREDENTIALS = "confirm_credentials"; @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockGenericFragment.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } @@ -74,11 +74,14 @@ public class ChooseLockGeneric extends PreferenceActivity { private static final String KEY_UNLOCK_SET_PATTERN = "unlock_set_pattern"; private static final int CONFIRM_EXISTING_REQUEST = 100; private static final int FALLBACK_REQUEST = 101; + private static final int ENABLE_ENCRYPTION_REQUEST = 102; private static final String PASSWORD_CONFIRMED = "password_confirmed"; - private static final String CONFIRM_CREDENTIALS = "confirm_credentials"; + private static final String WAITING_FOR_CONFIRMATION = "waiting_for_confirmation"; private static final String FINISH_PENDING = "finish_pending"; public static final String MINIMUM_QUALITY_KEY = "minimum_quality"; + public static final String ENCRYPT_REQUESTED_QUALITY = "encrypt_requested_quality"; + public static final String ENCRYPT_REQUESTED_DISABLED = "encrypt_requested_disabled"; private static final boolean ALWAY_SHOW_TUTORIAL = true; @@ -88,6 +91,10 @@ public class ChooseLockGeneric extends PreferenceActivity { private boolean mPasswordConfirmed = false; private boolean mWaitingForConfirmation = false; private boolean mFinishPending = false; + private int mEncryptionRequestQuality; + private boolean mEncryptionRequestDisabled; + private boolean mRequirePassword; + private LockPatternUtils mLockPatternUtils; @Override public void onCreate(Bundle savedInstanceState) { @@ -96,6 +103,7 @@ public class ChooseLockGeneric extends PreferenceActivity { mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); mKeyStore = KeyStore.getInstance(); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this.getActivity()); + mLockPatternUtils = new LockPatternUtils(getActivity()); // Defaults to needing to confirm credentials final boolean confirmCredentials = getActivity().getIntent() @@ -108,6 +116,9 @@ public class ChooseLockGeneric extends PreferenceActivity { mPasswordConfirmed = savedInstanceState.getBoolean(PASSWORD_CONFIRMED); mWaitingForConfirmation = savedInstanceState.getBoolean(WAITING_FOR_CONFIRMATION); mFinishPending = savedInstanceState.getBoolean(FINISH_PENDING); + mEncryptionRequestQuality = savedInstanceState.getInt(ENCRYPT_REQUESTED_QUALITY); + mEncryptionRequestDisabled = savedInstanceState.getBoolean( + ENCRYPT_REQUESTED_DISABLED); } if (mPasswordConfirmed) { @@ -148,16 +159,16 @@ public class ChooseLockGeneric extends PreferenceActivity { updateUnlockMethodAndFinish( DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, false); } else if (KEY_UNLOCK_SET_BIOMETRIC_WEAK.equals(key)) { - updateUnlockMethodAndFinish( + maybeEnableEncryption( DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, false); }else if (KEY_UNLOCK_SET_PATTERN.equals(key)) { - updateUnlockMethodAndFinish( + maybeEnableEncryption( DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, false); } else if (KEY_UNLOCK_SET_PIN.equals(key)) { - updateUnlockMethodAndFinish( + maybeEnableEncryption( DevicePolicyManager.PASSWORD_QUALITY_NUMERIC, false); } else if (KEY_UNLOCK_SET_PASSWORD.equals(key)) { - updateUnlockMethodAndFinish( + maybeEnableEncryption( DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, false); } else { handled = false; @@ -165,6 +176,31 @@ public class ChooseLockGeneric extends PreferenceActivity { return handled; } + /** + * If the device has encryption already enabled, then ask the user if they + * also want to encrypt the phone with this password. + * + * @param quality + * @param disabled + */ + private void maybeEnableEncryption(int quality, boolean disabled) { + if (Process.myUserHandle().isOwner() && LockPatternUtils.isDeviceEncryptionEnabled()) { + mEncryptionRequestQuality = quality; + mEncryptionRequestDisabled = disabled; + // If accessibility is enabled and the user hasn't seen this dialog before, set the + // default state to agree with that which is compatible with accessibility + // (password not required). + final boolean accEn = AccessibilityManager.getInstance(getActivity()).isEnabled(); + final boolean required = mLockPatternUtils.isCredentialRequiredToDecrypt(!accEn); + Intent intent = EncryptionInterstitial.createStartIntent( + getActivity(), quality, required); + startActivityForResult(intent, ENABLE_ENCRYPTION_REQUEST); + } else { + mRequirePassword = false; // device encryption not enabled or not device owner. + updateUnlockMethodAndFinish(quality, disabled); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -187,10 +223,15 @@ public class ChooseLockGeneric extends PreferenceActivity { if (requestCode == CONFIRM_EXISTING_REQUEST && resultCode == Activity.RESULT_OK) { mPasswordConfirmed = true; updatePreferencesOrFinish(); - } else if(requestCode == FALLBACK_REQUEST) { + } else if (requestCode == FALLBACK_REQUEST) { mChooseLockSettingsHelper.utils().deleteTempGallery(); getActivity().setResult(resultCode); finish(); + } else if (requestCode == ENABLE_ENCRYPTION_REQUEST + && resultCode == Activity.RESULT_OK) { + mRequirePassword = data.getBooleanExtra( + EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); + updateUnlockMethodAndFinish(mEncryptionRequestQuality, mEncryptionRequestDisabled); } else { getActivity().setResult(Activity.RESULT_CANCELED); finish(); @@ -204,6 +245,8 @@ public class ChooseLockGeneric extends PreferenceActivity { outState.putBoolean(PASSWORD_CONFIRMED, mPasswordConfirmed); outState.putBoolean(WAITING_FOR_CONFIRMATION, mWaitingForConfirmation); outState.putBoolean(FINISH_PENDING, mFinishPending); + outState.putInt(ENCRYPT_REQUESTED_QUALITY, mEncryptionRequestQuality); + outState.putBoolean(ENCRYPT_REQUESTED_DISABLED, mEncryptionRequestDisabled); } private void updatePreferencesOrFinish() { @@ -220,6 +263,7 @@ public class ChooseLockGeneric extends PreferenceActivity { } addPreferencesFromResource(R.xml.security_settings_picker); disableUnusablePreferences(quality, allowBiometric); + updatePreferenceSummaryIfNeeded(); } else { updateUnlockMethodAndFinish(quality, false); } @@ -229,20 +273,7 @@ public class ChooseLockGeneric extends PreferenceActivity { private int upgradeQuality(int quality, MutableBoolean allowBiometric) { quality = upgradeQualityForDPM(quality); quality = upgradeQualityForKeyStore(quality); - int encryptionQuality = upgradeQualityForEncryption(quality); - if (encryptionQuality > quality) { - //The first case checks whether biometric is allowed, prior to the user making - //their selection from the list - if (allowBiometric != null) { - allowBiometric.value = quality <= - DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; - } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK) { - //When the user has selected biometric we shouldn't change that due to - //encryption - return quality; - } - } - return encryptionQuality; + return quality; } private int upgradeQualityForDPM(int quality) { @@ -254,27 +285,6 @@ public class ChooseLockGeneric extends PreferenceActivity { 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. - * - * ASSUMPTION: Setting quality is sufficient (e.g. minimum lengths will be set - * appropriately.) - */ - private int upgradeQualityForEncryption(int quality) { - // Don't upgrade quality for secondary users. Encryption requirements don't apply. - if (!Process.myUserHandle().equals(UserHandle.OWNER)) return quality; - int encryptionStatus = mDPM.getStorageEncryptionStatus(); - boolean encrypted = (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) - || (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVATING); - if (encrypted) { - 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) { @@ -319,7 +329,7 @@ public class ChooseLockGeneric extends PreferenceActivity { } else if (KEY_UNLOCK_SET_PATTERN.equals(key)) { enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; } else if (KEY_UNLOCK_SET_PIN.equals(key)) { - enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; + enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; } else if (KEY_UNLOCK_SET_PASSWORD.equals(key)) { enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; } @@ -333,6 +343,32 @@ public class ChooseLockGeneric extends PreferenceActivity { } } + private void updatePreferenceSummaryIfNeeded() { + if (LockPatternUtils.isDeviceEncrypted()) { + return; + } + + if (AccessibilityManager.getInstance(getActivity()).getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()) { + return; + } + + CharSequence summary = getString(R.string.secure_lock_encryption_warning); + + PreferenceScreen screen = getPreferenceScreen(); + final int preferenceCount = screen.getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + Preference preference = screen.getPreference(i); + switch (preference.getKey()) { + case KEY_UNLOCK_SET_PATTERN: + case KEY_UNLOCK_SET_PIN: + case KEY_UNLOCK_SET_PASSWORD: { + preference.setSummary(summary); + } break; + } + } + } + /** * Check whether the key is allowed for fallback (e.g. bio sensor). Returns true if it's * supported as a backup. @@ -389,13 +425,8 @@ public class ChooseLockGeneric extends PreferenceActivity { minLength = MIN_PASSWORD_LENGTH; } final int maxLength = mDPM.getPasswordMaximumLength(quality); - Intent intent = new Intent().setClass(getActivity(), ChooseLockPassword.class); - intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality); - intent.putExtra(ChooseLockPassword.PASSWORD_MIN_KEY, minLength); - intent.putExtra(ChooseLockPassword.PASSWORD_MAX_KEY, maxLength); - intent.putExtra(CONFIRM_CREDENTIALS, false); - intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, - isFallback); + Intent intent = ChooseLockPassword.createIntent(getActivity(), quality, isFallback, + minLength, maxLength, mRequirePassword, false /* confirm credentials */); if (isFallback) { startActivityForResult(intent, FALLBACK_REQUEST); return; @@ -405,11 +436,8 @@ public class ChooseLockGeneric extends PreferenceActivity { startActivity(intent); } } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) { - Intent intent = new Intent(getActivity(), ChooseLockPattern.class); - intent.putExtra("key_lock_method", "pattern"); - intent.putExtra(CONFIRM_CREDENTIALS, false); - intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, - isFallback); + Intent intent = ChooseLockPattern.createIntent(getActivity(), + isFallback, mRequirePassword, false /* confirm credentials */); if (isFallback) { startActivityForResult(intent, FALLBACK_REQUEST); return; diff --git a/src/com/android/settings/ChooseLockPassword.java b/src/com/android/settings/ChooseLockPassword.java index f43738f..b72d5c5 100644 --- a/src/com/android/settings/ChooseLockPassword.java +++ b/src/com/android/settings/ChooseLockPassword.java @@ -19,17 +19,19 @@ package com.android.settings; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.PasswordEntryKeyboardHelper; import com.android.internal.widget.PasswordEntryKeyboardView; -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; +import com.android.settings.notification.RedactionInterstitial; import android.app.Activity; import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.inputmethodservice.KeyboardView; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.preference.PreferenceActivity; +import android.provider.Settings; import android.text.Editable; import android.text.InputType; import android.text.Selection; @@ -41,13 +43,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.View.OnClickListener; -import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -public class ChooseLockPassword extends PreferenceActivity { +public class ChooseLockPassword extends SettingsActivity { public static final String PASSWORD_MIN_KEY = "lockscreen.password_min"; public static final String PASSWORD_MAX_KEY = "lockscreen.password_max"; public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters"; @@ -61,10 +62,22 @@ public class ChooseLockPassword extends PreferenceActivity { public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPasswordFragment.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } + public static Intent createIntent(Context context, int quality, final boolean isFallback, + int minLength, final int maxLength, boolean requirePasswordToDecrypt, + boolean confirmCredentials) { + Intent intent = new Intent().setClass(context, ChooseLockPassword.class); + intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality); + intent.putExtra(PASSWORD_MIN_KEY, minLength); + intent.putExtra(PASSWORD_MAX_KEY, maxLength); + intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials); + intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, isFallback); + intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePasswordToDecrypt); + return intent; + } + @Override protected boolean isValidFragment(String fragmentName) { if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true; @@ -79,7 +92,7 @@ public class ChooseLockPassword extends PreferenceActivity { //WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); super.onCreate(savedInstanceState); CharSequence msg = getText(R.string.lockpassword_choose_your_password_header); - showBreadCrumbs(msg, msg); + setTitle(msg); } public static class ChooseLockPasswordFragment extends Fragment @@ -99,6 +112,7 @@ public class ChooseLockPassword extends PreferenceActivity { private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; private ChooseLockSettingsHelper mChooseLockSettingsHelper; private Stage mUiStage = Stage.Introduction; + private boolean mDone = false; private TextView mHeaderText; private String mFirstPin; private KeyboardView mKeyboardView; @@ -137,9 +151,6 @@ public class ChooseLockPassword extends PreferenceActivity { R.string.lockpassword_confirm_pins_dont_match, R.string.lockpassword_continue_label); - /** - * @param headerMessage The message displayed at the top. - */ Stage(int hintInAlpha, int hintInNumeric, int nextButtonText) { this.alphaHint = hintInAlpha; this.numericHint = hintInNumeric; @@ -235,13 +246,13 @@ public class ChooseLockPassword extends PreferenceActivity { updateStage(mUiStage); } } - // Update the breadcrumb (title) if this is embedded in a PreferenceActivity - if (activity instanceof PreferenceActivity) { - final PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + mDone = false; + if (activity instanceof SettingsActivity) { + final SettingsActivity sa = (SettingsActivity) activity; int id = mIsAlphaMode ? R.string.lockpassword_choose_your_password_header : R.string.lockpassword_choose_your_pin_header; CharSequence title = getText(id); - preferenceActivity.showBreadCrumbs(title, title); + sa.setTitle(title); } return view; @@ -337,10 +348,18 @@ public class ChooseLockPassword extends PreferenceActivity { } } if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC == mRequestedQuality - && (letters > 0 || symbols > 0)) { - // This shouldn't be possible unless user finds some way to bring up - // soft keyboard - return getString(R.string.lockpassword_pin_contains_non_digits); + || DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX == mRequestedQuality) { + if (letters > 0 || symbols > 0) { + // This shouldn't be possible unless user finds some way to bring up + // soft keyboard + return getString(R.string.lockpassword_pin_contains_non_digits); + } + // Check for repeated characters or sequences (e.g. '1234', '0000', '2468') + final int sequence = LockPatternUtils.maxLengthSequence(password); + if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX == mRequestedQuality + && sequence > LockPatternUtils.MAX_ALLOWED_SEQUENCE) { + return getString(R.string.lockpassword_pin_no_sequential_digits); + } } else if (DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality) { if (letters < mPasswordMinLetters) { return String.format(getResources().getQuantityString( @@ -383,10 +402,13 @@ public class ChooseLockPassword extends PreferenceActivity { return getString(mIsAlphaMode ? R.string.lockpassword_password_recently_used : R.string.lockpassword_pin_recently_used); } + return null; } private void handleNext() { + if (mDone) return; + final String pin = mPasswordEntry.getText().toString(); if (TextUtils.isEmpty(pin)) { return; @@ -404,9 +426,14 @@ public class ChooseLockPassword extends PreferenceActivity { final boolean isFallback = getActivity().getIntent().getBooleanExtra( LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false); mLockPatternUtils.clearLock(isFallback); + final boolean required = getActivity().getIntent().getBooleanExtra( + EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); + mLockPatternUtils.setCredentialRequiredToDecrypt(required); mLockPatternUtils.saveLockPassword(pin, mRequestedQuality, isFallback); getActivity().setResult(RESULT_FINISHED); getActivity().finish(); + mDone = true; + startActivity(RedactionInterstitial.createStartIntent(getActivity())); } else { CharSequence tmp = mPasswordEntry.getText(); if (tmp != null) { diff --git a/src/com/android/settings/ChooseLockPattern.java b/src/com/android/settings/ChooseLockPattern.java index 328312c..3d3ef16 100644 --- a/src/com/android/settings/ChooseLockPattern.java +++ b/src/com/android/settings/ChooseLockPattern.java @@ -17,20 +17,21 @@ package com.android.settings; import com.google.android.collect.Lists; - import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; import com.android.internal.widget.LockPatternView.Cell; -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; +import com.android.settings.notification.RedactionInterstitial; import static com.android.internal.widget.LockPatternView.DisplayMode; import android.app.Activity; import android.app.Fragment; +import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.preference.PreferenceActivity; +import android.provider.Settings; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -49,7 +50,7 @@ import java.util.List; * - asks for confirmation / restart * - saves chosen password when confirmed */ -public class ChooseLockPattern extends PreferenceActivity { +public class ChooseLockPattern extends SettingsActivity { /** * Used by the choose lock pattern wizard to indicate the wizard is * finished, and each activity in the wizard should finish. @@ -65,10 +66,19 @@ public class ChooseLockPattern extends PreferenceActivity { public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPatternFragment.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } + public static Intent createIntent(Context context, final boolean isFallback, + boolean requirePassword, boolean confirmCredentials) { + Intent intent = new Intent(context, ChooseLockPattern.class); + intent.putExtra("key_lock_method", "pattern"); + intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials); + intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, isFallback); + intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePassword); + return intent; + } + @Override protected boolean isValidFragment(String fragmentName) { if (ChooseLockPatternFragment.class.getName().equals(fragmentName)) return true; @@ -80,7 +90,7 @@ public class ChooseLockPattern extends PreferenceActivity { // requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); CharSequence msg = getText(R.string.lockpassword_choose_your_pattern_header); - showBreadCrumbs(msg, msg); + setTitle(msg); } @Override @@ -292,6 +302,7 @@ public class ChooseLockPattern extends PreferenceActivity { } private Stage mUiStage = Stage.Introduction; + private boolean mDone = false; private Runnable mClearPatternRunnable = new Runnable() { public void run() { @@ -365,6 +376,7 @@ public class ChooseLockPattern extends PreferenceActivity { } updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); } + mDone = false; return view; } @@ -521,11 +533,16 @@ public class ChooseLockPattern extends PreferenceActivity { } private void saveChosenPatternAndFinish() { + if (mDone) return; LockPatternUtils utils = mChooseLockSettingsHelper.utils(); final boolean lockVirgin = !utils.isPatternEverChosen(); final boolean isFallback = getActivity().getIntent() .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false); + + final boolean required = getActivity().getIntent().getBooleanExtra( + EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); + utils.setCredentialRequiredToDecrypt(required); utils.saveLockPattern(mChosenPattern, isFallback); utils.setLockPatternEnabled(true); @@ -535,6 +552,8 @@ public class ChooseLockPattern extends PreferenceActivity { getActivity().setResult(RESULT_FINISHED); getActivity().finish(); + mDone = true; + startActivity(RedactionInterstitial.createStartIntent(getActivity())); } } } diff --git a/src/com/android/settings/ChooseLockSettingsHelper.java b/src/com/android/settings/ChooseLockSettingsHelper.java index a069712..3086a7a 100644 --- a/src/com/android/settings/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/ChooseLockSettingsHelper.java @@ -25,6 +25,7 @@ import android.content.Intent; public final class ChooseLockSettingsHelper { + static final String EXTRA_KEY_TYPE = "type"; static final String EXTRA_KEY_PASSWORD = "password"; private LockPatternUtils mLockPatternUtils; @@ -53,17 +54,32 @@ public final class ChooseLockSettingsHelper { * @see #onActivityResult(int, int, android.content.Intent) */ boolean launchConfirmationActivity(int request, CharSequence message, CharSequence details) { + return launchConfirmationActivity(request, message, details, false); + } + + /** + * If a pattern, password or PIN exists, prompt the user before allowing them to change it. + * @param message optional message to display about the action about to be done + * @param details optional detail message to display + * @param returnCredentials if true, put credentials into intent. Note that if this is true, + this can only be called internally. + * @return true if one exists and we launched an activity to confirm it + * @see #onActivityResult(int, int, android.content.Intent) + */ + boolean launchConfirmationActivity(int request, CharSequence message, CharSequence details, + boolean returnCredentials) { boolean launched = false; switch (mLockPatternUtils.getKeyguardStoredPasswordQuality()) { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: - launched = confirmPattern(request, message, details); + launched = confirmPattern(request, message, details, returnCredentials); break; case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: // TODO: update UI layout for ConfirmPassword to show message and details - launched = confirmPassword(request); + launched = confirmPassword(request, message, returnCredentials); break; } return launched; @@ -73,10 +89,12 @@ public final class ChooseLockSettingsHelper { * Launch screen to confirm the existing lock pattern. * @param message shown in header of ConfirmLockPattern if not null * @param details shown in footer of ConfirmLockPattern if not null + * @param returnCredentials if true, put credentials into intent. * @see #onActivityResult(int, int, android.content.Intent) * @return true if we launched an activity to confirm pattern */ - private boolean confirmPattern(int request, CharSequence message, CharSequence details) { + private boolean confirmPattern(int request, CharSequence message, + CharSequence details, boolean returnCredentials) { if (!mLockPatternUtils.isLockPatternEnabled() || !mLockPatternUtils.savedPatternExists()) { return false; } @@ -84,7 +102,10 @@ public final class ChooseLockSettingsHelper { // supply header and footer text in the intent intent.putExtra(ConfirmLockPattern.HEADER_TEXT, message); intent.putExtra(ConfirmLockPattern.FOOTER_TEXT, details); - intent.setClassName("com.android.settings", "com.android.settings.ConfirmLockPattern"); + intent.setClassName("com.android.settings", + returnCredentials + ? ConfirmLockPattern.InternalActivity.class.getName() + : ConfirmLockPattern.class.getName()); if (mFragment != null) { mFragment.startActivityForResult(intent, request); } else { @@ -95,13 +116,21 @@ public final class ChooseLockSettingsHelper { /** * Launch screen to confirm the existing lock password. + * @param message shown in header of ConfirmLockPassword if not null + * @param returnCredentials if true, put credentials into intent. * @see #onActivityResult(int, int, android.content.Intent) * @return true if we launched an activity to confirm password */ - private boolean confirmPassword(int request) { + private boolean confirmPassword(int request, CharSequence message, + boolean returnCredentials) { if (!mLockPatternUtils.isLockPasswordEnabled()) return false; final Intent intent = new Intent(); - intent.setClassName("com.android.settings", "com.android.settings.ConfirmLockPassword"); + // supply header text in the intent + intent.putExtra(ConfirmLockPattern.HEADER_TEXT, message); + intent.setClassName("com.android.settings", + returnCredentials + ? ConfirmLockPassword.InternalActivity.class.getName() + : ConfirmLockPassword.class.getName()); if (mFragment != null) { mFragment.startActivityForResult(intent, request); } else { diff --git a/src/com/android/settings/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/ConfirmDeviceCredentialActivity.java new file mode 100644 index 0000000..beb2d97 --- /dev/null +++ b/src/com/android/settings/ConfirmDeviceCredentialActivity.java @@ -0,0 +1,65 @@ + +/* + * Copyright (C) 2014 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.KeyguardManager; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/** + * Launch this when you want to confirm the user is present by asking them to enter their + * PIN/password/pattern. + */ +public class ConfirmDeviceCredentialActivity extends Activity { + public static final String TAG = ConfirmDeviceCredentialActivity.class.getSimpleName(); + + public static Intent createIntent(CharSequence title, CharSequence details) { + Intent intent = new Intent(); + intent.setClassName("com.android.settings", + ConfirmDeviceCredentialActivity.class.getName()); + intent.putExtra(KeyguardManager.EXTRA_TITLE, title); + intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, details); + return intent; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + String title = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); + String details = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); + + ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); + if (!helper.launchConfirmationActivity(0 /* request code */, title, details)) { + Log.d(TAG, "No pattern, password or PIN set."); + setResult(Activity.RESULT_OK); + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + boolean credentialsConfirmed = (resultCode == Activity.RESULT_OK); + Log.d(TAG, "Device credentials confirmed: " + credentialsConfirmed); + setResult(credentialsConfirmed ? Activity.RESULT_OK : Activity.RESULT_CANCELED); + finish(); + } +} diff --git a/src/com/android/settings/ConfirmLockPassword.java b/src/com/android/settings/ConfirmLockPassword.java index 38d4a89..c74e861 100644 --- a/src/com/android/settings/ConfirmLockPassword.java +++ b/src/com/android/settings/ConfirmLockPassword.java @@ -16,10 +16,10 @@ package com.android.settings; +import android.text.TextUtils; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.PasswordEntryKeyboardHelper; import com.android.internal.widget.PasswordEntryKeyboardView; -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; import android.app.Activity; import android.app.Fragment; @@ -28,8 +28,8 @@ import android.content.Intent; import android.os.Bundle; import android.os.CountDownTimer; import android.os.Handler; -import android.preference.PreferenceActivity; import android.os.SystemClock; +import android.os.storage.StorageManager; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -38,19 +38,23 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -public class ConfirmLockPassword extends PreferenceActivity { +public class ConfirmLockPassword extends SettingsActivity { + + public static final String PACKAGE = "com.android.settings"; + public static final String HEADER_TEXT = PACKAGE + ".ConfirmLockPattern.header"; + + public static class InternalActivity extends ConfirmLockPassword { + } @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPasswordFragment.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } @@ -67,11 +71,13 @@ public class ConfirmLockPassword extends PreferenceActivity { //WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); super.onCreate(savedInstanceState); CharSequence msg = getText(R.string.lockpassword_confirm_your_password_header); - showBreadCrumbs(msg, msg); + setTitle(msg); } public static class ConfirmLockPasswordFragment extends Fragment implements OnClickListener, OnEditorActionListener, TextWatcher { + private static final String KEY_NUM_WRONG_CONFIRM_ATTEMPTS + = "confirm_lock_password_fragment.key_num_wrong_confirm_attempts"; private static final long ERROR_MESSAGE_TIMEOUT = 3000; private TextView mPasswordEntry; private LockPatternUtils mLockPatternUtils; @@ -93,6 +99,10 @@ public class ConfirmLockPassword extends PreferenceActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mLockPatternUtils = new LockPatternUtils(getActivity()); + if (savedInstanceState != null) { + mNumWrongConfirmAttempts = savedInstanceState.getInt( + KEY_NUM_WRONG_CONFIRM_ATTEMPTS, 0); + } } @Override @@ -116,7 +126,15 @@ public class ConfirmLockPassword extends PreferenceActivity { mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == storedQuality; - mHeaderText.setText(getDefaultHeader()); + + Intent intent = getActivity().getIntent(); + if (intent != null) { + CharSequence headerMessage = intent.getCharSequenceExtra(HEADER_TEXT); + if (TextUtils.isEmpty(headerMessage)) { + headerMessage = getString(getDefaultHeader()); + } + mHeaderText.setText(headerMessage); + } final Activity activity = getActivity(); mKeyboardHelper = new PasswordEntryKeyboardHelper(activity, @@ -130,12 +148,11 @@ public class ConfirmLockPassword extends PreferenceActivity { mPasswordEntry.setInputType(mIsAlpha ? currentType : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD)); - // Update the breadcrumb (title) if this is embedded in a PreferenceActivity - if (activity instanceof PreferenceActivity) { - final PreferenceActivity preferenceActivity = (PreferenceActivity) activity; + if (activity instanceof SettingsActivity) { + final SettingsActivity sa = (SettingsActivity) activity; int id = getDefaultHeader(); CharSequence title = getText(id); - preferenceActivity.showBreadCrumbs(title, title); + sa.setTitle(title); } return view; @@ -167,12 +184,23 @@ public class ConfirmLockPassword extends PreferenceActivity { } } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_NUM_WRONG_CONFIRM_ATTEMPTS, mNumWrongConfirmAttempts); + } + private void handleNext() { final String pin = mPasswordEntry.getText().toString(); if (mLockPatternUtils.checkPassword(pin)) { Intent intent = new Intent(); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin); + if (getActivity() instanceof ConfirmLockPassword.InternalActivity) { + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, + mIsAlpha ? StorageManager.CRYPT_TYPE_PASSWORD + : StorageManager.CRYPT_TYPE_PIN); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin); + } getActivity().setResult(RESULT_OK, intent); getActivity().finish(); diff --git a/src/com/android/settings/ConfirmLockPattern.java b/src/com/android/settings/ConfirmLockPattern.java index 3a1f06c..caf691d 100644 --- a/src/com/android/settings/ConfirmLockPattern.java +++ b/src/com/android/settings/ConfirmLockPattern.java @@ -27,7 +27,7 @@ import android.content.Intent; import android.os.CountDownTimer; import android.os.SystemClock; import android.os.Bundle; -import android.preference.PreferenceActivity; +import android.os.storage.StorageManager; import android.widget.TextView; import android.view.LayoutInflater; import android.view.View; @@ -41,7 +41,10 @@ import java.util.List; * Sets an activity result of {@link Activity#RESULT_OK} when the user * successfully confirmed their pattern. */ -public class ConfirmLockPattern extends PreferenceActivity { +public class ConfirmLockPattern extends SettingsActivity { + + public static class InternalActivity extends ConfirmLockPattern { + } /** * Names of {@link CharSequence} fields within the originating {@link Intent} @@ -65,14 +68,13 @@ public class ConfirmLockPattern extends PreferenceActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CharSequence msg = getText(R.string.lockpassword_confirm_your_pattern_header); - showBreadCrumbs(msg, msg); + setTitle(msg); } @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPatternFragment.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } @@ -267,8 +269,12 @@ public class ConfirmLockPattern extends PreferenceActivity { if (mLockPatternUtils.checkPattern(pattern)) { Intent intent = new Intent(); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, - LockPatternUtils.patternToString(pattern)); + if (getActivity() instanceof ConfirmLockPattern.InternalActivity) { + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, + StorageManager.CRYPT_TYPE_PATTERN); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, + LockPatternUtils.patternToString(pattern)); + } getActivity().setResult(Activity.RESULT_OK, intent); getActivity().finish(); diff --git a/src/com/android/settings/CreateShortcut.java b/src/com/android/settings/CreateShortcut.java index f71df1d..0bf265f 100644 --- a/src/com/android/settings/CreateShortcut.java +++ b/src/com/android/settings/CreateShortcut.java @@ -19,7 +19,6 @@ package com.android.settings; import android.app.LauncherActivity; import android.content.Intent; import android.content.pm.ResolveInfo; -import android.os.Bundle; import android.view.View; import android.widget.ListView; diff --git a/src/com/android/settings/CredentialStorage.java b/src/com/android/settings/CredentialStorage.java index fcf208a..57b5384 100644 --- a/src/com/android/settings/CredentialStorage.java +++ b/src/com/android/settings/CredentialStorage.java @@ -19,6 +19,7 @@ package com.android.settings; import android.app.Activity; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; @@ -26,6 +27,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.os.Process; +import android.os.UserManager; import android.security.Credentials; import android.security.KeyChain.KeyChainConnection; import android.security.KeyChain; @@ -38,8 +40,8 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; -import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternUtils; import com.android.org.bouncycastle.asn1.ASN1InputStream; import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; @@ -119,16 +121,20 @@ public final class CredentialStorage extends Activity { Intent intent = getIntent(); String action = intent.getAction(); - - if (ACTION_RESET.equals(action)) { - new ResetDialog(); - } else { - if (ACTION_INSTALL.equals(action) - && "com.android.certinstaller".equals(getCallingPackage())) { - mInstallBundle = intent.getExtras(); + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { + if (ACTION_RESET.equals(action)) { + new ResetDialog(); + } else { + if (ACTION_INSTALL.equals(action) + && "com.android.certinstaller".equals(getCallingPackage())) { + mInstallBundle = intent.getExtras(); + } + // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL + handleUnlockOrInstall(); } - // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL - handleUnlockOrInstall(); + } else { + finish(); } } @@ -270,7 +276,6 @@ public final class CredentialStorage extends Activity { private ResetDialog() { AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) .setTitle(android.R.string.dialog_alert_title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(R.string.credentials_reset_hint) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, this) @@ -340,7 +345,6 @@ public final class CredentialStorage extends Activity { private ConfigureKeyGuardDialog() { AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) .setTitle(android.R.string.dialog_alert_title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(R.string.credentials_configure_lock_screen_hint) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, this) @@ -374,7 +378,8 @@ public final class CredentialStorage extends Activity { boolean launched = new ChooseLockSettingsHelper(this) .launchConfirmationActivity(CONFIRM_KEY_GUARD_REQUEST, res.getText(R.string.credentials_install_gesture_prompt), - res.getText(R.string.credentials_install_gesture_explanation)); + res.getText(R.string.credentials_install_gesture_explanation), + true); return launched; } diff --git a/src/com/android/settings/CryptKeeper.java b/src/com/android/settings/CryptKeeper.java index 23ec70e..2242fad 100644 --- a/src/com/android/settings/CryptKeeper.java +++ b/src/com/android/settings/CryptKeeper.java @@ -21,7 +21,9 @@ import android.app.StatusBarManager; 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.res.Resources.NotFoundException; import android.media.AudioManager; import android.os.AsyncTask; import android.os.Bundle; @@ -34,11 +36,14 @@ import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; import android.os.storage.IMountService; +import android.os.storage.StorageManager; import android.provider.Settings; +import android.telecom.TelecomManager; import android.telephony.TelephonyManager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.format.DateUtils; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -57,9 +62,13 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.android.internal.statusbar.StatusBarIcon; -import com.android.internal.telephony.ITelephony; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import com.android.internal.widget.LockPatternView.Cell; + +import static com.android.internal.widget.LockPatternView.DisplayMode; import java.util.List; @@ -108,13 +117,42 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList private boolean mValidationRequested; /** A flag to indicate that the volume is in a bad state (e.g. partially encrypted). */ private boolean mEncryptionGoneBad; + /** If gone bad, should we show encryption failed (false) or corrupt (true)*/ + private boolean mCorrupt; /** A flag to indicate when the back event should be ignored */ private boolean mIgnoreBack = false; private int mCooldown; PowerManager.WakeLock mWakeLock; private EditText mPasswordEntry; + private LockPatternView mLockPatternView; /** Number of calls to {@link #notifyUser()} to ignore before notifying. */ private int mNotificationCountdown = 0; + /** Number of calls to {@link #notifyUser()} before we release the wakelock */ + private int mReleaseWakeLockCountdown = 0; + private int mStatusString = R.string.enter_password; + + // how long we wait to clear a wrong pattern + private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 1500; + + // how long we wait to clear a right pattern + private static final int RIGHT_PATTERN_CLEAR_TIMEOUT_MS = 500; + + // When the user enters a short pin/password, run this to show an error, + // but don't count it against attempts. + private final Runnable mFakeUnlockAttemptRunnable = new Runnable() { + public void run() { + handleBadAttempt(1 /* failedAttempt */); + } + }; + + // TODO: this should be tuned to match minimum decryption timeout + private static final int FAKE_ATTEMPT_DELAY = 1000; + + private Runnable mClearPatternRunnable = new Runnable() { + public void run() { + mLockPatternView.clearPattern(); + } + }; /** * Used to propagate state through configuration changes (e.g. screen rotation) @@ -127,23 +165,14 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList } } - /** - * Activity used to fade the screen to black after the password is entered. - */ - public static class FadeToBlack extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.crypt_keeper_blank); - } - /** Ignore all back events. */ - @Override - public void onBackPressed() { - return; + private class DecryptTask extends AsyncTask<String, Void, Integer> { + private void hide(int id) { + View view = findViewById(id); + if (view != null) { + view.setVisibility(View.GONE); + } } - } - private class DecryptTask extends AsyncTask<String, Void, Integer> { @Override protected Integer doInBackground(String... params) { final IMountService service = getMountService(); @@ -158,35 +187,84 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList @Override protected void onPostExecute(Integer failedAttempts) { if (failedAttempts == 0) { - // The password was entered successfully. Start the Blank activity - // so this activity animates to black before the devices starts. Note - // It has 1 second to complete the animation or it will be frozen - // until the boot animation comes back up. - Intent intent = new Intent(CryptKeeper.this, FadeToBlack.class); - finish(); - startActivity(intent); + // The password was entered successfully. Simply do nothing + // and wait for the service restart to switch to surfacefligner + if (mLockPatternView != null) { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + mLockPatternView.postDelayed(mClearPatternRunnable, RIGHT_PATTERN_CLEAR_TIMEOUT_MS); + } + hide(R.id.passwordEntry); + hide(R.id.switch_ime_button); + hide(R.id.lockPattern); + hide(R.id.status); + hide(R.id.owner_info); + hide(R.id.emergencyCallButton); } else if (failedAttempts == MAX_FAILED_ATTEMPTS) { // Factory reset the device. - sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); - } else if ((failedAttempts % COOL_DOWN_ATTEMPTS) == 0) { - mCooldown = COOL_DOWN_INTERVAL; - cooldown(); + Intent intent = new Intent(Intent.ACTION_MASTER_CLEAR); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + intent.putExtra(Intent.EXTRA_REASON, "CryptKeeper.MAX_FAILED_ATTEMPTS"); + sendBroadcast(intent); + } else if (failedAttempts == -1) { + // Right password, but decryption failed. Tell user bad news ... + setContentView(R.layout.crypt_keeper_progress); + showFactoryReset(true); + return; + } else { + handleBadAttempt(failedAttempts); + } + } + } + + private void handleBadAttempt(Integer failedAttempts) { + // Wrong entry. Handle pattern case. + if (mLockPatternView != null) { + mLockPatternView.setDisplayMode(DisplayMode.Wrong); + mLockPatternView.removeCallbacks(mClearPatternRunnable); + mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); + } + if ((failedAttempts % COOL_DOWN_ATTEMPTS) == 0) { + mCooldown = COOL_DOWN_INTERVAL; + cooldown(); + } else { + final TextView status = (TextView) findViewById(R.id.status); + + int remainingAttempts = MAX_FAILED_ATTEMPTS - failedAttempts; + if (remainingAttempts < COOL_DOWN_ATTEMPTS) { + CharSequence warningTemplate = getText(R.string.crypt_keeper_warn_wipe); + CharSequence warning = TextUtils.expandTemplate(warningTemplate, + Integer.toString(remainingAttempts)); + status.setText(warning); } else { - final TextView status = (TextView) findViewById(R.id.status); status.setText(R.string.try_again); - // Reenable the password entry + } + + if (mLockPatternView != null) { + mLockPatternView.setDisplayMode(DisplayMode.Wrong); + } + // Reenable the password entry + if (mPasswordEntry != null) { mPasswordEntry.setEnabled(true); + final InputMethodManager imm = (InputMethodManager) getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mPasswordEntry, 0); + setBackFunctionality(true); + } + if (mLockPatternView != null) { + mLockPatternView.setEnabled(true); } } } private class ValidationTask extends AsyncTask<Void, Void, Boolean> { + int state; + @Override protected Boolean doInBackground(Void... params) { final IMountService service = getMountService(); try { Log.d(TAG, "Validating encryption state."); - int state = service.getEncryptionState(); + state = service.getEncryptionState(); if (state == IMountService.ENCRYPTION_STATE_NONE) { Log.w(TAG, "Unexpectedly in CryptKeeper even though there is no encryption."); return true; // Unexpected, but fine, I guess... @@ -204,6 +282,7 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList if (Boolean.FALSE.equals(result)) { Log.w(TAG, "Incomplete, or corrupted encryption detected. Prompting user to wipe."); mEncryptionGoneBad = true; + mCorrupt = state == IMountService.ENCRYPTION_STATE_ERROR_CORRUPT; } else { Log.d(TAG, "Encryption state validated. Proceeding to configure UI"); } @@ -243,6 +322,8 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList | StatusBarManager.DISABLE_SEARCH | StatusBarManager.DISABLE_RECENT; + protected static final int MIN_LENGTH_BEFORE_REPORT = LockPatternUtils.MIN_LOCK_PATTERN_SIZE; + /** @return whether or not this Activity was started for debugging the UI only. */ private boolean isDebugView() { return getIntent().hasExtra(EXTRA_FORCE_VIEW); @@ -274,6 +355,14 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList // Notify the user again in 5 seconds. mHandler.removeMessages(MESSAGE_NOTIFY); mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY, 5 * 1000); + + if (mWakeLock.isHeld()) { + if (mReleaseWakeLockCountdown > 0) { + --mReleaseWakeLockCountdown; + } else { + mWakeLock.release(); + } + } } /** @@ -311,6 +400,13 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList return; } + try { + if (getResources().getBoolean(R.bool.crypt_keeper_allow_rotation)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } catch (NotFoundException e) { + } + // Disable the status bar, but do NOT disable back because the user needs a way to go // from keyboard settings and back to the password screen. mStatusBar = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE); @@ -345,7 +441,7 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList private void setupUi() { if (mEncryptionGoneBad || isDebugView(FORCE_VIEW_ERROR)) { setContentView(R.layout.crypt_keeper_progress); - showFactoryReset(); + showFactoryReset(mCorrupt); return; } @@ -354,8 +450,57 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList setContentView(R.layout.crypt_keeper_progress); encryptionProgressInit(); } else if (mValidationComplete || isDebugView(FORCE_VIEW_PASSWORD)) { - setContentView(R.layout.crypt_keeper_password_entry); - passwordEntryInit(); + new AsyncTask<Void, Void, Void>() { + int type = StorageManager.CRYPT_TYPE_PASSWORD; + String owner_info; + boolean pattern_visible; + + @Override + public Void doInBackground(Void... v) { + try { + final IMountService service = getMountService(); + type = service.getPasswordType(); + owner_info = service.getField("OwnerInfo"); + pattern_visible = !("0".equals(service.getField("PatternVisible"))); + } catch (Exception e) { + Log.e(TAG, "Error calling mount service " + e); + } + + return null; + } + + @Override + public void onPostExecute(java.lang.Void v) { + if(type == StorageManager.CRYPT_TYPE_PIN) { + setContentView(R.layout.crypt_keeper_pin_entry); + mStatusString = R.string.enter_pin; + } else if (type == StorageManager.CRYPT_TYPE_PATTERN) { + setContentView(R.layout.crypt_keeper_pattern_entry); + setBackFunctionality(false); + mStatusString = R.string.enter_pattern; + } else { + setContentView(R.layout.crypt_keeper_password_entry); + mStatusString = R.string.enter_password; + } + final TextView status = (TextView) findViewById(R.id.status); + status.setText(mStatusString); + + final TextView ownerInfo = (TextView) findViewById(R.id.owner_info); + ownerInfo.setText(owner_info); + ownerInfo.setSelected(true); // Required for marquee'ing to work + + passwordEntryInit(); + + if (mLockPatternView != null) { + mLockPatternView.setInStealthMode(!pattern_visible); + } + + if (mCooldown > 0) { + setBackFunctionality(false); + cooldown(); // in case we are cooling down and coming back from emergency dialler + } + } + }.execute(); } else if (!mValidationRequested) { // We're supposed to be encrypted, but no validation has been done. new ValidationTask().execute((Void[]) null); @@ -418,7 +563,13 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList updateProgress(); } - private void showFactoryReset() { + /** + * Show factory reset screen allowing the user to reset their phone when + * there is nothing else we can do + * @param corrupt true if userdata is corrupt, false if encryption failed + * partway through + */ + private void showFactoryReset(final boolean corrupt) { // Hide the encryption-bot to make room for the "factory reset" button findViewById(R.id.encroid).setVisibility(View.GONE); @@ -429,13 +580,22 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList @Override public void onClick(View v) { // Factory reset the device. - sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); + Intent intent = new Intent(Intent.ACTION_MASTER_CLEAR); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + intent.putExtra(Intent.EXTRA_REASON, + "CryptKeeper.showFactoryReset() corrupt=" + corrupt); + sendBroadcast(intent); } }); // Alert the user of the failure. - ((TextView) findViewById(R.id.title)).setText(R.string.crypt_keeper_failed_title); - ((TextView) findViewById(R.id.status)).setText(R.string.crypt_keeper_failed_summary); + if (corrupt) { + ((TextView) findViewById(R.id.title)).setText(R.string.crypt_keeper_data_corrupt_title); + ((TextView) findViewById(R.id.status)).setText(R.string.crypt_keeper_data_corrupt_summary); + } else { + ((TextView) findViewById(R.id.title)).setText(R.string.crypt_keeper_failed_title); + ((TextView) findViewById(R.id.status)).setText(R.string.crypt_keeper_failed_summary); + } final View view = findViewById(R.id.bottom_divider); // TODO(viki): Why would the bottom divider be missing in certain layouts? Investigate. @@ -448,27 +608,44 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList final String state = SystemProperties.get("vold.encrypt_progress"); if ("error_partially_encrypted".equals(state)) { - showFactoryReset(); + showFactoryReset(false); return; } - int progress = 0; + // Get status as percentage first + CharSequence status = getText(R.string.crypt_keeper_setup_description); + int percent = 0; try { // Force a 50% progress state when debugging the view. - progress = isDebugView() ? 50 : Integer.parseInt(state); + percent = isDebugView() ? 50 : Integer.parseInt(state); } catch (Exception e) { Log.w(TAG, "Error parsing progress: " + e.toString()); } + String progress = Integer.toString(percent); - final CharSequence status = getText(R.string.crypt_keeper_setup_description); + // Now try to get status as time remaining and replace as appropriate Log.v(TAG, "Encryption progress: " + progress); + try { + final String timeProperty = SystemProperties.get("vold.encrypt_time_remaining"); + int time = Integer.parseInt(timeProperty); + if (time >= 0) { + // Round up to multiple of 10 - this way display is less jerky + time = (time + 9) / 10 * 10; + progress = DateUtils.formatElapsedTime(time); + status = getText(R.string.crypt_keeper_setup_time_remaining); + } + } catch (Exception e) { + // Will happen if no time etc - show percentage + } + final TextView tv = (TextView) findViewById(R.id.status); if (tv != null) { - tv.setText(TextUtils.expandTemplate(status, Integer.toString(progress))); + tv.setText(TextUtils.expandTemplate(status, progress)); } - // Check the progress every 5 seconds + + // Check the progress every 1 seconds mHandler.removeMessages(MESSAGE_UPDATE_PROGRESS); - mHandler.sendEmptyMessageDelayed(MESSAGE_UPDATE_PROGRESS, 5000); + mHandler.sendEmptyMessageDelayed(MESSAGE_UPDATE_PROGRESS, 1000); } /** Disable password input for a while to force the user to waste time between retries */ @@ -477,10 +654,26 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList if (mCooldown <= 0) { // Re-enable the password entry and back presses. - mPasswordEntry.setEnabled(true); - setBackFunctionality(true); - status.setText(R.string.enter_password); + if (mPasswordEntry != null) { + mPasswordEntry.setEnabled(true); + final InputMethodManager imm = (InputMethodManager) getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mPasswordEntry, 0); + setBackFunctionality(true); + } + if (mLockPatternView != null) { + mLockPatternView.setEnabled(true); + } + status.setText(mStatusString); } else { + // Disable the password entry and back presses. + if (mPasswordEntry != null) { + mPasswordEntry.setEnabled(false); + } + if (mLockPatternView != null) { + mLockPatternView.setEnabled(false); + } + CharSequence template = getText(R.string.crypt_keeper_cooldown); status.setText(TextUtils.expandTemplate(template, Integer.toString(mCooldown))); @@ -503,18 +696,58 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList } } - private void passwordEntryInit() { + private void fakeUnlockAttempt(View postingView) { + postingView.postDelayed(mFakeUnlockAttemptRunnable, FAKE_ATTEMPT_DELAY); + } + + protected LockPatternView.OnPatternListener mChooseNewLockPatternListener = + new LockPatternView.OnPatternListener() { + + @Override + public void onPatternStart() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + } + + @Override + public void onPatternCleared() { + } + + @Override + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + mLockPatternView.setEnabled(false); + if (pattern.size() >= MIN_LENGTH_BEFORE_REPORT) { + new DecryptTask().execute(LockPatternUtils.patternToString(pattern)); + } else { + // Allow user to make as many of these as they want. + fakeUnlockAttempt(mLockPatternView); + } + } + + @Override + public void onPatternCellAdded(List<Cell> pattern) { + } + }; + + private void passwordEntryInit() { + // Password/pin case mPasswordEntry = (EditText) findViewById(R.id.passwordEntry); - mPasswordEntry.setOnEditorActionListener(this); - mPasswordEntry.requestFocus(); - // Become quiet when the user interacts with the Edit text screen. - mPasswordEntry.setOnKeyListener(this); - mPasswordEntry.setOnTouchListener(this); - mPasswordEntry.addTextChangedListener(this); + if (mPasswordEntry != null){ + mPasswordEntry.setOnEditorActionListener(this); + mPasswordEntry.requestFocus(); + // Become quiet when the user interacts with the Edit text screen. + mPasswordEntry.setOnKeyListener(this); + mPasswordEntry.setOnTouchListener(this); + mPasswordEntry.addTextChangedListener(this); + } + + // Pattern case + mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); + if (mLockPatternView != null) { + mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); + } // Disable the Emergency call button if the device has no voice telephone capability - final TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (!tm.isVoiceCapable()) { + if (!getTelephonyManager().isVoiceCapable()) { final View emergencyCall = findViewById(R.id.emergencyCallButton); if (emergencyCall != null) { Log.d(TAG, "Removing the emergency Call button"); @@ -544,23 +777,30 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList if (pm != null) { mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); mWakeLock.acquire(); + // Keep awake for 10 minutes - if the user hasn't been alerted by then + // best not to just drain their battery + mReleaseWakeLockCountdown = 96; // 96 * 5 secs per click + 120 secs before we show this = 600 } } + // Asynchronously throw up the IME, since there are issues with requesting it to be shown // immediately. - mHandler.postDelayed(new Runnable() { - @Override public void run() { - imm.showSoftInputUnchecked(0, null); - } - }, 0); + if (mLockPatternView == null && mCooldown <= 0) { + mHandler.postDelayed(new Runnable() { + @Override public void run() { + imm.showSoftInputUnchecked(0, null); + } + }, 0); + } updateEmergencyCallButtonState(); // Notify the user in 120 seconds that we are waiting for him to enter the password. mHandler.removeMessages(MESSAGE_NOTIFY); mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY, 120 * 1000); - // Dismiss keyguard while this screen is showing. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + // Dismiss secure & non-secure keyguards while this screen is showing. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); } /** @@ -637,8 +877,13 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList mPasswordEntry.setEnabled(false); setBackFunctionality(false); - Log.d(TAG, "Attempting to send command to decrypt"); - new DecryptTask().execute(password); + if (password.length() >= LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { + Log.d(TAG, "Attempting to send command to decrypt"); + new DecryptTask().execute(password); + } else { + // Allow user to make as many of these as they want. + fakeUnlockAttempt(mPasswordEntry); + } return true; } @@ -662,7 +907,7 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList */ private final void setAirplaneModeIfNecessary() { final boolean isLteDevice = - TelephonyManager.getDefault().getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE; + getTelephonyManager().getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE; if (!isLteDevice) { Log.d(TAG, "Going into airplane mode."); Settings.Global.putInt(getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); @@ -698,17 +943,12 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList return; } - final int newState = TelephonyManager.getDefault().getCallState(); int textId; - if (newState == TelephonyManager.CALL_STATE_OFFHOOK) { - // Show "return to call" text and show phone icon + if (getTelecomManager().isInCall()) { + // Show "return to call" textId = R.string.cryptkeeper_return_to_call; - final int phoneCallIcon = R.drawable.stat_sys_phone_call; - emergencyCall.setCompoundDrawablesWithIntrinsicBounds(phoneCallIcon, 0, 0, 0); } else { textId = R.string.cryptkeeper_emergency_call; - final int emergencyIcon = R.drawable.ic_emergency; - emergencyCall.setCompoundDrawablesWithIntrinsicBounds(emergencyIcon, 0, 0, 0); } emergencyCall.setText(textId); } @@ -718,31 +958,31 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList } private void takeEmergencyCallAction() { - if (TelephonyManager.getDefault().getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) { - resumeCall(); + TelecomManager telecomManager = getTelecomManager(); + if (telecomManager.isInCall()) { + telecomManager.showInCallScreen(false /* showDialpad */); } else { launchEmergencyDialer(); } } - private void resumeCall() { - final 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() { final Intent intent = new Intent(ACTION_EMERGENCY_DIAL); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + setBackFunctionality(true); startActivity(intent); } + private TelephonyManager getTelephonyManager() { + return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + } + + private TelecomManager getTelecomManager() { + return (TelecomManager) getSystemService(Context.TELECOM_SERVICE); + } + /** * Listen to key events so we can disable sounds when we get a keyinput in EditText. */ diff --git a/src/com/android/settings/CryptKeeperConfirm.java b/src/com/android/settings/CryptKeeperConfirm.java index 4822a83..4d6f26b 100644 --- a/src/com/android/settings/CryptKeeperConfirm.java +++ b/src/com/android/settings/CryptKeeperConfirm.java @@ -25,6 +25,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.ServiceManager; +import android.os.UserHandle; import android.os.storage.IMountService; import android.util.Log; import android.view.LayoutInflater; @@ -32,8 +33,14 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import com.android.internal.widget.LockPatternUtils; + +import java.util.Locale; + public class CryptKeeperConfirm extends Fragment { + private static final String TAG = "CryptKeeperConfirm"; + public static class Blank extends Activity { private Handler mHandler = new Handler(); @@ -72,7 +79,7 @@ public class CryptKeeperConfirm extends Fragment { IMountService mountService = IMountService.Stub.asInterface(service); try { Bundle args = getIntent().getExtras(); - mountService.encryptStorage(args.getString("password")); + mountService.encryptStorage(args.getInt("type", -1), args.getString("password")); } catch (Exception e) { Log.e("CryptKeeper", "Error while encrypting...", e); } @@ -90,10 +97,40 @@ public class CryptKeeperConfirm extends Fragment { return; } + /* WARNING - nasty hack! + Settings for the lock screen are not available to the crypto + screen (CryptKeeper) at boot. We duplicate the ones that + CryptKeeper needs to the crypto key/value store when they are + modified (see LockPatternUtils). + However, prior to encryption, the crypto key/value store is not + persisted across reboots, thus modified settings are lost to + CryptKeeper. + In order to make sure CryptKeeper had the correct settings after + first encrypting, we thus need to rewrite them, which ensures the + crypto key/value store is up to date. On encryption, this store + is then persisted, and the settings will be there on future + reboots. + */ + + // 1. The owner info. + LockPatternUtils utils = new LockPatternUtils(getActivity()); + utils.setVisiblePatternEnabled(utils.isVisiblePatternEnabled()); + if (utils.isOwnerInfoEnabled()) { + utils.setOwnerInfo(utils.getOwnerInfo(UserHandle.USER_OWNER), + UserHandle.USER_OWNER); + } Intent intent = new Intent(getActivity(), Blank.class); intent.putExtras(getArguments()); - startActivity(intent); + + // 2. The system locale. + try { + IBinder service = ServiceManager.getService("mount"); + IMountService mountService = IMountService.Stub.asInterface(service); + mountService.setField("SystemLocale", Locale.getDefault().toLanguageTag()); + } catch (Exception e) { + Log.e(TAG, "Error storing locale for decryption UI", e); + } } }; diff --git a/src/com/android/settings/CryptKeeperSettings.java b/src/com/android/settings/CryptKeeperSettings.java index 58d97a8..c572738 100644 --- a/src/com/android/settings/CryptKeeperSettings.java +++ b/src/com/android/settings/CryptKeeperSettings.java @@ -29,8 +29,8 @@ import android.content.IntentFilter; import android.content.res.Resources; import android.os.BatteryManager; import android.os.Bundle; +import android.os.storage.StorageManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -42,10 +42,6 @@ public class CryptKeeperSettings extends Fragment { private static final int KEYGUARD_REQUEST = 55; - // This is the minimum acceptable password quality. If the current password quality is - // lower than this, encryption should not be activated. - 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. private static final int MIN_BATTERY_LEVEL = 80; @@ -91,7 +87,6 @@ public class CryptKeeperSettings extends Fragment { // TODO replace (or follow) this dialog with an explicit launch into password UI new AlertDialog.Builder(getActivity()) .setTitle(R.string.crypt_keeper_dialog_need_password_title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(R.string.crypt_keeper_dialog_need_password_message) .setPositiveButton(android.R.string.ok, null) .create() @@ -158,24 +153,19 @@ public class CryptKeeperSettings extends Fragment { * @return true if confirmation launched */ private boolean runKeyguardConfirmation(int request) { - // 1. Confirm that we have a sufficient PIN/Password to continue - LockPatternUtils lockPatternUtils = new LockPatternUtils(getActivity()); - int quality = lockPatternUtils.getActivePasswordQuality(); - if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK - && lockPatternUtils.isLockPasswordEnabled()) { - // Use the alternate as the quality. We expect this to be - // PASSWORD_QUALITY_SOMETHING(pattern) or PASSWORD_QUALITY_NUMERIC(PIN). - quality = lockPatternUtils.getKeyguardStoredPasswordQuality(); - } - if (quality < MIN_PASSWORD_QUALITY) { - return false; - } - // 2. Ask the user to confirm the current PIN/Password Resources res = getActivity().getResources(); - return new ChooseLockSettingsHelper(getActivity(), this) - .launchConfirmationActivity(request, - res.getText(R.string.master_clear_gesture_prompt), - res.getText(R.string.master_clear_gesture_explanation)); + ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(getActivity(), this); + + if (helper.utils().getKeyguardStoredPasswordQuality() + == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { + showFinalConfirmation(StorageManager.CRYPT_TYPE_DEFAULT, ""); + return true; + } + + return helper.launchConfirmationActivity(request, + res.getText(R.string.master_clear_gesture_prompt), + res.getText(R.string.crypt_keeper_confirm_encrypt), + true); } @Override @@ -189,18 +179,20 @@ 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) { + int type = data.getIntExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, -1); String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); if (!TextUtils.isEmpty(password)) { - showFinalConfirmation(password); + showFinalConfirmation(type, password); } } } - private void showFinalConfirmation(String password) { + private void showFinalConfirmation(int type, String password) { Preference preference = new Preference(getActivity()); preference.setFragment(CryptKeeperConfirm.class.getName()); preference.setTitle(R.string.crypt_keeper_confirm_title); + preference.getExtras().putInt("type", type); preference.getExtras().putString("password", password); - ((PreferenceActivity) getActivity()).onPreferenceStartFragment(null, preference); + ((SettingsActivity) getActivity()).onPreferenceStartFragment(null, preference); } } diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java index db1ae29..872de1a 100644 --- a/src/com/android/settings/DataUsageSummary.java +++ b/src/com/android/settings/DataUsageSummary.java @@ -53,10 +53,9 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; -import android.app.FragmentManager; import android.app.FragmentTransaction; import android.app.LoaderManager.LoaderCallbacks; -import android.content.ContentResolver; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -64,6 +63,7 @@ import android.content.Loader; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -86,9 +86,8 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.text.format.DateUtils; @@ -110,9 +109,6 @@ 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; @@ -136,9 +132,11 @@ import com.android.settings.net.NetworkPolicyEditor; import com.android.settings.net.SummaryForAllUidLoader; import com.android.settings.net.UidDetail; import com.android.settings.net.UidDetailProvider; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; import com.android.settings.widget.ChartDataUsageView; import com.android.settings.widget.ChartDataUsageView.DataUsageChartListener; -import com.android.settings.widget.PieChartView; import com.google.android.collect.Lists; import libcore.util.Objects; @@ -152,7 +150,7 @@ import java.util.Locale; * Panel showing data usage history across various networks, including options * to inspect based on usage cycle and control through {@link NetworkPolicy}. */ -public class DataUsageSummary extends Fragment { +public class DataUsageSummary extends HighlightingFragment implements Indexable { private static final String TAG = "DataUsage"; private static final boolean LOGD = false; @@ -170,7 +168,6 @@ public class DataUsageSummary extends Fragment { private static final String TAB_ETHERNET = "ethernet"; private static final String TAG_CONFIRM_DATA_DISABLE = "confirmDataDisable"; - private static final String TAG_CONFIRM_DATA_ROAMING = "confirmDataRoaming"; private static final String TAG_CONFIRM_LIMIT = "confirmLimit"; private static final String TAG_CYCLE_EDITOR = "cycleEditor"; private static final String TAG_WARNING_EDITOR = "warningEditor"; @@ -178,16 +175,20 @@ public class DataUsageSummary extends Fragment { private static final String TAG_CONFIRM_RESTRICT = "confirmRestrict"; private static final String TAG_DENIED_RESTRICT = "deniedRestrict"; private static final String TAG_CONFIRM_APP_RESTRICT = "confirmAppRestrict"; - private static final String TAG_CONFIRM_AUTO_SYNC_CHANGE = "confirmAutoSyncChange"; private static final String TAG_APP_DETAILS = "appDetails"; + private static final String DATA_USAGE_ENABLE_MOBILE_KEY = "data_usage_enable_mobile"; + private static final String DATA_USAGE_DISABLE_MOBILE_LIMIT_KEY = + "data_usage_disable_mobile_limit"; + private static final String DATA_USAGE_CYCLE_KEY = "data_usage_cycle"; + private static final int LOADER_CHART_DATA = 2; private static final int LOADER_SUMMARY = 3; private INetworkManagementService mNetworkService; private INetworkStatsService mStatsService; private NetworkPolicyManager mPolicyManager; - private ConnectivityManager mConnService; + private TelephonyManager mTelephonyManager; private INetworkStatsSession mStatsSession; @@ -210,29 +211,33 @@ public class DataUsageSummary extends Fragment { private ViewGroup mNetworkSwitchesContainer; private LinearLayout mNetworkSwitches; + private boolean mDataEnabledSupported; private Switch mDataEnabled; private View mDataEnabledView; - private CheckBox mDisableAtLimit; + private boolean mDisableAtLimitSupported; + private Switch mDisableAtLimit; private View mDisableAtLimitView; private View mCycleView; private Spinner mCycleSpinner; private CycleAdapter mCycleAdapter; + private TextView mCycleSummary; private ChartDataUsageView mChart; - private TextView mUsageSummary; + private View mDisclaimer; private TextView mEmpty; + private View mStupidPadding; private View mAppDetail; private ImageView mAppIcon; private ViewGroup mAppTitles; - private PieChartView mAppPieChart; + private TextView mAppTotal; private TextView mAppForeground; private TextView mAppBackground; private Button mAppSettings; private LinearLayout mAppSwitches; - private CheckBox mAppRestrict; + private Switch mAppRestrict; private View mAppRestrictView; private boolean mShowWifi = false; @@ -250,9 +255,11 @@ public class DataUsageSummary extends Fragment { private String mCurrentTab = null; private String mIntentTab = null; - private MenuItem mMenuDataRoaming; private MenuItem mMenuRestrictBackground; - private MenuItem mMenuAutoSync; + private MenuItem mMenuShowWifi; + private MenuItem mMenuShowEthernet; + private MenuItem mMenuSimCards; + private MenuItem mMenuCellularNetworks; /** Flag used to ignore listeners during binding. */ private boolean mBinding; @@ -269,7 +276,7 @@ public class DataUsageSummary extends Fragment { mStatsService = INetworkStatsService.Stub.asInterface( ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); mPolicyManager = NetworkPolicyManager.from(context); - mConnService = ConnectivityManager.from(context); + mTelephonyManager = TelephonyManager.from(context); mPrefs = getActivity().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); @@ -350,26 +357,36 @@ public class DataUsageSummary extends Fragment { mNetworkSwitches = (LinearLayout) mHeader.findViewById(R.id.network_switches); mDataEnabled = new Switch(inflater.getContext()); + mDataEnabled.setClickable(false); + mDataEnabled.setFocusable(false); mDataEnabledView = inflatePreference(inflater, mNetworkSwitches, mDataEnabled); - mDataEnabled.setOnCheckedChangeListener(mDataEnabledListener); + mDataEnabledView.setTag(R.id.preference_highlight_key, + DATA_USAGE_ENABLE_MOBILE_KEY); + mDataEnabledView.setClickable(true); + mDataEnabledView.setFocusable(true); + mDataEnabledView.setOnClickListener(mDataEnabledListener); mNetworkSwitches.addView(mDataEnabledView); - mDisableAtLimit = new CheckBox(inflater.getContext()); + mDisableAtLimit = new Switch(inflater.getContext()); mDisableAtLimit.setClickable(false); mDisableAtLimit.setFocusable(false); mDisableAtLimitView = inflatePreference(inflater, mNetworkSwitches, mDisableAtLimit); + mDisableAtLimitView.setTag(R.id.preference_highlight_key, + DATA_USAGE_DISABLE_MOBILE_LIMIT_KEY); mDisableAtLimitView.setClickable(true); mDisableAtLimitView.setFocusable(true); 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); + mCycleView = inflater.inflate(R.layout.data_usage_cycles, mNetworkSwitches, false); + mCycleView.setTag(R.id.preference_highlight_key, DATA_USAGE_CYCLE_KEY); + mCycleSpinner = (Spinner) mCycleView.findViewById(R.id.cycles_spinner); + mCycleAdapter = new CycleAdapter(context); + mCycleSpinner.setAdapter(mCycleAdapter); + mCycleSpinner.setOnItemSelectedListener(mCycleListener); + mCycleSummary = (TextView) mCycleView.findViewById(R.id.cycle_summary); + mNetworkSwitches.addView(mCycleView); + } mChart = (ChartDataUsageView) mHeader.findViewById(R.id.chart); mChart.setListener(mChartListener); @@ -380,15 +397,13 @@ public class DataUsageSummary extends Fragment { mAppDetail = mHeader.findViewById(R.id.app_detail); mAppIcon = (ImageView) mAppDetail.findViewById(R.id.app_icon); mAppTitles = (ViewGroup) mAppDetail.findViewById(R.id.app_titles); - mAppPieChart = (PieChartView) mAppDetail.findViewById(R.id.app_pie_chart); mAppForeground = (TextView) mAppDetail.findViewById(R.id.app_foreground); mAppBackground = (TextView) mAppDetail.findViewById(R.id.app_background); 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 = new Switch(inflater.getContext()); mAppRestrict.setClickable(false); mAppRestrict.setFocusable(false); mAppRestrictView = inflatePreference(inflater, mAppSwitches, mAppRestrict); @@ -398,10 +413,12 @@ public class DataUsageSummary extends Fragment { mAppSwitches.addView(mAppRestrictView); } - mUsageSummary = (TextView) mHeader.findViewById(R.id.usage_summary); + mDisclaimer = mHeader.findViewById(R.id.disclaimer); mEmpty = (TextView) mHeader.findViewById(android.R.id.empty); + mStupidPadding = mHeader.findViewById(R.id.stupid_padding); - mAdapter = new DataUsageAdapter(mUidDetailProvider, mInsetSide); + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + mAdapter = new DataUsageAdapter(um, mUidDetailProvider, mInsetSide); mListView.setOnItemClickListener(mListListener); mListView.setAdapter(mAdapter); @@ -409,8 +426,8 @@ public class DataUsageSummary extends Fragment { } @Override - public void onResume() { - super.onResume(); + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); // pick default tab based on incoming intent final Intent intent = getActivity().getIntent(); @@ -419,6 +436,18 @@ public class DataUsageSummary extends Fragment { // this kicks off chain reaction which creates tabs, binds the body to // selected network, and binds chart, cycles and detail list. updateTabs(); + } + + @Override + public void onResume() { + super.onResume(); + + getView().post(new Runnable() { + @Override + public void run() { + highlightViewIfNeeded(); + } + }); // kick off background task to update stats new AsyncTask<Void, Void, Void>() { @@ -454,39 +483,24 @@ public class DataUsageSummary extends Fragment { final boolean appDetailMode = isAppDetailMode(); final boolean isOwner = ActivityManager.getCurrentUser() == UserHandle.USER_OWNER; - mMenuDataRoaming = menu.findItem(R.id.data_usage_menu_roaming); - mMenuDataRoaming.setVisible(hasReadyMobileRadio(context) && !appDetailMode); - mMenuDataRoaming.setChecked(getDataRoaming()); - - mMenuRestrictBackground = menu.findItem(R.id.data_usage_menu_restrict_background); - mMenuRestrictBackground.setVisible( - hasReadyMobileRadio(context) && isOwner && !appDetailMode); - mMenuRestrictBackground.setChecked(mPolicyManager.getRestrictBackground()); - - mMenuAutoSync = menu.findItem(R.id.data_usage_menu_auto_sync); - mMenuAutoSync.setChecked(ContentResolver.getMasterSyncAutomatically()); - mMenuAutoSync.setVisible(!appDetailMode); - - final MenuItem split4g = menu.findItem(R.id.data_usage_menu_split_4g); - split4g.setVisible(hasReadyMobile4gRadio(context) && isOwner && !appDetailMode); - split4g.setChecked(isMobilePolicySplit()); - - final MenuItem showWifi = menu.findItem(R.id.data_usage_menu_show_wifi); + mMenuShowWifi = menu.findItem(R.id.data_usage_menu_show_wifi); if (hasWifiRadio(context) && hasReadyMobileRadio(context)) { - showWifi.setVisible(!appDetailMode); - showWifi.setChecked(mShowWifi); + mMenuShowWifi.setVisible(!appDetailMode); } else { - showWifi.setVisible(false); + mMenuShowWifi.setVisible(false); } - final MenuItem showEthernet = menu.findItem(R.id.data_usage_menu_show_ethernet); + mMenuShowEthernet = menu.findItem(R.id.data_usage_menu_show_ethernet); if (hasEthernet(context) && hasReadyMobileRadio(context)) { - showEthernet.setVisible(!appDetailMode); - showEthernet.setChecked(mShowEthernet); + mMenuShowEthernet.setVisible(!appDetailMode); } else { - showEthernet.setVisible(false); + mMenuShowEthernet.setVisible(false); } + mMenuRestrictBackground = menu.findItem(R.id.data_usage_menu_restrict_background); + mMenuRestrictBackground.setVisible( + hasReadyMobileRadio(context) && isOwner && !appDetailMode); + final MenuItem metered = menu.findItem(R.id.data_usage_menu_metered); if (hasReadyMobileRadio(context) || hasWifiRadio(context)) { metered.setVisible(!appDetailMode); @@ -494,6 +508,14 @@ public class DataUsageSummary extends Fragment { metered.setVisible(false); } + // TODO: show when multiple sims available + mMenuSimCards = menu.findItem(R.id.data_usage_menu_sim_cards); + mMenuSimCards.setVisible(false); + + mMenuCellularNetworks = menu.findItem(R.id.data_usage_menu_cellular_networks); + mMenuCellularNetworks.setVisible(hasReadyMobileRadio(context) + && !appDetailMode && isOwner); + final MenuItem help = menu.findItem(R.id.data_usage_menu_help); String helpUrl; if (!TextUtils.isEmpty(helpUrl = getResources().getString(R.string.help_url_data_usage))) { @@ -501,23 +523,35 @@ public class DataUsageSummary extends Fragment { } else { help.setVisible(false); } + + updateMenuTitles(); + } + + private void updateMenuTitles() { + if (mPolicyManager.getRestrictBackground()) { + mMenuRestrictBackground.setTitle(R.string.data_usage_menu_allow_background); + } else { + mMenuRestrictBackground.setTitle(R.string.data_usage_menu_restrict_background); + } + + if (mShowWifi) { + mMenuShowWifi.setTitle(R.string.data_usage_menu_hide_wifi); + } else { + mMenuShowWifi.setTitle(R.string.data_usage_menu_show_wifi); + } + + if (mShowEthernet) { + mMenuShowEthernet.setTitle(R.string.data_usage_menu_hide_ethernet); + } else { + mMenuShowEthernet.setTitle(R.string.data_usage_menu_show_ethernet); + } } @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(); + final boolean restrictBackground = !mPolicyManager.getRestrictBackground(); if (restrictBackground) { ConfirmRestrictFragment.show(this); } else { @@ -526,39 +560,35 @@ public class DataUsageSummary extends Fragment { } 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(); + mShowWifi = !mShowWifi; mPrefs.edit().putBoolean(PREF_SHOW_WIFI, mShowWifi).apply(); - item.setChecked(mShowWifi); + updateMenuTitles(); updateTabs(); return true; } case R.id.data_usage_menu_show_ethernet: { - mShowEthernet = !item.isChecked(); + mShowEthernet = !mShowEthernet; mPrefs.edit().putBoolean(PREF_SHOW_ETHERNET, mShowEthernet).apply(); - item.setChecked(mShowEthernet); + updateMenuTitles(); updateTabs(); return true; } - case R.id.data_usage_menu_metered: { - final PreferenceActivity activity = (PreferenceActivity) getActivity(); - activity.startPreferencePanel(DataUsageMeteredSettings.class.getCanonicalName(), null, - R.string.data_usage_metered_title, null, this, 0); + case R.id.data_usage_menu_sim_cards: { + // TODO: hook up to sim cards return true; } - case R.id.data_usage_menu_auto_sync: { - if (ActivityManager.isUserAMonkey()) { - Log.d("SyncState", "ignoring monkey's attempt to flip global sync state"); - } else { - ConfirmAutoSyncChangeFragment.show(this, !item.isChecked()); - } + case R.id.data_usage_menu_cellular_networks: { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName("com.android.phone", + "com.android.phone.MobileNetworkSettings")); + startActivity(intent); + return true; + } + case R.id.data_usage_menu_metered: { + final SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(DataUsageMeteredSettings.class.getCanonicalName(), null, + R.string.data_usage_metered_title, null, this, 0); return true; } } @@ -575,11 +605,6 @@ public class DataUsageSummary extends Fragment { TrafficStats.closeQuietly(mStatsSession); - if (this.isRemoving()) { - getFragmentManager() - .popBackStack(TAG_APP_DETAILS, FragmentManager.POP_BACK_STACK_INCLUSIVE); - } - super.onDestroy(); } @@ -699,15 +724,14 @@ public class DataUsageSummary extends Fragment { mListView.setVisibility(View.VISIBLE); } - final boolean tabChanged = !currentTab.equals(mCurrentTab); mCurrentTab = currentTab; if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab); - mDataEnabledView.setVisibility(isOwner ? View.VISIBLE : View.GONE); + mDataEnabledSupported = isOwner; + mDisableAtLimitSupported = true; // TODO: remove mobile tabs when SIM isn't ready - final TelephonyManager tele = TelephonyManager.from(context); if (TAB_MOBILE.equals(currentTab)) { setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_mobile); @@ -728,14 +752,14 @@ public class DataUsageSummary extends Fragment { } else if (TAB_WIFI.equals(currentTab)) { // wifi doesn't have any controls - mDataEnabledView.setVisibility(View.GONE); - mDisableAtLimitView.setVisibility(View.GONE); + mDataEnabledSupported = false; + mDisableAtLimitSupported = false; mTemplate = buildTemplateWifiWildcard(); } else if (TAB_ETHERNET.equals(currentTab)) { // ethernet doesn't have any controls - mDataEnabledView.setVisibility(View.GONE); - mDisableAtLimitView.setVisibility(View.GONE); + mDataEnabledSupported = false; + mDisableAtLimitSupported = false; mTemplate = buildTemplateEthernet(); } else { @@ -788,12 +812,32 @@ public class DataUsageSummary extends Fragment { mAppIcon.setImageDrawable(detail.icon); mAppTitles.removeAllViews(); + + View title = null; if (detail.detailLabels != null) { - for (CharSequence label : detail.detailLabels) { - mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, label)); + final int n = detail.detailLabels.length; + for (int i = 0; i < n; ++i) { + CharSequence label = detail.detailLabels[i]; + CharSequence contentDescription = detail.detailContentDescriptions[i]; + title = inflater.inflate(R.layout.data_usage_app_title, mAppTitles, false); + TextView appTitle = (TextView) title.findViewById(R.id.app_title); + appTitle.setText(label); + appTitle.setContentDescription(contentDescription); + mAppTitles.addView(title); } } else { - mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, detail.label)); + title = inflater.inflate(R.layout.data_usage_app_title, mAppTitles, false); + TextView appTitle = (TextView) title.findViewById(R.id.app_title); + appTitle.setText(detail.label); + appTitle.setContentDescription(detail.contentDescription); + mAppTitles.addView(title); + } + + // Remember last slot for summary + if (title != null) { + mAppTotal = (TextView) title.findViewById(R.id.app_summary); + } else { + mAppTotal = null; } // enable settings button when package provides it @@ -812,11 +856,24 @@ public class DataUsageSummary extends Fragment { } } + mAppSettings.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!isAdded()) { + return; + } + + // TODO: target towards entire UID instead of just first package + getActivity().startActivityAsUser(mAppSettingsIntent, + new UserHandle(UserHandle.getUserId(uid))); + } + }); mAppSettings.setEnabled(matchFound); mAppSettings.setVisibility(View.VISIBLE); } else { mAppSettingsIntent = null; + mAppSettings.setOnClickListener(null); mAppSettings.setVisibility(View.GONE); } @@ -859,13 +916,13 @@ public class DataUsageSummary extends Fragment { // TODO: deprecate and remove this once enabled flag is on policy return mMobileDataEnabled; } else { - return mConnService.getMobileDataEnabled(); + return mTelephonyManager.getDataEnabled(); } } private void setMobileDataEnabled(boolean enabled) { if (LOGD) Log.d(TAG, "setMobileDataEnabled()"); - mConnService.setMobileDataEnabled(enabled); + mTelephonyManager.setDataEnabled(enabled); mMobileDataEnabled = enabled; updatePolicy(false); } @@ -884,22 +941,9 @@ public class DataUsageSummary extends Fragment { } } - private boolean getDataRoaming() { - final ContentResolver resolver = getActivity().getContentResolver(); - return Settings.Global.getInt(resolver, Settings.Global.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.Global.putInt(resolver, Settings.Global.DATA_ROAMING, enabled ? 1 : 0); - mMenuDataRoaming.setChecked(enabled); - } - public void setRestrictBackground(boolean restrictBackground) { mPolicyManager.setRestrictBackground(restrictBackground); - mMenuRestrictBackground.setChecked(restrictBackground); + updateMenuTitles(); } private boolean getAppRestrictBackground() { @@ -921,10 +965,12 @@ public class DataUsageSummary extends Fragment { * current {@link #mTemplate}. */ private void updatePolicy(boolean refreshCycle) { + boolean dataEnabledVisible = mDataEnabledSupported; + boolean disableAtLimitVisible = mDisableAtLimitSupported; + if (isAppDetailMode()) { - mNetworkSwitches.setVisibility(View.GONE); - } else { - mNetworkSwitches.setVisibility(View.VISIBLE); + dataEnabledVisible = false; + disableAtLimitVisible = false; } // TODO: move enabled state directly into policy @@ -936,7 +982,6 @@ public class DataUsageSummary extends Fragment { final NetworkPolicy policy = mPolicyEditor.getPolicy(mTemplate); if (isNetworkPolicyModifiable(policy)) { - mDisableAtLimitView.setVisibility(View.VISIBLE); mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED); if (!isAppDetailMode()) { mChart.bindNetworkPolicy(policy); @@ -944,10 +989,13 @@ public class DataUsageSummary extends Fragment { } else { // controls are disabled; don't bind warning/limit sweeps - mDisableAtLimitView.setVisibility(View.GONE); + disableAtLimitVisible = false; mChart.bindNetworkPolicy(null); } + mDataEnabledView.setVisibility(dataEnabledVisible ? View.VISIBLE : View.GONE); + mDisableAtLimitView.setVisibility(disableAtLimitVisible ? View.VISIBLE : View.GONE); + if (refreshCycle) { // generate cycle list based on policy and available history updateCycleList(policy); @@ -1027,12 +1075,12 @@ public class DataUsageSummary extends Fragment { } } - private OnCheckedChangeListener mDataEnabledListener = new OnCheckedChangeListener() { + private View.OnClickListener mDataEnabledListener = new View.OnClickListener() { @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + public void onClick(View v) { if (mBinding) return; - final boolean dataEnabled = isChecked; + final boolean dataEnabled = !mDataEnabled.isChecked(); final String currentTab = mCurrentTab; if (TAB_MOBILE.equals(currentTab)) { if (dataEnabled) { @@ -1078,16 +1126,6 @@ public class DataUsageSummary extends Fragment { } }; - private OnClickListener mAppSettingsListener = new OnClickListener() { - @Override - public void onClick(View v) { - if (!isAdded()) return; - - // TODO: target torwards entire UID instead of just first package - startActivity(mAppSettingsIntent); - } - }; - private OnItemClickListener mListListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { @@ -1156,15 +1194,11 @@ public class DataUsageSummary extends Fragment { final long defaultBytes = entry.rxBytes + entry.txBytes; entry = mChartData.detailForeground.getValues(start, end, now, entry); final long foregroundBytes = entry.rxBytes + entry.txBytes; + final long totalBytes = defaultBytes + foregroundBytes; - mAppPieChart.setOriginAngle(175); - - mAppPieChart.removeAllSlices(); - mAppPieChart.addSlice(foregroundBytes, Color.parseColor("#d88d3a")); - mAppPieChart.addSlice(defaultBytes, Color.parseColor("#666666")); - - mAppPieChart.generatePath(); - + if (mAppTotal != null) { + mAppTotal.setText(Formatter.formatFileSize(context, totalBytes)); + } mAppBackground.setText(Formatter.formatFileSize(context, defaultBytes)); mAppForeground.setText(Formatter.formatFileSize(context, foregroundBytes)); @@ -1173,11 +1207,15 @@ public class DataUsageSummary extends Fragment { getLoaderManager().destroyLoader(LOADER_SUMMARY); + mCycleSummary.setVisibility(View.GONE); + } else { if (mChartData != null) { entry = mChartData.network.getValues(start, end, now, null); } + mCycleSummary.setVisibility(View.VISIBLE); + // kick off loader for detailed stats getLoaderManager().restartLoader(LOADER_SUMMARY, SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryCallbacks); @@ -1185,18 +1223,19 @@ public class DataUsageSummary extends Fragment { final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0; final String totalPhrase = Formatter.formatFileSize(context, totalBytes); - final String rangePhrase = formatDateRange(context, start, end); + mCycleSummary.setText(totalPhrase); - final int summaryRes; if (TAB_MOBILE.equals(mCurrentTab) || TAB_3G.equals(mCurrentTab) || TAB_4G.equals(mCurrentTab)) { - summaryRes = R.string.data_usage_total_during_range_mobile; + if (isAppDetailMode()) { + mDisclaimer.setVisibility(View.GONE); + } else { + mDisclaimer.setVisibility(View.VISIBLE); + } } else { - summaryRes = R.string.data_usage_total_during_range; + mDisclaimer.setVisibility(View.GONE); } - mUsageSummary.setText(getString(summaryRes, totalPhrase, rangePhrase)); - // initial layout is finished above, ensure we have transitions ensureLayoutTransitions(); } @@ -1256,6 +1295,7 @@ public class DataUsageSummary extends Fragment { private void updateEmptyVisible() { final boolean isEmpty = mAdapter.isEmpty() && !isAppDetailMode(); mEmpty.setVisibility(isEmpty ? View.VISIBLE : View.GONE); + mStupidPadding.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } }; @@ -1287,12 +1327,6 @@ public class DataUsageSummary extends Fragment { private DataUsageChartListener mChartListener = new DataUsageChartListener() { @Override - public void onInspectRangeChanged() { - if (LOGD) Log.d(TAG, "onInspectRangeChanged()"); - updateDetailData(); - } - - @Override public void onWarningChanged() { setPolicyWarningBytes(mChart.getWarningBytes()); } @@ -1382,8 +1416,8 @@ public class DataUsageSummary extends Fragment { private final CycleChangeItem mChangeItem; public CycleAdapter(Context context) { - super(context, android.R.layout.simple_spinner_item); - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + super(context, R.layout.data_usage_cycle_item); + setDropDownViewResource(R.layout.data_usage_cycle_item_dropdown); mChangeItem = new CycleChangeItem(context); } @@ -1425,11 +1459,21 @@ public class DataUsageSummary extends Fragment { } public static class AppItem implements Comparable<AppItem>, Parcelable { + public static final int CATEGORY_USER = 0; + public static final int CATEGORY_APP_TITLE = 1; + public static final int CATEGORY_APP = 2; + public final int key; public boolean restricted; + public int category; + public SparseBooleanArray uids = new SparseBooleanArray(); public long total; + public AppItem() { + this.key = 0; + } + public AppItem(int key) { this.key = key; } @@ -1458,7 +1502,11 @@ public class DataUsageSummary extends Fragment { @Override public int compareTo(AppItem another) { - return Long.compare(another.total, total); + int comparison = Integer.compare(category, another.category); + if (comparison == 0) { + comparison = Long.compare(another.total, total); + } + return comparison; } public static final Creator<AppItem> CREATOR = new Creator<AppItem>() { @@ -1480,13 +1528,15 @@ public class DataUsageSummary extends Fragment { public static class DataUsageAdapter extends BaseAdapter { private final UidDetailProvider mProvider; private final int mInsetSide; + private final UserManager mUm; private ArrayList<AppItem> mItems = Lists.newArrayList(); private long mLargest; - public DataUsageAdapter(UidDetailProvider provider, int insetSide) { + public DataUsageAdapter(final UserManager userManager, UidDetailProvider provider, int insetSide) { mProvider = checkNotNull(provider); mInsetSide = insetSide; + mUm = userManager; } /** @@ -1494,8 +1544,10 @@ public class DataUsageSummary extends Fragment { */ public void bindStats(NetworkStats stats, int[] restrictedUids) { mItems.clear(); + mLargest = 0; final int currentUserId = ActivityManager.getCurrentUser(); + final List<UserHandle> profiles = mUm.getUserProfiles(); final SparseArray<AppItem> knownItems = new SparseArray<AppItem>(); NetworkStats.Entry entry = null; @@ -1505,32 +1557,43 @@ public class DataUsageSummary extends Fragment { // Decide how to collapse items together final int uid = entry.uid; + final int collapseKey; + final int category; + final int userId = UserHandle.getUserId(uid); if (UserHandle.isApp(uid)) { - if (UserHandle.getUserId(uid) == currentUserId) { + if (profiles.contains(new UserHandle(userId))) { + if (userId != currentUserId) { + // Add to a managed user item. + final int managedKey = UidDetailProvider.buildKeyForUser(userId); + accumulate(managedKey, knownItems, entry, + AppItem.CATEGORY_USER); + } + // Add to app item. collapseKey = uid; + category = AppItem.CATEGORY_APP; } else { - collapseKey = UidDetailProvider.buildKeyForUser(UserHandle.getUserId(uid)); + // Add to other user item. + collapseKey = UidDetailProvider.buildKeyForUser(userId); + category = AppItem.CATEGORY_USER; } } else if (uid == UID_REMOVED || uid == UID_TETHERING) { collapseKey = uid; + category = AppItem.CATEGORY_APP; } else { collapseKey = android.os.Process.SYSTEM_UID; + category = AppItem.CATEGORY_APP; } - - AppItem item = knownItems.get(collapseKey); - if (item == null) { - item = new AppItem(collapseKey); - mItems.add(item); - knownItems.put(item.key, item); - } - item.addUid(uid); - item.total += entry.rxBytes + entry.txBytes; + accumulate(collapseKey, knownItems, entry, category); } - for (int uid : restrictedUids) { - // Only splice in restricted state for current user - if (UserHandle.getUserId(uid) != currentUserId) continue; + final int restrictedUidsMax = restrictedUids.length; + for (int i = 0; i < restrictedUidsMax; ++i) { + final int uid = restrictedUids[i]; + // Only splice in restricted state for current user or managed users + if (!profiles.contains(new UserHandle(UserHandle.getUserId(uid)))) { + continue; + } AppItem item = knownItems.get(uid); if (item == null) { @@ -1542,11 +1605,43 @@ public class DataUsageSummary extends Fragment { item.restricted = true; } + if (!mItems.isEmpty()) { + final AppItem title = new AppItem(); + title.category = AppItem.CATEGORY_APP_TITLE; + mItems.add(title); + } + Collections.sort(mItems); - mLargest = (mItems.size() > 0) ? mItems.get(0).total : 0; notifyDataSetChanged(); } + /** + * Accumulate data usage of a network stats entry for the item mapped by the collapse key. + * Creates the item if needed. + * + * @param collapseKey the collapse key used to map the item. + * @param knownItems collection of known (already existing) items. + * @param entry the network stats entry to extract data usage from. + * @param itemCategory the item is categorized on the list view by this category. Must be + * either AppItem.APP_ITEM_CATEGORY or AppItem.MANAGED_USER_ITEM_CATEGORY + */ + private void accumulate(int collapseKey, final SparseArray<AppItem> knownItems, + NetworkStats.Entry entry, int itemCategory) { + final int uid = entry.uid; + AppItem item = knownItems.get(collapseKey); + if (item == null) { + item = new AppItem(collapseKey); + item.category = itemCategory; + mItems.add(item); + knownItems.put(item.key, item); + } + item.addUid(uid); + item.total += entry.rxBytes + entry.txBytes; + if (mLargest < item.total) { + mLargest = item.total; + } + } + @Override public int getCount() { return mItems.size(); @@ -1562,37 +1657,82 @@ public class DataUsageSummary extends Fragment { return mItems.get(position).key; } + /** + * See {@link #getItemViewType} for the view types. + */ @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); + public int getViewTypeCount() { + return 2; + } - if (mInsetSide > 0) { - convertView.setPaddingRelative(mInsetSide, 0, mInsetSide, 0); - } + /** + * Returns 1 for separator items and 0 for anything else. + */ + @Override + public int getItemViewType(int position) { + final AppItem item = mItems.get(position); + if (item.category == AppItem.CATEGORY_APP_TITLE) { + return 1; + } else { + return 0; } + } - final Context context = parent.getContext(); + @Override + public boolean areAllItemsEnabled() { + return false; + } - final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1); - final ProgressBar progress = (ProgressBar) convertView.findViewById( - android.R.id.progress); + @Override + public boolean isEnabled(int position) { + if (position > mItems.size()) { + throw new ArrayIndexOutOfBoundsException(); + } + return getItemViewType(position) == 0; + } - // kick off async load of app details + @Override + public View getView(int position, View convertView, ViewGroup parent) { final AppItem item = mItems.get(position); - UidDetailTask.bindView(mProvider, item, convertView); + if (getItemViewType(position) == 1) { + if (convertView == null) { + convertView = inflateCategoryHeader(LayoutInflater.from(parent.getContext()), + parent); + } + + final TextView title = (TextView) convertView.findViewById(android.R.id.title); + title.setText(R.string.data_usage_app); - if (item.restricted && item.total <= 0) { - text1.setText(R.string.data_usage_app_restricted); - progress.setVisibility(View.GONE); } else { - text1.setText(Formatter.formatFileSize(context, item.total)); - progress.setVisibility(View.VISIBLE); - } + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()).inflate( + R.layout.data_usage_item, parent, false); + + if (mInsetSide > 0) { + convertView.setPaddingRelative(mInsetSide, 0, mInsetSide, 0); + } + } + + final Context context = parent.getContext(); + + final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1); + final ProgressBar progress = (ProgressBar) convertView.findViewById( + android.R.id.progress); + + // kick off async load of app details + UidDetailTask.bindView(mProvider, item, convertView); + + if (item.restricted && item.total <= 0) { + text1.setText(R.string.data_usage_app_restricted); + progress.setVisibility(View.GONE); + } else { + text1.setText(Formatter.formatFileSize(context, item.total)); + progress.setVisibility(View.VISIBLE); + } - final int percentTotal = mLargest != 0 ? (int) (item.total * 100 / mLargest) : 0; - progress.setProgress(percentTotal); + final int percentTotal = mLargest != 0 ? (int) (item.total * 100 / mLargest) : 0; + progress.setProgress(percentTotal); + } return convertView; } @@ -1617,7 +1757,8 @@ public class DataUsageSummary extends Fragment { final FragmentTransaction ft = parent.getFragmentManager().beginTransaction(); ft.add(fragment, TAG_APP_DETAILS); ft.addToBackStack(TAG_APP_DETAILS); - ft.setBreadCrumbTitle(label); + ft.setBreadCrumbTitle( + parent.getResources().getString(R.string.data_usage_app_summary_title)); ft.commitAllowingStateLoss(); } @@ -1649,10 +1790,12 @@ public class DataUsageSummary extends Fragment { public static void show(DataUsageSummary parent) { if (!parent.isAdded()) return; + final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate); + if (policy == null) return; + final Resources res = parent.getResources(); final CharSequence message; - final long minLimitBytes = (long) ( - parent.mPolicyEditor.getPolicy(parent.mTemplate).warningBytes * 1.2f); + final long minLimitBytes = (long) (policy.warningBytes * 1.2f); final long limitBytes; // TODO: customize default limits based on network template @@ -1926,46 +2069,6 @@ public class DataUsageSummary extends Fragment { /** * Dialog to request user confirmation before setting - * {@link android.provider.Settings.Global#DATA_ROAMING}. - */ - public static class ConfirmDataRoamingFragment extends DialogFragment { - public static void show(DataUsageSummary parent) { - if (!parent.isAdded()) return; - - final ConfirmDataRoamingFragment dialog = new ConfirmDataRoamingFragment(); - dialog.setTargetFragment(parent, 0); - dialog.show(parent.getFragmentManager(), TAG_CONFIRM_DATA_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); - if (Utils.hasMultipleUsers(context)) { - builder.setMessage(R.string.roaming_warning_multiuser); - } else { - builder.setMessage(R.string.roaming_warning); - } - - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - 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 { @@ -2068,56 +2171,6 @@ public class DataUsageSummary extends Fragment { } /** - * Dialog to inform user about changing auto-sync setting - */ - public static class ConfirmAutoSyncChangeFragment extends DialogFragment { - private static final String SAVE_ENABLING = "enabling"; - private boolean mEnabling; - - public static void show(DataUsageSummary parent, boolean enabling) { - if (!parent.isAdded()) return; - - final ConfirmAutoSyncChangeFragment dialog = new ConfirmAutoSyncChangeFragment(); - dialog.mEnabling = enabling; - dialog.setTargetFragment(parent, 0); - dialog.show(parent.getFragmentManager(), TAG_CONFIRM_AUTO_SYNC_CHANGE); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Context context = getActivity(); - if (savedInstanceState != null) { - mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING); - } - - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - if (!mEnabling) { - builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title); - builder.setMessage(R.string.data_usage_auto_sync_off_dialog); - } else { - builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title); - builder.setMessage(R.string.data_usage_auto_sync_on_dialog); - } - - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - ContentResolver.setMasterSyncAutomatically(mEnabling); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - - return builder.create(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(SAVE_ENABLING, mEnabling); - } - } - - /** * Compute default tab that should be selected, based on * {@link NetworkPolicyManager#EXTRA_NETWORK_TEMPLATE} extra. */ @@ -2177,6 +2230,7 @@ public class DataUsageSummary extends Fragment { if (detail != null) { icon.setImageDrawable(detail.icon); title.setText(detail.label); + title.setContentDescription(detail.contentDescription); } else { icon.setImageDrawable(null); title.setText(null); @@ -2286,12 +2340,12 @@ public class DataUsageSummary extends Fragment { 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; + private static View inflateCategoryHeader(LayoutInflater inflater, ViewGroup root) { + final TypedArray a = inflater.getContext().obtainStyledAttributes(null, + com.android.internal.R.styleable.Preference, + com.android.internal.R.attr.preferenceCategoryStyle, 0); + final int resId = a.getResourceId(com.android.internal.R.styleable.Preference_layout, 0); + return inflater.inflate(resId, root, false); } /** @@ -2388,4 +2442,47 @@ public class DataUsageSummary extends Fragment { summary.setVisibility(View.VISIBLE); summary.setText(string); } + + /** + * For search + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_summary_title); + data.screenTitle = res.getString(R.string.data_usage_summary_title); + result.add(data); + + // Mobile data + data = new SearchIndexableRaw(context); + data.key = DATA_USAGE_ENABLE_MOBILE_KEY; + data.title = res.getString(R.string.data_usage_enable_mobile); + data.screenTitle = res.getString(R.string.data_usage_summary_title); + result.add(data); + + // Set mobile data limit + data = new SearchIndexableRaw(context); + data.key = DATA_USAGE_DISABLE_MOBILE_LIMIT_KEY; + data.title = res.getString(R.string.data_usage_disable_mobile_limit); + data.screenTitle = res.getString(R.string.data_usage_summary_title); + result.add(data); + + // Data usage cycle + data = new SearchIndexableRaw(context); + data.key = DATA_USAGE_CYCLE_KEY; + data.title = res.getString(R.string.data_usage_cycle); + data.screenTitle = res.getString(R.string.data_usage_summary_title); + result.add(data); + + return result; + } + }; + } diff --git a/src/com/android/settings/DateTimeSettings.java b/src/com/android/settings/DateTimeSettings.java index f02f838..8eb9c52 100644 --- a/src/com/android/settings/DateTimeSettings.java +++ b/src/com/android/settings/DateTimeSettings.java @@ -16,6 +16,7 @@ package com.android.settings; +import android.app.admin.DevicePolicyManager; import android.app.Activity; import android.app.AlarmManager; import android.app.DatePickerDialog; @@ -28,7 +29,6 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; -import android.os.SystemClock; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; @@ -42,7 +42,6 @@ import android.text.format.DateFormat; import android.view.View; import android.widget.DatePicker; import android.widget.TimePicker; - import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -91,12 +90,23 @@ public class DateTimeSettings extends SettingsPreferenceFragment boolean autoTimeEnabled = getAutoState(Settings.Global.AUTO_TIME); boolean autoTimeZoneEnabled = getAutoState(Settings.Global.AUTO_TIME_ZONE); + mAutoTimePref = (CheckBoxPreference) findPreference(KEY_AUTO_TIME); + + DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context + .DEVICE_POLICY_SERVICE); + if (dpm.getAutoTimeRequired()) { + mAutoTimePref.setEnabled(false); + + // If Settings.Global.AUTO_TIME is false it will be set to true + // by the device policy manager very soon. + // Note that this app listens to that change. + } + Intent intent = getActivity().getIntent(); boolean isFirstRun = intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false); mDummyDate = Calendar.getInstance(); - mAutoTimePref = (CheckBoxPreference) findPreference(KEY_AUTO_TIME); mAutoTimePref.setChecked(autoTimeEnabled); mAutoTimeZonePref = (CheckBoxPreference) findPreference(KEY_AUTO_TIME_ZONE); // Override auto-timezone if it's a wifi-only device or if we're still in setup wizard. @@ -308,9 +318,10 @@ public class DateTimeSettings extends SettingsPreferenceFragment removeDialog(DIALOG_TIMEPICKER); showDialog(DIALOG_TIMEPICKER); } else if (preference == mTime24Pref) { - set24Hour(((CheckBoxPreference)mTime24Pref).isChecked()); + final boolean is24Hour = ((CheckBoxPreference)mTime24Pref).isChecked(); + set24Hour(is24Hour); updateTimeAndDateDisplay(getActivity()); - timeUpdated(); + timeUpdated(is24Hour); } return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -321,8 +332,9 @@ public class DateTimeSettings extends SettingsPreferenceFragment updateTimeAndDateDisplay(getActivity()); } - private void timeUpdated() { + private void timeUpdated(boolean is24Hour) { Intent timeChanged = new Intent(Intent.ACTION_TIME_CHANGED); + timeChanged.putExtra(Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT, is24Hour); getActivity().sendBroadcast(timeChanged); } diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java index f3a22ca..f5704c1 100644 --- a/src/com/android/settings/DevelopmentSettings.java +++ b/src/com/android/settings/DevelopmentSettings.java @@ -16,7 +16,6 @@ package com.android.settings; -import android.app.ActionBar; import android.app.Activity; import android.app.ActivityManagerNative; import android.app.AlertDialog; @@ -27,55 +26,58 @@ import android.bluetooth.BluetoothAdapter; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.hardware.usb.IUsbManager; +import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; -import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemProperties; import android.os.UserHandle; +import android.os.UserManager; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; import android.view.HardwareRenderer; import android.view.IWindowManager; import android.view.View; -import android.widget.CompoundButton; +import android.view.accessibility.AccessibilityManager; import android.widget.Switch; import android.widget.TextView; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.SwitchBar; import dalvik.system.VMRuntime; -import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; /* * Displays preferences for application developers. */ -public class DevelopmentSettings extends RestrictedSettingsFragment +public class DevelopmentSettings extends SettingsPreferenceFragment implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener, - OnPreferenceChangeListener, CompoundButton.OnCheckedChangeListener { + OnPreferenceChangeListener, SwitchBar.OnSwitchChangeListener, Indexable { private static final String TAG = "DevelopmentSettings"; /** @@ -93,8 +95,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private static final String ENABLE_TERMINAL = "enable_terminal"; private static final String KEEP_SCREEN_ON = "keep_screen_on"; private static final String BT_HCI_SNOOP_LOG = "bt_hci_snoop_log"; - private static final String SELECT_RUNTIME_KEY = "select_runtime"; - private static final String SELECT_RUNTIME_PROPERTY = "persist.sys.dalvik.vm.lib"; + private static final String ENABLE_OEM_UNLOCK = "oem_unlock_enable"; private static final String ALLOW_MOCK_LOCATION = "allow_mock_location"; private static final String HDCP_CHECKING_KEY = "hdcp_checking"; private static final String HDCP_CHECKING_PROPERTY = "persist.sys.hdcp_checking"; @@ -108,11 +109,16 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private static final String DEBUG_APP_KEY = "debug_app"; private static final String WAIT_FOR_DEBUGGER_KEY = "wait_for_debugger"; private static final String VERIFY_APPS_OVER_USB_KEY = "verify_apps_over_usb"; + private static final String DEBUG_VIEW_ATTRIBUTES = "debug_view_attributes"; private static final String STRICT_MODE_KEY = "strict_mode"; private static final String POINTER_LOCATION_KEY = "pointer_location"; private static final String SHOW_TOUCHES_KEY = "show_touches"; private static final String SHOW_SCREEN_UPDATES_KEY = "show_screen_updates"; private static final String DISABLE_OVERLAYS_KEY = "disable_overlays"; + private static final String SIMULATE_COLOR_SPACE = "simulate_color_space"; + private static final String USE_NUPLAYER_KEY = "use_nuplayer"; + private static final String USB_AUDIO_KEY = "usb_audio"; + private static final String USE_AWESOMEPLAYER_PROPERTY = "persist.sys.media.use-awesome"; private static final String SHOW_CPU_USAGE_KEY = "show_cpu_usage"; private static final String FORCE_HARDWARE_UI_KEY = "force_hw_ui"; private static final String FORCE_MSAA_KEY = "force_msaa"; @@ -130,6 +136,12 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private static final String DEBUG_DEBUGGING_CATEGORY_KEY = "debug_debugging_category"; private static final String DEBUG_APPLICATIONS_CATEGORY_KEY = "debug_applications_category"; private static final String WIFI_DISPLAY_CERTIFICATION_KEY = "wifi_display_certification"; + private static final String WIFI_VERBOSE_LOGGING_KEY = "wifi_verbose_logging"; + private static final String WIFI_AGGRESSIVE_HANDOVER_KEY = "wifi_aggressive_handover"; + private static final String WIFI_ALLOW_SCAN_WITH_TRAFFIC_KEY = "wifi_allow_scan_with_traffic"; + private static final String SELECT_LOGD_SIZE_KEY = "select_logd_size"; + private static final String SELECT_LOGD_SIZE_PROPERTY = "persist.logd.size"; + private static final String SELECT_LOGD_DEFAULT_SIZE_PROPERTY = "ro.logd.size"; private static final String OPENGL_TRACES_KEY = "enable_opengl_traces"; @@ -139,6 +151,8 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private static final String SHOW_ALL_ANRS_KEY = "show_all_anrs"; + private static final String PROCESS_STATS = "proc_stats"; + private static final String TAG_CONFIRM_ENFORCE = "confirm_enforce"; private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; @@ -147,11 +161,17 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private static final int RESULT_DEBUG_APP = 1000; + private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; + + private static String DEFAULT_LOG_RING_BUFFER_SIZE_IN_BYTES = "262144"; // 256K + private IWindowManager mWindowManager; private IBackupManager mBackupManager; private DevicePolicyManager mDpm; + private UserManager mUm; + private WifiManager mWifiManager; - private Switch mEnabledSwitch; + private SwitchBar mSwitchBar; private boolean mLastEnabledState; private boolean mHaveDebugSettings; private boolean mDontPokeProperties; @@ -163,15 +183,20 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private CheckBoxPreference mBugreportInPower; private CheckBoxPreference mKeepScreenOn; private CheckBoxPreference mBtHciSnoopLog; + private CheckBoxPreference mEnableOemUnlock; private CheckBoxPreference mAllowMockLocation; - private PreferenceScreen mPassword; + private CheckBoxPreference mDebugViewAttributes; + private PreferenceScreen mPassword; private String mDebugApp; private Preference mDebugAppPref; private CheckBoxPreference mWaitForDebugger; private CheckBoxPreference mVerifyAppsOverUsb; private CheckBoxPreference mWifiDisplayCertification; + private CheckBoxPreference mWifiVerboseLogging; + private CheckBoxPreference mWifiAggressiveHandover; + private CheckBoxPreference mWifiAllowScansWithTraffic; private CheckBoxPreference mStrictMode; private CheckBoxPreference mPointerLocation; private CheckBoxPreference mShowTouches; @@ -185,6 +210,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private CheckBoxPreference mDebugLayout; private CheckBoxPreference mForceRtlLayout; private ListPreference mDebugHwOverdraw; + private ListPreference mLogdSize; private ListPreference mTrackFrameTime; private ListPreference mShowNonRectClip; private ListPreference mWindowAnimationScale; @@ -193,29 +219,31 @@ public class DevelopmentSettings extends RestrictedSettingsFragment private ListPreference mOverlayDisplayDevices; private ListPreference mOpenGLTraces; + private ListPreference mSimulateColorSpace; + + private CheckBoxPreference mUseNuplayer; + private CheckBoxPreference mUSBAudio; private CheckBoxPreference mImmediatelyDestroyActivities; + private ListPreference mAppProcessLimit; private CheckBoxPreference mShowAllANRs; + private PreferenceScreen mProcessStats; private final ArrayList<Preference> mAllPrefs = new ArrayList<Preference>(); + private final ArrayList<CheckBoxPreference> mResetCbPrefs = new ArrayList<CheckBoxPreference>(); private final HashSet<Preference> mDisabledPrefs = new HashSet<Preference>(); - // To track whether a confirmation dialog was clicked. private boolean mDialogClicked; private Dialog mEnableDialog; private Dialog mAdbDialog; - private Dialog mAdbKeysDialog; + private Dialog mAdbKeysDialog; private boolean mUnavailable; - public DevelopmentSettings() { - super(RESTRICTIONS_PIN_SET); - } - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -224,8 +252,12 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mBackupManager = IBackupManager.Stub.asInterface( ServiceManager.getService(Context.BACKUP_SERVICE)); mDpm = (DevicePolicyManager)getActivity().getSystemService(Context.DEVICE_POLICY_SERVICE); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); + + mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); - if (android.os.Process.myUserHandle().getIdentifier() != UserHandle.USER_OWNER) { + if (android.os.Process.myUserHandle().getIdentifier() != UserHandle.USER_OWNER + || mUm.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES)) { mUnavailable = true; setPreferenceScreen(new PreferenceScreen(getActivity(), null)); return; @@ -243,6 +275,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment debugDebuggingCategory.removePreference(mClearAdbKeys); } } + mAllPrefs.add(mClearAdbKeys); mEnableTerminal = findAndInitCheckboxPref(ENABLE_TERMINAL); if (!isPackageInstalled(getActivity(), TERMINAL_APP_PACKAGE)) { debugDebuggingCategory.removePreference(mEnableTerminal); @@ -253,10 +286,17 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mBugreportInPower = findAndInitCheckboxPref(BUGREPORT_IN_POWER_KEY); mKeepScreenOn = findAndInitCheckboxPref(KEEP_SCREEN_ON); mBtHciSnoopLog = findAndInitCheckboxPref(BT_HCI_SNOOP_LOG); + mEnableOemUnlock = findAndInitCheckboxPref(ENABLE_OEM_UNLOCK); + if (!showEnableOemUnlockPreference()) { + removePreference(mEnableOemUnlock); + mEnableOemUnlock = null; + } mAllowMockLocation = findAndInitCheckboxPref(ALLOW_MOCK_LOCATION); + mDebugViewAttributes = findAndInitCheckboxPref(DEBUG_VIEW_ATTRIBUTES); mPassword = (PreferenceScreen) findPreference(LOCAL_BACKUP_PASSWORD); mAllPrefs.add(mPassword); + if (!android.os.Process.myUserHandle().equals(UserHandle.OWNER)) { disableForUser(mEnableAdb); disableForUser(mClearAdbKeys); @@ -291,16 +331,25 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mForceRtlLayout = findAndInitCheckboxPref(FORCE_RTL_LAYOUT_KEY); mDebugHwOverdraw = addListPreference(DEBUG_HW_OVERDRAW_KEY); mWifiDisplayCertification = findAndInitCheckboxPref(WIFI_DISPLAY_CERTIFICATION_KEY); + mWifiVerboseLogging = findAndInitCheckboxPref(WIFI_VERBOSE_LOGGING_KEY); + mWifiAggressiveHandover = findAndInitCheckboxPref(WIFI_AGGRESSIVE_HANDOVER_KEY); + mWifiAllowScansWithTraffic = findAndInitCheckboxPref(WIFI_ALLOW_SCAN_WITH_TRAFFIC_KEY); + mLogdSize = addListPreference(SELECT_LOGD_SIZE_KEY); + mWindowAnimationScale = addListPreference(WINDOW_ANIMATION_SCALE_KEY); mTransitionAnimationScale = addListPreference(TRANSITION_ANIMATION_SCALE_KEY); mAnimatorDurationScale = addListPreference(ANIMATOR_DURATION_SCALE_KEY); mOverlayDisplayDevices = addListPreference(OVERLAY_DISPLAY_DEVICES_KEY); mOpenGLTraces = addListPreference(OPENGL_TRACES_KEY); + mSimulateColorSpace = addListPreference(SIMULATE_COLOR_SPACE); + mUseNuplayer = findAndInitCheckboxPref(USE_NUPLAYER_KEY); + mUSBAudio = findAndInitCheckboxPref(USB_AUDIO_KEY); mImmediatelyDestroyActivities = (CheckBoxPreference) findPreference( IMMEDIATELY_DESTROY_ACTIVITIES_KEY); mAllPrefs.add(mImmediatelyDestroyActivities); mResetCbPrefs.add(mImmediatelyDestroyActivities); + mAppProcessLimit = addListPreference(APP_PROCESS_LIMIT_KEY); mShowAllANRs = (CheckBoxPreference) findPreference( @@ -308,17 +357,14 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mAllPrefs.add(mShowAllANRs); mResetCbPrefs.add(mShowAllANRs); - Preference selectRuntime = findPreference(SELECT_RUNTIME_KEY); - if (selectRuntime != null) { - mAllPrefs.add(selectRuntime); - filterRuntimeOptions(selectRuntime); - } - Preference hdcpChecking = findPreference(HDCP_CHECKING_KEY); if (hdcpChecking != null) { mAllPrefs.add(hdcpChecking); removePreferenceForProduction(hdcpChecking); } + + mProcessStats = (PreferenceScreen) findPreference(PROCESS_STATS); + mAllPrefs.add(mProcessStats); } private ListPreference addListPreference(String prefKey) { @@ -349,37 +395,15 @@ public class DevelopmentSettings extends RestrictedSettingsFragment public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final Activity activity = getActivity(); - mEnabledSwitch = new Switch(activity); + final SettingsActivity activity = (SettingsActivity) getActivity(); - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - mEnabledSwitch.setPaddingRelative(0, 0, padding, 0); - if (mUnavailable) { - mEnabledSwitch.setEnabled(false); + mSwitchBar = activity.getSwitchBar(); + if (mUnavailable) { + mSwitchBar.setEnabled(false); return; } - mEnabledSwitch.setOnCheckedChangeListener(this); - } - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, - ActionBar.DISPLAY_SHOW_CUSTOM); - activity.getActionBar().setCustomView(mEnabledSwitch, new ActionBar.LayoutParams( - ActionBar.LayoutParams.WRAP_CONTENT, - ActionBar.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_VERTICAL | Gravity.END)); - } - - @Override - public void onStop() { - super.onStop(); - final Activity activity = getActivity(); - activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM); - activity.getActionBar().setCustomView(null); + mSwitchBar.addOnSwitchChangeListener(this); } private boolean removePreferenceForProduction(Preference preference) { @@ -430,7 +454,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment final ContentResolver cr = getActivity().getContentResolver(); mLastEnabledState = Settings.Global.getInt(cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; - mEnabledSwitch.setChecked(mLastEnabledState); + mSwitchBar.setChecked(mLastEnabledState); setPrefsEnabledState(mLastEnabledState); if (mHaveDebugSettings && !mLastEnabledState) { @@ -441,9 +465,21 @@ public class DevelopmentSettings extends RestrictedSettingsFragment Settings.Global.putInt(getActivity().getContentResolver(), Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 1); mLastEnabledState = true; - mEnabledSwitch.setChecked(mLastEnabledState); + mSwitchBar.setChecked(mLastEnabledState); setPrefsEnabledState(mLastEnabledState); } + mSwitchBar.show(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (mUnavailable) { + return; + } + mSwitchBar.removeOnSwitchChangeListener(this); + mSwitchBar.hide(); } void updateCheckBox(CheckBoxPreference checkBox, boolean value) { @@ -468,9 +504,13 @@ public class DevelopmentSettings extends RestrictedSettingsFragment Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0) != 0); updateCheckBox(mBtHciSnoopLog, Settings.Secure.getInt(cr, Settings.Secure.BLUETOOTH_HCI_LOG, 0) != 0); + if (mEnableOemUnlock != null) { + updateCheckBox(mEnableOemUnlock, Utils.isOemUnlockEnabled(getActivity())); + } updateCheckBox(mAllowMockLocation, Settings.Secure.getInt(cr, Settings.Secure.ALLOW_MOCK_LOCATION, 0) != 0); - updateRuntimeValue(); + updateCheckBox(mDebugViewAttributes, Settings.Global.getInt(cr, + Settings.Global.DEBUG_VIEW_ATTRIBUTES, 0) != 0); updateHdcpValues(); updatePasswordSummary(); updateDebuggerOptions(); @@ -496,7 +536,14 @@ public class DevelopmentSettings extends RestrictedSettingsFragment updateVerifyAppsOverUsbOptions(); updateBugreportOptions(); updateForceRtlOptions(); + updateLogdSizeValues(); updateWifiDisplayCertificationOptions(); + updateWifiVerboseLoggingOptions(); + updateWifiAggressiveHandoverOptions(); + updateWifiAllowScansWithTrafficOptions(); + updateSimulateColorSpace(); + updateUseNuplayerOptions(); + updateUSBAudioOptions(); } private void resetDangerousOptions() { @@ -509,9 +556,14 @@ public class DevelopmentSettings extends RestrictedSettingsFragment } } resetDebuggerOptions(); + writeLogdSizeOption(null); writeAnimationScaleOption(0, mWindowAnimationScale, null); writeAnimationScaleOption(1, mTransitionAnimationScale, null); writeAnimationScaleOption(2, mAnimatorDurationScale, null); + // Only poke the color space setting if we control it. + if (usingDevelopmentColorSpace()) { + writeSimulateColorSpace(-1); + } writeOverlayDisplayDevicesOptions(null); writeAppProcessLimitOptions(null); mHaveDebugSettings = false; @@ -520,53 +572,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment pokeSystemProperties(); } - void filterRuntimeOptions(Preference selectRuntime) { - ListPreference pref = (ListPreference) selectRuntime; - ArrayList<String> validValues = new ArrayList<String>(); - ArrayList<String> validSummaries = new ArrayList<String>(); - String[] values = getResources().getStringArray(R.array.select_runtime_values); - String[] summaries = getResources().getStringArray(R.array.select_runtime_summaries); - for (int i = 0; i < values.length; i++) { - String value = values[i]; - String summary = summaries[i]; - if (new File("/system/lib/" + value).exists()) { - validValues.add(value); - validSummaries.add(summary); - } - } - int count = validValues.size(); - if (count <= 1) { - // no choices, so remove preference - removePreference(selectRuntime); - } else { - pref.setEntryValues(validValues.toArray(new String[count])); - pref.setEntries(validSummaries.toArray(new String[count])); - } - } - - private String currentRuntimeValue() { - return SystemProperties.get(SELECT_RUNTIME_PROPERTY, VMRuntime.getRuntime().vmLibrary()); - } - - private void updateRuntimeValue() { - ListPreference selectRuntime = (ListPreference) findPreference(SELECT_RUNTIME_KEY); - if (selectRuntime != null) { - String currentValue = currentRuntimeValue(); - String[] values = getResources().getStringArray(R.array.select_runtime_values); - String[] summaries = getResources().getStringArray(R.array.select_runtime_summaries); - int index = 0; - for (int i = 0; i < values.length; i++) { - if (currentValue.equals(values[i])) { - index = i; - break; - } - } - selectRuntime.setValue(values[index]); - selectRuntime.setSummary(summaries[index]); - selectRuntime.setOnPreferenceChangeListener(this); - } - } - private void updateHdcpValues() { ListPreference hdcpChecking = (ListPreference) findPreference(HDCP_CHECKING_KEY); if (hdcpChecking != null) { @@ -682,6 +687,10 @@ public class DevelopmentSettings extends RestrictedSettingsFragment Settings.Global.PACKAGE_VERIFIER_SETTING_VISIBLE, 1) > 0; } + private static boolean showEnableOemUnlockPreference() { + return !SystemProperties.get(PERSISTENT_DATA_BLOCK_PROP).equals(""); + } + private void updateBugreportOptions() { if ("user".equals(Build.TYPE)) { final ContentResolver resolver = getActivity().getContentResolver(); @@ -933,6 +942,83 @@ public class DevelopmentSettings extends RestrictedSettingsFragment pokeSystemProperties(); } + private void updateSimulateColorSpace() { + final ContentResolver cr = getContentResolver(); + final boolean enabled = Settings.Secure.getInt( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0) != 0; + if (enabled) { + final String mode = Integer.toString(Settings.Secure.getInt( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, + AccessibilityManager.DALTONIZER_DISABLED)); + mSimulateColorSpace.setValue(mode); + final int index = mSimulateColorSpace.findIndexOfValue(mode); + if (index < 0) { + // We're using a mode controlled by accessibility preferences. + mSimulateColorSpace.setSummary(getString(R.string.daltonizer_type_overridden, + getString(R.string.accessibility_display_daltonizer_preference_title))); + } else { + mSimulateColorSpace.setSummary("%s"); + } + } else { + mSimulateColorSpace.setValue( + Integer.toString(AccessibilityManager.DALTONIZER_DISABLED)); + } + } + + /** + * @return <code>true</code> if the color space preference is currently + * controlled by development settings + */ + private boolean usingDevelopmentColorSpace() { + final ContentResolver cr = getContentResolver(); + final boolean enabled = Settings.Secure.getInt( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0) != 0; + if (enabled) { + final String mode = Integer.toString(Settings.Secure.getInt( + cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, + AccessibilityManager.DALTONIZER_DISABLED)); + final int index = mSimulateColorSpace.findIndexOfValue(mode); + if (index >= 0) { + // We're using a mode controlled by developer preferences. + return true; + } + } + return false; + } + + private void writeSimulateColorSpace(Object value) { + final ContentResolver cr = getContentResolver(); + final int newMode = Integer.parseInt(value.toString()); + if (newMode < 0) { + Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0); + } else { + Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 1); + Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, newMode); + } + } + + private void updateUseNuplayerOptions() { + updateCheckBox( + mUseNuplayer, !SystemProperties.getBoolean(USE_AWESOMEPLAYER_PROPERTY, false)); + } + + private void writeUseNuplayerOptions() { + SystemProperties.set( + USE_AWESOMEPLAYER_PROPERTY, mUseNuplayer.isChecked() ? "false" : "true"); + pokeSystemProperties(); + } + + private void updateUSBAudioOptions() { + updateCheckBox(mUSBAudio, Settings.Secure.getInt(getContentResolver(), + Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0) != 0); + } + + private void writeUSBAudioOptions() { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, + mUSBAudio.isChecked() ? 1 : 0); + } + private void updateForceRtlOptions() { updateCheckBox(mForceRtlLayout, Settings.Global.getInt(getActivity().getContentResolver(), Settings.Global.DEVELOPMENT_FORCE_RTL, 0) != 0); @@ -958,6 +1044,82 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mWifiDisplayCertification.isChecked() ? 1 : 0); } + private void updateWifiVerboseLoggingOptions() { + boolean enabled = mWifiManager.getVerboseLoggingLevel() > 0; + updateCheckBox(mWifiVerboseLogging, enabled); + } + + private void writeWifiVerboseLoggingOptions() { + mWifiManager.enableVerboseLogging(mWifiVerboseLogging.isChecked() ? 1 : 0); + } + + private void updateWifiAggressiveHandoverOptions() { + boolean enabled = mWifiManager.getAggressiveHandover() > 0; + updateCheckBox(mWifiAggressiveHandover, enabled); + } + + private void writeWifiAggressiveHandoverOptions() { + mWifiManager.enableAggressiveHandover(mWifiAggressiveHandover.isChecked() ? 1 : 0); + } + + private void updateWifiAllowScansWithTrafficOptions() { + boolean enabled = mWifiManager.getAllowScansWithTraffic() > 0; + updateCheckBox(mWifiAllowScansWithTraffic, enabled); + } + + private void writeWifiAllowScansWithTrafficOptions() { + mWifiManager.setAllowScansWithTraffic(mWifiAllowScansWithTraffic.isChecked() ? 1 : 0); + } + + private void updateLogdSizeValues() { + if (mLogdSize != null) { + String currentValue = SystemProperties.get(SELECT_LOGD_SIZE_PROPERTY); + if (currentValue == null) { + currentValue = SystemProperties.get(SELECT_LOGD_DEFAULT_SIZE_PROPERTY); + if (currentValue == null) { + currentValue = "256K"; + } + } + String[] values = getResources().getStringArray(R.array.select_logd_size_values); + String[] titles = getResources().getStringArray(R.array.select_logd_size_titles); + if (SystemProperties.get("ro.config.low_ram").equals("true")) { + mLogdSize.setEntries(R.array.select_logd_size_lowram_titles); + titles = getResources().getStringArray(R.array.select_logd_size_lowram_titles); + } + String[] summaries = getResources().getStringArray(R.array.select_logd_size_summaries); + int index = 1; // punt to second entry if not found + for (int i = 0; i < titles.length; i++) { + if (currentValue.equals(values[i]) + || currentValue.equals(titles[i])) { + index = i; + break; + } + } + mLogdSize.setValue(values[index]); + mLogdSize.setSummary(summaries[index]); + mLogdSize.setOnPreferenceChangeListener(this); + } + } + + private void writeLogdSizeOption(Object newValue) { + String currentValue = SystemProperties.get(SELECT_LOGD_DEFAULT_SIZE_PROPERTY); + if (currentValue != null) { + DEFAULT_LOG_RING_BUFFER_SIZE_IN_BYTES = currentValue; + } + final String size = (newValue != null) ? + newValue.toString() : DEFAULT_LOG_RING_BUFFER_SIZE_IN_BYTES; + SystemProperties.set(SELECT_LOGD_SIZE_PROPERTY, size); + pokeSystemProperties(); + try { + Process p = Runtime.getRuntime().exec("logcat -b all -G " + size); + p.waitFor(); + Log.i(TAG, "Logcat ring buffer sizes set to: " + size); + } catch (Exception e) { + Log.w(TAG, "Cannot set logcat ring buffer sizes", e); + } + updateLogdSizeValues(); + } + private void updateCpuUsageOptions() { updateCheckBox(mShowCpuUsage, Settings.Global.getInt(getActivity().getContentResolver(), Settings.Global.SHOW_PROCESSES, 0) != 0); @@ -1116,28 +1278,28 @@ public class DevelopmentSettings extends RestrictedSettingsFragment } @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (buttonView == mEnabledSwitch) { - if (isChecked != mLastEnabledState) { - if (isChecked) { - mDialogClicked = false; - if (mEnableDialog != null) dismissDialogs(); - mEnableDialog = new AlertDialog.Builder(getActivity()).setMessage( - getActivity().getResources().getString( - R.string.dev_settings_warning_message)) - .setTitle(R.string.dev_settings_warning_title) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setPositiveButton(android.R.string.yes, this) - .setNegativeButton(android.R.string.no, this) - .show(); - mEnableDialog.setOnDismissListener(this); - } else { - resetDangerousOptions(); - Settings.Global.putInt(getActivity().getContentResolver(), - Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0); - mLastEnabledState = isChecked; - setPrefsEnabledState(mLastEnabledState); - } + public void onSwitchChanged(Switch switchView, boolean isChecked) { + if (switchView != mSwitchBar.getSwitch()) { + return; + } + if (isChecked != mLastEnabledState) { + if (isChecked) { + mDialogClicked = false; + if (mEnableDialog != null) dismissDialogs(); + mEnableDialog = new AlertDialog.Builder(getActivity()).setMessage( + getActivity().getResources().getString( + R.string.dev_settings_warning_message)) + .setTitle(R.string.dev_settings_warning_title) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.no, this) + .show(); + mEnableDialog.setOnDismissListener(this); + } else { + resetDangerousOptions(); + Settings.Global.putInt(getActivity().getContentResolver(), + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0); + mLastEnabledState = isChecked; + setPrefsEnabledState(mLastEnabledState); } } } @@ -1168,7 +1330,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mAdbDialog = new AlertDialog.Builder(getActivity()).setMessage( getActivity().getResources().getString(R.string.adb_warning_message)) .setTitle(R.string.adb_warning_title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setPositiveButton(android.R.string.yes, this) .setNegativeButton(android.R.string.no, this) .show(); @@ -1200,13 +1361,19 @@ public class DevelopmentSettings extends RestrictedSettingsFragment Settings.Global.putInt(getActivity().getContentResolver(), Settings.Global.STAY_ON_WHILE_PLUGGED_IN, mKeepScreenOn.isChecked() ? - (BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB) : 0); + (BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB) : 0); } else if (preference == mBtHciSnoopLog) { writeBtHciSnoopLogOptions(); + } else if (preference == mEnableOemUnlock) { + Utils.setOemUnlockEnabled(getActivity(), mEnableOemUnlock.isChecked()); } else if (preference == mAllowMockLocation) { Settings.Secure.putInt(getActivity().getContentResolver(), Settings.Secure.ALLOW_MOCK_LOCATION, mAllowMockLocation.isChecked() ? 1 : 0); + } else if (preference == mDebugViewAttributes) { + Settings.Global.putInt(getActivity().getContentResolver(), + Settings.Global.DEBUG_VIEW_ATTRIBUTES, + mDebugViewAttributes.isChecked() ? 1 : 0); } else if (preference == mDebugAppPref) { startActivityForResult(new Intent(getActivity(), AppPicker.class), RESULT_DEBUG_APP); } else if (preference == mWaitForDebugger) { @@ -1243,6 +1410,16 @@ public class DevelopmentSettings extends RestrictedSettingsFragment writeForceRtlOptions(); } else if (preference == mWifiDisplayCertification) { writeWifiDisplayCertificationOptions(); + } else if (preference == mWifiVerboseLogging) { + writeWifiVerboseLoggingOptions(); + } else if (preference == mWifiAggressiveHandover) { + writeWifiAggressiveHandoverOptions(); + } else if (preference == mWifiAllowScansWithTraffic) { + writeWifiAllowScansWithTrafficOptions(); + } else if (preference == mUseNuplayer) { + writeUseNuplayerOptions(); + } else if (preference == mUSBAudio) { + writeUSBAudioOptions(); } else { return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -1252,38 +1429,14 @@ public class DevelopmentSettings extends RestrictedSettingsFragment @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if (SELECT_RUNTIME_KEY.equals(preference.getKey())) { - final String oldRuntimeValue = VMRuntime.getRuntime().vmLibrary(); - final String newRuntimeValue = newValue.toString(); - if (!newRuntimeValue.equals(oldRuntimeValue)) { - final Context context = getActivity(); - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(context.getResources().getString(R.string.select_runtime_warning_message, - oldRuntimeValue, newRuntimeValue)); - builder.setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - SystemProperties.set(SELECT_RUNTIME_PROPERTY, newRuntimeValue); - pokeSystemProperties(); - PowerManager pm = (PowerManager) - context.getSystemService(Context.POWER_SERVICE); - pm.reboot(null); - } - }); - builder.setNegativeButton(android.R.string.cancel, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - updateRuntimeValue(); - } - }); - builder.show(); - } - return true; - } else if (HDCP_CHECKING_KEY.equals(preference.getKey())) { + if (HDCP_CHECKING_KEY.equals(preference.getKey())) { SystemProperties.set(HDCP_CHECKING_PROPERTY, newValue.toString()); updateHdcpValues(); pokeSystemProperties(); return true; + } else if (preference == mLogdSize) { + writeLogdSizeOption(newValue); + return true; } else if (preference == mWindowAnimationScale) { writeAnimationScaleOption(0, mWindowAnimationScale, newValue); return true; @@ -1311,6 +1464,9 @@ public class DevelopmentSettings extends RestrictedSettingsFragment } else if (preference == mAppProcessLimit) { writeAppProcessLimitOptions(newValue); return true; + } else if (preference == mSimulateColorSpace) { + writeSimulateColorSpace(newValue); + return true; } return false; } @@ -1362,7 +1518,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment setPrefsEnabledState(mLastEnabledState); } else { // Reset the toggle - mEnabledSwitch.setChecked(false); + mSwitchBar.setChecked(false); } } } @@ -1376,7 +1532,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mAdbDialog = null; } else if (dialog == mEnableDialog) { if (!mDialogClicked) { - mEnabledSwitch.setChecked(false); + mSwitchBar.setChecked(false); } mEnableDialog = null; } @@ -1429,4 +1585,44 @@ public class DevelopmentSettings extends RestrictedSettingsFragment return false; } } + + /** + * For Search. + */ + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + private boolean isShowingDeveloperOptions(Context context) { + return context.getSharedPreferences(DevelopmentSettings.PREF_FILE, + Context.MODE_PRIVATE).getBoolean( + DevelopmentSettings.PREF_SHOW, + android.os.Build.TYPE.equals("eng")); + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + + if (!isShowingDeveloperOptions(context)) { + return null; + } + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.development_prefs; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + if (!isShowingDeveloperOptions(context)) { + return null; + } + + final List<String> keys = new ArrayList<String>(); + if (!showEnableOemUnlockPreference()) { + keys.add(ENABLE_OEM_UNLOCK); + } + return keys; + } + }; } diff --git a/src/com/android/settings/DeviceAdminAdd.java b/src/com/android/settings/DeviceAdminAdd.java index 6234038..ed95500 100644 --- a/src/com/android/settings/DeviceAdminAdd.java +++ b/src/com/android/settings/DeviceAdminAdd.java @@ -16,6 +16,8 @@ package com.android.settings; +import android.app.AppOpsManager; + import org.xmlpull.v1.XmlPullParserException; import android.app.Activity; @@ -30,14 +32,19 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.RemoteCallback; import android.os.RemoteException; +import android.os.UserHandle; import android.text.TextUtils.TruncateAt; +import android.util.EventLog; import android.util.Log; import android.view.Display; import android.view.View; @@ -50,68 +57,103 @@ import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; public class DeviceAdminAdd extends Activity { static final String TAG = "DeviceAdminAdd"; - + static final int DIALOG_WARNING = 1; private static final int MAX_ADD_MSG_LINES_PORTRAIT = 5; private static final int MAX_ADD_MSG_LINES_LANDSCAPE = 2; private static final int MAX_ADD_MSG_LINES = 15; - + Handler mHandler; - + DevicePolicyManager mDPM; + AppOpsManager mAppOps; DeviceAdminInfo mDeviceAdmin; CharSequence mAddMsgText; - + String mProfileOwnerName; + ImageView mAdminIcon; TextView mAdminName; TextView mAdminDescription; TextView mAddMsg; + TextView mProfileOwnerWarning; ImageView mAddMsgExpander; boolean mAddMsgEllipsized = true; TextView mAdminWarning; ViewGroup mAdminPolicies; Button mActionButton; Button mCancelButton; - + final ArrayList<View> mAddingPolicies = new ArrayList<View>(); final ArrayList<View> mActivePolicies = new ArrayList<View>(); - + boolean mAdding; boolean mRefreshing; - + boolean mWaitingForRemoveMsg; + boolean mAddingProfileOwner; + int mCurSysAppOpMode; + int mCurToastAppOpMode; + @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); mHandler = new Handler(getMainLooper()); - + mDPM = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE); + mAppOps = (AppOpsManager)getSystemService(Context.APP_OPS_SERVICE); + PackageManager packageManager = getPackageManager(); if ((getIntent().getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { Log.w(TAG, "Cannot start ADD_DEVICE_ADMIN as a new task"); finish(); return; } - - ComponentName cn = (ComponentName)getIntent().getParcelableExtra( + + String action = getIntent().getAction(); + ComponentName who = (ComponentName)getIntent().getParcelableExtra( DevicePolicyManager.EXTRA_DEVICE_ADMIN); - if (cn == null) { - Log.w(TAG, "No component specified in " + getIntent().getAction()); + if (who == null) { + Log.w(TAG, "No component specified in " + action); finish(); return; } + if (action != null && action.equals(DevicePolicyManager.ACTION_SET_PROFILE_OWNER)) { + setResult(RESULT_CANCELED); + setFinishOnTouchOutside(true); + mAddingProfileOwner = true; + mProfileOwnerName = + getIntent().getStringExtra(DevicePolicyManager.EXTRA_PROFILE_OWNER_NAME); + String callingPackage = getCallingPackage(); + if (callingPackage == null || !callingPackage.equals(who.getPackageName())) { + Log.e(TAG, "Unknown or incorrect caller"); + finish(); + return; + } + try { + PackageInfo packageInfo = packageManager.getPackageInfo(callingPackage, 0); + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + Log.e(TAG, "Cannot set a non-system app as a profile owner"); + finish(); + return; + } + } catch (NameNotFoundException nnfe) { + Log.e(TAG, "Cannot find the package " + callingPackage); + finish(); + return; + } + } + ActivityInfo ai; try { - ai = getPackageManager().getReceiverInfo(cn, PackageManager.GET_META_DATA); + ai = packageManager.getReceiverInfo(who, PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Unable to retrieve device policy " + cn, e); + Log.w(TAG, "Unable to retrieve device policy " + who, e); finish(); return; } @@ -119,8 +161,8 @@ public class DeviceAdminAdd extends Activity { // When activating, make sure the given component name is actually a valid device admin. // No need to check this when deactivating, because it is safe to deactivate an active // invalid device admin. - if (!mDPM.isAdminActive(cn)) { - List<ResolveInfo> avail = getPackageManager().queryBroadcastReceivers( + if (!mDPM.isAdminActive(who)) { + List<ResolveInfo> avail = packageManager.queryBroadcastReceivers( new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); int count = avail == null ? 0 : avail.size(); @@ -144,7 +186,7 @@ public class DeviceAdminAdd extends Activity { } } if (!found) { - Log.w(TAG, "Request to add invalid device admin: " + cn); + Log.w(TAG, "Request to add invalid device admin: " + who); finish(); return; } @@ -155,25 +197,25 @@ public class DeviceAdminAdd extends Activity { try { mDeviceAdmin = new DeviceAdminInfo(this, ri); } catch (XmlPullParserException e) { - Log.w(TAG, "Unable to retrieve device policy " + cn, e); + Log.w(TAG, "Unable to retrieve device policy " + who, e); finish(); return; } catch (IOException e) { - Log.w(TAG, "Unable to retrieve device policy " + cn, e); + Log.w(TAG, "Unable to retrieve device policy " + who, e); finish(); return; } - + // This admin already exists, an we have two options at this point. If new policy // bits are set, show the user the new list. If nothing has changed, simply return // "OK" immediately. if (DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN.equals(getIntent().getAction())) { mRefreshing = false; - if (mDPM.isAdminActive(cn)) { + if (mDPM.isAdminActive(who)) { ArrayList<DeviceAdminInfo.PolicyInfo> newPolicies = mDeviceAdmin.getUsedPolicies(); for (int i = 0; i < newPolicies.size(); i++) { DeviceAdminInfo.PolicyInfo pi = newPolicies.get(i); - if (!mDPM.hasGrantedPolicy(cn, pi.ident)) { + if (!mDPM.hasGrantedPolicy(who, pi.ident)) { mRefreshing = true; break; } @@ -186,13 +228,22 @@ public class DeviceAdminAdd extends Activity { } } } + + // If we're trying to add a profile owner and user setup hasn't completed yet, no + // need to prompt for permission. Just add and finish. + if (mAddingProfileOwner && !mDPM.hasUserSetupCompleted()) { + addAndFinish(); + return; + } + mAddMsgText = getIntent().getCharSequenceExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION); setContentView(R.layout.device_admin_add); - + mAdminIcon = (ImageView)findViewById(R.id.admin_icon); mAdminName = (TextView)findViewById(R.id.admin_name); mAdminDescription = (TextView)findViewById(R.id.admin_description); + mProfileOwnerWarning = (TextView) findViewById(R.id.profile_owner_warning); mAddMsg = (TextView)findViewById(R.id.add_msg); mAddMsgExpander = (ImageView) findViewById(R.id.add_msg_expander); @@ -210,6 +261,8 @@ public class DeviceAdminAdd extends Activity { mCancelButton = (Button) findViewById(R.id.cancel_button); mCancelButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + EventLog.writeEvent(EventLogTags.EXP_DET_DEVICE_ADMIN_DECLINED_BY_USER, + mDeviceAdmin.getActivityInfo().applicationInfo.uid); finish(); } }); @@ -217,26 +270,15 @@ public class DeviceAdminAdd extends Activity { mActionButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (mAdding) { - try { - mDPM.setActiveAdmin(mDeviceAdmin.getComponent(), mRefreshing); - setResult(Activity.RESULT_OK); - } catch (RuntimeException e) { - // Something bad happened... could be that it was - // already set, though. - Log.w(TAG, "Exception trying to activate admin " - + mDeviceAdmin.getComponent(), e); - if (mDPM.isAdminActive(mDeviceAdmin.getComponent())) { - setResult(Activity.RESULT_OK); - } - } - finish(); - } else { + addAndFinish(); + } else if (!mWaitingForRemoveMsg) { try { // Don't allow the admin to put a dialog up in front // of us while we interact with the user. ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } + mWaitingForRemoveMsg = true; mDPM.getRemoveWarning(mDeviceAdmin.getComponent(), new RemoteCallback(mHandler) { @Override @@ -245,32 +287,98 @@ public class DeviceAdminAdd extends Activity { ? bundle.getCharSequence( DeviceAdminReceiver.EXTRA_DISABLE_WARNING) : null; - if (msg == null) { - try { - ActivityManagerNative.getDefault().resumeAppSwitches(); - } catch (RemoteException e) { - } - mDPM.removeActiveAdmin(mDeviceAdmin.getComponent()); - finish(); - } else { - Bundle args = new Bundle(); - args.putCharSequence( - DeviceAdminReceiver.EXTRA_DISABLE_WARNING, msg); - showDialog(DIALOG_WARNING, args); - } + continueRemoveAction(msg); } }); + // Don't want to wait too long. + getWindow().getDecorView().getHandler().postDelayed(new Runnable() { + @Override public void run() { + continueRemoveAction(null); + } + }, 2*1000); } } }); } - + + void addAndFinish() { + try { + mDPM.setActiveAdmin(mDeviceAdmin.getComponent(), mRefreshing); + EventLog.writeEvent(EventLogTags.EXP_DET_DEVICE_ADMIN_ACTIVATED_BY_USER, + mDeviceAdmin.getActivityInfo().applicationInfo.uid); + setResult(Activity.RESULT_OK); + } catch (RuntimeException e) { + // Something bad happened... could be that it was + // already set, though. + Log.w(TAG, "Exception trying to activate admin " + + mDeviceAdmin.getComponent(), e); + if (mDPM.isAdminActive(mDeviceAdmin.getComponent())) { + setResult(Activity.RESULT_OK); + } + } + if (mAddingProfileOwner) { + try { + mDPM.setProfileOwner(mDeviceAdmin.getComponent(), + mProfileOwnerName, UserHandle.myUserId()); + } catch (RuntimeException re) { + setResult(Activity.RESULT_CANCELED); + } + } + finish(); + } + + void continueRemoveAction(CharSequence msg) { + if (!mWaitingForRemoveMsg) { + return; + } + mWaitingForRemoveMsg = false; + if (msg == null) { + try { + ActivityManagerNative.getDefault().resumeAppSwitches(); + } catch (RemoteException e) { + } + mDPM.removeActiveAdmin(mDeviceAdmin.getComponent()); + finish(); + } else { + try { + // Continue preventing anything from coming in front. + ActivityManagerNative.getDefault().stopAppSwitches(); + } catch (RemoteException e) { + } + Bundle args = new Bundle(); + args.putCharSequence( + DeviceAdminReceiver.EXTRA_DISABLE_WARNING, msg); + showDialog(DIALOG_WARNING, args); + } + } + @Override protected void onResume() { super.onResume(); updateInterface(); + // As long as we are running, don't let this admin overlay stuff on top of the screen. + final int uid = mDeviceAdmin.getActivityInfo().applicationInfo.uid; + final String pkg = mDeviceAdmin.getActivityInfo().applicationInfo.packageName; + mCurSysAppOpMode = mAppOps.checkOp(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, pkg); + mCurToastAppOpMode = mAppOps.checkOp(AppOpsManager.OP_TOAST_WINDOW, uid, pkg); + mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, pkg, AppOpsManager.MODE_IGNORED); + mAppOps.setMode(AppOpsManager.OP_TOAST_WINDOW, uid, pkg, AppOpsManager.MODE_IGNORED); } - + + @Override + protected void onPause() { + super.onPause(); + // As long as we are running, don't let this admin overlay stuff on top of the screen. + final int uid = mDeviceAdmin.getActivityInfo().applicationInfo.uid; + final String pkg = mDeviceAdmin.getActivityInfo().applicationInfo.packageName; + mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, pkg, mCurSysAppOpMode); + mAppOps.setMode(AppOpsManager.OP_TOAST_WINDOW, uid, pkg, mCurToastAppOpMode); + try { + ActivityManagerNative.getDefault().resumeAppSwitches(); + } catch (RemoteException e) { + } + } + @Override protected Dialog onCreateDialog(int id, Bundle args) { switch (id) { @@ -282,6 +390,10 @@ public class DeviceAdminAdd extends Activity { builder.setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + try { + ActivityManagerNative.getDefault().resumeAppSwitches(); + } catch (RemoteException e) { + } mDPM.removeActiveAdmin(mDeviceAdmin.getComponent()); finish(); } @@ -291,17 +403,17 @@ public class DeviceAdminAdd extends Activity { } default: return super.onCreateDialog(id, args); - + } } - + static void setViewVisibility(ArrayList<View> views, int visibility) { final int N = views.size(); for (int i=0; i<N; i++) { views.get(i).setVisibility(visibility); } } - + void updateInterface() { mAdminIcon.setImageDrawable(mDeviceAdmin.loadIcon(getPackageManager())); mAdminName.setText(mDeviceAdmin.loadLabel(getPackageManager())); @@ -312,6 +424,9 @@ public class DeviceAdminAdd extends Activity { } catch (Resources.NotFoundException e) { mAdminDescription.setVisibility(View.GONE); } + if (mAddingProfileOwner) { + mProfileOwnerWarning.setVisibility(View.VISIBLE); + } if (mAddMsgText != null) { mAddMsg.setText(mAddMsgText); mAddMsg.setVisibility(View.VISIBLE); @@ -319,7 +434,8 @@ public class DeviceAdminAdd extends Activity { mAddMsg.setVisibility(View.GONE); mAddMsgExpander.setVisibility(View.GONE); } - if (!mRefreshing && mDPM.isAdminActive(mDeviceAdmin.getComponent())) { + if (!mRefreshing && !mAddingProfileOwner + && mDPM.isAdminActive(mDeviceAdmin.getComponent())) { if (mActivePolicies.size() == 0) { ArrayList<DeviceAdminInfo.PolicyInfo> policies = mDeviceAdmin.getUsedPolicies(); for (int i=0; i<policies.size(); i++) { @@ -352,7 +468,11 @@ public class DeviceAdminAdd extends Activity { setViewVisibility(mActivePolicies, View.GONE); mAdminWarning.setText(getString(R.string.device_admin_warning, mDeviceAdmin.getActivityInfo().applicationInfo.loadLabel(getPackageManager()))); - setTitle(getText(R.string.add_device_admin_msg)); + if (mAddingProfileOwner) { + setTitle(getText(R.string.profile_owner_add_title)); + } else { + setTitle(getText(R.string.add_device_admin_msg)); + } mActionButton.setText(getText(R.string.add_device_admin)); mAdding = true; } diff --git a/src/com/android/settings/DeviceAdminSettings.java b/src/com/android/settings/DeviceAdminSettings.java index fa1c7f4..bc22637 100644 --- a/src/com/android/settings/DeviceAdminSettings.java +++ b/src/com/android/settings/DeviceAdminSettings.java @@ -20,6 +20,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.app.Activity; +import android.app.AlertDialog; import android.app.ListFragment; import android.app.admin.DeviceAdminInfo; import android.app.admin.DeviceAdminReceiver; @@ -30,8 +31,13 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; import android.util.Log; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -44,19 +50,25 @@ import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.Collection; public class DeviceAdminSettings extends ListFragment { static final String TAG = "DeviceAdminSettings"; - + static final int DIALOG_WARNING = 1; - - DevicePolicyManager mDPM; - final HashSet<ComponentName> mActiveAdmins = new HashSet<ComponentName>(); - final ArrayList<DeviceAdminInfo> mAvailableAdmins = new ArrayList<DeviceAdminInfo>(); - String mDeviceOwnerPkg; + private DevicePolicyManager mDPM; + private UserManager mUm; + + /** + * Internal collection of device admin info objects for all profiles associated with the current + * user. + */ + private final SparseArray<ArrayList<DeviceAdminInfo>> + mAdminsByProfile = new SparseArray<ArrayList<DeviceAdminInfo>>(); + + private String mDeviceOwnerPkg; + private SparseArray<ComponentName> mProfileOwnerComponents = new SparseArray<ComponentName>(); @Override public void onCreate(Bundle icicle) { @@ -67,82 +79,73 @@ public class DeviceAdminSettings extends ListFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mDPM = (DevicePolicyManager) getActivity().getSystemService(Context.DEVICE_POLICY_SERVICE); + mUm = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); return inflater.inflate(R.layout.device_admin_settings, container, false); } @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Utils.forceCustomPadding(getListView(), true /* additive padding */); + } + + @Override public void onResume() { super.onResume(); mDeviceOwnerPkg = mDPM.getDeviceOwner(); if (mDeviceOwnerPkg != null && !mDPM.isDeviceOwner(mDeviceOwnerPkg)) { mDeviceOwnerPkg = null; } + mProfileOwnerComponents.clear(); + final List<UserHandle> profiles = mUm.getUserProfiles(); + final int profilesSize = profiles.size(); + for (int i = 0; i < profilesSize; ++i) { + final int profileId = profiles.get(i).getIdentifier(); + mProfileOwnerComponents.put(profileId, mDPM.getProfileOwnerAsUser(profileId)); + } updateList(); } + /** + * Update the internal collection of available admins for all profiles associated with the + * current user. + */ void updateList() { - mActiveAdmins.clear(); - List<ComponentName> cur = mDPM.getActiveAdmins(); - if (cur != null) { - for (int i=0; i<cur.size(); i++) { - mActiveAdmins.add(cur.get(i)); - } - } - - mAvailableAdmins.clear(); - List<ResolveInfo> avail = getActivity().getPackageManager().queryBroadcastReceivers( - new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), - PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); - if (avail == null) { - avail = Collections.emptyList(); - } + mAdminsByProfile.clear(); - // Some admins listed in mActiveAdmins may not have been found by the above query. - // We thus add them separately. - Set<ComponentName> activeAdminsNotInAvail = new HashSet<ComponentName>(mActiveAdmins); - for (ResolveInfo ri : avail) { - ComponentName riComponentName = - new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); - activeAdminsNotInAvail.remove(riComponentName); - } - if (!activeAdminsNotInAvail.isEmpty()) { - avail = new ArrayList<ResolveInfo>(avail); - PackageManager packageManager = getActivity().getPackageManager(); - for (ComponentName unlistedActiveAdmin : activeAdminsNotInAvail) { - List<ResolveInfo> resolved = packageManager.queryBroadcastReceivers( - new Intent().setComponent(unlistedActiveAdmin), - PackageManager.GET_META_DATA - | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); - if (resolved != null) { - avail.addAll(resolved); - } - } + final List<UserHandle> profiles = mUm.getUserProfiles(); + final int profilesSize = profiles.size(); + for (int i = 0; i < profilesSize; ++i) { + final int profileId = profiles.get(i).getIdentifier(); + updateAvailableAdminsForProfile(profileId); } - for (int i = 0, count = avail.size(); i < count; i++) { - ResolveInfo ri = avail.get(i); - try { - DeviceAdminInfo dpi = new DeviceAdminInfo(getActivity(), ri); - if (dpi.isVisible() || mActiveAdmins.contains(dpi.getComponent())) { - mAvailableAdmins.add(dpi); - } - } catch (XmlPullParserException e) { - Log.w(TAG, "Skipping " + ri.activityInfo, e); - } catch (IOException e) { - Log.w(TAG, "Skipping " + ri.activityInfo, e); - } - } - getListView().setAdapter(new PolicyListAdapter()); } @Override public void onListItemClick(ListView l, View v, int position, long id) { - DeviceAdminInfo dpi = (DeviceAdminInfo)l.getAdapter().getItem(position); - Intent intent = new Intent(); - intent.setClass(getActivity(), DeviceAdminAdd.class); - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, dpi.getComponent()); - startActivity(intent); + Object o = l.getAdapter().getItem(position); + if (!(o instanceof DeviceAdminInfo)) { + // race conditions may cause this + return; + } + DeviceAdminInfo dpi = (DeviceAdminInfo) o; + final Activity activity = getActivity(); + final int userId = getUserId(dpi); + if (userId == UserHandle.myUserId() || !isProfileOwner(dpi)) { + Intent intent = new Intent(); + intent.setClass(activity, DeviceAdminAdd.class); + intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, dpi.getComponent()); + activity.startActivityAsUser(intent, new UserHandle(userId)); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(getString(R.string.managed_profile_device_admin_info, + dpi.loadLabel(activity.getPackageManager()))); + builder.setPositiveButton(android.R.string.ok, null); + builder.create().show(); + } } static class ViewHolder { @@ -151,57 +154,138 @@ public class DeviceAdminSettings extends ListFragment { CheckBox checkbox; TextView description; } - + class PolicyListAdapter extends BaseAdapter { final LayoutInflater mInflater; - + PolicyListAdapter() { mInflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); } + @Override public boolean hasStableIds() { - return true; + return false; } - + + @Override public int getCount() { - return mAvailableAdmins.size(); + int adminCount = 0; + final int profileCount = mAdminsByProfile.size(); + for (int i = 0; i < profileCount; ++i) { + adminCount += mAdminsByProfile.valueAt(i).size(); + } + // Add 'profileCount' for title items. + return adminCount + profileCount; } + /** + * The item for the given position in the list. + * + * @return a String object for title items and a DeviceAdminInfo object for actual device + * admins. + */ + @Override public Object getItem(int position) { - return mAvailableAdmins.get(position); + if (position < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + // The position of the item in the list of admins. + // We start from the given position and discount the length of the upper lists until we + // get the one for the right profile + int adminPosition = position; + final int n = mAdminsByProfile.size(); + int i = 0; + for (; i < n; ++i) { + // The elements in that section including the title item (that's why adding one). + final int listSize = mAdminsByProfile.valueAt(i).size() + 1; + if (adminPosition < listSize) { + break; + } + adminPosition -= listSize; + } + if (i == n) { + throw new ArrayIndexOutOfBoundsException(); + } + // If countdown == 0 that means the title item + if (adminPosition == 0) { + Resources res = getActivity().getResources(); + if (mAdminsByProfile.keyAt(i) == UserHandle.myUserId()) { + return res.getString(R.string.personal_device_admin_title); + } else { + return res.getString(R.string.managed_device_admin_title); + } + } else { + // Subtracting one for the title. + return mAdminsByProfile.valueAt(i).get(adminPosition - 1); + } } + @Override public long getItemId(int position) { return position; } + @Override public boolean areAllItemsEnabled() { return false; } + /** + * See {@link #getItemViewType} for the view types. + */ + @Override + public int getViewTypeCount() { + return 2; + } + + /** + * Returns 1 for title items and 0 for anything else. + */ + @Override + public int getItemViewType(int position) { + Object o = getItem(position); + return (o instanceof String) ? 1 : 0; + } + + @Override public boolean isEnabled(int position) { - DeviceAdminInfo info = mAvailableAdmins.get(position); - if (mActiveAdmins.contains(info.getComponent()) - && info.getPackageName().equals(mDeviceOwnerPkg)) { + Object o = getItem(position); + return isEnabled(o); + } + + private boolean isEnabled(Object o) { + if (!(o instanceof DeviceAdminInfo)) { + // Title item + return false; + } + DeviceAdminInfo info = (DeviceAdminInfo) o; + if (isActiveAdmin(info) && getUserId(info) == UserHandle.myUserId() + && (isDeviceOwner(info) || isProfileOwner(info))) { return false; - } else { - return true; } + return true; } + @Override public View getView(int position, View convertView, ViewGroup parent) { - View v; - if (convertView == null) { - v = newView(parent); + Object o = getItem(position); + if (o instanceof DeviceAdminInfo) { + if (convertView == null) { + convertView = newDeviceAdminView(parent); + } + bindView(convertView, (DeviceAdminInfo) o); } else { - v = convertView; + if (convertView == null) { + convertView = newTitleView(parent); + } + final TextView title = (TextView) convertView.findViewById(android.R.id.title); + title.setText((String)o); } - bindView(v, position); - return v; + return convertView; } - - public View newView(ViewGroup parent) { + + private View newDeviceAdminView(ViewGroup parent) { View v = mInflater.inflate(R.layout.device_admin_item, parent, false); ViewHolder h = new ViewHolder(); h.icon = (ImageView)v.findViewById(R.id.icon); @@ -211,24 +295,170 @@ public class DeviceAdminSettings extends ListFragment { v.setTag(h); return v; } - - public void bindView(View view, int position) { + + private View newTitleView(ViewGroup parent) { + final TypedArray a = mInflater.getContext().obtainStyledAttributes(null, + com.android.internal.R.styleable.Preference, + com.android.internal.R.attr.preferenceCategoryStyle, 0); + final int resId = a.getResourceId(com.android.internal.R.styleable.Preference_layout, + 0); + return mInflater.inflate(resId, parent, false); + } + + private void bindView(View view, DeviceAdminInfo item) { final Activity activity = getActivity(); ViewHolder vh = (ViewHolder) view.getTag(); - DeviceAdminInfo item = mAvailableAdmins.get(position); - vh.icon.setImageDrawable(item.loadIcon(activity.getPackageManager())); + Drawable activityIcon = item.loadIcon(activity.getPackageManager()); + Drawable badgedIcon = activity.getPackageManager().getUserBadgedIcon( + activityIcon, new UserHandle(getUserId(item))); + vh.icon.setImageDrawable(badgedIcon); vh.name.setText(item.loadLabel(activity.getPackageManager())); - vh.checkbox.setChecked(mActiveAdmins.contains(item.getComponent())); - final boolean activeOwner = vh.checkbox.isChecked() - && item.getPackageName().equals(mDeviceOwnerPkg); + vh.checkbox.setChecked(isActiveAdmin(item)); + final boolean enabled = isEnabled(item); try { vh.description.setText(item.loadDescription(activity.getPackageManager())); } catch (Resources.NotFoundException e) { } - vh.checkbox.setEnabled(!activeOwner); - vh.name.setEnabled(!activeOwner); - vh.description.setEnabled(!activeOwner); - vh.icon.setEnabled(!activeOwner); + vh.checkbox.setEnabled(enabled); + vh.name.setEnabled(enabled); + vh.description.setEnabled(enabled); + vh.icon.setEnabled(enabled); + } + } + + private boolean isDeviceOwner(DeviceAdminInfo item) { + return getUserId(item) == UserHandle.myUserId() + && item.getPackageName().equals(mDeviceOwnerPkg); + } + + private boolean isProfileOwner(DeviceAdminInfo item) { + ComponentName profileOwner = mProfileOwnerComponents.get(getUserId(item)); + return item.getComponent().equals(profileOwner); + } + + private boolean isActiveAdmin(DeviceAdminInfo item) { + return mDPM.isAdminActiveAsUser(item.getComponent(), getUserId(item)); + } + + /** + * Add device admins to the internal collection that belong to a profile. + * + * @param profileId the profile identifier. + */ + private void updateAvailableAdminsForProfile(final int profileId) { + // We are adding the union of two sets 'A' and 'B' of device admins to mAvailableAdmins. + // Set 'A' is the set of active admins for the profile whereas set 'B' is the set of + // listeners to DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED for the profile. + + // Add all of set 'A' to mAvailableAdmins. + List<ComponentName> activeAdminsListForProfile = mDPM.getActiveAdminsAsUser(profileId); + addActiveAdminsForProfile(activeAdminsListForProfile, profileId); + + // Collect set 'B' and add B-A to mAvailableAdmins. + addDeviceAdminBroadcastReceiversForProfile(activeAdminsListForProfile, profileId); + } + + /** + * Add a profile's device admins that are receivers of + * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} to the internal collection if they + * haven't been added yet. + * + * @param alreadyAddedComponents the set of active admin component names. Receivers of + * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} whose component is in this + * set are not added to the internal collection again. + * @param profileId the identifier of the profile + */ + private void addDeviceAdminBroadcastReceiversForProfile( + Collection<ComponentName> alreadyAddedComponents, final int profileId) { + final PackageManager pm = getActivity().getPackageManager(); + List<ResolveInfo> enabledForProfile = pm.queryBroadcastReceivers( + new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), + PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + profileId); + if (enabledForProfile == null) { + enabledForProfile = Collections.emptyList(); + } + final int n = enabledForProfile.size(); + ArrayList<DeviceAdminInfo> deviceAdmins = mAdminsByProfile.get(profileId); + if (deviceAdmins == null) { + deviceAdmins = new ArrayList<DeviceAdminInfo>(n); + } + for (int i = 0; i < n; ++i) { + ResolveInfo resolveInfo = enabledForProfile.get(i); + ComponentName riComponentName = + new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + if (alreadyAddedComponents == null + || !alreadyAddedComponents.contains(riComponentName)) { + DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo(resolveInfo); + // add only visible ones (note: active admins are added regardless of visibility) + if (deviceAdminInfo != null && deviceAdminInfo.isVisible()) { + deviceAdmins.add(deviceAdminInfo); + } + } + } + if (!deviceAdmins.isEmpty()) { + mAdminsByProfile.put(profileId, deviceAdmins); + } + } + + /** + * Add a {@link DeviceAdminInfo} object to the internal collection of available admins for all + * active admin components associated with a profile. + * + * @param profileId a profile identifier. + */ + private void addActiveAdminsForProfile(final List<ComponentName> activeAdmins, + final int profileId) { + if (activeAdmins != null) { + final PackageManager packageManager = getActivity().getPackageManager(); + final int n = activeAdmins.size(); + ArrayList<DeviceAdminInfo> deviceAdmins = new ArrayList<DeviceAdminInfo>(n); + for (int i = 0; i < n; ++i) { + ComponentName activeAdmin = activeAdmins.get(i); + List<ResolveInfo> resolved = packageManager.queryBroadcastReceivers( + new Intent().setComponent(activeAdmin), PackageManager.GET_META_DATA + | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, profileId); + if (resolved != null) { + final int resolvedMax = resolved.size(); + for (int j = 0; j < resolvedMax; ++j) { + DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo(resolved.get(j)); + if (deviceAdminInfo != null) { + deviceAdmins.add(deviceAdminInfo); + } + } + } + } + if (!deviceAdmins.isEmpty()) { + mAdminsByProfile.put(profileId, deviceAdmins); + } } } + + /** + * Creates a device admin info object for the resolved intent that points to the component of + * the device admin. + * + * @param resolved resolved intent. + * @return new {@link DeviceAdminInfo} object or null if there was an error. + */ + private DeviceAdminInfo createDeviceAdminInfo(ResolveInfo resolved) { + try { + return new DeviceAdminInfo(getActivity(), resolved); + } catch (XmlPullParserException e) { + Log.w(TAG, "Skipping " + resolved.activityInfo, e); + } catch (IOException e) { + Log.w(TAG, "Skipping " + resolved.activityInfo, e); + } + return null; + } + + /** + * Extracts the user id from a device admin info object. + * @param adminInfo the device administrator info. + * @return identifier of the user associated with the device admin. + */ + private int getUserId(DeviceAdminInfo adminInfo) { + return UserHandle.getUserId(adminInfo.getActivityInfo().applicationInfo.uid); + } } diff --git a/src/com/android/settings/DeviceInfoSettings.java b/src/com/android/settings/DeviceInfoSettings.java index 7e94741..b6d8fef 100644 --- a/src/com/android/settings/DeviceInfoSettings.java +++ b/src/com/android/settings/DeviceInfoSettings.java @@ -19,34 +19,47 @@ package com.android.settings; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Parcel; +import android.os.RemoteException; import android.os.SELinux; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Index; +import com.android.settings.search.Indexable; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class DeviceInfoSettings extends RestrictedSettingsFragment { +public class DeviceInfoSettings extends SettingsPreferenceFragment implements Indexable { private static final String LOG_TAG = "DeviceInfoSettings"; - private static final String FILENAME_PROC_VERSION = "/proc/version"; private static final String FILENAME_MSV = "/sys/board_properties/soc/msv"; private static final String KEY_CONTAINER = "container"; - private static final String KEY_TEAM = "team"; - private static final String KEY_CONTRIBUTORS = "contributors"; private static final String KEY_REGULATORY_INFO = "regulatory_info"; private static final String KEY_TERMS = "terms"; private static final String KEY_LICENSE = "license"; @@ -63,6 +76,8 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { private static final String KEY_UPDATE_SETTING = "additional_system_update_settings"; private static final String KEY_EQUIPMENT_ID = "fcc_equipment_id"; private static final String PROPERTY_EQUIPMENT_ID = "ro.ril.fccid"; + private static final String KEY_DEVICE_FEEDBACK = "device_feedback"; + private static final String KEY_SAFETY_LEGAL = "safetylegal"; static final int TAPS_TO_BE_A_DEVELOPER = 7; @@ -70,20 +85,12 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { int mDevHitCountdown; Toast mDevHitToast; - public DeviceInfoSettings() { - super(null /* Don't PIN protect the entire screen */); - } - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.device_info_settings); - // We only call ensurePinRestrictedPreference() when mDevHitCountdown == 0. - // This will keep us from entering developer mode without a PIN. - protectByRestrictions(KEY_BUILD_NUMBER); - setStringSummary(KEY_FIRMWARE_VERSION, Build.VERSION.RELEASE); findPreference(KEY_FIRMWARE_VERSION).setEnabled(true); setValueSummary(KEY_BASEBAND_VERSION, "gsm.version.baseband"); @@ -107,7 +114,7 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { PROPERTY_SELINUX_STATUS); // Remove Safety information preference if PROPERTY_URL_SAFETYLEGAL is not set - removePreferenceIfPropertyMissing(getPreferenceScreen(), "safetylegal", + removePreferenceIfPropertyMissing(getPreferenceScreen(), KEY_SAFETY_LEGAL, PROPERTY_URL_SAFETYLEGAL); // Remove Equipment id preference if FCC ID is not set by RIL @@ -119,6 +126,11 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { getPreferenceScreen().removePreference(findPreference(KEY_BASEBAND_VERSION)); } + // Dont show feedback option if there is no reporter. + if (TextUtils.isEmpty(getFeedbackReporterPackage(getActivity()))) { + getPreferenceScreen().removePreference(findPreference(KEY_DEVICE_FEEDBACK)); + } + /* * Settings is a generic app and should not contain any device-specific * info. @@ -132,8 +144,6 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_COPYRIGHT, Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_TEAM, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); // These are contained by the root preference screen parentPreference = getPreferenceScreen(); @@ -145,16 +155,19 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { // Remove for secondary users removePreference(KEY_SYSTEM_UPDATE_SETTINGS); } - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_CONTRIBUTORS, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); // Read platform settings for additional system update setting removePreferenceIfBoolFalse(KEY_UPDATE_SETTING, R.bool.config_additional_system_update_setting_enable); - // Remove regulatory information if not enabled. - removePreferenceIfBoolFalse(KEY_REGULATORY_INFO, - R.bool.config_show_regulatory_info); + // Remove regulatory information if none present. + final Intent intent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO); + if (getPackageManager().queryIntentActivities(intent, 0).isEmpty()) { + Preference pref = findPreference(KEY_REGULATORY_INFO); + if (pref != null) { + getPreferenceScreen().removePreference(pref); + } + } } @Override @@ -185,12 +198,10 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { // Don't enable developer options for secondary users. if (UserHandle.myUserId() != UserHandle.USER_OWNER) return true; + final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); + if (um.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES)) return true; + if (mDevHitCountdown > 0) { - if (mDevHitCountdown == 1) { - if (super.ensurePinRestrictedPreference(preference)) { - return true; - } - } mDevHitCountdown--; if (mDevHitCountdown == 0) { getActivity().getSharedPreferences(DevelopmentSettings.PREF_FILE, @@ -202,6 +213,11 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { mDevHitToast = Toast.makeText(getActivity(), R.string.show_dev_on, Toast.LENGTH_LONG); mDevHitToast.show(); + // This is good time to index the Developer Options + Index.getInstance( + getActivity().getApplicationContext()).updateFromClassNameResource( + DevelopmentSettings.class.getName(), true, true); + } else if (mDevHitCountdown > 0 && mDevHitCountdown < (TAPS_TO_BE_A_DEVELOPER-2)) { if (mDevHitToast != null) { @@ -220,6 +236,8 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { Toast.LENGTH_LONG); mDevHitToast.show(); } + } else if (preference.getKey().equals(KEY_DEVICE_FEEDBACK)) { + sendFeedback(); } return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -265,6 +283,16 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { } } + private void sendFeedback() { + String reporterPackage = getFeedbackReporterPackage(getActivity()); + if (TextUtils.isEmpty(reporterPackage)) { + return; + } + Intent intent = new Intent(Intent.ACTION_BUG_REPORT); + intent.setPackage(reporterPackage); + startActivityForResult(intent, 0); + } + /** * Reads a line from the specified file. * @param filename the file to read from @@ -341,4 +369,118 @@ public class DeviceInfoSettings extends RestrictedSettingsFragment { } return ""; } + + private static String getFeedbackReporterPackage(Context context) { + final String feedbackReporter = + context.getResources().getString(R.string.oem_preferred_feedback_reporter); + if (TextUtils.isEmpty(feedbackReporter)) { + // Reporter not configured. Return. + return feedbackReporter; + } + // Additional checks to ensure the reporter is on system image, and reporter is + // configured to listen to the intent. Otherwise, dont show the "send feedback" option. + final Intent intent = new Intent(Intent.ACTION_BUG_REPORT); + + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resolvedPackages = + pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); + for (ResolveInfo info : resolvedPackages) { + if (info.activityInfo != null) { + if (!TextUtils.isEmpty(info.activityInfo.packageName)) { + try { + ApplicationInfo ai = pm.getApplicationInfo(info.activityInfo.packageName, 0); + if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + // Package is on the system image + if (TextUtils.equals( + info.activityInfo.packageName, feedbackReporter)) { + return feedbackReporter; + } + } + } catch (PackageManager.NameNotFoundException e) { + // No need to do anything here. + } + } + } + } + return null; + } + + /** + * For Search. + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.device_info_settings; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final List<String> keys = new ArrayList<String>(); + if (isPropertyMissing(PROPERTY_SELINUX_STATUS)) { + keys.add(KEY_SELINUX_STATUS); + } + if (isPropertyMissing(PROPERTY_URL_SAFETYLEGAL)) { + keys.add(KEY_SAFETY_LEGAL); + } + if (isPropertyMissing(PROPERTY_EQUIPMENT_ID)) { + keys.add(KEY_EQUIPMENT_ID); + } + // Remove Baseband version if wifi-only device + if (Utils.isWifiOnly(context)) { + keys.add((KEY_BASEBAND_VERSION)); + } + // Dont show feedback option if there is no reporter. + if (TextUtils.isEmpty(getFeedbackReporterPackage(context))) { + keys.add(KEY_DEVICE_FEEDBACK); + } + if (!checkIntentAction(context, "android.settings.TERMS")) { + keys.add(KEY_TERMS); + } + if (!checkIntentAction(context, "android.settings.LICENSE")) { + keys.add(KEY_LICENSE); + } + if (!checkIntentAction(context, "android.settings.COPYRIGHT")) { + keys.add(KEY_COPYRIGHT); + } + if (UserHandle.myUserId() != UserHandle.USER_OWNER) { + keys.add(KEY_SYSTEM_UPDATE_SETTINGS); + } + if (!context.getResources().getBoolean( + R.bool.config_additional_system_update_setting_enable)) { + keys.add(KEY_UPDATE_SETTING); + } + return keys; + } + + private boolean isPropertyMissing(String property) { + return SystemProperties.get(property).equals(""); + } + + private boolean checkIntentAction(Context context, String action) { + final Intent intent = new Intent(action); + + // Find the activity that is in the system image + final PackageManager pm = context.getPackageManager(); + final List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); + final int listSize = list.size(); + + for (int i = 0; i < listSize; i++) { + ResolveInfo resolveInfo = list.get(i); + if ((resolveInfo.activityInfo.applicationInfo.flags & + ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } + + return false; + } + }; + } + diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java index b0c944d..ddd6728 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -16,8 +16,20 @@ package com.android.settings; +import com.android.internal.view.RotationPolicy; +import com.android.settings.notification.DropDownPreference; +import com.android.settings.notification.DropDownPreference.Callback; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; + +import static android.provider.Settings.Secure.DOZE_ENABLED; +import static android.provider.Settings.Secure.WAKE_GESTURE_ENABLED; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; +import android.app.Activity; import android.app.ActivityManagerNative; import android.app.Dialog; import android.app.admin.DevicePolicyManager; @@ -25,71 +37,60 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Build; import android.os.Bundle; import android.os.RemoteException; -import android.preference.CheckBoxPreference; +import android.os.SystemProperties; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.SearchIndexableResource; import android.provider.Settings; -import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; import android.util.Log; -import com.android.internal.view.RotationPolicy; -import com.android.settings.DreamSettings; - import java.util.ArrayList; +import java.util.List; public class DisplaySettings extends SettingsPreferenceFragment implements - Preference.OnPreferenceChangeListener, OnPreferenceClickListener { + Preference.OnPreferenceChangeListener, OnPreferenceClickListener, Indexable { private static final String TAG = "DisplaySettings"; /** If there is no setting in the provider, use this. */ private static final int FALLBACK_SCREEN_TIMEOUT_VALUE = 30000; private static final String KEY_SCREEN_TIMEOUT = "screen_timeout"; - private static final String KEY_ACCELEROMETER = "accelerometer"; private static final String KEY_FONT_SIZE = "font_size"; - private static final String KEY_NOTIFICATION_PULSE = "notification_pulse"; private static final String KEY_SCREEN_SAVER = "screensaver"; + private static final String KEY_LIFT_TO_WAKE = "lift_to_wake"; + private static final String KEY_DOZE = "doze"; + private static final String KEY_AUTO_BRIGHTNESS = "auto_brightness"; + private static final String KEY_AUTO_ROTATE = "auto_rotate"; private static final int DLG_GLOBAL_CHANGE_WARNING = 1; - private CheckBoxPreference mAccelerometer; private WarnedListPreference mFontSizePref; - private CheckBoxPreference mNotificationPulse; private final Configuration mCurConfig = new Configuration(); private ListPreference mScreenTimeoutPreference; private Preference mScreenSaverPreference; - - private final RotationPolicy.RotationPolicyListener mRotationPolicyListener = - new RotationPolicy.RotationPolicyListener() { - @Override - public void onChange() { - updateAccelerometerRotationCheckbox(); - } - }; + private SwitchPreference mLiftToWakePreference; + private SwitchPreference mDozePreference; + private SwitchPreference mAutoBrightnessPreference; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ContentResolver resolver = getActivity().getContentResolver(); + final Activity activity = getActivity(); + final ContentResolver resolver = activity.getContentResolver(); addPreferencesFromResource(R.xml.display_settings); - mAccelerometer = (CheckBoxPreference) findPreference(KEY_ACCELEROMETER); - mAccelerometer.setPersistent(false); - if (!RotationPolicy.isRotationSupported(getActivity()) - || RotationPolicy.isRotationLockToggleSupported(getActivity())) { - // If rotation lock is supported, then we do not provide this option in - // Display settings. However, is still available in Accessibility settings, - // if the device supports rotation. - getPreferenceScreen().removePreference(mAccelerometer); - } - mScreenSaverPreference = findPreference(KEY_SCREEN_SAVER); if (mScreenSaverPreference != null && getResources().getBoolean( @@ -108,22 +109,87 @@ public class DisplaySettings extends SettingsPreferenceFragment implements mFontSizePref = (WarnedListPreference) findPreference(KEY_FONT_SIZE); mFontSizePref.setOnPreferenceChangeListener(this); mFontSizePref.setOnPreferenceClickListener(this); - mNotificationPulse = (CheckBoxPreference) findPreference(KEY_NOTIFICATION_PULSE); - if (mNotificationPulse != null - && getResources().getBoolean( - com.android.internal.R.bool.config_intrusiveNotificationLed) == false) { - getPreferenceScreen().removePreference(mNotificationPulse); + + if (isAutomaticBrightnessAvailable(getResources())) { + mAutoBrightnessPreference = (SwitchPreference) findPreference(KEY_AUTO_BRIGHTNESS); + mAutoBrightnessPreference.setOnPreferenceChangeListener(this); } else { - try { - mNotificationPulse.setChecked(Settings.System.getInt(resolver, - Settings.System.NOTIFICATION_LIGHT_PULSE) == 1); - mNotificationPulse.setOnPreferenceChangeListener(this); - } catch (SettingNotFoundException snfe) { - Log.e(TAG, Settings.System.NOTIFICATION_LIGHT_PULSE + " not found"); + removePreference(KEY_AUTO_BRIGHTNESS); + } + + if (isLiftToWakeAvailable(activity)) { + mLiftToWakePreference = (SwitchPreference) findPreference(KEY_LIFT_TO_WAKE); + mLiftToWakePreference.setOnPreferenceChangeListener(this); + } else { + removePreference(KEY_LIFT_TO_WAKE); + } + + if (isDozeAvailable(activity)) { + mDozePreference = (SwitchPreference) findPreference(KEY_DOZE); + mDozePreference.setOnPreferenceChangeListener(this); + } else { + removePreference(KEY_DOZE); + } + + if (RotationPolicy.isRotationLockToggleVisible(activity)) { + DropDownPreference rotatePreference = + (DropDownPreference) findPreference(KEY_AUTO_ROTATE); + rotatePreference.addItem(activity.getString(R.string.display_auto_rotate_rotate), + false); + int rotateLockedResourceId; + // The following block sets the string used when rotation is locked. + // If the device locks specifically to portrait or landscape (rather than current + // rotation), then we use a different string to include this information. + if (allowAllRotations(activity)) { + rotateLockedResourceId = R.string.display_auto_rotate_stay_in_current; + } else { + if (RotationPolicy.getRotationLockOrientation(activity) + == Configuration.ORIENTATION_PORTRAIT) { + rotateLockedResourceId = + R.string.display_auto_rotate_stay_in_portrait; + } else { + rotateLockedResourceId = + R.string.display_auto_rotate_stay_in_landscape; + } } + rotatePreference.addItem(activity.getString(rotateLockedResourceId), true); + rotatePreference.setSelectedItem(RotationPolicy.isRotationLocked(activity) ? + 1 : 0); + rotatePreference.setCallback(new Callback() { + @Override + public boolean onItemSelected(int pos, Object value) { + RotationPolicy.setRotationLock(activity, (Boolean) value); + return true; + } + }); + } else { + removePreference(KEY_AUTO_ROTATE); } } + private static boolean allowAllRotations(Context context) { + return Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_allowAllRotations); + } + + private static boolean isLiftToWakeAvailable(Context context) { + SensorManager sensors = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + return sensors != null && sensors.getDefaultSensor(Sensor.TYPE_WAKE_GESTURE) != null; + } + + private static boolean isDozeAvailable(Context context) { + String name = Build.IS_DEBUGGABLE ? SystemProperties.get("debug.doze.component") : null; + if (TextUtils.isEmpty(name)) { + name = context.getResources().getString( + com.android.internal.R.string.config_dozeComponent); + } + return !TextUtils.isEmpty(name); + } + + private static boolean isAutomaticBrightnessAvailable(Resources res) { + return res.getBoolean(com.android.internal.R.bool.config_automatic_brightness_available); + } + private void updateTimeoutPreferenceDescription(long currentTimeout) { ListPreference preference = mScreenTimeoutPreference; String summary; @@ -225,22 +291,10 @@ public class DisplaySettings extends SettingsPreferenceFragment implements @Override public void onResume() { super.onResume(); - - RotationPolicy.registerRotationPolicyListener(getActivity(), - mRotationPolicyListener); - updateState(); } @Override - public void onPause() { - super.onPause(); - - RotationPolicy.unregisterRotationPolicyListener(getActivity(), - mRotationPolicyListener); - } - - @Override public Dialog onCreateDialog(int dialogId) { if (dialogId == DLG_GLOBAL_CHANGE_WARNING) { return Utils.buildGlobalChangeWarningDialog(getActivity(), @@ -255,9 +309,27 @@ public class DisplaySettings extends SettingsPreferenceFragment implements } private void updateState() { - updateAccelerometerRotationCheckbox(); readFontSizePreference(mFontSizePref); updateScreenSaverSummary(); + + // Update auto brightness if it is available. + if (mAutoBrightnessPreference != null) { + int brightnessMode = Settings.System.getInt(getContentResolver(), + SCREEN_BRIGHTNESS_MODE, SCREEN_BRIGHTNESS_MODE_MANUAL); + mAutoBrightnessPreference.setChecked(brightnessMode != SCREEN_BRIGHTNESS_MODE_MANUAL); + } + + // Update lift-to-wake if it is available. + if (mLiftToWakePreference != null) { + int value = Settings.Secure.getInt(getContentResolver(), WAKE_GESTURE_ENABLED, 0); + mLiftToWakePreference.setChecked(value != 0); + } + + // Update doze if it is available. + if (mDozePreference != null) { + int value = Settings.Secure.getInt(getContentResolver(), DOZE_ENABLED, 1); + mDozePreference.setChecked(value != 0); + } } private void updateScreenSaverSummary() { @@ -267,12 +339,6 @@ public class DisplaySettings extends SettingsPreferenceFragment implements } } - private void updateAccelerometerRotationCheckbox() { - if (getActivity() == null) return; - - mAccelerometer.setChecked(!RotationPolicy.isRotationLocked(getActivity())); - } - public void writeFontSizePreference(Object objValue) { try { mCurConfig.fontScale = Float.parseFloat(objValue.toString()); @@ -284,15 +350,6 @@ public class DisplaySettings extends SettingsPreferenceFragment implements @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (preference == mAccelerometer) { - RotationPolicy.setRotationLockForAccessibility( - getActivity(), !mAccelerometer.isChecked()); - } else if (preference == mNotificationPulse) { - boolean value = mNotificationPulse.isChecked(); - Settings.System.putInt(getContentResolver(), Settings.System.NOTIFICATION_LIGHT_PULSE, - value ? 1 : 0); - return true; - } return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -311,7 +368,19 @@ public class DisplaySettings extends SettingsPreferenceFragment implements if (KEY_FONT_SIZE.equals(key)) { writeFontSizePreference(objValue); } - + if (preference == mAutoBrightnessPreference) { + boolean auto = (Boolean) objValue; + Settings.System.putInt(getContentResolver(), SCREEN_BRIGHTNESS_MODE, + auto ? SCREEN_BRIGHTNESS_MODE_AUTOMATIC : SCREEN_BRIGHTNESS_MODE_MANUAL); + } + if (preference == mLiftToWakePreference) { + boolean value = (Boolean) objValue; + Settings.Secure.putInt(getContentResolver(), WAKE_GESTURE_ENABLED, value ? 1 : 0); + } + if (preference == mDozePreference) { + boolean value = (Boolean) objValue; + Settings.Secure.putInt(getContentResolver(), DOZE_ENABLED, value ? 1 : 0); + } return true; } @@ -327,4 +396,42 @@ public class DisplaySettings extends SettingsPreferenceFragment implements } return false; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + ArrayList<SearchIndexableResource> result = + new ArrayList<SearchIndexableResource>(); + + SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.display_settings; + result.add(sir); + + return result; + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + ArrayList<String> result = new ArrayList<String>(); + if (!context.getResources().getBoolean( + com.android.internal.R.bool.config_dreamsSupported)) { + result.add(KEY_SCREEN_SAVER); + } + if (!isAutomaticBrightnessAvailable(context.getResources())) { + result.add(KEY_AUTO_BRIGHTNESS); + } + if (!isLiftToWakeAvailable(context)) { + result.add(KEY_LIFT_TO_WAKE); + } + if (!isDozeAvailable(context)) { + result.add(KEY_DOZE); + } + if (!RotationPolicy.isRotationLockToggleVisible(context)) { + result.add(KEY_AUTO_ROTATE); + } + return result; + } + }; } diff --git a/src/com/android/settings/DreamSettings.java b/src/com/android/settings/DreamSettings.java index cb91f39..38cba7a 100644 --- a/src/com/android/settings/DreamSettings.java +++ b/src/com/android/settings/DreamSettings.java @@ -16,7 +16,6 @@ package com.android.settings; -import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -26,9 +25,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.util.Log; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -40,8 +37,6 @@ import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageView; import android.widget.ListView; import android.widget.RadioButton; @@ -49,10 +44,12 @@ import android.widget.Switch; import android.widget.TextView; import com.android.settings.DreamBackend.DreamInfo; +import com.android.settings.widget.SwitchBar; import java.util.List; -public class DreamSettings extends SettingsPreferenceFragment { +public class DreamSettings extends SettingsPreferenceFragment implements + SwitchBar.OnSwitchChangeListener { private static final String TAG = DreamSettings.class.getSimpleName(); static final boolean DEBUG = false; private static final int DIALOG_WHEN_TO_DREAM = 1; @@ -63,7 +60,7 @@ public class DreamSettings extends SettingsPreferenceFragment { private Context mContext; private DreamBackend mBackend; private DreamInfoAdapter mAdapter; - private Switch mSwitch; + private SwitchBar mSwitchBar; private MenuItem[] mMenuItemsWhenEnabled; private boolean mRefreshing; @@ -83,37 +80,33 @@ public class DreamSettings extends SettingsPreferenceFragment { public void onCreate(Bundle icicle) { logd("onCreate(%s)", icicle); super.onCreate(icicle); - Activity activity = getActivity(); - mBackend = new DreamBackend(activity); - mSwitch = new Switch(activity); - mSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (!mRefreshing) { - mBackend.setEnabled(isChecked); - refreshFromBackend(); - } - } - }); - - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - mSwitch.setPaddingRelative(0, 0, padding, 0); - activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, - ActionBar.DISPLAY_SHOW_CUSTOM); - activity.getActionBar().setCustomView(mSwitch, new ActionBar.LayoutParams( - ActionBar.LayoutParams.WRAP_CONTENT, - ActionBar.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_VERTICAL | Gravity.END)); + mBackend = new DreamBackend(getActivity()); setHasOptionsMenu(true); } @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + if (!mRefreshing) { + mBackend.setEnabled(isChecked); + refreshFromBackend(); + } + } + + @Override + public void onStart() { + logd("onStart()"); + super.onStart(); + } + + @Override public void onDestroyView() { - getActivity().getActionBar().setCustomView(null); + logd("onDestroyView()"); super.onDestroyView(); + + mSwitchBar.removeOnSwitchChangeListener(this); + mSwitchBar.hide(); } @Override @@ -122,7 +115,6 @@ public class DreamSettings extends SettingsPreferenceFragment { super.onActivityCreated(savedInstanceState); ListView listView = getListView(); - listView.setItemsCanFocus(true); TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); @@ -131,6 +123,11 @@ public class DreamSettings extends SettingsPreferenceFragment { mAdapter = new DreamInfoAdapter(mContext); listView.setAdapter(mAdapter); + + final SettingsActivity sa = (SettingsActivity) getActivity(); + mSwitchBar = sa.getSwitchBar(); + mSwitchBar.addOnSwitchChangeListener(this); + mSwitchBar.show(); } @Override @@ -141,7 +138,7 @@ public class DreamSettings extends SettingsPreferenceFragment { // create "start" action MenuItem start = createMenuItem(menu, R.string.screensaver_settings_dream_start, - MenuItem.SHOW_AS_ACTION_ALWAYS, + MenuItem.SHOW_AS_ACTION_NEVER, isEnabled, new Runnable(){ @Override public void run() { @@ -151,7 +148,7 @@ public class DreamSettings extends SettingsPreferenceFragment { // create "when to dream" overflow menu item MenuItem whenToDream = createMenuItem(menu, R.string.screensaver_settings_when_to_dream, - MenuItem.SHOW_AS_ACTION_IF_ROOM, + MenuItem.SHOW_AS_ACTION_NEVER, isEnabled, new Runnable() { @Override @@ -216,6 +213,7 @@ public class DreamSettings extends SettingsPreferenceFragment { public void onPause() { logd("onPause()"); super.onPause(); + mContext.unregisterReceiver(mPackageReceiver); } @@ -262,8 +260,8 @@ public class DreamSettings extends SettingsPreferenceFragment { logd("refreshFromBackend()"); mRefreshing = true; boolean dreamsEnabled = mBackend.isEnabled(); - if (mSwitch.isChecked() != dreamsEnabled) - mSwitch.setChecked(dreamsEnabled); + if (mSwitchBar.isChecked() != dreamsEnabled) + mSwitchBar.setChecked(dreamsEnabled); mAdapter.clear(); if (dreamsEnabled) { diff --git a/src/com/android/settings/EncryptionInterstitial.java b/src/com/android/settings/EncryptionInterstitial.java new file mode 100644 index 0000000..e836aed --- /dev/null +++ b/src/com/android/settings/EncryptionInterstitial.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2014 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 com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; + +import java.util.List; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.widget.RadioButton; +import android.widget.TextView; + +public class EncryptionInterstitial extends SettingsActivity { + + private static final String EXTRA_PASSWORD_QUALITY = "extra_password_quality"; + public static final String EXTRA_REQUIRE_PASSWORD = "extra_require_password"; + + @Override + public Intent getIntent() { + Intent modIntent = new Intent(super.getIntent()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, EncryptionInterstitialFragment.class.getName()); + return modIntent; + } + + @Override + protected boolean isValidFragment(String fragmentName) { + return EncryptionInterstitialFragment.class.getName().equals(fragmentName); + } + + public static Intent createStartIntent(Context ctx, int quality, + boolean requirePasswordDefault) { + return new Intent(ctx, EncryptionInterstitial.class) + .putExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, true) + .putExtra(EXTRA_PREFS_SET_BACK_TEXT, (String) null) + .putExtra(EXTRA_PREFS_SET_NEXT_TEXT, ctx.getString( + R.string.encryption_continue_button)) + .putExtra(EXTRA_PASSWORD_QUALITY, quality) + .putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, R.string.encryption_interstitial_header) + .putExtra(EXTRA_REQUIRE_PASSWORD, requirePasswordDefault); + } + + public static class EncryptionInterstitialFragment extends SettingsPreferenceFragment + implements View.OnClickListener, OnClickListener { + + private static final int ACCESSIBILITY_WARNING_DIALOG = 1; + private RadioButton mRequirePasswordToDecryptButton; + private RadioButton mDontRequirePasswordToDecryptButton; + private TextView mEncryptionMessage; + private boolean mPasswordRequired; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final int layoutId = R.layout.encryption_interstitial; + View view = inflater.inflate(layoutId, container, false); + mRequirePasswordToDecryptButton = + (RadioButton) view.findViewById(R.id.encrypt_require_password); + mDontRequirePasswordToDecryptButton = + (RadioButton) view.findViewById(R.id.encrypt_dont_require_password); + mEncryptionMessage = + (TextView) view.findViewById(R.id.encryption_message); + int quality = getActivity().getIntent().getIntExtra(EXTRA_PASSWORD_QUALITY, 0); + final int msgId; + final int enableId; + final int disableId; + switch (quality) { + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + msgId = R.string.encryption_interstitial_message_pattern; + enableId = R.string.encrypt_require_pattern; + disableId = R.string.encrypt_dont_require_pattern; + break; + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: + msgId = R.string.encryption_interstitial_message_pin; + enableId = R.string.encrypt_require_pin; + disableId = R.string.encrypt_dont_require_pin; + break; + default: + msgId = R.string.encryption_interstitial_message_password; + enableId = R.string.encrypt_require_password; + disableId = R.string.encrypt_dont_require_password; + break; + } + mEncryptionMessage.setText(msgId); + + mRequirePasswordToDecryptButton.setOnClickListener(this); + mRequirePasswordToDecryptButton.setText(enableId); + + mDontRequirePasswordToDecryptButton.setOnClickListener(this); + mDontRequirePasswordToDecryptButton.setText(disableId); + + setRequirePasswordState(getActivity().getIntent().getBooleanExtra( + EXTRA_REQUIRE_PASSWORD, true)); + return view; + } + + @Override + public void onClick(View v) { + if (v == mRequirePasswordToDecryptButton) { + final boolean accEn = AccessibilityManager.getInstance(getActivity()).isEnabled(); + if (accEn && !mPasswordRequired) { + setRequirePasswordState(false); // clear the UI state + showDialog(ACCESSIBILITY_WARNING_DIALOG); + } else { + setRequirePasswordState(true); + } + } else { + setRequirePasswordState(false); + } + } + + @Override + public Dialog onCreateDialog(int dialogId) { + switch(dialogId) { + case ACCESSIBILITY_WARNING_DIALOG: { + final int quality = new LockPatternUtils(getActivity()) + .getKeyguardStoredPasswordQuality(); + final int titleId; + final int messageId; + switch (quality) { + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + titleId = R.string.encrypt_talkback_dialog_require_pattern; + messageId = R.string.encrypt_talkback_dialog_message_pattern; + break; + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: + titleId = R.string.encrypt_talkback_dialog_require_pin; + messageId = R.string.encrypt_talkback_dialog_message_pin; + break; + default: + titleId = R.string.encrypt_talkback_dialog_require_password; + messageId = R.string.encrypt_talkback_dialog_message_password; + break; + } + + + List<AccessibilityServiceInfo> list = + AccessibilityManager.getInstance(getActivity()) + .getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + final CharSequence exampleAccessibility; + if (list.isEmpty()) { + // This should never happen. But we shouldn't crash + exampleAccessibility = ""; + } else { + exampleAccessibility = list.get(0).getResolveInfo() + .loadLabel(getPackageManager()); + } + return new AlertDialog.Builder(getActivity()) + .setTitle(titleId) + .setMessage(getString(messageId, exampleAccessibility)) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + } + default: throw new IllegalArgumentException(); + } + } + + private void setRequirePasswordState(boolean required) { + mPasswordRequired = required; + mRequirePasswordToDecryptButton.setChecked(required); + mDontRequirePasswordToDecryptButton.setChecked(!required); + + // Updates value returned by SettingsActivity.onActivityResult(). + SettingsActivity sa = (SettingsActivity)getActivity(); + Intent resultIntentData = sa.getResultIntentData(); + resultIntentData = resultIntentData == null ? new Intent() : resultIntentData; + resultIntentData.putExtra(EXTRA_REQUIRE_PASSWORD, mPasswordRequired); + sa.setResultIntentData(resultIntentData); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + setRequirePasswordState(true); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + setRequirePasswordState(false); + } + } + } +} diff --git a/src/com/android/settings/EventLogTags.logtags b/src/com/android/settings/EventLogTags.logtags index 3e87c53..b21623c 100644 --- a/src/com/android/settings/EventLogTags.logtags +++ b/src/com/android/settings/EventLogTags.logtags @@ -4,3 +4,9 @@ option java_package com.android.settings # log the type of screen lock when user sets lock screen 90200 lock_screen_type (type|3) + +# log whether user accepted and activated device admin +90201 exp_det_device_admin_activated_by_user (app_signature|3) + +# log whether user declined activation of device admin +90202 exp_det_device_admin_declined_by_user (app_signature|3) diff --git a/src/com/android/settings/HighlightingFragment.java b/src/com/android/settings/HighlightingFragment.java new file mode 100644 index 0000000..4a233b4 --- /dev/null +++ b/src/com/android/settings/HighlightingFragment.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 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.Fragment; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; + +public class HighlightingFragment extends Fragment { + + private static final String TAG = "HighlightSettingsFragment"; + + private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 400; + private static final String SAVE_HIGHLIGHTED_KEY = "android:view_highlighted"; + + private String mViewKey; + private boolean mViewHighlighted = false; + private Drawable mHighlightDrawable; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (icicle != null) { + mViewHighlighted = icicle.getBoolean(SAVE_HIGHLIGHTED_KEY); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mViewHighlighted); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Bundle args = getArguments(); + if (args != null) { + mViewKey = args.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY); + highlightViewIfNeeded(); + } + } + + public void highlightViewIfNeeded() { + if (!mViewHighlighted &&!TextUtils.isEmpty(mViewKey)) { + highlightView(mViewKey); + } + } + + private Drawable getHighlightDrawable() { + if (mHighlightDrawable == null) { + mHighlightDrawable = getActivity().getDrawable(R.drawable.preference_highlight); + } + return mHighlightDrawable; + } + + private void highlightView(String key) { + final Drawable highlight = getHighlightDrawable(); + + // Try locating the View thru its Tag / Key + final View view = findViewForKey(getView(), key); + if (view != null ) { + view.setBackground(highlight); + + getView().postDelayed(new Runnable() { + @Override + public void run() { + final int centerX = view.getWidth() / 2; + final int centerY = view.getHeight() / 2; + highlight.setHotspot(centerX, centerY); + view.setPressed(true); + view.setPressed(false); + } + }, DELAY_HIGHLIGHT_DURATION_MILLIS); + + mViewHighlighted = true; + } + } + + private View findViewForKey(View root, String key) { + if (checkTag(root, key)) { + return root; + } + if (root instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) root; + final int count = group.getChildCount(); + for (int n = 0; n < count; n++) { + final View child = group.getChildAt(n); + final View view = findViewForKey(child, key); + if (view != null) { + return view; + } + } + } + return null; + } + + private boolean checkTag(View view, String key) { + final Object tag = view.getTag(R.id.preference_highlight_key); + if (tag == null || !(tag instanceof String)) { + return false; + } + final String viewKey = (String) tag; + return (!TextUtils.isEmpty(viewKey) && viewKey.equals(key)); + } +} diff --git a/src/com/android/settings/HomeSettings.java b/src/com/android/settings/HomeSettings.java index eb659e2..6da1848 100644 --- a/src/com/android/settings/HomeSettings.java +++ b/src/com/android/settings/HomeSettings.java @@ -17,8 +17,11 @@ package com.android.settings; import java.util.ArrayList; +import java.util.List; +import android.app.Activity; import android.app.ActivityManager; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -29,24 +32,37 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.pm.UserInfo; import android.graphics.ColorFilter; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; +import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.RadioButton; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Index; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; -public class HomeSettings extends SettingsPreferenceFragment { +public class HomeSettings extends SettingsPreferenceFragment implements Indexable { static final String TAG = "HomeSettings"; + // Boolean extra, indicates only launchers that support managed profiles should be shown. + // Note: must match the constant defined in ManagedProvisioning + private static final String EXTRA_SUPPORT_MANAGED_PROFILES = "support_managed_profiles"; + static final int REQUESTING_UNINSTALL = 10; public static final String HOME_PREFS = "home_prefs"; @@ -54,14 +70,23 @@ public class HomeSettings extends SettingsPreferenceFragment { public static final String HOME_SHOW_NOTICE = "show"; - PreferenceGroup mPrefGroup; + private class HomePackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + buildHomeActivitiesList(); + Index.getInstance(context).updateFromClassNameResource( + HomeSettings.class.getName(), true, true); + } + } - PackageManager mPm; - ComponentName[] mHomeComponentSet; - ArrayList<HomeAppPreference> mPrefs; - HomeAppPreference mCurrentHome = null; - final IntentFilter mHomeFilter; - boolean mShowNotice; + private PreferenceGroup mPrefGroup; + private PackageManager mPm; + private ComponentName[] mHomeComponentSet; + private ArrayList<HomeAppPreference> mPrefs; + private HomeAppPreference mCurrentHome = null; + private final IntentFilter mHomeFilter; + private boolean mShowNotice; + private HomePackageReceiver mHomePackageReceiver = new HomePackageReceiver(); public HomeSettings() { mHomeFilter = new IntentFilter(Intent.ACTION_MAIN); @@ -97,6 +122,8 @@ public class HomeSettings extends SettingsPreferenceFragment { mPm.replacePreferredActivity(mHomeFilter, IntentFilter.MATCH_CATEGORY_EMPTY, mHomeComponentSet, newHome.activityName); + + getActivity().setResult(Activity.RESULT_OK); } void uninstallApp(HomeAppPreference pref) { @@ -138,13 +165,13 @@ public class HomeSettings extends SettingsPreferenceFragment { if (mPrefs.size() < 2) { if (mShowNotice) { mShowNotice = false; - Settings.requestHomeNotice(); + SettingsActivity.requestHomeNotice(); } finishFragment(); } } - void buildHomeActivitiesList() { + private void buildHomeActivitiesList() { ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); @@ -154,6 +181,10 @@ public class HomeSettings extends SettingsPreferenceFragment { mPrefs = new ArrayList<HomeAppPreference>(); mHomeComponentSet = new ComponentName[homeActivities.size()]; int prefIndex = 0; + boolean supportManagedProfilesExtra = + getActivity().getIntent().getBooleanExtra(EXTRA_SUPPORT_MANAGED_PROFILES, false); + boolean mustSupportManagedProfile = hasManagedProfile() + || supportManagedProfilesExtra; for (int i = 0; i < homeActivities.size(); i++) { final ResolveInfo candidate = homeActivities.get(i); final ActivityInfo info = candidate.activityInfo; @@ -162,11 +193,19 @@ public class HomeSettings extends SettingsPreferenceFragment { try { Drawable icon = info.loadIcon(mPm); CharSequence name = info.loadLabel(mPm); - HomeAppPreference pref = new HomeAppPreference(context, activityName, prefIndex, - icon, name, this, info); + HomeAppPreference pref; + + if (mustSupportManagedProfile && !launcherHasManagedProfilesFeature(candidate)) { + pref = new HomeAppPreference(context, activityName, prefIndex, + icon, name, this, info, false /* not enabled */, + getResources().getString(R.string.home_work_profile_not_supported)); + } else { + pref = new HomeAppPreference(context, activityName, prefIndex, + icon, name, this, info, true /* enabled */, null); + } + mPrefs.add(pref); mPrefGroup.addPreference(pref); - pref.setEnabled(true); if (activityName.equals(currentDefaultHome)) { mCurrentHome = pref; } @@ -177,6 +216,10 @@ public class HomeSettings extends SettingsPreferenceFragment { } if (mCurrentHome != null) { + if (mCurrentHome.isEnabled()) { + getActivity().setResult(Activity.RESULT_OK); + } + new Handler().post(new Runnable() { public void run() { mCurrentHome.setChecked(true); @@ -185,6 +228,30 @@ public class HomeSettings extends SettingsPreferenceFragment { } } + private boolean hasManagedProfile() { + Context context = getActivity(); + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + List<UserInfo> profiles = userManager.getProfiles(context.getUserId()); + for (UserInfo userInfo : profiles) { + if (userInfo.isManagedProfile()) return true; + } + return false; + } + + private boolean launcherHasManagedProfilesFeature(ResolveInfo resolveInfo) { + try { + ApplicationInfo appInfo = getPackageManager().getApplicationInfo( + resolveInfo.activityInfo.packageName, 0 /* default flags */); + return versionNumberAtLeastL(appInfo.targetSdkVersion); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private boolean versionNumberAtLeastL(int versionNumber) { + return versionNumber >= Build.VERSION_CODES.LOLLIPOP; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -200,10 +267,24 @@ public class HomeSettings extends SettingsPreferenceFragment { @Override public void onResume() { super.onResume(); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addDataScheme("package"); + getActivity().registerReceiver(mHomePackageReceiver, filter); + buildHomeActivitiesList(); } - class HomeAppPreference extends Preference { + @Override + public void onPause() { + super.onPause(); + getActivity().unregisterReceiver(mHomePackageReceiver); + } + + private class HomeAppPreference extends Preference { ComponentName activityName; int index; HomeSettings fragment; @@ -214,12 +295,14 @@ public class HomeSettings extends SettingsPreferenceFragment { String uninstallTarget; public HomeAppPreference(Context context, ComponentName activity, - int i, Drawable icon, CharSequence title, - HomeSettings parent, ActivityInfo info) { + int i, Drawable icon, CharSequence title, HomeSettings parent, ActivityInfo info, + boolean enabled, CharSequence summary) { super(context); setLayoutResource(R.layout.preference_home_app); setIcon(icon); setTitle(title); + setEnabled(enabled); + setSummary(summary); activityName = activity; fragment = parent; index = i; @@ -274,13 +357,15 @@ public class HomeSettings extends SettingsPreferenceFragment { icon.setEnabled(false); icon.setColorFilter(grayscaleFilter); } else { + icon.setEnabled(true); icon.setOnClickListener(mDeleteClickListener); icon.setTag(indexObj); } View v = view.findViewById(R.id.home_app_pref); - v.setOnClickListener(mHomeClickListener); v.setTag(indexObj); + + v.setOnClickListener(mHomeClickListener); } void setChecked(boolean state) { @@ -290,4 +375,59 @@ public class HomeSettings extends SettingsPreferenceFragment { } } } + + /** + * For search + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + final PackageManager pm = context.getPackageManager(); + final ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); + pm.getHomeActivities(homeActivities); + + final SharedPreferences sp = context.getSharedPreferences( + HomeSettings.HOME_PREFS, Context.MODE_PRIVATE); + final boolean doShowHome = sp.getBoolean(HomeSettings.HOME_PREFS_DO_SHOW, false); + + // We index Home Launchers only if there are more than one or if we are showing the + // Home tile into the Dashboard + if (homeActivities.size() > 1 || doShowHome) { + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.home_settings); + data.screenTitle = res.getString(R.string.home_settings); + data.keywords = res.getString(R.string.keywords_home); + result.add(data); + + for (int i = 0; i < homeActivities.size(); i++) { + final ResolveInfo resolveInfo = homeActivities.get(i); + final ActivityInfo activityInfo = resolveInfo.activityInfo; + + CharSequence name; + try { + name = activityInfo.loadLabel(pm); + if (TextUtils.isEmpty(name)) { + continue; + } + } catch (Exception e) { + Log.v(TAG, "Problem dealing with Home " + activityInfo.name, e); + continue; + } + + data = new SearchIndexableRaw(context); + data.title = name.toString(); + data.screenTitle = res.getString(R.string.home_settings); + result.add(data); + } + } + + return result; + } + }; } diff --git a/src/com/android/settings/KeyguardAppWidgetPickActivity.java b/src/com/android/settings/KeyguardAppWidgetPickActivity.java deleted file mode 100644 index 7e801be..0000000 --- a/src/com/android/settings/KeyguardAppWidgetPickActivity.java +++ /dev/null @@ -1,634 +0,0 @@ -/* - * Copyright (C) 2012 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.ActivityManager; -import android.app.LauncherActivity.IconResizer; -import android.appwidget.AppWidgetHost; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.IWindowManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.internal.widget.LockPatternUtils; - -import java.lang.ref.WeakReference; -import java.util.List; - -/** - * Displays a list of {@link AppWidgetProviderInfo} widgets, along with any - * injected special widgets specified through - * {@link AppWidgetManager#EXTRA_CUSTOM_INFO} and - * {@link AppWidgetManager#EXTRA_CUSTOM_EXTRAS}. - * <p> - * When an installed {@link AppWidgetProviderInfo} is selected, this activity - * will bind it to the given {@link AppWidgetManager#EXTRA_APPWIDGET_ID}, - * otherwise it will return the requested extras. - */ -public class KeyguardAppWidgetPickActivity extends Activity - implements GridView.OnItemClickListener, - AppWidgetLoader.ItemConstructor<KeyguardAppWidgetPickActivity.Item> { - private static final String TAG = "KeyguardAppWidgetPickActivity"; - private static final int REQUEST_PICK_APPWIDGET = 126; - private static final int REQUEST_CREATE_APPWIDGET = 127; - - private AppWidgetLoader<Item> mAppWidgetLoader; - private List<Item> mItems; - private GridView mGridView; - private AppWidgetAdapter mAppWidgetAdapter; - private AppWidgetManager mAppWidgetManager; - private int mAppWidgetId; - // Might make it possible to make this be false in future - private boolean mAddingToKeyguard = true; - private Intent mResultData; - private LockPatternUtils mLockPatternUtils; - private Bundle mExtraConfigureOptions; - - @Override - protected void onCreate(Bundle savedInstanceState) { - getWindow().addPrivateFlags( - WindowManager.LayoutParams.PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR); - setContentView(R.layout.keyguard_appwidget_picker_layout); - super.onCreate(savedInstanceState); - - // Set default return data - setResultData(RESULT_CANCELED, null); - - // Read the appWidgetId passed our direction, otherwise bail if not found - final Intent intent = getIntent(); - if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { - mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - } else { - finish(); - } - mExtraConfigureOptions = intent.getBundleExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS); - - mGridView = (GridView) findViewById(R.id.widget_list); - DisplayMetrics dm = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(dm); - int maxGridWidth = getResources().getDimensionPixelSize( - R.dimen.keyguard_appwidget_picker_max_width); - - if (maxGridWidth < dm.widthPixels) { - mGridView.getLayoutParams().width = maxGridWidth; - } - mAppWidgetManager = AppWidgetManager.getInstance(this); - mAppWidgetLoader = new AppWidgetLoader<Item>(this, mAppWidgetManager, this); - mItems = mAppWidgetLoader.getItems(getIntent()); - mAppWidgetAdapter = new AppWidgetAdapter(this, mItems); - mGridView.setAdapter(mAppWidgetAdapter); - mGridView.setOnItemClickListener(this); - - mLockPatternUtils = new LockPatternUtils(this); // TEMP-- we want to delete this - } - - /** - * Convenience method for setting the result code and intent. This method - * correctly injects the {@link AppWidgetManager#EXTRA_APPWIDGET_ID} that - * most hosts expect returned. - */ - void setResultData(int code, Intent intent) { - Intent result = intent != null ? intent : new Intent(); - result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - mResultData = result; - setResult(code, result); - } - - /** - * Item that appears in the AppWidget picker grid. - */ - public static class Item implements AppWidgetLoader.LabelledItem { - protected static IconResizer sResizer; - - - CharSequence label; - int appWidgetPreviewId; - int iconId; - String packageName; - String className; - Bundle extras; - private WidgetPreviewLoader mWidgetPreviewLoader; - private Context mContext; - - /** - * Create a list item from given label and icon. - */ - Item(Context context, CharSequence label) { - this.label = label; - mContext = context; - } - - void loadWidgetPreview(ImageView v) { - mWidgetPreviewLoader = new WidgetPreviewLoader(mContext, v); - mWidgetPreviewLoader.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); - } - - void cancelLoadingWidgetPreview() { - if (mWidgetPreviewLoader != null) { - mWidgetPreviewLoader.cancel(false); - mWidgetPreviewLoader = null; - } - } - - /** - * Build the {@link Intent} described by this item. If this item - * can't create a valid {@link android.content.ComponentName}, it will return - * {@link Intent#ACTION_CREATE_SHORTCUT} filled with the item label. - */ - Intent getIntent() { - Intent intent = new Intent(); - if (packageName != null && className != null) { - // Valid package and class, so fill details as normal intent - intent.setClassName(packageName, className); - if (extras != null) { - intent.putExtras(extras); - } - } else { - // No valid package or class, so treat as shortcut with label - intent.setAction(Intent.ACTION_CREATE_SHORTCUT); - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label); - } - return intent; - } - - public CharSequence getLabel() { - return label; - } - - class WidgetPreviewLoader extends AsyncTask<Void, Bitmap, Void> { - private Resources mResources; - private PackageManager mPackageManager; - private int mIconDpi; - private ImageView mView; - public WidgetPreviewLoader(Context context, ImageView v) { - super(); - mResources = context.getResources(); - mPackageManager = context.getPackageManager(); - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mIconDpi = activityManager.getLauncherLargeIconDensity(); - mView = v; - } - public Void doInBackground(Void... params) { - if (!isCancelled()) { - int appWidgetPreviewWidth = - mResources.getDimensionPixelSize(R.dimen.appwidget_preview_width); - int appWidgetPreviewHeight = - mResources.getDimensionPixelSize(R.dimen.appwidget_preview_height); - Bitmap b = getWidgetPreview(new ComponentName(packageName, className), - appWidgetPreviewId, iconId, - appWidgetPreviewWidth, appWidgetPreviewHeight); - publishProgress(b); - } - return null; - } - public void onProgressUpdate(Bitmap... values) { - if (!isCancelled()) { - Bitmap b = values[0]; - mView.setImageBitmap(b); - } - } - abstract class WeakReferenceThreadLocal<T> { - private ThreadLocal<WeakReference<T>> mThreadLocal; - public WeakReferenceThreadLocal() { - mThreadLocal = new ThreadLocal<WeakReference<T>>(); - } - - abstract T initialValue(); - - public void set(T t) { - mThreadLocal.set(new WeakReference<T>(t)); - } - - public T get() { - WeakReference<T> reference = mThreadLocal.get(); - T obj; - if (reference == null) { - obj = initialValue(); - mThreadLocal.set(new WeakReference<T>(obj)); - return obj; - } else { - obj = reference.get(); - if (obj == null) { - obj = initialValue(); - mThreadLocal.set(new WeakReference<T>(obj)); - } - return obj; - } - } - } - - class CanvasCache extends WeakReferenceThreadLocal<Canvas> { - @Override - protected Canvas initialValue() { - return new Canvas(); - } - } - - class PaintCache extends WeakReferenceThreadLocal<Paint> { - @Override - protected Paint initialValue() { - return null; - } - } - - class BitmapCache extends WeakReferenceThreadLocal<Bitmap> { - @Override - protected Bitmap initialValue() { - return null; - } - } - - class RectCache extends WeakReferenceThreadLocal<Rect> { - @Override - protected Rect initialValue() { - return new Rect(); - } - } - - // Used for drawing widget previews - CanvasCache sCachedAppWidgetPreviewCanvas = new CanvasCache(); - RectCache sCachedAppWidgetPreviewSrcRect = new RectCache(); - RectCache sCachedAppWidgetPreviewDestRect = new RectCache(); - PaintCache sCachedAppWidgetPreviewPaint = new PaintCache(); - - private Bitmap getWidgetPreview(ComponentName provider, int previewImage, - int iconId, int maxWidth, int maxHeight) { - // Load the preview image if possible - String packageName = provider.getPackageName(); - if (maxWidth < 0) maxWidth = Integer.MAX_VALUE; - if (maxHeight < 0) maxHeight = Integer.MAX_VALUE; - - - int appIconSize = mResources.getDimensionPixelSize(R.dimen.app_icon_size); - - Drawable drawable = null; - if (previewImage != 0) { - drawable = mPackageManager.getDrawable(packageName, previewImage, null); - if (drawable == null) { - Log.w(TAG, "Can't load widget preview drawable 0x" + - Integer.toHexString(previewImage) + " for provider: " + provider); - } - } - - int bitmapWidth; - int bitmapHeight; - Bitmap defaultPreview = null; - boolean widgetPreviewExists = (drawable != null); - if (widgetPreviewExists) { - bitmapWidth = drawable.getIntrinsicWidth(); - bitmapHeight = drawable.getIntrinsicHeight(); - } else { - // Generate a preview image if we couldn't load one - bitmapWidth = appIconSize; - bitmapHeight = appIconSize; - defaultPreview = Bitmap.createBitmap(bitmapWidth, bitmapHeight, - Config.ARGB_8888); - - try { - Drawable icon = null; - if (iconId > 0) - icon = getFullResIcon(packageName, iconId); - if (icon != null) { - renderDrawableToBitmap(icon, defaultPreview, 0, - 0, appIconSize, appIconSize); - } - } catch (Resources.NotFoundException e) { - } - } - - // Scale to fit width only - let the widget preview be clipped in the - // vertical dimension - float scale = 1f; - if (bitmapWidth > maxWidth) { - scale = maxWidth / (float) bitmapWidth; - } - int finalPreviewWidth = (int) (scale * bitmapWidth); - int finalPreviewHeight = (int) (scale * bitmapHeight); - - bitmapWidth = finalPreviewWidth; - bitmapHeight = Math.min(finalPreviewHeight, maxHeight); - - Bitmap preview = Bitmap.createBitmap(bitmapWidth, bitmapHeight, - Config.ARGB_8888); - - // Draw the scaled preview into the final bitmap - if (widgetPreviewExists) { - renderDrawableToBitmap(drawable, preview, 0, 0, finalPreviewWidth, - finalPreviewHeight); - } else { - final Canvas c = sCachedAppWidgetPreviewCanvas.get(); - final Rect src = sCachedAppWidgetPreviewSrcRect.get(); - final Rect dest = sCachedAppWidgetPreviewDestRect.get(); - c.setBitmap(preview); - src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); - dest.set(0, 0, finalPreviewWidth, finalPreviewHeight); - - Paint p = sCachedAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setFilterBitmap(true); - sCachedAppWidgetPreviewPaint.set(p); - } - c.drawBitmap(defaultPreview, src, dest, p); - c.setBitmap(null); - } - return preview; - } - public Drawable getFullResDefaultActivityIcon() { - return getFullResIcon(Resources.getSystem(), - android.R.mipmap.sym_def_app_icon); - } - - public Drawable getFullResIcon(Resources resources, int iconId) { - Drawable d; - try { - d = resources.getDrawableForDensity(iconId, mIconDpi); - } catch (Resources.NotFoundException e) { - d = null; - } - - return (d != null) ? d : getFullResDefaultActivityIcon(); - } - - public Drawable getFullResIcon(String packageName, int iconId) { - Resources resources; - try { - resources = mPackageManager.getResourcesForApplication(packageName); - } catch (PackageManager.NameNotFoundException e) { - resources = null; - } - if (resources != null) { - if (iconId != 0) { - return getFullResIcon(resources, iconId); - } - } - return getFullResDefaultActivityIcon(); - } - - private void renderDrawableToBitmap(Drawable d, Bitmap bitmap, int x, int y, int w, int h) { - renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f); - } - - private void renderDrawableToBitmap(Drawable d, Bitmap bitmap, int x, int y, int w, int h, - float scale) { - if (bitmap != null) { - Canvas c = new Canvas(bitmap); - c.scale(scale, scale); - Rect oldBounds = d.copyBounds(); - d.setBounds(x, y, x + w, y + h); - d.draw(c); - d.setBounds(oldBounds); // Restore the bounds - c.setBitmap(null); - } - } - } - } - - @Override - public Item createItem(Context context, AppWidgetProviderInfo info, Bundle extras) { - CharSequence label = info.label; - - Item item = new Item(context, label); - item.appWidgetPreviewId = info.previewImage; - item.iconId = info.icon; - item.packageName = info.provider.getPackageName(); - item.className = info.provider.getClassName(); - item.extras = extras; - return item; - } - - protected static class AppWidgetAdapter extends BaseAdapter { - private final LayoutInflater mInflater; - private final List<Item> mItems; - - /** - * Create an adapter for the given items. - */ - public AppWidgetAdapter(Context context, List<Item> items) { - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mItems = items; - } - - /** - * {@inheritDoc} - */ - public int getCount() { - return mItems.size(); - } - - /** - * {@inheritDoc} - */ - public Object getItem(int position) { - return mItems.get(position); - } - - /** - * {@inheritDoc} - */ - public long getItemId(int position) { - return position; - } - - /** - * {@inheritDoc} - */ - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = mInflater.inflate(R.layout.keyguard_appwidget_item, parent, false); - } - - Item item = (Item) getItem(position); - TextView textView = (TextView) convertView.findViewById(R.id.label); - textView.setText(item.label); - ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); - iconView.setImageDrawable(null); - item.loadWidgetPreview(iconView); - return convertView; - } - - public void cancelAllWidgetPreviewLoaders() { - for (int i = 0; i < mItems.size(); i++) { - mItems.get(i).cancelLoadingWidgetPreview(); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - Item item = mItems.get(position); - Intent intent = item.getIntent(); - - int result; - if (item.extras != null) { - // If these extras are present it's because this entry is custom. - // Don't try to bind it, just pass it back to the app. - result = RESULT_OK; - setResultData(result, intent); - } else { - try { - if (mAddingToKeyguard && mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { - // Found in KeyguardHostView.java - final int KEYGUARD_HOST_ID = 0x4B455947; - int userId = ActivityManager.getCurrentUser(); - mAppWidgetId = AppWidgetHost.allocateAppWidgetIdForPackage(KEYGUARD_HOST_ID, - userId, "com.android.keyguard"); - } - mAppWidgetManager.bindAppWidgetId( - mAppWidgetId, intent.getComponent(), mExtraConfigureOptions); - result = RESULT_OK; - } catch (IllegalArgumentException e) { - // This is thrown if they're already bound, or otherwise somehow - // bogus. Set the result to canceled, and exit. The app *should* - // clean up at this point. We could pass the error along, but - // it's not clear that that's useful -- the widget will simply not - // appear. - result = RESULT_CANCELED; - } - setResultData(result, null); - } - if (mAddingToKeyguard) { - onActivityResult(REQUEST_PICK_APPWIDGET, result, mResultData); - } else { - finish(); - } - } - - protected void onDestroy() { - if (mAppWidgetAdapter != null) { - mAppWidgetAdapter.cancelAllWidgetPreviewLoaders(); - } - super.onDestroy(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_PICK_APPWIDGET || requestCode == REQUEST_CREATE_APPWIDGET) { - int appWidgetId; - if (data == null) { - appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID ; - } else { - appWidgetId = data.getIntExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - } - if (requestCode == REQUEST_PICK_APPWIDGET && resultCode == Activity.RESULT_OK) { - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); - - AppWidgetProviderInfo appWidget = null; - appWidget = appWidgetManager.getAppWidgetInfo(appWidgetId); - - if (appWidget.configure != null) { - // Launch over to configure widget, if needed - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); - intent.setComponent(appWidget.configure); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - - startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); - } else { - // Otherwise just add it - onActivityResult(REQUEST_CREATE_APPWIDGET, Activity.RESULT_OK, data); - } - } else if (requestCode == REQUEST_CREATE_APPWIDGET && resultCode == Activity.RESULT_OK) { - mLockPatternUtils.addAppWidget(appWidgetId, 0); - finishDelayedAndShowLockScreen(appWidgetId); - } else { - if (mAddingToKeyguard && - mAppWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { - int userId = ActivityManager.getCurrentUser(); - AppWidgetHost.deleteAppWidgetIdForSystem(mAppWidgetId, userId); - } - finishDelayedAndShowLockScreen(AppWidgetManager.INVALID_APPWIDGET_ID); - } - } - } - - private void finishDelayedAndShowLockScreen(int appWidgetId) { - IBinder b = ServiceManager.getService(Context.WINDOW_SERVICE); - IWindowManager iWm = IWindowManager.Stub.asInterface(b); - Bundle opts = null; - if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { - opts = new Bundle(); - opts.putInt(LockPatternUtils.KEYGUARD_SHOW_APPWIDGET, appWidgetId); - } - try { - iWm.lockNow(opts); - } catch (RemoteException e) { - } - - // Change background to all black - ViewGroup root = (ViewGroup) findViewById(R.id.layout_root); - root.setBackgroundColor(0xFF000000); - // Hide all children - final int childCount = root.getChildCount(); - for (int i = 0; i < childCount; i++) { - root.getChildAt(i).setVisibility(View.INVISIBLE); - } - mGridView.postDelayed(new Runnable() { - public void run() { - finish(); - } - }, 500); - } - - void startActivityForResultSafely(Intent intent, int requestCode) { - try { - startActivityForResult(intent, requestCode); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - } catch (SecurityException e) { - Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Settings does not have the permission to launch " + intent, e); - } - } -} diff --git a/src/com/android/settings/LocalePicker.java b/src/com/android/settings/LocalePicker.java index 6600703..c6158b1 100644 --- a/src/com/android/settings/LocalePicker.java +++ b/src/com/android/settings/LocalePicker.java @@ -48,15 +48,6 @@ public class LocalePicker extends com.android.internal.app.LocalePicker } @Override - protected boolean isInDeveloperMode() { - final boolean showDev = getActivity().getSharedPreferences(DevelopmentSettings.PREF_FILE, - Context.MODE_PRIVATE).getBoolean( - DevelopmentSettings.PREF_SHOW, - android.os.Build.TYPE.equals("eng")); - return showDev; - } - - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null && savedInstanceState.containsKey(SAVE_TARGET_LOCALE)) { diff --git a/src/com/android/settings/ManagedProfileSetup.java b/src/com/android/settings/ManagedProfileSetup.java new file mode 100644 index 0000000..198abe0 --- /dev/null +++ b/src/com/android/settings/ManagedProfileSetup.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 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.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.util.Log; +import android.os.UserHandle; +import android.os.UserManager; + +import java.util.List; + +import static android.content.pm.PackageManager.GET_ACTIVITIES; +import static android.content.pm.PackageManager.GET_META_DATA; +import static android.content.pm.PackageManager.GET_RESOLVED_FILTER; + +/** + * Listens to {@link Intent.ACTION_BOOT_COMPLETED} and {@link Intent.ACTION_PRE_BOOT_COMPLETED} + * performs setup steps for a managed profile (disables the launcher icon of the Settings app and + * adds cross-profile intent filters for the appropriate Settings activities). + */ +public class ManagedProfileSetup extends BroadcastReceiver { + private static final String TAG = "Settings"; + private static final String PRIMARY_PROFILE_SETTING = + "com.android.settings.PRIMARY_PROFILE_CONTROLLED"; + + @Override + public void onReceive(Context context, Intent broadcast) { + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + if (!Utils.isManagedProfile(um)) { + return; + } + Log.i(TAG, "Received broadcast: " + broadcast.getAction() + + ". Setting up intent forwarding for managed profile."); + final PackageManager pm = context.getPackageManager(); + // Clear any previous intent forwarding we set up + pm.clearCrossProfileIntentFilters(UserHandle.myUserId()); + + // Set up intent forwarding for implicit intents + Intent intent = new Intent(); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setPackage(context.getPackageName()); + + // Resolves activities for the managed profile (which we're running as) + List<ResolveInfo> resolvedIntents = pm.queryIntentActivities(intent, + GET_ACTIVITIES | GET_META_DATA | GET_RESOLVED_FILTER); + final int count = resolvedIntents.size(); + for (int i = 0; i < count; i++) { + ResolveInfo info = resolvedIntents.get(i); + if (info.filter != null && info.activityInfo != null + && info.activityInfo.metaData != null) { + boolean shouldForward = info.activityInfo.metaData.getBoolean( + PRIMARY_PROFILE_SETTING); + if (shouldForward) { + pm.addCrossProfileIntentFilter(info.filter, UserHandle.myUserId(), + UserHandle.USER_OWNER, PackageManager.SKIP_CURRENT_PROFILE); + } + } + } + + // Disable launcher icon + // Note: This needs to happen after forwarding intents, otherwise the main Settings + // intent gets lost + ComponentName settingsComponentName = new ComponentName(context, Settings.class); + pm.setComponentEnabledSetting(settingsComponentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); + } +} diff --git a/src/com/android/settings/MasterClear.java b/src/com/android/settings/MasterClear.java index 262aca3..f789b93 100644 --- a/src/com/android/settings/MasterClear.java +++ b/src/com/android/settings/MasterClear.java @@ -28,10 +28,10 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Environment; +import android.os.Process; import android.os.SystemProperties; import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -55,7 +55,6 @@ public class MasterClear extends Fragment { private static final String TAG = "MasterClear"; private static final int KEYGUARD_REQUEST = 55; - private static final int PIN_REQUEST = 56; static final String ERASE_EXTERNAL_EXTRA = "erase_sd"; @@ -63,7 +62,6 @@ public class MasterClear extends Fragment { private Button mInitiateButton; private View mExternalStorageContainer; private CheckBox mExternalStorage; - private boolean mPinConfirmed; /** * Keyguard validation is run using the standard {@link ConfirmLockPattern} @@ -79,25 +77,11 @@ public class MasterClear extends Fragment { res.getText(R.string.master_clear_gesture_explanation)); } - private boolean runRestrictionsChallenge() { - if (UserManager.get(getActivity()).hasRestrictionsChallenge()) { - startActivityForResult( - new Intent(Intent.ACTION_RESTRICTIONS_CHALLENGE), PIN_REQUEST); - return true; - } - return false; - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == PIN_REQUEST) { - if (resultCode == Activity.RESULT_OK) { - mPinConfirmed = true; - } - return; - } else if (requestCode != KEYGUARD_REQUEST) { + if (requestCode != KEYGUARD_REQUEST) { return; } @@ -115,7 +99,7 @@ public class MasterClear extends Fragment { preference.setFragment(MasterClearConfirm.class.getName()); preference.setTitle(R.string.master_clear_confirm_title); preference.getExtras().putBoolean(ERASE_EXTERNAL_EXTRA, mExternalStorage.isChecked()); - ((PreferenceActivity) getActivity()).onPreferenceStartFragment(null, preference); + ((SettingsActivity) getActivity()).onPreferenceStartFragment(null, preference); } /** @@ -126,10 +110,6 @@ public class MasterClear extends Fragment { private final Button.OnClickListener mInitiateListener = new Button.OnClickListener() { public void onClick(View v) { - mPinConfirmed = false; - if (runRestrictionsChallenge()) { - return; - } if (!runKeyguardConfirmation(KEYGUARD_REQUEST)) { showFinalConfirmation(); } @@ -255,22 +235,15 @@ public class MasterClear extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (!Process.myUserHandle().isOwner() + || UserManager.get(getActivity()).hasUserRestriction( + UserManager.DISALLOW_FACTORY_RESET)) { + return inflater.inflate(R.layout.master_clear_disallowed_screen, null); + } + mContentView = inflater.inflate(R.layout.master_clear, null); establishInitialState(); return mContentView; } - - @Override - public void onResume() { - super.onResume(); - - // If this is the second step after restrictions pin challenge - if (mPinConfirmed) { - mPinConfirmed = false; - if (!runKeyguardConfirmation(KEYGUARD_REQUEST)) { - showFinalConfirmation(); - } - } - } } diff --git a/src/com/android/settings/MasterClearConfirm.java b/src/com/android/settings/MasterClearConfirm.java index 9c15c73..3521aa3 100644 --- a/src/com/android/settings/MasterClearConfirm.java +++ b/src/com/android/settings/MasterClearConfirm.java @@ -16,19 +16,21 @@ package com.android.settings; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.os.AsyncTask; +import android.service.persistentdata.PersistentDataBlockManager; import com.android.internal.os.storage.ExternalStorageFormatter; -import com.android.internal.widget.LockPatternUtils; -import android.app.Activity; import android.app.Fragment; import android.content.Intent; -import android.content.res.Resources; import android.os.Bundle; +import android.os.UserManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.CheckBox; /** * Confirm and execute a reset of the device to a clean "just out of the box" @@ -44,7 +46,6 @@ public class MasterClearConfirm extends Fragment { private View mContentView; private boolean mEraseSdCard; - private Button mFinalButton; /** * The user has gone through the multiple confirmation, so now we go ahead @@ -58,28 +59,79 @@ public class MasterClearConfirm extends Fragment { return; } - if (mEraseSdCard) { - Intent intent = new Intent(ExternalStorageFormatter.FORMAT_AND_FACTORY_RESET); - intent.setComponent(ExternalStorageFormatter.COMPONENT_NAME); - getActivity().startService(intent); + final PersistentDataBlockManager pdbManager = (PersistentDataBlockManager) + getActivity().getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE); + + if (pdbManager != null && !pdbManager.getOemUnlockEnabled()) { + // if OEM unlock is enabled, this will be wiped during FR process. + final ProgressDialog progressDialog = getProgressDialog(); + progressDialog.show(); + + // need to prevent orientation changes as we're about to go into + // a long IO request, so we won't be able to access inflate resources on flash + final int oldOrientation = getActivity().getRequestedOrientation(); + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + pdbManager.wipe(); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + progressDialog.hide(); + getActivity().setRequestedOrientation(oldOrientation); + doMasterClear(); + } + }.execute(); } else { - getActivity().sendBroadcast(new Intent("android.intent.action.MASTER_CLEAR")); - // Intent handling is asynchronous -- assume it will happen soon. + doMasterClear(); } } + + private ProgressDialog getProgressDialog() { + final ProgressDialog progressDialog = new ProgressDialog(getActivity()); + progressDialog.setIndeterminate(true); + progressDialog.setCancelable(false); + progressDialog.setTitle( + getActivity().getString(R.string.master_clear_progress_title)); + progressDialog.setMessage( + getActivity().getString(R.string.master_clear_progress_text)); + return progressDialog; + } }; + private void doMasterClear() { + if (mEraseSdCard) { + Intent intent = new Intent(ExternalStorageFormatter.FORMAT_AND_FACTORY_RESET); + intent.putExtra(Intent.EXTRA_REASON, "MasterClearConfirm"); + intent.setComponent(ExternalStorageFormatter.COMPONENT_NAME); + getActivity().startService(intent); + } else { + Intent intent = new Intent(Intent.ACTION_MASTER_CLEAR); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + intent.putExtra(Intent.EXTRA_REASON, "MasterClearConfirm"); + getActivity().sendBroadcast(intent); + // Intent handling is asynchronous -- assume it will happen soon. + } + } + /** * Configure the UI for the final confirmation interaction */ private void establishFinalConfirmationState() { - mFinalButton = (Button) mContentView.findViewById(R.id.execute_master_clear); - mFinalButton.setOnClickListener(mFinalClickListener); + mContentView.findViewById(R.id.execute_master_clear) + .setOnClickListener(mFinalClickListener); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (UserManager.get(getActivity()).hasUserRestriction( + UserManager.DISALLOW_FACTORY_RESET)) { + return inflater.inflate(R.layout.master_clear_disallowed_screen, null); + } mContentView = inflater.inflate(R.layout.master_clear_confirm, null); establishFinalConfirmationState(); return mContentView; @@ -90,6 +142,6 @@ public class MasterClearConfirm extends Fragment { super.onCreate(savedInstanceState); Bundle args = getArguments(); - mEraseSdCard = args != null ? args.getBoolean(MasterClear.ERASE_EXTERNAL_EXTRA) : false; + mEraseSdCard = args != null && args.getBoolean(MasterClear.ERASE_EXTERNAL_EXTRA); } } diff --git a/src/com/android/settings/OwnerInfoSettings.java b/src/com/android/settings/OwnerInfoSettings.java index 2f7721b..0761f38 100644 --- a/src/com/android/settings/OwnerInfoSettings.java +++ b/src/com/android/settings/OwnerInfoSettings.java @@ -60,18 +60,11 @@ public class OwnerInfoSettings extends Fragment { mView = inflater.inflate(R.layout.ownerinfo, container, false); mUserId = UserHandle.myUserId(); mLockPatternUtils = new LockPatternUtils(getActivity()); - initView(mView); + initView(); return mView; } - private void initView(View view) { - final ContentResolver res = getActivity().getContentResolver(); - String info = mLockPatternUtils.getOwnerInfo(mUserId); - boolean enabled = mLockPatternUtils.isOwnerInfoEnabled(); - mCheckbox = (CheckBox) mView.findViewById(R.id.show_owner_info_on_lockscreen_checkbox); - mOwnerInfo = (EditText) mView.findViewById(R.id.owner_info_edit_text); - mOwnerInfo.setText(info); - mOwnerInfo.setEnabled(enabled); + private void initView() { mNickname = (EditText) mView.findViewById(R.id.owner_info_nickname); if (!mShowNickname) { mNickname.setVisibility(View.GONE); @@ -79,6 +72,10 @@ public class OwnerInfoSettings extends Fragment { mNickname.setText(UserManager.get(getActivity()).getUserName()); mNickname.setSelected(true); } + + final boolean enabled = mLockPatternUtils.isOwnerInfoEnabled(); + + mCheckbox = (CheckBox) mView.findViewById(R.id.show_owner_info_on_lockscreen_checkbox); mCheckbox.setChecked(enabled); if (UserHandle.myUserId() != UserHandle.USER_OWNER) { if (UserManager.get(getActivity()).isLinkedUser()) { @@ -93,6 +90,14 @@ public class OwnerInfoSettings extends Fragment { mOwnerInfo.setEnabled(isChecked); // disable text field if not enabled } }); + + String info = mLockPatternUtils.getOwnerInfo(mUserId); + + mOwnerInfo = (EditText) mView.findViewById(R.id.owner_info_edit_text); + mOwnerInfo.setEnabled(enabled); + if (!TextUtils.isEmpty(info)) { + mOwnerInfo.setText(info); + } } @Override @@ -102,7 +107,6 @@ public class OwnerInfoSettings extends Fragment { } void saveChanges() { - ContentResolver res = getActivity().getContentResolver(); String info = mOwnerInfo.getText().toString(); mLockPatternUtils.setOwnerInfo(info, mUserId); if (mShowNickname) { @@ -114,5 +118,4 @@ public class OwnerInfoSettings extends Fragment { } } } - } diff --git a/src/com/android/settings/PinnedHeaderListFragment.java b/src/com/android/settings/PinnedHeaderListFragment.java new file mode 100644 index 0000000..2ef5f02 --- /dev/null +++ b/src/com/android/settings/PinnedHeaderListFragment.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 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.ListFragment; +import android.view.View; +import android.view.ViewGroup; + +/** + * A ListFragment with a pinned header + */ +public class PinnedHeaderListFragment extends ListFragment { + + public PinnedHeaderListFragment() { + super(); + } + + /** + * Set the pinned header view. This can only be done when the ListView is already created. + * + * @param pinnedHeaderView the view to be used for the pinned header view. + */ + public void setPinnedHeaderView(View pinnedHeaderView) { + ((ViewGroup) getListView().getParent()).addView(pinnedHeaderView, 0); + } + + /** + * Clear the pinned header view. This can only be done when the ListView is already created. + */ + public void clearPinnedHeaderView() { + ((ViewGroup) getListView().getParent()).removeViewAt(0); + } +} diff --git a/src/com/android/settings/PrivacySettings.java b/src/com/android/settings/PrivacySettings.java index d936f46..0a9f086 100644 --- a/src/com/android/settings/PrivacySettings.java +++ b/src/com/android/settings/PrivacySettings.java @@ -24,13 +24,24 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.preference.CheckBoxPreference; +import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.SearchIndexableResource; import android.provider.Settings; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable.SearchIndexProvider; + +import java.util.ArrayList; +import java.util.List; + /** * Gesture lock pattern settings. */ @@ -43,11 +54,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 PERSONAL_DATA_CATEGORY = "personal_data_category"; private IBackupManager mBackupManager; - private CheckBoxPreference mBackup; - private CheckBoxPreference mAutoRestore; + private SwitchPreference mBackup; + private SwitchPreference mAutoRestore; private Dialog mConfirmDialog; private PreferenceScreen mConfigure; + private boolean mEnabled; private static final int DIALOG_ERASE_BACKUP = 2; private int mDialogType; @@ -55,16 +68,30 @@ public class PrivacySettings extends SettingsPreferenceFragment implements @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Don't allow any access if this is a secondary user + mEnabled = Process.myUserHandle().isOwner(); + if (!mEnabled) { + return; + } + addPreferencesFromResource(R.xml.privacy_settings); final PreferenceScreen screen = getPreferenceScreen(); - mBackupManager = IBackupManager.Stub.asInterface( ServiceManager.getService(Context.BACKUP_SERVICE)); - mBackup = (CheckBoxPreference) screen.findPreference(BACKUP_DATA); - mAutoRestore = (CheckBoxPreference) screen.findPreference(AUTO_RESTORE); + mBackup = (SwitchPreference) screen.findPreference(BACKUP_DATA); + mBackup.setOnPreferenceChangeListener(preferenceChangeListener); + + mAutoRestore = (SwitchPreference) screen.findPreference(AUTO_RESTORE); + mAutoRestore.setOnPreferenceChangeListener(preferenceChangeListener); + mConfigure = (PreferenceScreen) screen.findPreference(CONFIGURE_ACCOUNT); + if (UserManager.get(getActivity()).hasUserRestriction( + UserManager.DISALLOW_FACTORY_RESET)) { + screen.removePreference(findPreference(PERSONAL_DATA_CATEGORY)); + } + // Vendor specific if (getActivity().getPackageManager(). resolveContentProvider(GSETTINGS_PROVIDER, 0) == null) { @@ -78,7 +105,9 @@ public class PrivacySettings extends SettingsPreferenceFragment implements super.onResume(); // Refresh UI - updateToggles(); + if (mEnabled) { + updateToggles(); + } } @Override @@ -91,35 +120,41 @@ public class PrivacySettings extends SettingsPreferenceFragment implements super.onStop(); } - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { - if (preference == mBackup) { - if (!mBackup.isChecked()) { - showEraseBackupDialog(); - } else { - setBackupEnabled(true); + private OnPreferenceChangeListener preferenceChangeListener = new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (!(preference instanceof SwitchPreference)) { + return true; } - } else if (preference == mAutoRestore) { - boolean curState = mAutoRestore.isChecked(); - try { - mBackupManager.setAutoRestore(curState); - } catch (RemoteException e) { - mAutoRestore.setChecked(!curState); + boolean nextValue = (Boolean) newValue; + boolean result = false; + if (preference == mBackup) { + if (nextValue == false) { + // Don't change Switch status until user makes choice in dialog + // so return false here. + showEraseBackupDialog(); + } else { + setBackupEnabled(true); + result = true; + } + } else if (preference == mAutoRestore) { + try { + mBackupManager.setAutoRestore(nextValue); + result = true; + } catch (RemoteException e) { + mAutoRestore.setChecked(!nextValue); + } } + return result; } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } + }; private void showEraseBackupDialog() { - mBackup.setChecked(true); - mDialogType = DIALOG_ERASE_BACKUP; CharSequence msg = getResources().getText(R.string.backup_erase_dialog_message); // TODO: DialogFragment? mConfirmDialog = new AlertDialog.Builder(getActivity()).setMessage(msg) .setTitle(R.string.backup_erase_dialog_title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, this) .show(); @@ -153,7 +188,7 @@ public class PrivacySettings extends SettingsPreferenceFragment implements mConfigure.setEnabled(configureEnabled); mConfigure.setIntent(configIntent); setConfigureSummary(configSummary); -} + } private void setConfigureSummary(String summary) { if (summary != null) { @@ -173,13 +208,20 @@ public class PrivacySettings extends SettingsPreferenceFragment implements } } + @Override public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - //updateProviders(); - if (mDialogType == DIALOG_ERASE_BACKUP) { + // Dialog is triggered before Switch status change, that means marking the Switch to + // true in showEraseBackupDialog() method will be override by following status change. + // So we do manual switching here due to users' response. + if (mDialogType == DIALOG_ERASE_BACKUP) { + // Accept turning off backup + if (which == DialogInterface.BUTTON_POSITIVE) { setBackupEnabled(false); - updateConfigureSummary(); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + // Reject turning off backup + setBackupEnabled(true); } + updateConfigureSummary(); } mDialogType = 0; } @@ -208,4 +250,40 @@ public class PrivacySettings extends SettingsPreferenceFragment implements protected int getHelpResource() { return R.string.help_url_backup_reset; } -} + + /** + * For Search. + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new PrivacySearchIndexProvider(); + + private static class PrivacySearchIndexProvider extends BaseSearchIndexProvider { + + boolean mIsPrimary; + + public PrivacySearchIndexProvider() { + super(); + + mIsPrimary = UserHandle.myUserId() == UserHandle.USER_OWNER; + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + + List<SearchIndexableResource> result = new ArrayList<SearchIndexableResource>(); + + // For non-primary user, no backup or reset is available + if (!mIsPrimary) { + return result; + } + + SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.privacy_settings; + result.add(sir); + + return result; + } + } + +}
\ No newline at end of file diff --git a/src/com/android/settings/ProgressCategory.java b/src/com/android/settings/ProgressCategory.java index 6fe34bf..c85dd0b 100644 --- a/src/com/android/settings/ProgressCategory.java +++ b/src/com/android/settings/ProgressCategory.java @@ -21,17 +21,35 @@ import android.preference.Preference; import android.util.AttributeSet; import android.view.View; +/** + * A category with a progress spinner + */ public class ProgressCategory extends ProgressCategoryBase { - private final int mEmptyTextRes; + private int mEmptyTextRes; private boolean mProgress = false; private Preference mNoDeviceFoundPreference; private boolean mNoDeviceFoundAdded; + public ProgressCategory(Context context) { + this(context, null); + } + + public ProgressCategory(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + public ProgressCategory(Context context, AttributeSet attrs, - int emptyTextRes) { - super(context, attrs); + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ProgressCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setLayoutResource(R.layout.preference_progress_category); + } + + public void setEmptyTextRes(int emptyTextRes) { mEmptyTextRes = emptyTextRes; } diff --git a/src/com/android/settings/ProgressCategoryBase.java b/src/com/android/settings/ProgressCategoryBase.java index d120b94..c08806c 100644 --- a/src/com/android/settings/ProgressCategoryBase.java +++ b/src/com/android/settings/ProgressCategoryBase.java @@ -21,8 +21,21 @@ import android.preference.PreferenceCategory; import android.util.AttributeSet; public abstract class ProgressCategoryBase extends PreferenceCategory { + public ProgressCategoryBase(Context context) { + this(context, null); + } + public ProgressCategoryBase(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public ProgressCategoryBase(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr, 0); + } + + public ProgressCategoryBase(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /** diff --git a/src/com/android/settings/ProxySelector.java b/src/com/android/settings/ProxySelector.java index 21e717a..6941271 100644 --- a/src/com/android/settings/ProxySelector.java +++ b/src/com/android/settings/ProxySelector.java @@ -28,7 +28,7 @@ import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.Proxy; -import android.net.ProxyProperties; +import android.net.ProxyInfo; import android.os.Bundle; import android.provider.Settings; import android.text.Selection; @@ -45,8 +45,6 @@ import android.widget.EditText; import android.widget.TextView; import java.net.InetSocketAddress; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class ProxySelector extends Fragment implements DialogCreatable { private static final String TAG = "ProxySelector"; @@ -58,22 +56,6 @@ public class ProxySelector extends Fragment implements DialogCreatable { Button mClearButton; Button mDefaultButton; - // Allows underscore char to supports proxies that do not - // follow the spec - private static final String HC = "a-zA-Z0-9\\_"; - - // Matches blank input, ips, and domain names - private static final String HOSTNAME_REGEXP = - "^$|^[" + HC + "]+(\\-[" + HC + "]+)*(\\.[" + HC + "]+(\\-[" + HC + "]+)*)*$"; - private static final Pattern HOSTNAME_PATTERN; - private static final String EXCLUSION_REGEXP = - "$|^(\\*)?\\.?[" + HC + "]+(\\-[" + HC + "]+)*(\\.[" + HC + "]+(\\-[" + HC + "]+)*)*$"; - private static final Pattern EXCLUSION_PATTERN; - static { - HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP); - EXCLUSION_PATTERN = Pattern.compile(EXCLUSION_REGEXP); - } - private static final int ERROR_DIALOG_ID = 0; private SettingsDialogFragment mDialogFragment; @@ -167,11 +149,11 @@ public class ProxySelector extends Fragment implements DialogCreatable { ConnectivityManager cm = (ConnectivityManager)getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); - ProxyProperties proxy = cm.getGlobalProxy(); + ProxyInfo proxy = cm.getGlobalProxy(); if (proxy != null) { hostname = proxy.getHost(); port = proxy.getPort(); - exclList = proxy.getExclusionList(); + exclList = proxy.getExclusionListAsString(); } if (hostname == null) { @@ -203,35 +185,24 @@ public class ProxySelector extends Fragment implements DialogCreatable { * @return 0 on success, string resource ID on failure */ public static int validate(String hostname, String port, String exclList) { - Matcher match = HOSTNAME_PATTERN.matcher(hostname); - String exclListArray[] = exclList.split(","); - - if (!match.matches()) return R.string.proxy_error_invalid_host; - - for (String excl : exclListArray) { - Matcher m = EXCLUSION_PATTERN.matcher(excl); - if (!m.matches()) return R.string.proxy_error_invalid_exclusion_list; - } - - if (hostname.length() > 0 && port.length() == 0) { - return R.string.proxy_error_empty_port; - } - - if (port.length() > 0) { - if (hostname.length() == 0) { + switch (Proxy.validate(hostname, port, exclList)) { + case Proxy.PROXY_VALID: + return 0; + case Proxy.PROXY_HOSTNAME_EMPTY: return R.string.proxy_error_empty_host_set_port; - } - int portVal = -1; - try { - portVal = Integer.parseInt(port); - } catch (NumberFormatException ex) { + case Proxy.PROXY_HOSTNAME_INVALID: + return R.string.proxy_error_invalid_host; + case Proxy.PROXY_PORT_EMPTY: + return R.string.proxy_error_empty_port; + case Proxy.PROXY_PORT_INVALID: return R.string.proxy_error_invalid_port; - } - if (portVal <= 0 || portVal > 0xFFFF) { - return R.string.proxy_error_invalid_port; - } + case Proxy.PROXY_EXCLLIST_INVALID: + return R.string.proxy_error_invalid_exclusion_list; + default: + // should neven happen + Log.e(TAG, "Unknown proxy settings error"); + return -1; } - return 0; } /** @@ -245,7 +216,7 @@ public class ProxySelector extends Fragment implements DialogCreatable { int port = 0; int result = validate(hostname, portStr, exclList); - if (result > 0) { + if (result != 0) { showDialog(ERROR_DIALOG_ID); return false; } @@ -258,7 +229,7 @@ public class ProxySelector extends Fragment implements DialogCreatable { return false; } } - ProxyProperties p = new ProxyProperties(hostname, port, exclList); + ProxyInfo p = new ProxyInfo(hostname, port, exclList); // FIXME: The best solution would be to make a better UI that would // disable editing of the text boxes if the user chooses to use the // default settings. i.e. checking a box to always use the default diff --git a/src/com/android/settings/RadioInfo.java b/src/com/android/settings/RadioInfo.java index df93626..b0a4a53 100644 --- a/src/com/android/settings/RadioInfo.java +++ b/src/com/android/settings/RadioInfo.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; -import android.net.ConnectivityManager; import android.net.TrafficStats; import android.net.Uri; import android.os.AsyncResult; @@ -32,6 +31,7 @@ import android.os.Message; import android.os.SystemProperties; import android.telephony.CellInfo; import android.telephony.CellLocation; +import android.telephony.DataConnectionRealTimeInfo; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.TelephonyManager; @@ -103,6 +103,7 @@ public class RadioInfo extends Activity { private TextView mLocation; private TextView mNeighboringCids; private TextView mCellInfo; + private TextView mDcRtInfoTv; private TextView resets; private TextView attempts; private TextView successes; @@ -171,6 +172,12 @@ public class RadioInfo extends Activity { log("onCellInfoChanged: arrayCi=" + arrayCi); updateCellInfoTv(arrayCi); } + + @Override + public void onDataConnectionRealTimeInfoChanged(DataConnectionRealTimeInfo dcRtInfo) { + log("onDataConnectionRealTimeInfoChanged: dcRtInfo=" + dcRtInfo); + updateDcRtInfoTv(dcRtInfo); + } }; private Handler mHandler = new Handler() { @@ -264,6 +271,7 @@ public class RadioInfo extends Activity { mLocation = (TextView) findViewById(R.id.location); mNeighboringCids = (TextView) findViewById(R.id.neighboring); mCellInfo = (TextView) findViewById(R.id.cellinfo); + mDcRtInfoTv = (TextView) findViewById(R.id.dcrtinfo); resets = (TextView) findViewById(R.id.resets); attempts = (TextView) findViewById(R.id.attempts); @@ -366,7 +374,8 @@ public class RadioInfo extends Activity { | PhoneStateListener.LISTEN_CELL_LOCATION | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR - | PhoneStateListener.LISTEN_CELL_INFO); + | PhoneStateListener.LISTEN_CELL_INFO + | PhoneStateListener.LISTEN_DATA_CONNECTION_REAL_TIME_INFO); } @Override @@ -541,6 +550,10 @@ public class RadioInfo extends Activity { mCellInfo.setText(value.toString()); } + private final void updateDcRtInfoTv(DataConnectionRealTimeInfo dcRtInfo) { + mDcRtInfoTv.setText(dcRtInfo.toString()); + } + private final void updateMessageWaiting() { mMwi.setText(String.valueOf(mMwiValue)); @@ -903,15 +916,13 @@ public class RadioInfo extends Activity { private MenuItem.OnMenuItemClickListener mToggleData = new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - ConnectivityManager cm = - (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); int state = mTelephonyManager.getDataState(); switch (state) { case TelephonyManager.DATA_CONNECTED: - cm.setMobileDataEnabled(false); + phone.setDataEnabled(false); break; case TelephonyManager.DATA_DISCONNECTED: - cm.setMobileDataEnabled(true); + phone.setDataEnabled(true); break; default: // do nothing diff --git a/src/com/android/settings/RestrictedSettingsFragment.java b/src/com/android/settings/RestrictedSettingsFragment.java index 34eda1e..7d7599f 100644 --- a/src/com/android/settings/RestrictedSettingsFragment.java +++ b/src/com/android/settings/RestrictedSettingsFragment.java @@ -23,62 +23,59 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.RestrictionsManager; import android.os.Bundle; +import android.os.PersistableBundle; import android.os.UserManager; import android.preference.CheckBoxPreference; import android.preference.Preference; /** - * Base class for settings activities that should be pin protected when in restricted mode. + * Base class for settings screens that should be pin protected when in restricted mode. * The constructor for this class will take the restriction key that this screen should be - * locked by. If {@link UserManager.hasRestrictionsPin()} and - * {@link UserManager.hasUserRestriction(String)} returns true for the restriction key, then - * the user will have to enter the restrictions pin before seeing the Settings screen. + * locked by. If {@link RestrictionsManager.hasRestrictionsProvider()} and + * {@link UserManager.hasUserRestriction()}, then the user will have to enter the restrictions + * pin before seeing the Settings screen. * * If this settings screen should be pin protected whenever - * {@link UserManager.hasUserRestriction(String)} returns true, pass in - * {@link RESTRICTIONS_PIN_SET} to the constructor instead of a restrictions key. + * {@link RestrictionsManager.hasRestrictionsProvider()} returns true, pass in + * {@link RESTRICT_IF_OVERRIDABLE} to the constructor instead of a restrictions key. */ public class RestrictedSettingsFragment extends SettingsPreferenceFragment { - protected static final String RESTRICTIONS_PIN_SET = "restrictions_pin_set"; + protected static final String RESTRICT_IF_OVERRIDABLE = "restrict_if_overridable"; - private static final String EXTRA_PREFERENCE = "pref"; - private static final String EXTRA_CHECKBOX_STATE = "isChecked"; - // Should be unique across all settings screens that use this. + // No RestrictedSettingsFragment screens should use this number in startActivityForResult. private static final int REQUEST_PIN_CHALLENGE = 12309; private static final String KEY_CHALLENGE_SUCCEEDED = "chsc"; private static final String KEY_CHALLENGE_REQUESTED = "chrq"; - private static final String KEY_RESUME_ACTION_BUNDLE = "rsmb"; // If the restriction PIN is entered correctly. private boolean mChallengeSucceeded; private boolean mChallengeRequested; - private Bundle mResumeActionBundle; private UserManager mUserManager; + private RestrictionsManager mRestrictionsManager; private final String mRestrictionKey; - private final HashSet<Preference> mProtectedByRestictionsPrefs = new HashSet<Preference>(); - // Receiver to clear pin status when the screen is turned off. private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - mChallengeSucceeded = false; - if (shouldBePinProtected(mRestrictionKey)) { - ensurePin(null); + if (!mChallengeRequested) { + mChallengeSucceeded = false; + mChallengeRequested = false; } } }; /** * @param restrictionKey The restriction key to check before pin protecting - * this settings page. Pass in {@link RESTRICTIONS_PIN_SET} if it should - * be PIN protected whenever a restrictions pin is set. Pass in - * null if it should never be PIN protected. + * this settings page. Pass in {@link RESTRICT_IF_OVERRIDABLE} if it should + * be protected whenever a restrictions provider is set. Pass in + * null if it should never be protected. */ public RestrictedSettingsFragment(String restrictionKey) { mRestrictionKey = restrictionKey; @@ -88,24 +85,25 @@ public class RestrictedSettingsFragment extends SettingsPreferenceFragment { public void onCreate(Bundle icicle) { super.onCreate(icicle); + mRestrictionsManager = (RestrictionsManager) getSystemService(Context.RESTRICTIONS_SERVICE); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); if (icicle != null) { mChallengeSucceeded = icicle.getBoolean(KEY_CHALLENGE_SUCCEEDED, false); mChallengeRequested = icicle.getBoolean(KEY_CHALLENGE_REQUESTED, false); - mResumeActionBundle = icicle.getBundle(KEY_RESUME_ACTION_BUNDLE); } + + IntentFilter offFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF); + offFilter.addAction(Intent.ACTION_USER_PRESENT); + getActivity().registerReceiver(mScreenOffReceiver, offFilter); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putBoolean(KEY_CHALLENGE_REQUESTED, mChallengeRequested); - if (mResumeActionBundle != null) { - outState.putBundle(KEY_RESUME_ACTION_BUNDLE, mResumeActionBundle); - } if (getActivity().isChangingConfigurations()) { + outState.putBoolean(KEY_CHALLENGE_REQUESTED, mChallengeRequested); outState.putBoolean(KEY_CHALLENGE_SUCCEEDED, mChallengeSucceeded); } } @@ -113,55 +111,26 @@ public class RestrictedSettingsFragment extends SettingsPreferenceFragment { @Override public void onResume() { super.onResume(); - if (shouldBePinProtected(mRestrictionKey)) { - ensurePin(null); - } else { - // If the whole screen is not pin protected, reset mChallengeSucceeded so next - // time user uses a protected preference, they are prompted for pin again. - mChallengeSucceeded = false; + + if (shouldBeProviderProtected(mRestrictionKey)) { + ensurePin(); } - IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); - filter.addAction(Intent.ACTION_USER_PRESENT); - getActivity().registerReceiver(mScreenOffReceiver, filter); } @Override - public void onPause() { - super.onPause(); + public void onDestroy() { getActivity().unregisterReceiver(mScreenOffReceiver); + super.onDestroy(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_PIN_CHALLENGE) { - Bundle resumeActionBundle = mResumeActionBundle; - mResumeActionBundle = null; - mChallengeRequested = false; if (resultCode == Activity.RESULT_OK) { mChallengeSucceeded = true; - String prefKey = resumeActionBundle == null ? - null : resumeActionBundle.getString(EXTRA_PREFERENCE); - if (prefKey != null) { - Preference pref = findPreference(prefKey); - if (pref != null) { - // Make sure the checkbox state is the same as it was when we launched the - // pin challenge. - if (pref instanceof CheckBoxPreference - && resumeActionBundle.containsKey(EXTRA_CHECKBOX_STATE)) { - boolean isChecked = - resumeActionBundle.getBoolean(EXTRA_CHECKBOX_STATE, false); - ((CheckBoxPreference)pref).setChecked(isChecked); - } - if (!onPreferenceTreeClick(getPreferenceScreen(), pref)) { - Intent prefIntent = pref.getIntent(); - if (prefIntent != null) { - pref.getContext().startActivity(prefIntent); - } - } - } - } - } else if (!isDetached()) { - finishFragment(); + mChallengeRequested = false; + } else { + mChallengeSucceeded = false; } return; } @@ -169,93 +138,54 @@ public class RestrictedSettingsFragment extends SettingsPreferenceFragment { super.onActivityResult(requestCode, resultCode, data); } - private void ensurePin(Preference preference) { - if (!mChallengeSucceeded) { - final UserManager um = UserManager.get(getActivity()); - if (!mChallengeRequested) { - if (um.hasRestrictionsChallenge()) { - mResumeActionBundle = new Bundle(); - if (preference != null) { - mResumeActionBundle.putString(EXTRA_PREFERENCE, preference.getKey()); - if (preference instanceof CheckBoxPreference) { - mResumeActionBundle.putBoolean(EXTRA_CHECKBOX_STATE, - ((CheckBoxPreference)preference).isChecked()); - } - } - Intent requestPin = new Intent(Intent.ACTION_RESTRICTIONS_CHALLENGE); - startActivityForResult(requestPin, REQUEST_PIN_CHALLENGE); - mChallengeRequested = true; - } + private void ensurePin() { + if (!mChallengeSucceeded && !mChallengeRequested + && mRestrictionsManager.hasRestrictionsProvider()) { + Intent intent = mRestrictionsManager.createLocalApprovalIntent(); + if (intent != null) { + mChallengeRequested = true; + mChallengeSucceeded = false; + PersistableBundle request = new PersistableBundle(); + request.putString(RestrictionsManager.REQUEST_KEY_MESSAGE, + getResources().getString(R.string.restr_pin_enter_admin_pin)); + intent.putExtra(RestrictionsManager.EXTRA_REQUEST_BUNDLE, request); + startActivityForResult(intent, REQUEST_PIN_CHALLENGE); } } - mChallengeSucceeded = false; } /** - * Returns true if this activity is restricted, but no restriction pin has been set. + * Returns true if this activity is restricted, but no restrictions provider has been set. * Used to determine if the settings UI should disable UI. */ - protected boolean isRestrictedAndNotPinProtected() { - if (mRestrictionKey == null || RESTRICTIONS_PIN_SET.equals(mRestrictionKey)) { + protected boolean isRestrictedAndNotProviderProtected() { + if (mRestrictionKey == null || RESTRICT_IF_OVERRIDABLE.equals(mRestrictionKey)) { return false; } return mUserManager.hasUserRestriction(mRestrictionKey) - && !mUserManager.hasRestrictionsChallenge(); + && !mRestrictionsManager.hasRestrictionsProvider(); + } + + protected boolean hasChallengeSucceeded() { + return (mChallengeRequested && mChallengeSucceeded) || !mChallengeRequested; } /** - * Called to trigger the pin entry if the given restriction key is locked down. - * @param restrictionsKey The restriction key or {@link RESTRICTIONS_PIN_SET} if - * pin entry should get triggered if there is a pin set. + * Returns true if this restrictions key is locked down. */ - protected boolean restrictionsPinCheck(String restrictionsKey, Preference preference) { - if (shouldBePinProtected(restrictionsKey) && !mChallengeSucceeded) { - ensurePin(preference); - return false; - } else { - return true; - } - } - - protected boolean hasChallengeSucceeded() { - return mChallengeSucceeded; - } - - /** - * Returns true if this restrictions key is locked down. - */ - protected boolean shouldBePinProtected(String restrictionKey) { - if (restrictionKey == null) { - return false; - } - boolean restricted = RESTRICTIONS_PIN_SET.equals(restrictionKey) - || mUserManager.hasUserRestriction(restrictionKey); - return restricted && mUserManager.hasRestrictionsChallenge(); - } - - /** - * If the preference is one that was added by protectByRestrictions(), then it will - * prompt the user for the restrictions pin if they haven't entered it already. - * Intended to be called at the top of onPreferenceTreeClick. If this function returns - * true, then onPreferenceTreeClick should return true. - */ - boolean ensurePinRestrictedPreference(Preference preference) { - return mProtectedByRestictionsPrefs.contains(preference) - && !restrictionsPinCheck(RESTRICTIONS_PIN_SET, preference); - } + protected boolean shouldBeProviderProtected(String restrictionKey) { + if (restrictionKey == null) { + return false; + } + boolean restricted = RESTRICT_IF_OVERRIDABLE.equals(restrictionKey) + || mUserManager.hasUserRestriction(mRestrictionKey); + return restricted && mRestrictionsManager.hasRestrictionsProvider(); + } /** - * Call this with any preferences that should require the PIN to be entered - * before they are accessible. + * Returns whether restricted or actionable UI elements should be removed or disabled. */ - protected void protectByRestrictions(Preference pref) { - if (pref != null) { - mProtectedByRestictionsPrefs.add(pref); - } - } - - protected void protectByRestrictions(String key) { - Preference pref = findPreference(key); - protectByRestrictions(pref); - } + protected boolean isUiRestricted() { + return isRestrictedAndNotProviderProtected() || !hasChallengeSucceeded(); + } } diff --git a/src/com/android/settings/RingerVolumePreference.java b/src/com/android/settings/RingerVolumePreference.java deleted file mode 100644 index dd81ded..0000000 --- a/src/com/android/settings/RingerVolumePreference.java +++ /dev/null @@ -1,388 +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; - -import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; - -import com.android.internal.telephony.TelephonyIntents; - -import android.app.Dialog; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -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; -import android.os.Message; -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; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.SeekBar; -import android.widget.TextView; - -/** - * Special preference type that allows configuration of both the ring volume and - * notification volume. - */ -public class RingerVolumePreference extends VolumePreference { - private static final String TAG = "RingerVolumePreference"; - private static final int MSG_RINGER_MODE_CHANGED = 101; - - private SeekBarVolumizer [] mSeekBarVolumizer; - - // These arrays must all match in length and order - private static final int[] SEEKBAR_ID = new int[] { - R.id.media_volume_seekbar, - R.id.ringer_volume_seekbar, - R.id.notification_volume_seekbar, - R.id.alarm_volume_seekbar - }; - - private static final int[] SEEKBAR_TYPE = new int[] { - 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.alarm_mute_button - }; - - private static final int[] SEEKBAR_SECTION_ID = new int[] { - R.id.media_section, - R.id.ringer_section, - R.id.notification_section, - R.id.alarm_section - }; - - private static final int[] SEEKBAR_MUTED_RES_ID = new int[] { - 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_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 - }; - - private ImageView[] mCheckBoxes = new ImageView[SEEKBAR_MUTED_RES_ID.length]; - private SeekBar[] mSeekBars = new SeekBar[SEEKBAR_ID.length]; - - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - updateSlidersAndMutedStates(); - } - }; - - @Override - public void createActionButtons() { - setPositiveButtonText(android.R.string.ok); - setNegativeButtonText(null); - } - - private void updateSlidersAndMutedStates() { - for (int i = 0; i < SEEKBAR_TYPE.length; i++) { - int streamType = SEEKBAR_TYPE[i]; - boolean muted = mAudioManager.isStreamMute(streamType); - - if (mCheckBoxes[i] != null) { - if (((streamType == AudioManager.STREAM_RING) || - (streamType == AudioManager.STREAM_NOTIFICATION)) && - (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE)) { - mCheckBoxes[i].setImageResource( - com.android.internal.R.drawable.ic_audio_ring_notif_vibrate); - } else { - mCheckBoxes[i].setImageResource( - muted ? SEEKBAR_MUTED_RES_ID[i] : SEEKBAR_UNMUTED_RES_ID[i]); - } - } - if (mSeekBars[i] != null) { - final int volume = mAudioManager.getStreamVolume(streamType); - mSeekBars[i].setProgress(volume); - if (streamType != mAudioManager.getMasterStreamType() && muted) { - mSeekBars[i].setEnabled(false); - } else { - mSeekBars[i].setEnabled(true); - } - } - } - } - - private BroadcastReceiver mRingModeChangedReceiver; - private AudioManager mAudioManager; - - //private SeekBarVolumizer mNotificationSeekBarVolumizer; - //private TextView mNotificationVolumeTitle; - - public RingerVolumePreference(Context context, AttributeSet attrs) { - super(context, attrs); - - // The always visible seekbar is for ring volume - setStreamType(AudioManager.STREAM_RING); - - setDialogLayoutResource(R.layout.preference_dialog_ringervolume); - //setDialogIcon(R.drawable.ic_settings_sound); - - mSeekBarVolumizer = new SeekBarVolumizer[SEEKBAR_ID.length]; - - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - } - - @Override - protected void onBindDialogView(View view) { - super.onBindDialogView(view); - - for (int i = 0; i < SEEKBAR_ID.length; i++) { - SeekBar seekBar = (SeekBar) view.findViewById(SEEKBAR_ID[i]); - mSeekBars[i] = seekBar; - if (SEEKBAR_TYPE[i] == AudioManager.STREAM_MUSIC) { - mSeekBarVolumizer[i] = new SeekBarVolumizer(getContext(), seekBar, - SEEKBAR_TYPE[i], getMediaVolumeUri(getContext())); - } else { - mSeekBarVolumizer[i] = new SeekBarVolumizer(getContext(), seekBar, - SEEKBAR_TYPE[i]); - } - } - - // Register callbacks for mute/unmute buttons - for (int i = 0; i < mCheckBoxes.length; i++) { - ImageView checkbox = (ImageView) view.findViewById(CHECKBOX_VIEW_ID[i]); - mCheckBoxes[i] = checkbox; - } - - // Load initial states from AudioManager - updateSlidersAndMutedStates(); - - // Listen for updates from AudioManager - if (mRingModeChangedReceiver == null) { - final IntentFilter filter = new IntentFilter(); - filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); - mRingModeChangedReceiver = new BroadcastReceiver() { - 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)); - } - } - }; - getContext().registerReceiver(mRingModeChangedReceiver, filter); - } - - boolean useMasterVolume = getContext().getResources(). - getBoolean(com.android.internal.R.bool.config_useMasterVolume); - if (useMasterVolume) { - // If config_useMasterVolume is true, all streams are treated as STREAM_MASTER. - // So hide all except a stream. - int id; - if (Utils.isVoiceCapable(getContext())) { - id = R.id.ringer_section; - } else { - id = R.id.media_section; - } - for (int i = 0; i < SEEKBAR_SECTION_ID.length; i++) { - if (SEEKBAR_SECTION_ID[i] != id) { - view.findViewById(SEEKBAR_SECTION_ID[i]).setVisibility(View.GONE); - } - } - } else { - // 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) { - return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" - + context.getPackageName() - + "/" + R.raw.media_volume); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (!positiveResult) { - for (SeekBarVolumizer vol : mSeekBarVolumizer) { - if (vol != null) vol.revertVolume(); - } - } - cleanup(); - } - - @Override - public void onActivityStop() { - super.onActivityStop(); - - for (SeekBarVolumizer vol : mSeekBarVolumizer) { - if (vol != null) vol.stopSample(); - } - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - boolean isdown = (event.getAction() == KeyEvent.ACTION_DOWN); - switch (keyCode) { - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_MUTE: - return true; - default: - return false; - } - } - - @Override - protected void onSampleStarting(SeekBarVolumizer volumizer) { - super.onSampleStarting(volumizer); - for (SeekBarVolumizer vol : mSeekBarVolumizer) { - if (vol != null && vol != volumizer) vol.stopSample(); - } - } - - private void cleanup() { - for (int i = 0; i < SEEKBAR_ID.length; i++) { - if (mSeekBarVolumizer[i] != null) { - Dialog dialog = getDialog(); - if (dialog != null && dialog.isShowing()) { - // Stopped while dialog was showing, revert changes - mSeekBarVolumizer[i].revertVolume(); - } - mSeekBarVolumizer[i].stop(); - mSeekBarVolumizer[i] = null; - } - } - if (mRingModeChangedReceiver != null) { - getContext().unregisterReceiver(mRingModeChangedReceiver); - mRingModeChangedReceiver = null; - } - } - - @Override - protected Parcelable onSaveInstanceState() { - final Parcelable superState = super.onSaveInstanceState(); - if (isPersistent()) { - // No need to save instance state since it's persistent - return superState; - } - - final SavedState myState = new SavedState(superState); - VolumeStore[] volumeStore = myState.getVolumeStore(SEEKBAR_ID.length); - for (int i = 0; i < SEEKBAR_ID.length; i++) { - SeekBarVolumizer vol = mSeekBarVolumizer[i]; - if (vol != null) { - vol.onSaveInstanceState(volumeStore[i]); - } - } - return myState; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (state == null || !state.getClass().equals(SavedState.class)) { - // Didn't save state for us in onSaveInstanceState - super.onRestoreInstanceState(state); - return; - } - - SavedState myState = (SavedState) state; - super.onRestoreInstanceState(myState.getSuperState()); - VolumeStore[] volumeStore = myState.getVolumeStore(SEEKBAR_ID.length); - for (int i = 0; i < SEEKBAR_ID.length; i++) { - SeekBarVolumizer vol = mSeekBarVolumizer[i]; - if (vol != null) { - vol.onRestoreInstanceState(volumeStore[i]); - } - } - } - - private static class SavedState extends BaseSavedState { - VolumeStore [] mVolumeStore; - - public SavedState(Parcel source) { - super(source); - mVolumeStore = new VolumeStore[SEEKBAR_ID.length]; - for (int i = 0; i < SEEKBAR_ID.length; i++) { - mVolumeStore[i] = new VolumeStore(); - mVolumeStore[i].volume = source.readInt(); - mVolumeStore[i].originalVolume = source.readInt(); - } - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - for (int i = 0; i < SEEKBAR_ID.length; i++) { - dest.writeInt(mVolumeStore[i].volume); - dest.writeInt(mVolumeStore[i].originalVolume); - } - } - - VolumeStore[] getVolumeStore(int count) { - if (mVolumeStore == null || mVolumeStore.length != count) { - mVolumeStore = new VolumeStore[count]; - for (int i = 0; i < count; i++) { - mVolumeStore[i] = new VolumeStore(); - } - } - return mVolumeStore; - } - - public SavedState(Parcelable superState) { - super(superState); - } - - 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/ScreenPinningSettings.java b/src/com/android/settings/ScreenPinningSettings.java new file mode 100644 index 0000000..ada5b17 --- /dev/null +++ b/src/com/android/settings/ScreenPinningSettings.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 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 java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; + +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.widget.SwitchBar; + +/** + * Screen pinning settings. + */ +public class ScreenPinningSettings extends SettingsPreferenceFragment + implements SwitchBar.OnSwitchChangeListener, Indexable { + + private SwitchBar mSwitchBar; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final SettingsActivity activity = (SettingsActivity) getActivity(); + + mSwitchBar = activity.getSwitchBar(); + mSwitchBar.addOnSwitchChangeListener(this); + mSwitchBar.show(); + mSwitchBar.setChecked(isLockToAppEnabled()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + mSwitchBar.removeOnSwitchChangeListener(this); + mSwitchBar.hide(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.screen_pinning_instructions, null); + } + + private boolean isLockToAppEnabled() { + try { + return Settings.System.getInt(getContentResolver(), Settings.System.LOCK_TO_APP_ENABLED) + != 0; + } catch (SettingNotFoundException e) { + return false; + } + } + + private void setLockToAppEnabled(boolean isEnabled) { + Settings.System.putInt(getContentResolver(), Settings.System.LOCK_TO_APP_ENABLED, + isEnabled ? 1 : 0); + } + + /** + * Listens to the state change of the lock-to-app master switch. + */ + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + setLockToAppEnabled(isChecked); + } + + /** + * For search + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.screen_pinning_title); + data.screenTitle = res.getString(R.string.screen_pinning_title); + result.add(data); + + // Screen pinning description. + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.screen_pinning_description); + data.screenTitle = res.getString(R.string.screen_pinning_title); + result.add(data); + + return result; + } + }; +} diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index e4dcea1..ecededc 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -17,43 +17,56 @@ package com.android.settings; -import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; - import android.app.Activity; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import android.preference.CheckBoxPreference; +import android.preference.SwitchPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.provider.SearchIndexableResource; import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; import android.security.KeyStore; +import android.service.trust.TrustAgentService; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.TrustAgentUtils.TrustAgentComponentInfo; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Index; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; import java.util.ArrayList; import java.util.List; +import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; + /** * Gesture lock pattern settings. */ -public class SecuritySettings extends RestrictedSettingsFragment - implements OnPreferenceChangeListener, DialogInterface.OnClickListener { +public class SecuritySettings extends SettingsPreferenceFragment + implements OnPreferenceChangeListener, DialogInterface.OnClickListener, Indexable { + private static final String TRUST_AGENT_CLICK_INTENT = "trust_agent_click_intent"; static final String TAG = "SecuritySettings"; + private static final Intent TRUST_AGENT_INTENT = + new Intent(TrustAgentService.SERVICE_INTERFACE); // Lock Settings private static final String KEY_UNLOCK_SET_OR_CHANGE = "unlock_set_or_change"; @@ -66,53 +79,56 @@ public class SecuritySettings extends RestrictedSettingsFragment private static final String KEY_DEVICE_ADMIN_CATEGORY = "device_admin_category"; private static final String KEY_LOCK_AFTER_TIMEOUT = "lock_after_timeout"; private static final String KEY_OWNER_INFO_SETTINGS = "owner_info_settings"; - private static final String KEY_ENABLE_WIDGETS = "keyguard_enable_widgets"; + private static final String KEY_ADVANCED_SECURITY = "advanced_security"; + private static final String KEY_MANAGE_TRUST_AGENTS = "manage_trust_agents"; private static final int SET_OR_CHANGE_LOCK_METHOD_REQUEST = 123; private static final int CONFIRM_EXISTING_FOR_BIOMETRIC_WEAK_IMPROVE_REQUEST = 124; private static final int CONFIRM_EXISTING_FOR_BIOMETRIC_WEAK_LIVELINESS_OFF = 125; + private static final int CHANGE_TRUST_AGENT_SETTINGS = 126; // 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_CREDENTIAL_STORAGE_TYPE = "credential_storage_type"; - private static final String KEY_RESET_CREDENTIALS = "reset_credentials"; + private static final String KEY_RESET_CREDENTIALS = "credentials_reset"; private static final String KEY_CREDENTIALS_INSTALL = "credentials_install"; private static final String KEY_TOGGLE_INSTALL_APPLICATIONS = "toggle_install_applications"; - private static final String KEY_TOGGLE_VERIFY_APPLICATIONS = "toggle_verify_applications"; private static final String KEY_POWER_INSTANTLY_LOCKS = "power_button_instantly_locks"; private static final String KEY_CREDENTIALS_MANAGER = "credentials_management"; - private static final String KEY_NOTIFICATION_ACCESS = "manage_notification_access"; private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; + private static final String KEY_TRUST_AGENT = "trust_agent"; + private static final String KEY_SCREEN_PINNING = "screen_pinning_settings"; + + // These switch preferences need special handling since they're not all stored in Settings. + private static final String SWITCH_PREFERENCE_KEYS[] = { KEY_LOCK_AFTER_TIMEOUT, + KEY_LOCK_ENABLED, KEY_VISIBLE_PATTERN, KEY_BIOMETRIC_WEAK_LIVELINESS, + KEY_POWER_INSTANTLY_LOCKS, KEY_SHOW_PASSWORD, KEY_TOGGLE_INSTALL_APPLICATIONS }; + + // Only allow one trust agent on the platform. + private static final boolean ONLY_ONE_TRUST_AGENT = true; - private PackageManager mPM; private DevicePolicyManager mDPM; private ChooseLockSettingsHelper mChooseLockSettingsHelper; private LockPatternUtils mLockPatternUtils; private ListPreference mLockAfter; - private CheckBoxPreference mBiometricWeakLiveliness; - private CheckBoxPreference mVisiblePattern; + private SwitchPreference mBiometricWeakLiveliness; + private SwitchPreference mVisiblePattern; - private CheckBoxPreference mShowPassword; + private SwitchPreference mShowPassword; private KeyStore mKeyStore; private Preference mResetCredentials; - private CheckBoxPreference mToggleAppInstallation; + private SwitchPreference mToggleAppInstallation; private DialogInterface mWarnInstallApps; - private CheckBoxPreference mToggleVerifyApps; - private CheckBoxPreference mPowerButtonInstantlyLocks; - private CheckBoxPreference mEnableKeyguardWidgets; - - private Preference mNotificationAccess; + private SwitchPreference mPowerButtonInstantlyLocks; private boolean mIsPrimary; - public SecuritySettings() { - super(null /* Don't ask for restrictions pin on creation. */); - } + private Intent mTrustAgentClickIntent; @Override public void onCreate(Bundle savedInstanceState) { @@ -120,42 +136,40 @@ public class SecuritySettings extends RestrictedSettingsFragment mLockPatternUtils = new LockPatternUtils(getActivity()); - mPM = getActivity().getPackageManager(); mDPM = (DevicePolicyManager)getSystemService(Context.DEVICE_POLICY_SERVICE); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); - } - private PreferenceScreen createPreferenceHierarchy() { - PreferenceScreen root = getPreferenceScreen(); - if (root != null) { - root.removeAll(); + if (savedInstanceState != null + && savedInstanceState.containsKey(TRUST_AGENT_CLICK_INTENT)) { + mTrustAgentClickIntent = savedInstanceState.getParcelable(TRUST_AGENT_CLICK_INTENT); } - addPreferencesFromResource(R.xml.security_settings); - root = getPreferenceScreen(); + } - // Add options for lock/unlock screen + private static int getResIdForLockUnlockScreen(Context context, + LockPatternUtils lockPatternUtils) { int resid = 0; - if (!mLockPatternUtils.isSecure()) { + if (!lockPatternUtils.isSecure()) { // if there are multiple users, disable "None" setting - UserManager mUm = (UserManager) getSystemService(Context.USER_SERVICE); + UserManager mUm = (UserManager) context. getSystemService(Context.USER_SERVICE); List<UserInfo> users = mUm.getUsers(true); final boolean singleUser = users.size() == 1; - if (singleUser && mLockPatternUtils.isLockScreenDisabled()) { + if (singleUser && lockPatternUtils.isLockScreenDisabled()) { resid = R.xml.security_settings_lockscreen; } else { resid = R.xml.security_settings_chooser; } - } else if (mLockPatternUtils.usingBiometricWeak() && - mLockPatternUtils.isBiometricWeakInstalled()) { + } else if (lockPatternUtils.usingBiometricWeak() && + lockPatternUtils.isBiometricWeakInstalled()) { resid = R.xml.security_settings_biometric_weak; } else { - switch (mLockPatternUtils.getKeyguardStoredPasswordQuality()) { + switch (lockPatternUtils.getKeyguardStoredPasswordQuality()) { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: resid = R.xml.security_settings_pattern; break; case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: resid = R.xml.security_settings_pin; break; case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: @@ -165,8 +179,26 @@ public class SecuritySettings extends RestrictedSettingsFragment break; } } - addPreferencesFromResource(resid); + return resid; + } + /** + * Important! + * + * Don't forget to update the SecuritySearchIndexProvider if you are doing any change in the + * logic or adding/removing preferences here. + */ + private PreferenceScreen createPreferenceHierarchy() { + PreferenceScreen root = getPreferenceScreen(); + if (root != null) { + root.removeAll(); + } + addPreferencesFromResource(R.xml.security_settings); + root = getPreferenceScreen(); + + // Add options for lock/unlock screen + final int resid = getResIdForLockUnlockScreen(getActivity(), mLockPatternUtils); + addPreferencesFromResource(resid); // Add options for device encryption mIsPrimary = UserHandle.myUserId() == UserHandle.USER_OWNER; @@ -184,15 +216,40 @@ public class SecuritySettings extends RestrictedSettingsFragment } if (mIsPrimary) { - switch (mDPM.getStorageEncryptionStatus()) { - case DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE: + if (LockPatternUtils.isDeviceEncryptionEnabled()) { // The device is currently encrypted. addPreferencesFromResource(R.xml.security_settings_encrypted); - break; - case DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE: + } else { // This device supports encryption but isn't encrypted. addPreferencesFromResource(R.xml.security_settings_unencrypted); - break; + } + } + + // Trust Agent preferences + PreferenceGroup securityCategory = (PreferenceGroup) + root.findPreference(KEY_SECURITY_CATEGORY); + if (securityCategory != null) { + final boolean hasSecurity = mLockPatternUtils.isSecure(); + ArrayList<TrustAgentComponentInfo> agents = + getActiveTrustAgents(getPackageManager(), mLockPatternUtils); + for (int i = 0; i < agents.size(); i++) { + final TrustAgentComponentInfo agent = agents.get(i); + Preference trustAgentPreference = + new Preference(securityCategory.getContext()); + trustAgentPreference.setKey(KEY_TRUST_AGENT); + trustAgentPreference.setTitle(agent.title); + trustAgentPreference.setSummary(agent.summary); + // Create intent for this preference. + Intent intent = new Intent(); + intent.setComponent(agent.componentName); + intent.setAction(Intent.ACTION_MAIN); + trustAgentPreference.setIntent(intent); + // Add preference to the settings menu. + securityCategory.addPreference(trustAgentPreference); + if (!hasSecurity) { + trustAgentPreference.setEnabled(false); + trustAgentPreference.setSummary(R.string.disabled_because_no_backup_security); + } } } @@ -205,21 +262,27 @@ public class SecuritySettings extends RestrictedSettingsFragment // biometric weak liveliness mBiometricWeakLiveliness = - (CheckBoxPreference) root.findPreference(KEY_BIOMETRIC_WEAK_LIVELINESS); + (SwitchPreference) root.findPreference(KEY_BIOMETRIC_WEAK_LIVELINESS); // visible pattern - mVisiblePattern = (CheckBoxPreference) root.findPreference(KEY_VISIBLE_PATTERN); + mVisiblePattern = (SwitchPreference) root.findPreference(KEY_VISIBLE_PATTERN); // lock instantly on power key press - mPowerButtonInstantlyLocks = (CheckBoxPreference) root.findPreference( + mPowerButtonInstantlyLocks = (SwitchPreference) root.findPreference( KEY_POWER_INSTANTLY_LOCKS); + Preference trustAgentPreference = root.findPreference(KEY_TRUST_AGENT); + if (mPowerButtonInstantlyLocks != null && + trustAgentPreference != null && + trustAgentPreference.getTitle().length() > 0) { + mPowerButtonInstantlyLocks.setSummary(getString( + R.string.lockpattern_settings_power_button_instantly_locks_summary, + trustAgentPreference.getTitle())); + } // don't display visible pattern if biometric and backup is not pattern if (resid == R.xml.security_settings_biometric_weak && mLockPatternUtils.getKeyguardStoredPasswordQuality() != DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) { - PreferenceGroup securityCategory = (PreferenceGroup) - root.findPreference(KEY_SECURITY_CATEGORY); if (securityCategory != null && mVisiblePattern != null) { securityCategory.removePreference(root.findPreference(KEY_VISIBLE_PATTERN)); } @@ -241,34 +304,14 @@ public class SecuritySettings extends RestrictedSettingsFragment root.findPreference(KEY_SIM_LOCK).setEnabled(false); } } - - // Enable or disable keyguard widget checkbox based on DPM state - mEnableKeyguardWidgets = (CheckBoxPreference) root.findPreference(KEY_ENABLE_WIDGETS); - if (mEnableKeyguardWidgets != null) { - if (ActivityManager.isLowRamDeviceStatic() - || mLockPatternUtils.isLockScreenDisabled()) { - // Widgets take a lot of RAM, so disable them on low-memory devices - PreferenceGroup securityCategory - = (PreferenceGroup) root.findPreference(KEY_SECURITY_CATEGORY); - if (securityCategory != null) { - securityCategory.removePreference(root.findPreference(KEY_ENABLE_WIDGETS)); - mEnableKeyguardWidgets = null; - } - } else { - final boolean disabled = (0 != (mDPM.getKeyguardDisabledFeatures(null) - & DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL)); - if (disabled) { - mEnableKeyguardWidgets.setSummary( - R.string.security_enable_widgets_disabled_summary); - } else { - mEnableKeyguardWidgets.setSummary(""); - } - mEnableKeyguardWidgets.setEnabled(!disabled); - } + if (Settings.System.getInt(getContentResolver(), + Settings.System.LOCK_TO_APP_ENABLED, 0) != 0) { + root.findPreference(KEY_SCREEN_PINNING).setSummary( + getResources().getString(R.string.switch_on_text)); } // Show password - mShowPassword = (CheckBoxPreference) root.findPreference(KEY_SHOW_PASSWORD); + mShowPassword = (SwitchPreference) root.findPreference(KEY_SHOW_PASSWORD); mResetCredentials = root.findPreference(KEY_RESET_CREDENTIALS); // Credential storage @@ -281,73 +324,73 @@ public class SecuritySettings extends RestrictedSettingsFragment mKeyStore.isHardwareBacked() ? R.string.credential_storage_type_hardware : R.string.credential_storage_type_software; credentialStorageType.setSummary(storageSummaryRes); - } else { - removePreference(KEY_CREDENTIALS_MANAGER); + PreferenceGroup credentialsManager = (PreferenceGroup) + root.findPreference(KEY_CREDENTIALS_MANAGER); + credentialsManager.removePreference(root.findPreference(KEY_RESET_CREDENTIALS)); + credentialsManager.removePreference(root.findPreference(KEY_CREDENTIALS_INSTALL)); + credentialsManager.removePreference(root.findPreference(KEY_CREDENTIAL_STORAGE_TYPE)); } // Application install - PreferenceGroup deviceAdminCategory= (PreferenceGroup) + PreferenceGroup deviceAdminCategory = (PreferenceGroup) root.findPreference(KEY_DEVICE_ADMIN_CATEGORY); - mToggleAppInstallation = (CheckBoxPreference) findPreference( + mToggleAppInstallation = (SwitchPreference) findPreference( KEY_TOGGLE_INSTALL_APPLICATIONS); mToggleAppInstallation.setChecked(isNonMarketAppsAllowed()); - // Side loading of apps. mToggleAppInstallation.setEnabled(mIsPrimary); - - // Package verification, only visible to primary user and if enabled - mToggleVerifyApps = (CheckBoxPreference) findPreference(KEY_TOGGLE_VERIFY_APPLICATIONS); - if (mIsPrimary && showVerifierSetting()) { - if (isVerifierInstalled()) { - mToggleVerifyApps.setChecked(isVerifyAppsEnabled()); - } else { - mToggleVerifyApps.setChecked(false); - mToggleVerifyApps.setEnabled(false); - } - } else { - if (deviceAdminCategory != null) { - deviceAdminCategory.removePreference(mToggleVerifyApps); - } else { - mToggleVerifyApps.setEnabled(false); - } + if (um.hasUserRestriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES) + || um.hasUserRestriction(UserManager.DISALLOW_INSTALL_APPS)) { + mToggleAppInstallation.setEnabled(false); } - mNotificationAccess = findPreference(KEY_NOTIFICATION_ACCESS); - if (mNotificationAccess != null) { - final int total = NotificationAccessSettings.getListenersCount(mPM); - if (total == 0) { - if (deviceAdminCategory != null) { - deviceAdminCategory.removePreference(mNotificationAccess); - } - } else { - final int n = getNumEnabledNotificationListeners(); - if (n == 0) { - mNotificationAccess.setSummary(getResources().getString( - R.string.manage_notification_access_summary_zero)); - } else { - mNotificationAccess.setSummary(String.format(getResources().getQuantityString( - R.plurals.manage_notification_access_summary_nonzero, - n, n))); - } + // Advanced Security features + PreferenceGroup advancedCategory = + (PreferenceGroup)root.findPreference(KEY_ADVANCED_SECURITY); + if (advancedCategory != null) { + Preference manageAgents = advancedCategory.findPreference(KEY_MANAGE_TRUST_AGENTS); + if (manageAgents != null && !mLockPatternUtils.isSecure()) { + manageAgents.setEnabled(false); + manageAgents.setSummary(R.string.disabled_because_no_backup_security); } } - if (shouldBePinProtected(RESTRICTIONS_PIN_SET)) { - protectByRestrictions(mToggleAppInstallation); - protectByRestrictions(mToggleVerifyApps); - protectByRestrictions(mResetCredentials); - protectByRestrictions(root.findPreference(KEY_CREDENTIALS_INSTALL)); + // The above preferences come and go based on security state, so we need to update + // the index. This call is expected to be fairly cheap, but we may want to do something + // smarter in the future. + Index.getInstance(getActivity()) + .updateFromClassNameResource(SecuritySettings.class.getName(), true, true); + + for (int i = 0; i < SWITCH_PREFERENCE_KEYS.length; i++) { + final Preference pref = findPreference(SWITCH_PREFERENCE_KEYS[i]); + if (pref != null) pref.setOnPreferenceChangeListener(this); } return root; } - private int getNumEnabledNotificationListeners() { - final String flat = Settings.Secure.getString(getContentResolver(), - Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); - if (flat == null || "".equals(flat)) return 0; - final String[] components = flat.split(":"); - return components.length; + private static ArrayList<TrustAgentComponentInfo> getActiveTrustAgents( + PackageManager pm, LockPatternUtils utils) { + ArrayList<TrustAgentComponentInfo> result = new ArrayList<TrustAgentComponentInfo>(); + List<ResolveInfo> resolveInfos = pm.queryIntentServices(TRUST_AGENT_INTENT, + PackageManager.GET_META_DATA); + List<ComponentName> enabledTrustAgents = utils.getEnabledTrustAgents(); + if (enabledTrustAgents != null && !enabledTrustAgents.isEmpty()) { + for (int i = 0; i < resolveInfos.size(); i++) { + ResolveInfo resolveInfo = resolveInfos.get(i); + if (resolveInfo.serviceInfo == null) continue; + if (!TrustAgentUtils.checkProvidePermission(resolveInfo, pm)) continue; + TrustAgentComponentInfo trustAgentComponentInfo = + TrustAgentUtils.getSettingsComponent(pm, resolveInfo); + if (trustAgentComponentInfo.componentName == null || + !enabledTrustAgents.contains( + TrustAgentUtils.getComponentName(resolveInfo)) || + TextUtils.isEmpty(trustAgentComponentInfo.title)) continue; + result.add(trustAgentComponentInfo); + if (ONLY_ONE_TRUST_AGENT) break; + } + } + return result; } private boolean isNonMarketAppsAllowed() { @@ -365,25 +408,6 @@ public class SecuritySettings extends RestrictedSettingsFragment enabled ? 1 : 0); } - private boolean isVerifyAppsEnabled() { - return Settings.Global.getInt(getContentResolver(), - Settings.Global.PACKAGE_VERIFIER_ENABLE, 1) > 0; - } - - private boolean isVerifierInstalled() { - final PackageManager pm = getPackageManager(); - final Intent verification = new Intent(Intent.ACTION_PACKAGE_NEEDS_VERIFICATION); - verification.setType(PACKAGE_MIME_TYPE); - verification.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - final List<ResolveInfo> receivers = pm.queryBroadcastReceivers(verification, 0); - return (receivers.size() > 0) ? true : false; - } - - private boolean showVerifierSetting() { - return Settings.Global.getInt(getContentResolver(), - Settings.Global.PACKAGE_VERIFIER_SETTING_VISIBLE, 1) > 0; - } - private void warnAppInstallation() { // TODO: DialogFragment? mWarnInstallApps = new AlertDialog.Builder(getActivity()).setTitle( @@ -391,16 +415,17 @@ public class SecuritySettings extends RestrictedSettingsFragment .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) + .setNegativeButton(android.R.string.no, this) .show(); } @Override public void onClick(DialogInterface dialog, int which) { - if (dialog == mWarnInstallApps && which == DialogInterface.BUTTON_POSITIVE) { - setNonMarketAppsAllowed(true); + if (dialog == mWarnInstallApps) { + boolean turnOn = which == DialogInterface.BUTTON_POSITIVE; + setNonMarketAppsAllowed(turnOn); if (mToggleAppInstallation != null) { - mToggleAppInstallation.setChecked(true); + mToggleAppInstallation.setChecked(turnOn); } } } @@ -443,7 +468,14 @@ public class SecuritySettings extends RestrictedSettingsFragment best = i; } } - mLockAfter.setSummary(getString(R.string.lock_after_timeout_summary, entries[best])); + + Preference preference = getPreferenceScreen().findPreference(KEY_TRUST_AGENT); + if (preference != null && preference.getTitle().length() > 0) { + mLockAfter.setSummary(getString(R.string.lock_after_timeout_summary_with_exception, + entries[best], preference.getTitle())); + } else { + mLockAfter.setSummary(getString(R.string.lock_after_timeout_summary, entries[best])); + } } private void disableUnusableTimeouts(long maxTimeout) { @@ -476,6 +508,14 @@ public class SecuritySettings extends RestrictedSettingsFragment } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mTrustAgentClickIntent != null) { + outState.putParcelable(TRUST_AGENT_CLICK_INTENT, mTrustAgentClickIntent); + } + } + + @Override public void onResume() { super.onResume(); @@ -503,23 +543,14 @@ public class SecuritySettings extends RestrictedSettingsFragment if (mResetCredentials != null) { mResetCredentials.setEnabled(!mKeyStore.isEmpty()); } - - if (mEnableKeyguardWidgets != null) { - mEnableKeyguardWidgets.setChecked(lockPatternUtils.getWidgetsEnabled()); - } } @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (ensurePinRestrictedPreference(preference)) { - return true; - } final String key = preference.getKey(); - - final LockPatternUtils lockPatternUtils = mChooseLockSettingsHelper.utils(); if (KEY_UNLOCK_SET_OR_CHANGE.equals(key)) { startFragment(this, "com.android.settings.ChooseLockGeneric$ChooseLockGenericFragment", - SET_OR_CHANGE_LOCK_METHOD_REQUEST, null); + R.string.lock_settings_picker_title, SET_OR_CHANGE_LOCK_METHOD_REQUEST, null); } else if (KEY_BIOMETRIC_WEAK_IMPROVE_MATCHING.equals(key)) { ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this.getActivity(), this); @@ -531,59 +562,23 @@ public class SecuritySettings extends RestrictedSettingsFragment // can't be reached, but is here in case things change in the future startBiometricWeakImprove(); } - } else if (KEY_BIOMETRIC_WEAK_LIVELINESS.equals(key)) { - if (isToggled(preference)) { - lockPatternUtils.setBiometricWeakLivelinessEnabled(true); - } else { - // In this case the user has just unchecked the checkbox, but this action requires - // them to confirm their password. We need to re-check the checkbox until - // they've confirmed their password - mBiometricWeakLiveliness.setChecked(true); - ChooseLockSettingsHelper helper = - new ChooseLockSettingsHelper(this.getActivity(), this); - if (!helper.launchConfirmationActivity( - CONFIRM_EXISTING_FOR_BIOMETRIC_WEAK_LIVELINESS_OFF, null, null)) { - // If this returns false, it means no password confirmation is required, so - // go ahead and uncheck it here. - // Note: currently a backup is required for biometric_weak so this code path - // can't be reached, but is here in case things change in the future - lockPatternUtils.setBiometricWeakLivelinessEnabled(false); - mBiometricWeakLiveliness.setChecked(false); - } - } - } else if (KEY_LOCK_ENABLED.equals(key)) { - lockPatternUtils.setLockPatternEnabled(isToggled(preference)); - } else if (KEY_VISIBLE_PATTERN.equals(key)) { - lockPatternUtils.setVisiblePatternEnabled(isToggled(preference)); - } else if (KEY_POWER_INSTANTLY_LOCKS.equals(key)) { - lockPatternUtils.setPowerButtonInstantlyLocks(isToggled(preference)); - } else if (KEY_ENABLE_WIDGETS.equals(key)) { - lockPatternUtils.setWidgetsEnabled(mEnableKeyguardWidgets.isChecked()); - } else if (preference == mShowPassword) { - Settings.System.putInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, - mShowPassword.isChecked() ? 1 : 0); - } else if (preference == mToggleAppInstallation) { - if (mToggleAppInstallation.isChecked()) { - mToggleAppInstallation.setChecked(false); - warnAppInstallation(); - } else { - setNonMarketAppsAllowed(false); + } else if (KEY_TRUST_AGENT.equals(key)) { + ChooseLockSettingsHelper helper = + new ChooseLockSettingsHelper(this.getActivity(), this); + mTrustAgentClickIntent = preference.getIntent(); + if (!helper.launchConfirmationActivity(CHANGE_TRUST_AGENT_SETTINGS, null, null) && + mTrustAgentClickIntent != null) { + // If this returns false, it means no password confirmation is required. + startActivity(mTrustAgentClickIntent); + mTrustAgentClickIntent = null; } - } else if (KEY_TOGGLE_VERIFY_APPLICATIONS.equals(key)) { - Settings.Global.putInt(getContentResolver(), Settings.Global.PACKAGE_VERIFIER_ENABLE, - mToggleVerifyApps.isChecked() ? 1 : 0); } else { // If we didn't handle it, let preferences handle it. return super.onPreferenceTreeClick(preferenceScreen, preference); } - return true; } - private boolean isToggled(Preference pref) { - return ((CheckBoxPreference) pref).isChecked(); - } - /** * see confirmPatternThenDisableAndClear */ @@ -602,13 +597,22 @@ public class SecuritySettings extends RestrictedSettingsFragment // is called by grabbing the value from lockPatternUtils. We can't set it here // because mBiometricWeakLiveliness could be null return; + } else if (requestCode == CHANGE_TRUST_AGENT_SETTINGS && resultCode == Activity.RESULT_OK) { + if (mTrustAgentClickIntent != null) { + startActivity(mTrustAgentClickIntent); + mTrustAgentClickIntent = null; + } + return; } createPreferenceHierarchy(); } @Override public boolean onPreferenceChange(Preference preference, Object value) { - if (preference == mLockAfter) { + boolean result = true; + final String key = preference.getKey(); + final LockPatternUtils lockPatternUtils = mChooseLockSettingsHelper.utils(); + if (KEY_LOCK_AFTER_TIMEOUT.equals(key)) { int timeout = Integer.parseInt((String) value); try { Settings.Secure.putInt(getContentResolver(), @@ -617,8 +621,46 @@ public class SecuritySettings extends RestrictedSettingsFragment Log.e("SecuritySettings", "could not persist lockAfter timeout setting", e); } updateLockAfterPreferenceSummary(); + } else if (KEY_LOCK_ENABLED.equals(key)) { + lockPatternUtils.setLockPatternEnabled((Boolean) value); + } else if (KEY_VISIBLE_PATTERN.equals(key)) { + lockPatternUtils.setVisiblePatternEnabled((Boolean) value); + } else if (KEY_BIOMETRIC_WEAK_LIVELINESS.equals(key)) { + if ((Boolean) value) { + lockPatternUtils.setBiometricWeakLivelinessEnabled(true); + } else { + // In this case the user has just unchecked the checkbox, but this action requires + // them to confirm their password. We need to re-check the checkbox until + // they've confirmed their password + mBiometricWeakLiveliness.setChecked(true); + ChooseLockSettingsHelper helper = + new ChooseLockSettingsHelper(this.getActivity(), this); + if (!helper.launchConfirmationActivity( + CONFIRM_EXISTING_FOR_BIOMETRIC_WEAK_LIVELINESS_OFF, null, null)) { + // If this returns false, it means no password confirmation is required, so + // go ahead and uncheck it here. + // Note: currently a backup is required for biometric_weak so this code path + // can't be reached, but is here in case things change in the future + lockPatternUtils.setBiometricWeakLivelinessEnabled(false); + mBiometricWeakLiveliness.setChecked(false); + } + } + } else if (KEY_POWER_INSTANTLY_LOCKS.equals(key)) { + mLockPatternUtils.setPowerButtonInstantlyLocks((Boolean) value); + } else if (KEY_SHOW_PASSWORD.equals(key)) { + Settings.System.putInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, + ((Boolean) value) ? 1 : 0); + } else if (KEY_TOGGLE_INSTALL_APPLICATIONS.equals(key)) { + if ((Boolean) value) { + mToggleAppInstallation.setChecked(false); + warnAppInstallation(); + // Don't change Switch status until user makes choice in dialog, so return false. + result = false; + } else { + setNonMarketAppsAllowed(false); + } } - return true; + return result; } @Override @@ -631,4 +673,153 @@ public class SecuritySettings extends RestrictedSettingsFragment intent.setClassName("com.android.facelock", "com.android.facelock.AddToSetup"); startActivity(intent); } + + /** + * For Search. Please keep it in sync when updating "createPreferenceHierarchy()" + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new SecuritySearchIndexProvider(); + + private static class SecuritySearchIndexProvider extends BaseSearchIndexProvider { + + boolean mIsPrimary; + + public SecuritySearchIndexProvider() { + super(); + + mIsPrimary = UserHandle.myUserId() == UserHandle.USER_OWNER; + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + + List<SearchIndexableResource> result = new ArrayList<SearchIndexableResource>(); + + LockPatternUtils lockPatternUtils = new LockPatternUtils(context); + // Add options for lock/unlock screen + int resId = getResIdForLockUnlockScreen(context, lockPatternUtils); + + SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = resId; + result.add(sir); + + if (mIsPrimary) { + DevicePolicyManager dpm = (DevicePolicyManager) + context.getSystemService(Context.DEVICE_POLICY_SERVICE); + + switch (dpm.getStorageEncryptionStatus()) { + case DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE: + // The device is currently encrypted. + resId = R.xml.security_settings_encrypted; + break; + case DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE: + // This device supports encryption but isn't encrypted. + resId = R.xml.security_settings_unencrypted; + break; + } + + sir = new SearchIndexableResource(context); + sir.xmlResId = resId; + result.add(sir); + } + + // Append the rest of the settings + sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.security_settings_misc; + result.add(sir); + + return result; + } + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + final Resources res = context.getResources(); + + final String screenTitle = res.getString(R.string.security_settings_title); + + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = screenTitle; + data.screenTitle = screenTitle; + result.add(data); + + if (!mIsPrimary) { + int resId = (UserManager.get(context).isLinkedUser()) ? + R.string.profile_info_settings_title : R.string.user_info_settings_title; + + data = new SearchIndexableRaw(context); + data.title = res.getString(resId); + data.screenTitle = screenTitle; + result.add(data); + } + + // Credential storage + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + + if (!um.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { + KeyStore keyStore = KeyStore.getInstance(); + + final int storageSummaryRes = keyStore.isHardwareBacked() ? + R.string.credential_storage_type_hardware : + R.string.credential_storage_type_software; + + data = new SearchIndexableRaw(context); + data.title = res.getString(storageSummaryRes); + data.screenTitle = screenTitle; + result.add(data); + } + + // Advanced + final LockPatternUtils lockPatternUtils = new LockPatternUtils(context); + if (lockPatternUtils.isSecure()) { + ArrayList<TrustAgentComponentInfo> agents = + getActiveTrustAgents(context.getPackageManager(), lockPatternUtils); + for (int i = 0; i < agents.size(); i++) { + final TrustAgentComponentInfo agent = agents.get(i); + data = new SearchIndexableRaw(context); + data.title = agent.title; + data.screenTitle = screenTitle; + result.add(data); + } + } + return result; + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final List<String> keys = new ArrayList<String>(); + + LockPatternUtils lockPatternUtils = new LockPatternUtils(context); + // Add options for lock/unlock screen + int resId = getResIdForLockUnlockScreen(context, lockPatternUtils); + + // don't display visible pattern if biometric and backup is not pattern + if (resId == R.xml.security_settings_biometric_weak && + lockPatternUtils.getKeyguardStoredPasswordQuality() != + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) { + keys.add(KEY_VISIBLE_PATTERN); + } + + // Do not display SIM lock for devices without an Icc card + TelephonyManager tm = TelephonyManager.getDefault(); + if (!mIsPrimary || !tm.hasIccCard()) { + keys.add(KEY_SIM_LOCK); + } + + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + if (um.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { + keys.add(KEY_CREDENTIALS_MANAGER); + } + + // TrustAgent settings disappear when the user has no primary security. + if (!lockPatternUtils.isSecure()) { + keys.add(KEY_TRUST_AGENT); + keys.add(KEY_MANAGE_TRUST_AGENTS); + } + + return keys; + } + } + } diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index f6f49b8..84bf615 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -16,1105 +16,90 @@ package com.android.settings; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.nfc.NfcAdapter; -import android.os.Bundle; -import android.os.INetworkManagementService; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.os.UserManager; -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.ImageButton; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.Switch; -import android.widget.TextView; - -import com.android.internal.util.ArrayUtils; -import com.android.settings.accessibility.AccessibilitySettings; -import com.android.settings.accessibility.CaptionPropertiesFragment; -import com.android.settings.accessibility.ToggleAccessibilityServicePreferenceFragment; -import com.android.settings.accounts.AccountSyncSettings; -import com.android.settings.accounts.AuthenticatorHelper; -import com.android.settings.accounts.ManageAccountsSettings; import com.android.settings.applications.AppOpsSummary; -import com.android.settings.applications.ManageApplications; -import com.android.settings.applications.ProcessStatsUi; -import com.android.settings.bluetooth.BluetoothEnabler; -import com.android.settings.bluetooth.BluetoothSettings; -import com.android.settings.deviceinfo.Memory; -import com.android.settings.deviceinfo.UsbSettings; -import com.android.settings.fuelgauge.PowerUsageSummary; -import com.android.settings.inputmethod.InputMethodAndLanguageSettings; -import com.android.settings.inputmethod.KeyboardLayoutPickerFragment; -import com.android.settings.inputmethod.SpellCheckersSettings; -import com.android.settings.inputmethod.UserDictionaryList; -import com.android.settings.location.LocationSettings; -import com.android.settings.nfc.AndroidBeam; -import com.android.settings.nfc.PaymentSettings; -import com.android.settings.print.PrintJobSettingsFragment; -import com.android.settings.print.PrintServiceSettingsFragment; -import com.android.settings.print.PrintSettingsFragment; -import com.android.settings.tts.TextToSpeechSettings; -import com.android.settings.users.UserSettings; -import com.android.settings.vpn2.VpnSettings; -import com.android.settings.wfd.WifiDisplaySettings; -import com.android.settings.wifi.AdvancedWifiSettings; -import com.android.settings.wifi.WifiEnabler; -import com.android.settings.wifi.WifiSettings; -import com.android.settings.wifi.p2p.WifiP2pSettings; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; /** - * Top-level settings activity to handle single pane and double pane UI layout. + * Top-level Settings activity */ -public class Settings extends PreferenceActivity - implements ButtonBarHandler, OnAccountsUpdateListener { - - private static final String LOG_TAG = "Settings"; - - private static final String META_DATA_KEY_HEADER_ID = - "com.android.settings.TOP_LEVEL_HEADER_ID"; - private static final String META_DATA_KEY_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_UI_OPTIONS = "settings:ui_options"; - - 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"; - - static final int DIALOG_ONLY_ONE_HOME = 1; - - private static boolean sShowNoHomeNotice = false; - - private String mFragmentClass; - private int mTopLevelHeaderId; - private Header mFirstHeader; - private Header mCurrentHeader; - private Header mParentHeader; - private boolean mInLocalHeaderSwitch; - - // Show only these settings for restricted users - private int[] SETTINGS_FOR_RESTRICTED = { - R.id.wireless_section, - R.id.wifi_settings, - R.id.bluetooth_settings, - R.id.data_usage_settings, - R.id.wireless_settings, - R.id.device_section, - R.id.sound_settings, - R.id.display_settings, - R.id.storage_settings, - R.id.application_settings, - R.id.battery_settings, - R.id.personal_section, - R.id.location_settings, - R.id.security_settings, - R.id.language_settings, - R.id.user_settings, - R.id.account_settings, - R.id.account_add, - R.id.system_section, - R.id.date_time_settings, - R.id.about_settings, - R.id.accessibility_settings, - R.id.print_settings, - R.id.nfc_payment_settings, - R.id.home_settings - }; - - private SharedPreferences mDevelopmentPreferences; - private SharedPreferences.OnSharedPreferenceChangeListener mDevelopmentPreferencesListener; - - // TODO: Update Call Settings based on airplane mode state. - - protected HashMap<Integer, Integer> mHeaderIndexMap = new HashMap<Integer, Integer>(); - - private AuthenticatorHelper mAuthenticatorHelper; - private Header mLastHeader; - private boolean mListeningToAccountUpdates; - - private boolean mBatteryPresent = true; - private BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { - boolean batteryPresent = Utils.isBatteryPresent(intent); - - if (mBatteryPresent != batteryPresent) { - mBatteryPresent = batteryPresent; - invalidateHeaders(); - } - } - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - if (getIntent().hasExtra(EXTRA_UI_OPTIONS)) { - getWindow().setUiOptions(getIntent().getIntExtra(EXTRA_UI_OPTIONS, 0)); - } - - mAuthenticatorHelper = new AuthenticatorHelper(); - mAuthenticatorHelper.updateAuthDescriptions(this); - mAuthenticatorHelper.onAccountsUpdated(this, null); - - mDevelopmentPreferences = getSharedPreferences(DevelopmentSettings.PREF_FILE, - Context.MODE_PRIVATE); - - getMetaData(); - mInLocalHeaderSwitch = true; - super.onCreate(savedInstanceState); - mInLocalHeaderSwitch = false; - - if (!onIsHidingHeaders() && onIsMultiPane()) { - highlightHeader(mTopLevelHeaderId); - // Force the title so that it doesn't get overridden by a direct launch of - // a specific settings screen. - setTitle(R.string.settings_label); - } - - // Retrieve any saved state - if (savedInstanceState != null) { - mCurrentHeader = savedInstanceState.getParcelable(SAVE_KEY_CURRENT_HEADER); - mParentHeader = savedInstanceState.getParcelable(SAVE_KEY_PARENT_HEADER); - } - - // If the current header was saved, switch to it - if (savedInstanceState != null && mCurrentHeader != null) { - //switchToHeaderLocal(mCurrentHeader); - showBreadCrumbs(mCurrentHeader.title, null); - } - - if (mParentHeader != null) { - setParentTitle(mParentHeader.title, null, new OnClickListener() { - @Override - public void onClick(View v) { - switchToParent(mParentHeader.fragment); - } - }); - } - - // Override up navigation for multi-pane, since we handle it in the fragment breadcrumbs - if (onIsMultiPane()) { - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - // Save the current fragment, if it is the same as originally launched - if (mCurrentHeader != null) { - outState.putParcelable(SAVE_KEY_CURRENT_HEADER, mCurrentHeader); - } - if (mParentHeader != null) { - outState.putParcelable(SAVE_KEY_PARENT_HEADER, mParentHeader); - } - } - - @Override - public void onResume() { - super.onResume(); - - mDevelopmentPreferencesListener = new SharedPreferences.OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - invalidateHeaders(); - } - }; - mDevelopmentPreferences.registerOnSharedPreferenceChangeListener( - mDevelopmentPreferencesListener); - - ListAdapter listAdapter = getListAdapter(); - if (listAdapter instanceof HeaderAdapter) { - ((HeaderAdapter) listAdapter).resume(); - } - invalidateHeaders(); - - registerReceiver(mBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - } - - @Override - public void onPause() { - super.onPause(); - - unregisterReceiver(mBatteryInfoReceiver); - - ListAdapter listAdapter = getListAdapter(); - if (listAdapter instanceof HeaderAdapter) { - ((HeaderAdapter) listAdapter).pause(); - } - - mDevelopmentPreferences.unregisterOnSharedPreferenceChangeListener( - mDevelopmentPreferencesListener); - mDevelopmentPreferencesListener = null; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mListeningToAccountUpdates) { - AccountManager.get(this).removeOnAccountsUpdatedListener(this); - } - } - - @Override - public boolean onIsMultiPane() { - return false; - } - - private static final String[] ENTRY_FRAGMENTS = { - WirelessSettings.class.getName(), - WifiSettings.class.getName(), - AdvancedWifiSettings.class.getName(), - BluetoothSettings.class.getName(), - TetherSettings.class.getName(), - WifiP2pSettings.class.getName(), - VpnSettings.class.getName(), - DateTimeSettings.class.getName(), - LocalePicker.class.getName(), - InputMethodAndLanguageSettings.class.getName(), - SpellCheckersSettings.class.getName(), - UserDictionaryList.class.getName(), - UserDictionarySettings.class.getName(), - SoundSettings.class.getName(), - DisplaySettings.class.getName(), - DeviceInfoSettings.class.getName(), - ManageApplications.class.getName(), - ProcessStatsUi.class.getName(), - NotificationStation.class.getName(), - LocationSettings.class.getName(), - SecuritySettings.class.getName(), - PrivacySettings.class.getName(), - DeviceAdminSettings.class.getName(), - AccessibilitySettings.class.getName(), - CaptionPropertiesFragment.class.getName(), - TextToSpeechSettings.class.getName(), - Memory.class.getName(), - DevelopmentSettings.class.getName(), - UsbSettings.class.getName(), - AndroidBeam.class.getName(), - WifiDisplaySettings.class.getName(), - PowerUsageSummary.class.getName(), - AccountSyncSettings.class.getName(), - CryptKeeperSettings.class.getName(), - DataUsageSummary.class.getName(), - DreamSettings.class.getName(), - UserSettings.class.getName(), - NotificationAccessSettings.class.getName(), - ManageAccountsSettings.class.getName(), - PrintSettingsFragment.class.getName(), - PrintJobSettingsFragment.class.getName(), - TrustedCredentialsSettings.class.getName(), - PaymentSettings.class.getName(), - KeyboardLayoutPickerFragment.class.getName() - }; - - @Override - protected boolean isValidFragment(String fragmentName) { - // Almost all fragments are wrapped in this, - // except for a few that have their own activities. - for (int i = 0; i < ENTRY_FRAGMENTS.length; i++) { - if (ENTRY_FRAGMENTS[i].equals(fragmentName)) return true; - } - return false; - } - - private void switchToHeaderLocal(Header header) { - mInLocalHeaderSwitch = true; - switchToHeader(header); - mInLocalHeaderSwitch = false; - } - - @Override - public void switchToHeader(Header header) { - if (!mInLocalHeaderSwitch) { - mCurrentHeader = null; - mParentHeader = null; - } - super.switchToHeader(header); - } - - /** - * Switch to parent fragment and store the grand parent's info - * @param className name of the activity wrapper for the parent fragment. - */ - private void switchToParent(String className) { - final ComponentName cn = new ComponentName(this, className); - try { - final PackageManager pm = getPackageManager(); - final ActivityInfo parentInfo = pm.getActivityInfo(cn, PackageManager.GET_META_DATA); - - if (parentInfo != null && parentInfo.metaData != null) { - String fragmentClass = parentInfo.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS); - CharSequence fragmentTitle = parentInfo.loadLabel(pm); - Header parentHeader = new Header(); - parentHeader.fragment = fragmentClass; - parentHeader.title = fragmentTitle; - mCurrentHeader = parentHeader; - - switchToHeaderLocal(parentHeader); - highlightHeader(mTopLevelHeaderId); - - mParentHeader = new Header(); - mParentHeader.fragment - = parentInfo.metaData.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS); - mParentHeader.title = parentInfo.metaData.getString(META_DATA_KEY_PARENT_TITLE); - } - } catch (NameNotFoundException nnfe) { - Log.w(LOG_TAG, "Could not find parent activity : " + className); - } - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - // If it is not launched from history, then reset to top-level - if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { - if (mFirstHeader != null && !onIsHidingHeaders() && onIsMultiPane()) { - switchToHeaderLocal(mFirstHeader); - } - getListView().setSelectionFromTop(0, 0); - } - } - - private void highlightHeader(int id) { - if (id != 0) { - Integer index = mHeaderIndexMap.get(id); - if (index != null) { - getListView().setItemChecked(index, true); - if (isMultiPane()) { - getListView().smoothScrollToPosition(index); - } - } - } - } - - @Override - public Intent 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(superIntent); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment); - Bundle args = superIntent.getExtras(); - if (args != null) { - args = new Bundle(args); - } else { - args = new Bundle(); - } - args.putParcelable("intent", superIntent); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, superIntent.getExtras()); - return modIntent; - } - return superIntent; - } - - /** - * Checks if the component name in the intent is different from the Settings class and - * returns the class name to load as a fragment. - */ - protected String getStartingFragmentClass(Intent intent) { - if (mFragmentClass != null) return mFragmentClass; - - String intentClass = intent.getComponent().getClassName(); - if (intentClass.equals(getClass().getName())) return null; - - if ("com.android.settings.ManageApplications".equals(intentClass) - || "com.android.settings.RunningServices".equals(intentClass) - || "com.android.settings.applications.StorageUse".equals(intentClass)) { - // Old names of manage apps. - intentClass = com.android.settings.applications.ManageApplications.class.getName(); - } - - return intentClass; - } - - /** - * Override initial header when an activity-alias is causing Settings to be launched - * for a specific fragment encoded in the android:name parameter. - */ - @Override - public Header onGetInitialHeader() { - String fragmentClass = getStartingFragmentClass(super.getIntent()); - if (fragmentClass != null) { - Header header = new Header(); - header.fragment = fragmentClass; - header.title = getTitle(); - header.fragmentArguments = getIntent().getExtras(); - mCurrentHeader = header; - return header; - } - - return mFirstHeader; - } - - @Override - public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, - int titleRes, int shortTitleRes) { - Intent intent = super.onBuildStartFragmentIntent(fragmentName, args, - titleRes, shortTitleRes); - - // Some fragments want split ActionBar; these should stay in sync with - // uiOptions for fragments also defined as activities in manifest. - if (WifiSettings.class.getName().equals(fragmentName) || - WifiP2pSettings.class.getName().equals(fragmentName) || - BluetoothSettings.class.getName().equals(fragmentName) || - DreamSettings.class.getName().equals(fragmentName) || - LocationSettings.class.getName().equals(fragmentName) || - ToggleAccessibilityServicePreferenceFragment.class.getName().equals(fragmentName) || - PrintSettingsFragment.class.getName().equals(fragmentName) || - PrintServiceSettingsFragment.class.getName().equals(fragmentName)) { - intent.putExtra(EXTRA_UI_OPTIONS, ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW); - } - - intent.setClass(this, SubSettings.class); - return intent; - } - - /** - * Populate the activity with the top-level headers. - */ - @Override - public void onBuildHeaders(List<Header> headers) { - if (!onIsHidingHeaders()) { - loadHeadersFromResource(R.xml.settings_headers, headers); - updateHeaderList(headers); - } - } - - private void updateHeaderList(List<Header> target) { - final boolean showDev = mDevelopmentPreferences.getBoolean( - DevelopmentSettings.PREF_SHOW, - android.os.Build.TYPE.equals("eng")); - int i = 0; - - final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); - mHeaderIndexMap.clear(); - while (i < target.size()) { - Header header = target.get(i); - // Ids are integers, so downcasting - int id = (int) header.id; - if (id == R.id.operator_settings || id == R.id.manufacturer_settings) { - Utils.updateHeaderToSpecificActivityFromMetaDataOrRemove(this, target, header); - } else if (id == R.id.wifi_settings) { - // Remove WiFi Settings if WiFi service is not available. - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { - target.remove(i); - } - } else if (id == R.id.bluetooth_settings) { - // Remove Bluetooth Settings if Bluetooth service is not available. - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { - target.remove(i); - } - } else if (id == R.id.data_usage_settings) { - // Remove data usage when kernel module not enabled - final INetworkManagementService netManager = INetworkManagementService.Stub - .asInterface(ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); - try { - if (!netManager.isBandwidthControlEnabled()) { - target.remove(i); - } - } catch (RemoteException e) { - // ignored - } - } else if (id == R.id.battery_settings) { - // Remove battery settings when battery is not available. (e.g. TV) - - if (!mBatteryPresent) { - target.remove(i); - } - } else if (id == R.id.account_settings) { - int headerIndex = i + 1; - i = insertAccountsHeaders(target, headerIndex); - } else if (id == R.id.home_settings) { - if (!updateHomeSettingHeaders(header)) { - target.remove(i); - } - } else if (id == R.id.user_settings) { - if (!UserHandle.MU_ENABLED - || !UserManager.supportsMultipleUsers() - || Utils.isMonkeyRunning()) { - target.remove(i); - } - } else if (id == R.id.nfc_payment_settings) { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)) { - target.remove(i); - } else { - // Only show if NFC is on and we have the HCE feature - NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this); - if (!adapter.isEnabled() || !getPackageManager().hasSystemFeature( - PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { - target.remove(i); - } - } - } else if (id == R.id.development_settings) { - if (!showDev) { - target.remove(i); - } - } else if (id == R.id.account_add) { - if (um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) { - target.remove(i); - } - } - - if (i < target.size() && target.get(i) == header - && UserHandle.MU_ENABLED && UserHandle.myUserId() != 0 - && !ArrayUtils.contains(SETTINGS_FOR_RESTRICTED, id)) { - target.remove(i); - } - - // Increment if the current one wasn't removed by the Utils code. - if (i < target.size() && target.get(i) == header) { - // Hold on to the first header, when we need to reset to the top-level - if (mFirstHeader == null && - HeaderAdapter.getHeaderType(header) != HeaderAdapter.HEADER_TYPE_CATEGORY) { - mFirstHeader = header; - } - mHeaderIndexMap.put(id, i); - i++; - } - } - } - - private int insertAccountsHeaders(List<Header> target, int headerIndex) { - String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes(); - List<Header> accountHeaders = new ArrayList<Header>(accountTypes.length); - for (String accountType : accountTypes) { - CharSequence label = mAuthenticatorHelper.getLabelForType(this, accountType); - if (label == null) { - continue; - } - - Account[] accounts = AccountManager.get(this).getAccountsByType(accountType); - boolean skipToAccount = accounts.length == 1 - && !mAuthenticatorHelper.hasAccountPreferences(accountType); - Header accHeader = new Header(); - accHeader.title = label; - if (accHeader.extras == null) { - accHeader.extras = new Bundle(); - } - if (skipToAccount) { - accHeader.breadCrumbTitleRes = R.string.account_sync_settings_title; - accHeader.breadCrumbShortTitleRes = R.string.account_sync_settings_title; - accHeader.fragment = AccountSyncSettings.class.getName(); - accHeader.fragmentArguments = new Bundle(); - // Need this for the icon - accHeader.extras.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType); - accHeader.extras.putParcelable(AccountSyncSettings.ACCOUNT_KEY, accounts[0]); - accHeader.fragmentArguments.putParcelable(AccountSyncSettings.ACCOUNT_KEY, - accounts[0]); - } else { - accHeader.breadCrumbTitle = label; - accHeader.breadCrumbShortTitle = label; - accHeader.fragment = ManageAccountsSettings.class.getName(); - accHeader.fragmentArguments = new Bundle(); - accHeader.extras.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType); - accHeader.fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, - accountType); - if (!isMultiPane()) { - accHeader.fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_LABEL, - label.toString()); - } - } - accountHeaders.add(accHeader); - mAuthenticatorHelper.preloadDrawableForType(this, accountType); - } - - // Sort by label - Collections.sort(accountHeaders, new Comparator<Header>() { - @Override - public int compare(Header h1, Header h2) { - return h1.title.toString().compareTo(h2.title.toString()); - } - }); - - for (Header header : accountHeaders) { - target.add(headerIndex++, header); - } - if (!mListeningToAccountUpdates) { - AccountManager.get(this).addOnAccountsUpdatedListener(this, null, true); - mListeningToAccountUpdates = true; - } - return headerIndex; - } - - private boolean updateHomeSettingHeaders(Header header) { - // Once we decide to show Home settings, keep showing it forever - SharedPreferences sp = getSharedPreferences(HomeSettings.HOME_PREFS, Context.MODE_PRIVATE); - if (sp.getBoolean(HomeSettings.HOME_PREFS_DO_SHOW, false)) { - return true; - } - - try { - final ArrayList<ResolveInfo> homeApps = new ArrayList<ResolveInfo>(); - getPackageManager().getHomeActivities(homeApps); - if (homeApps.size() < 2) { - // When there's only one available home app, omit this settings - // category entirely at the top level UI. If the user just - // uninstalled the penultimate home app candidiate, we also - // now tell them about why they aren't seeing 'Home' in the list. - if (sShowNoHomeNotice) { - sShowNoHomeNotice = false; - NoHomeDialogFragment.show(this); - } - return false; - } else { - // Okay, we're allowing the Home settings category. Tell it, when - // invoked via this front door, that we'll need to be told about the - // case when the user uninstalls all but one home app. - if (header.fragmentArguments == null) { - header.fragmentArguments = new Bundle(); - } - header.fragmentArguments.putBoolean(HomeSettings.HOME_SHOW_NOTICE, true); - } - } catch (Exception e) { - // Can't look up the home activity; bail on configuring the icon - Log.w(LOG_TAG, "Problem looking up home activity!", e); - } - - sp.edit().putBoolean(HomeSettings.HOME_PREFS_DO_SHOW, true).apply(); - return true; - } - - private void getMetaData() { - try { - ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(), - PackageManager.GET_META_DATA); - if (ai == null || ai.metaData == null) return; - mTopLevelHeaderId = ai.metaData.getInt(META_DATA_KEY_HEADER_ID); - mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS); - - // Check if it has a parent specified and create a Header object - final int parentHeaderTitleRes = ai.metaData.getInt(META_DATA_KEY_PARENT_TITLE); - String parentFragmentClass = ai.metaData.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS); - if (parentFragmentClass != null) { - mParentHeader = new Header(); - mParentHeader.fragment = parentFragmentClass; - if (parentHeaderTitleRes != 0) { - mParentHeader.title = getResources().getString(parentHeaderTitleRes); - } - } - } catch (NameNotFoundException nnfe) { - // No recovery - } - } - - @Override - public boolean hasNextButton() { - return super.hasNextButton(); - } - - @Override - public Button getNextButton() { - return super.getNextButton(); - } - - public static class NoHomeDialogFragment extends DialogFragment { - public static void show(Activity parent) { - final NoHomeDialogFragment dialog = new NoHomeDialogFragment(); - dialog.show(parent.getFragmentManager(), null); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) - .setMessage(R.string.only_one_home_message) - .setPositiveButton(android.R.string.ok, null) - .create(); - } - } - - 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; - static final int HEADER_TYPE_BUTTON = 3; - private static final int HEADER_TYPE_COUNT = HEADER_TYPE_BUTTON + 1; - - private final WifiEnabler mWifiEnabler; - private final BluetoothEnabler mBluetoothEnabler; - private AuthenticatorHelper mAuthHelper; - private DevicePolicyManager mDevicePolicyManager; - - private static class HeaderViewHolder { - ImageView icon; - TextView title; - TextView summary; - Switch switch_; - ImageButton button_; - View divider_; - } - - 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 if (header.id == R.id.security_settings) { - return HEADER_TYPE_BUTTON; - } 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, - AuthenticatorHelper authenticatorHelper, DevicePolicyManager dpm) { - super(context, 0, objects); - - mAuthHelper = authenticatorHelper; - 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)); - mDevicePolicyManager = dpm; - } - - @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_BUTTON: - view = mInflater.inflate(R.layout.preference_header_button_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.button_ = (ImageButton) view.findViewById(R.id.buttonWidget); - holder.divider_ = view.findViewById(R.id.divider); - break; - - case HEADER_TYPE_NORMAL: - view = mInflater.inflate( - R.layout.preference_header_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); - 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_); - } - updateCommonHeaderView(header, holder); - break; - - case HEADER_TYPE_BUTTON: - if (header.id == R.id.security_settings) { - boolean hasCert = DevicePolicyManager.hasAnyCaCertsInstalled(); - if (hasCert) { - holder.button_.setVisibility(View.VISIBLE); - holder.divider_.setVisibility(View.VISIBLE); - boolean isManaged = mDevicePolicyManager.getDeviceOwner() != null; - if (isManaged) { - holder.button_.setImageResource(R.drawable.ic_settings_about); - } else { - holder.button_.setImageResource( - android.R.drawable.stat_notify_error); - } - holder.button_.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent( - android.provider.Settings.ACTION_MONITORING_CERT_INFO); - getContext().startActivity(intent); - } - }); - } else { - holder.button_.setVisibility(View.GONE); - holder.divider_.setVisibility(View.GONE); - } - } - updateCommonHeaderView(header, holder); - break; - - case HEADER_TYPE_NORMAL: - updateCommonHeaderView(header, holder); - break; - } - - return view; - } - - private void updateCommonHeaderView(Header header, HeaderViewHolder holder) { - if (header.extras != null - && header.extras.containsKey(ManageAccountsSettings.KEY_ACCOUNT_TYPE)) { - String accType = header.extras.getString( - ManageAccountsSettings.KEY_ACCOUNT_TYPE); - Drawable icon = mAuthHelper.getDrawableForType(getContext(), accType); - setHeaderIcon(holder, icon); - } else { - 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); - } - } - - private void setHeaderIcon(HeaderViewHolder holder, Drawable icon) { - ViewGroup.LayoutParams lp = holder.icon.getLayoutParams(); - lp.width = getContext().getResources().getDimensionPixelSize( - R.dimen.header_icon_width); - lp.height = lp.width; - holder.icon.setLayoutParams(lp); - holder.icon.setImageDrawable(icon); - } - - public void resume() { - mWifiEnabler.resume(); - mBluetoothEnabler.resume(); - } - - public void pause() { - mWifiEnabler.pause(); - mBluetoothEnabler.pause(); - } - } - - @Override - public void onHeaderClick(Header header, int position) { - boolean revert = false; - if (header.id == R.id.account_add) { - revert = true; - } - - super.onHeaderClick(header, position); - - if (revert && mLastHeader != null) { - highlightHeader((int) mLastHeader.id); - } else { - mLastHeader = header; - } - } - - @Override - public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { - // Override the fragment title for Wallpaper settings - int titleRes = pref.getTitleRes(); - if (pref.getFragment().equals(WallpaperTypeSettings.class.getName())) { - titleRes = R.string.wallpaper_settings_fragment_title; - } else if (pref.getFragment().equals(OwnerInfoSettings.class.getName()) - && UserHandle.myUserId() != UserHandle.USER_OWNER) { - if (UserManager.get(this).isLinkedUser()) { - titleRes = R.string.profile_info_settings_title; - } else { - titleRes = R.string.user_info_settings_title; - } - } - startPreferencePanel(pref.getFragment(), pref.getExtras(), titleRes, pref.getTitle(), - null, 0); - return true; - } - - @Override - public boolean shouldUpRecreateTask(Intent targetIntent) { - return super.shouldUpRecreateTask(new Intent(this, Settings.class)); - } - - @Override - public void setListAdapter(ListAdapter adapter) { - if (adapter == null) { - super.setListAdapter(null); - } else { - DevicePolicyManager dpm = - (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); - super.setListAdapter(new HeaderAdapter(this, getHeaders(), mAuthenticatorHelper, dpm)); - } - } - - @Override - public void onAccountsUpdated(Account[] accounts) { - // TODO: watch for package upgrades to invalidate cache; see 7206643 - mAuthenticatorHelper.updateAuthDescriptions(this); - mAuthenticatorHelper.onAccountsUpdated(this, accounts); - invalidateHeaders(); - } - - public static void requestHomeNotice() { - sShowNoHomeNotice = true; - } +public class Settings extends SettingsActivity { /* - * Settings subclasses for launching independently. - */ - 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 KeyboardLayoutPickerActivity 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 AppOpsSummaryActivity extends Settings { + * Settings subclasses for launching independently. + */ + public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ } + public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ } + public static class SimSettingsActivity extends SettingsActivity { /* empty */ } + public static class TetherSettingsActivity extends SettingsActivity { /* empty */ } + public static class VpnSettingsActivity extends SettingsActivity { /* empty */ } + public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ } + public static class StorageSettingsActivity extends SettingsActivity { /* empty */ } + public static class WifiSettingsActivity extends SettingsActivity { /* empty */ } + public static class WifiP2pSettingsActivity extends SettingsActivity { /* empty */ } + public static class InputMethodAndLanguageSettingsActivity extends SettingsActivity { /* empty */ } + public static class KeyboardLayoutPickerActivity extends SettingsActivity { /* empty */ } + public static class InputMethodAndSubtypeEnablerActivity extends SettingsActivity { /* empty */ } + public static class VoiceInputSettingsActivity extends SettingsActivity { /* empty */ } + public static class SpellCheckersSettingsActivity extends SettingsActivity { /* empty */ } + public static class LocalePickerActivity extends SettingsActivity { /* empty */ } + public static class UserDictionarySettingsActivity extends SettingsActivity { /* empty */ } + public static class HomeSettingsActivity extends SettingsActivity { /* empty */ } + public static class DisplaySettingsActivity extends SettingsActivity { /* empty */ } + public static class DeviceInfoSettingsActivity extends SettingsActivity { /* empty */ } + public static class ApplicationSettingsActivity extends SettingsActivity { /* empty */ } + public static class ManageApplicationsActivity extends SettingsActivity { /* empty */ } + public static class AppOpsSummaryActivity extends SettingsActivity { @Override public boolean isValidFragment(String className) { if (AppOpsSummary.class.getName().equals(className)) { return true; } return super.isValidFragment(className); - } + } } - public static class StorageUseActivity extends Settings { /* empty */ } - public static class DevelopmentSettingsActivity extends Settings { /* empty */ } - public static class AccessibilitySettingsActivity extends Settings { /* empty */ } - public static class CaptioningSettingsActivity 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 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 AndroidBeamSettingsActivity extends Settings { /* empty */ } - public static class WifiDisplaySettingsActivity extends Settings { /* empty */ } - public static class DreamSettingsActivity extends Settings { /* empty */ } - public static class NotificationStationActivity extends Settings { /* empty */ } - public static class UserSettingsActivity extends Settings { /* empty */ } - public static class NotificationAccessSettingsActivity extends Settings { /* empty */ } - public static class UsbSettingsActivity extends Settings { /* empty */ } - public static class TrustedCredentialsSettingsActivity extends Settings { /* empty */ } - public static class PaymentSettingsActivity extends Settings { /* empty */ } - public static class PrintSettingsActivity extends Settings { /* empty */ } - public static class PrintJobSettingsActivity extends Settings { /* empty */ } + public static class StorageUseActivity extends SettingsActivity { /* empty */ } + public static class DevelopmentSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccessibilitySettingsActivity extends SettingsActivity { /* empty */ } + public static class CaptioningSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccessibilityInversionSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccessibilityContrastSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccessibilityDaltonizerSettingsActivity extends SettingsActivity { /* empty */ } + public static class SecuritySettingsActivity extends SettingsActivity { /* empty */ } + public static class UsageAccessSettingsActivity extends SettingsActivity { /* empty */ } + public static class LocationSettingsActivity extends SettingsActivity { /* empty */ } + public static class PrivacySettingsActivity extends SettingsActivity { /* empty */ } + public static class RunningServicesActivity extends SettingsActivity { /* empty */ } + public static class ManageAccountsSettingsActivity extends SettingsActivity { /* empty */ } + public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ } + public static class BatterySaverSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccountSyncSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccountSettingsActivity extends SettingsActivity { /* empty */ } + public static class AccountSyncSettingsInAddAccountActivity extends SettingsActivity { /* empty */ } + public static class CryptKeeperSettingsActivity extends SettingsActivity { /* empty */ } + public static class DeviceAdminSettingsActivity extends SettingsActivity { /* empty */ } + public static class DataUsageSummaryActivity extends SettingsActivity { /* empty */ } + public static class AdvancedWifiSettingsActivity extends SettingsActivity { /* empty */ } + public static class SavedAccessPointsSettingsActivity extends SettingsActivity { /* empty */ } + public static class TextToSpeechSettingsActivity extends SettingsActivity { /* empty */ } + public static class AndroidBeamSettingsActivity extends SettingsActivity { /* empty */ } + public static class WifiDisplaySettingsActivity extends SettingsActivity { /* empty */ } + public static class DreamSettingsActivity extends SettingsActivity { /* empty */ } + public static class NotificationStationActivity extends SettingsActivity { /* empty */ } + public static class UserSettingsActivity extends SettingsActivity { /* empty */ } + public static class NotificationAccessSettingsActivity extends SettingsActivity { /* empty */ } + public static class ConditionProviderSettingsActivity extends SettingsActivity { /* empty */ } + public static class UsbSettingsActivity extends SettingsActivity { /* empty */ } + public static class TrustedCredentialsSettingsActivity extends SettingsActivity { /* empty */ } + public static class PaymentSettingsActivity extends SettingsActivity { /* empty */ } + public static class PrintSettingsActivity extends SettingsActivity { /* empty */ } + public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ } + public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ } + public static class NotificationSettingsActivity extends SettingsActivity { /* empty */ } + public static class NotificationAppListActivity extends SettingsActivity { /* empty */ } + public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ } + public static class OtherSoundSettingsActivity extends SettingsActivity { /* empty */ } + public static class QuickLaunchSettingsActivity extends SettingsActivity { /* empty */ } + + public static class TopLevelSettings extends SettingsActivity { /* empty */ } + public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } } + diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java new file mode 100644 index 0000000..37fcc87 --- /dev/null +++ b/src/com/android/settings/SettingsActivity.java @@ -0,0 +1,1358 @@ +/* + * Copyright (C) 2014 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.ActionBar; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.os.Handler; +import android.os.INetworkManagementService; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.text.TextUtils; +import android.transition.TransitionManager; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +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.widget.Button; +import android.widget.SearchView; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.XmlUtils; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.accessibility.CaptionPropertiesFragment; +import com.android.settings.accounts.AccountSettings; +import com.android.settings.accounts.AccountSyncSettings; +import com.android.settings.applications.InstalledAppDetails; +import com.android.settings.applications.ManageApplications; +import com.android.settings.applications.ProcessStatsUi; +import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.dashboard.DashboardCategory; +import com.android.settings.dashboard.DashboardSummary; +import com.android.settings.dashboard.DashboardTile; +import com.android.settings.dashboard.NoHomeDialogFragment; +import com.android.settings.dashboard.SearchResultsSummary; +import com.android.settings.deviceinfo.Memory; +import com.android.settings.deviceinfo.UsbSettings; +import com.android.settings.fuelgauge.BatterySaverSettings; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.notification.NotificationAppList; +import com.android.settings.notification.OtherSoundSettings; +import com.android.settings.quicklaunch.QuickLaunchSettings; +import com.android.settings.search.DynamicIndexableContentMonitor; +import com.android.settings.search.Index; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.inputmethod.KeyboardLayoutPickerFragment; +import com.android.settings.inputmethod.SpellCheckersSettings; +import com.android.settings.inputmethod.UserDictionaryList; +import com.android.settings.location.LocationSettings; +import com.android.settings.nfc.AndroidBeam; +import com.android.settings.nfc.PaymentSettings; +import com.android.settings.notification.AppNotificationSettings; +import com.android.settings.notification.ConditionProviderSettings; +import com.android.settings.notification.NotificationAccessSettings; +import com.android.settings.notification.NotificationSettings; +import com.android.settings.notification.NotificationStation; +import com.android.settings.notification.ZenModeSettings; +import com.android.settings.print.PrintJobSettingsFragment; +import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.sim.SimSettings; +import com.android.settings.tts.TextToSpeechSettings; +import com.android.settings.users.UserSettings; +import com.android.settings.voice.VoiceInputSettings; +import com.android.settings.vpn2.VpnSettings; +import com.android.settings.wfd.WifiDisplaySettings; +import com.android.settings.widget.SwitchBar; +import com.android.settings.wifi.AdvancedWifiSettings; +import com.android.settings.wifi.SavedAccessPointsWifiSettings; +import com.android.settings.wifi.WifiSettings; +import com.android.settings.wifi.p2p.WifiP2pSettings; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.android.settings.dashboard.DashboardTile.TILE_ID_UNDEFINED; + +public class SettingsActivity extends Activity + implements PreferenceManager.OnPreferenceTreeClickListener, + PreferenceFragment.OnPreferenceStartFragmentCallback, + ButtonBarHandler, FragmentManager.OnBackStackChangedListener, + SearchView.OnQueryTextListener, SearchView.OnCloseListener, + MenuItem.OnActionExpandListener { + + private static final String LOG_TAG = "Settings"; + + // Constants for state save/restore + private static final String SAVE_KEY_CATEGORIES = ":settings:categories"; + private static final String SAVE_KEY_SEARCH_MENU_EXPANDED = ":settings:search_menu_expanded"; + private static final String SAVE_KEY_SEARCH_QUERY = ":settings:search_query"; + private static final String SAVE_KEY_SHOW_HOME_AS_UP = ":settings:show_home_as_up"; + private static final String SAVE_KEY_SHOW_SEARCH = ":settings:show_search"; + private static final String SAVE_KEY_HOME_ACTIVITIES_COUNT = ":settings:home_activities_count"; + + /** + * When starting this activity, the invoking Intent can contain this extra + * string to specify which fragment should be initially displayed. + * <p/>Starting from Key Lime Pie, when this argument is passed in, the activity + * will call isValidFragment() to confirm that the fragment class name is valid for this + * activity. + */ + public static final String EXTRA_SHOW_FRAGMENT = ":settings:show_fragment"; + + /** + * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, + * this extra can also be specified to supply a Bundle of arguments to pass + * to that fragment when it is instantiated during the initial creation + * of the activity. + */ + public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; + + /** + * Fragment "key" argument passed thru {@link #EXTRA_SHOW_FRAGMENT_ARGUMENTS} + */ + public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; + + public static final String BACK_STACK_PREFS = ":settings:prefs"; + + // extras that allow any preference activity to be launched as part of a wizard + + // show Back and Next buttons? takes boolean parameter + // Back will then return RESULT_CANCELED and Next RESULT_OK + protected static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; + + // add a Skip button? + private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip"; + + // specify custom text for the Back or Next buttons, or cause a button to not appear + // at all by setting it to null + protected static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; + protected static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; + + /** + * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, + * those extra can also be specify to supply the title or title res id to be shown for + * that fragment. + */ + public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":settings:show_fragment_title"; + public static final String EXTRA_SHOW_FRAGMENT_TITLE_RESID = + ":settings:show_fragment_title_resid"; + public static final String EXTRA_SHOW_FRAGMENT_AS_SHORTCUT = + ":settings:show_fragment_as_shortcut"; + + public static final String EXTRA_SHOW_FRAGMENT_AS_SUBSETTING = + ":settings:show_fragment_as_subsetting"; + + private static final String META_DATA_KEY_FRAGMENT_CLASS = + "com.android.settings.FRAGMENT_CLASS"; + + private static final String EXTRA_UI_OPTIONS = "settings:ui_options"; + + private static final String EMPTY_QUERY = ""; + + private static boolean sShowNoHomeNotice = false; + + private String mFragmentClass; + + private CharSequence mInitialTitle; + private int mInitialTitleResId; + + // Show only these settings for restricted users + private int[] SETTINGS_FOR_RESTRICTED = { + R.id.wireless_section, + R.id.wifi_settings, + R.id.bluetooth_settings, + R.id.data_usage_settings, + R.id.sim_settings, + R.id.wireless_settings, + R.id.device_section, + R.id.notification_settings, + R.id.display_settings, + R.id.storage_settings, + R.id.application_settings, + R.id.battery_settings, + R.id.personal_section, + R.id.location_settings, + R.id.security_settings, + R.id.language_settings, + R.id.user_settings, + R.id.account_settings, + R.id.system_section, + R.id.date_time_settings, + R.id.about_settings, + R.id.accessibility_settings, + R.id.print_settings, + R.id.nfc_payment_settings, + R.id.home_settings, + R.id.dashboard + }; + + private static final String[] ENTRY_FRAGMENTS = { + WirelessSettings.class.getName(), + WifiSettings.class.getName(), + AdvancedWifiSettings.class.getName(), + SavedAccessPointsWifiSettings.class.getName(), + BluetoothSettings.class.getName(), + SimSettings.class.getName(), + TetherSettings.class.getName(), + WifiP2pSettings.class.getName(), + VpnSettings.class.getName(), + DateTimeSettings.class.getName(), + LocalePicker.class.getName(), + InputMethodAndLanguageSettings.class.getName(), + VoiceInputSettings.class.getName(), + SpellCheckersSettings.class.getName(), + UserDictionaryList.class.getName(), + UserDictionarySettings.class.getName(), + HomeSettings.class.getName(), + DisplaySettings.class.getName(), + DeviceInfoSettings.class.getName(), + ManageApplications.class.getName(), + ProcessStatsUi.class.getName(), + NotificationStation.class.getName(), + LocationSettings.class.getName(), + SecuritySettings.class.getName(), + UsageAccessSettings.class.getName(), + PrivacySettings.class.getName(), + DeviceAdminSettings.class.getName(), + AccessibilitySettings.class.getName(), + CaptionPropertiesFragment.class.getName(), + com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment.class.getName(), + TextToSpeechSettings.class.getName(), + Memory.class.getName(), + DevelopmentSettings.class.getName(), + UsbSettings.class.getName(), + AndroidBeam.class.getName(), + WifiDisplaySettings.class.getName(), + PowerUsageSummary.class.getName(), + AccountSyncSettings.class.getName(), + AccountSettings.class.getName(), + CryptKeeperSettings.class.getName(), + DataUsageSummary.class.getName(), + DreamSettings.class.getName(), + UserSettings.class.getName(), + NotificationAccessSettings.class.getName(), + ConditionProviderSettings.class.getName(), + PrintSettingsFragment.class.getName(), + PrintJobSettingsFragment.class.getName(), + TrustedCredentialsSettings.class.getName(), + PaymentSettings.class.getName(), + KeyboardLayoutPickerFragment.class.getName(), + ZenModeSettings.class.getName(), + NotificationSettings.class.getName(), + ChooseLockPassword.ChooseLockPasswordFragment.class.getName(), + ChooseLockPattern.ChooseLockPatternFragment.class.getName(), + InstalledAppDetails.class.getName(), + BatterySaverSettings.class.getName(), + NotificationAppList.class.getName(), + AppNotificationSettings.class.getName(), + OtherSoundSettings.class.getName(), + QuickLaunchSettings.class.getName(), + ApnSettings.class.getName() + }; + + + private static final String[] LIKE_SHORTCUT_INTENT_ACTION_ARRAY = { + "android.settings.APPLICATION_DETAILS_SETTINGS" + }; + + private SharedPreferences mDevelopmentPreferences; + private SharedPreferences.OnSharedPreferenceChangeListener mDevelopmentPreferencesListener; + + private boolean mBatteryPresent = true; + private BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + boolean batteryPresent = Utils.isBatteryPresent(intent); + + if (mBatteryPresent != batteryPresent) { + mBatteryPresent = batteryPresent; + invalidateCategories(true); + } + } + } + }; + + private final DynamicIndexableContentMonitor mDynamicIndexableContentMonitor = + new DynamicIndexableContentMonitor(); + + private ActionBar mActionBar; + private SwitchBar mSwitchBar; + + private Button mNextButton; + + private boolean mDisplayHomeAsUpEnabled; + private boolean mDisplaySearch; + + private boolean mIsShowingDashboard; + private boolean mIsShortcut; + + private ViewGroup mContent; + + private SearchView mSearchView; + private MenuItem mSearchMenuItem; + private boolean mSearchMenuItemExpanded = false; + private SearchResultsSummary mSearchResultsFragment; + private String mSearchQuery; + + // Categories + private ArrayList<DashboardCategory> mCategories = new ArrayList<DashboardCategory>(); + + private static final String MSG_DATA_FORCE_REFRESH = "msg_data_force_refresh"; + private static final int MSG_BUILD_CATEGORIES = 1; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_BUILD_CATEGORIES: { + final boolean forceRefresh = msg.getData().getBoolean(MSG_DATA_FORCE_REFRESH); + if (forceRefresh) { + buildDashboardCategories(mCategories); + } + } break; + } + } + }; + + private boolean mNeedToRevertToInitialFragment = false; + private int mHomeActivitiesCount = 1; + + private Intent mResultIntentData; + + public SwitchBar getSwitchBar() { + return mSwitchBar; + } + + public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) { + if (forceRefresh || mCategories.size() == 0) { + buildDashboardCategories(mCategories); + } + return mCategories; + } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { + // Override the fragment title for Wallpaper settings + int titleRes = pref.getTitleRes(); + if (pref.getFragment().equals(WallpaperTypeSettings.class.getName())) { + titleRes = R.string.wallpaper_settings_fragment_title; + } else if (pref.getFragment().equals(OwnerInfoSettings.class.getName()) + && UserHandle.myUserId() != UserHandle.USER_OWNER) { + if (UserManager.get(this).isLinkedUser()) { + titleRes = R.string.profile_info_settings_title; + } else { + titleRes = R.string.user_info_settings_title; + } + } + startPreferencePanel(pref.getFragment(), pref.getExtras(), titleRes, pref.getTitle(), + null, 0); + return true; + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + return false; + } + + private void invalidateCategories(boolean forceRefresh) { + if (!mHandler.hasMessages(MSG_BUILD_CATEGORIES)) { + Message msg = new Message(); + msg.what = MSG_BUILD_CATEGORIES; + msg.getData().putBoolean(MSG_DATA_FORCE_REFRESH, forceRefresh); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + Index.getInstance(this).update(); + } + + @Override + protected void onStart() { + super.onStart(); + + if (mNeedToRevertToInitialFragment) { + revertToInitialFragment(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (!mDisplaySearch) { + return false; + } + + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.options_menu, menu); + + // Cache the search query (can be overriden by the OnQueryTextListener) + final String query = mSearchQuery; + + mSearchMenuItem = menu.findItem(R.id.search); + mSearchView = (SearchView) mSearchMenuItem.getActionView(); + + if (mSearchMenuItem == null || mSearchView == null) { + return false; + } + + if (mSearchResultsFragment != null) { + mSearchResultsFragment.setSearchView(mSearchView); + } + + mSearchMenuItem.setOnActionExpandListener(this); + mSearchView.setOnQueryTextListener(this); + mSearchView.setOnCloseListener(this); + + if (mSearchMenuItemExpanded) { + mSearchMenuItem.expandActionView(); + } + mSearchView.setQuery(query, true /* submit */); + + return true; + } + + private static boolean isShortCutIntent(final Intent intent) { + Set<String> categories = intent.getCategories(); + return (categories != null) && categories.contains("com.android.settings.SHORTCUT"); + } + + private static boolean isLikeShortCutIntent(final Intent intent) { + String action = intent.getAction(); + if (action == null) { + return false; + } + for (int i = 0; i < LIKE_SHORTCUT_INTENT_ACTION_ARRAY.length; i++) { + if (LIKE_SHORTCUT_INTENT_ACTION_ARRAY[i].equals(action)) return true; + } + return false; + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + // Should happen before any call to getIntent() + getMetaData(); + + final Intent intent = getIntent(); + if (intent.hasExtra(EXTRA_UI_OPTIONS)) { + getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0)); + } + + mDevelopmentPreferences = getSharedPreferences(DevelopmentSettings.PREF_FILE, + Context.MODE_PRIVATE); + + // Getting Intent properties can only be done after the super.onCreate(...) + final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + + mIsShortcut = isShortCutIntent(intent) || isLikeShortCutIntent(intent) || + intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, false); + + final ComponentName cn = intent.getComponent(); + final String className = cn.getClassName(); + + mIsShowingDashboard = className.equals(Settings.class.getName()); + + // This is a "Sub Settings" when: + // - this is a real SubSettings + // - or :settings:show_fragment_as_subsetting is passed to the Intent + final boolean isSubSettings = className.equals(SubSettings.class.getName()) || + intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false); + + // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content insets + if (isSubSettings) { + // Check also that we are not a Theme Dialog as we don't want to override them + final int themeResId = getThemeResId(); + if (themeResId != R.style.Theme_DialogWhenLarge && + themeResId != R.style.Theme_SubSettingsDialogWhenLarge) { + setTheme(R.style.Theme_SubSettings); + } + } + + setContentView(mIsShowingDashboard ? + R.layout.settings_main_dashboard : R.layout.settings_main_prefs); + + mContent = (ViewGroup) findViewById(R.id.main_content); + + getFragmentManager().addOnBackStackChangedListener(this); + + if (mIsShowingDashboard) { + Index.getInstance(getApplicationContext()).update(); + } + + if (savedState != null) { + // We are restarting from a previous saved state; used that to initialize, instead + // of starting fresh. + mSearchMenuItemExpanded = savedState.getBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED); + mSearchQuery = savedState.getString(SAVE_KEY_SEARCH_QUERY); + + setTitleFromIntent(intent); + + ArrayList<DashboardCategory> categories = + savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES); + if (categories != null) { + mCategories.clear(); + mCategories.addAll(categories); + setTitleFromBackStack(); + } + + mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP); + mDisplaySearch = savedState.getBoolean(SAVE_KEY_SHOW_SEARCH); + mHomeActivitiesCount = savedState.getInt(SAVE_KEY_HOME_ACTIVITIES_COUNT, + 1 /* one home activity by default */); + } else { + if (!mIsShowingDashboard) { + // Search is shown we are launched thru a Settings "shortcut". UP will be shown + // only if it is a sub settings + if (mIsShortcut) { + mDisplayHomeAsUpEnabled = isSubSettings; + mDisplaySearch = false; + } else if (isSubSettings) { + mDisplayHomeAsUpEnabled = true; + mDisplaySearch = true; + } else { + mDisplayHomeAsUpEnabled = false; + mDisplaySearch = false; + } + setTitleFromIntent(intent); + + Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); + switchToFragment(initialFragmentName, initialArguments, true, false, + mInitialTitleResId, mInitialTitle, false); + } else { + // No UP affordance if we are displaying the main Dashboard + mDisplayHomeAsUpEnabled = false; + // Show Search affordance + mDisplaySearch = true; + mInitialTitleResId = R.string.dashboard_title; + switchToFragment(DashboardSummary.class.getName(), null, false, false, + mInitialTitleResId, mInitialTitle, false); + } + } + + mActionBar = getActionBar(); + if (mActionBar != null) { + mActionBar.setDisplayHomeAsUpEnabled(mDisplayHomeAsUpEnabled); + mActionBar.setHomeButtonEnabled(mDisplayHomeAsUpEnabled); + } + mSwitchBar = (SwitchBar) findViewById(R.id.switch_bar); + + // see if we should show Back/Next buttons + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { + + View buttonBar = findViewById(R.id.button_bar); + if (buttonBar != null) { + buttonBar.setVisibility(View.VISIBLE); + + Button backButton = (Button)findViewById(R.id.back_button); + backButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED, getResultIntentData()); + finish(); + } + }); + Button skipButton = (Button)findViewById(R.id.skip_button); + skipButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK, getResultIntentData()); + finish(); + } + }); + mNextButton = (Button)findViewById(R.id.next_button); + mNextButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + setResult(RESULT_OK, getResultIntentData()); + finish(); + } + }); + + // set our various button parameters + if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); + if (TextUtils.isEmpty(buttonText)) { + mNextButton.setVisibility(View.GONE); + } + else { + mNextButton.setText(buttonText); + } + } + if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { + String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); + if (TextUtils.isEmpty(buttonText)) { + backButton.setVisibility(View.GONE); + } + else { + backButton.setText(buttonText); + } + } + if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) { + skipButton.setVisibility(View.VISIBLE); + } + } + } + + mHomeActivitiesCount = getHomeActivitiesCount(); + } + + private int getHomeActivitiesCount() { + final ArrayList<ResolveInfo> homeApps = new ArrayList<ResolveInfo>(); + getPackageManager().getHomeActivities(homeApps); + return homeApps.size(); + } + + private void setTitleFromIntent(Intent intent) { + final int initialTitleResId = intent.getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, -1); + if (initialTitleResId > 0) { + mInitialTitle = null; + mInitialTitleResId = initialTitleResId; + setTitle(mInitialTitleResId); + } else { + mInitialTitleResId = -1; + final String initialTitle = intent.getStringExtra(EXTRA_SHOW_FRAGMENT_TITLE); + mInitialTitle = (initialTitle != null) ? initialTitle : getTitle(); + setTitle(mInitialTitle); + } + } + + @Override + public void onBackStackChanged() { + setTitleFromBackStack(); + } + + private int setTitleFromBackStack() { + final int count = getFragmentManager().getBackStackEntryCount(); + + if (count == 0) { + if (mInitialTitleResId > 0) { + setTitle(mInitialTitleResId); + } else { + setTitle(mInitialTitle); + } + return 0; + } + + FragmentManager.BackStackEntry bse = getFragmentManager().getBackStackEntryAt(count - 1); + setTitleFromBackStackEntry(bse); + + return count; + } + + private void setTitleFromBackStackEntry(FragmentManager.BackStackEntry bse) { + final CharSequence title; + final int titleRes = bse.getBreadCrumbTitleRes(); + if (titleRes > 0) { + title = getText(titleRes); + } else { + title = bse.getBreadCrumbTitle(); + } + if (title != null) { + setTitle(title); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (mCategories.size() > 0) { + outState.putParcelableArrayList(SAVE_KEY_CATEGORIES, mCategories); + } + + outState.putBoolean(SAVE_KEY_SHOW_HOME_AS_UP, mDisplayHomeAsUpEnabled); + outState.putBoolean(SAVE_KEY_SHOW_SEARCH, mDisplaySearch); + + if (mDisplaySearch) { + // The option menus are created if the ActionBar is visible and they are also created + // asynchronously. If you launch Settings with an Intent action like + // android.intent.action.POWER_USAGE_SUMMARY and at the same time your device is locked + // thru a LockScreen, onCreateOptionsMenu() is not yet called and references to the search + // menu item and search view are null. + boolean isExpanded = (mSearchMenuItem != null) && mSearchMenuItem.isActionViewExpanded(); + outState.putBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED, isExpanded); + + String query = (mSearchView != null) ? mSearchView.getQuery().toString() : EMPTY_QUERY; + outState.putString(SAVE_KEY_SEARCH_QUERY, query); + } + + outState.putInt(SAVE_KEY_HOME_ACTIVITIES_COUNT, mHomeActivitiesCount); + } + + @Override + public void onResume() { + super.onResume(); + + final int newHomeActivityCount = getHomeActivitiesCount(); + if (newHomeActivityCount != mHomeActivitiesCount) { + mHomeActivitiesCount = newHomeActivityCount; + invalidateCategories(true); + } + + mDevelopmentPreferencesListener = new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + invalidateCategories(true); + } + }; + mDevelopmentPreferences.registerOnSharedPreferenceChangeListener( + mDevelopmentPreferencesListener); + + registerReceiver(mBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + + mDynamicIndexableContentMonitor.register(this); + + if(mDisplaySearch && !TextUtils.isEmpty(mSearchQuery)) { + onQueryTextSubmit(mSearchQuery); + } + } + + @Override + public void onPause() { + super.onPause(); + + unregisterReceiver(mBatteryInfoReceiver); + mDynamicIndexableContentMonitor.unregister(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mDevelopmentPreferences.unregisterOnSharedPreferenceChangeListener( + mDevelopmentPreferencesListener); + mDevelopmentPreferencesListener = null; + } + + protected boolean isValidFragment(String fragmentName) { + // Almost all fragments are wrapped in this, + // except for a few that have their own activities. + for (int i = 0; i < ENTRY_FRAGMENTS.length; i++) { + if (ENTRY_FRAGMENTS[i].equals(fragmentName)) return true; + } + return false; + } + + @Override + public Intent 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) { + Intent modIntent = new Intent(superIntent); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment); + Bundle args = superIntent.getExtras(); + if (args != null) { + args = new Bundle(args); + } else { + args = new Bundle(); + } + args.putParcelable("intent", superIntent); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + return modIntent; + } + return superIntent; + } + + /** + * Checks if the component name in the intent is different from the Settings class and + * returns the class name to load as a fragment. + */ + private String getStartingFragmentClass(Intent intent) { + if (mFragmentClass != null) return mFragmentClass; + + String intentClass = intent.getComponent().getClassName(); + if (intentClass.equals(getClass().getName())) return null; + + if ("com.android.settings.ManageApplications".equals(intentClass) + || "com.android.settings.RunningServices".equals(intentClass) + || "com.android.settings.applications.StorageUse".equals(intentClass)) { + // Old names of manage apps. + intentClass = com.android.settings.applications.ManageApplications.class.getName(); + } + + return intentClass; + } + + /** + * Start a new fragment containing a preference panel. If the preferences + * are being displayed in multi-pane mode, the given fragment class will + * be instantiated and placed in the appropriate pane. If running in + * single-pane mode, a new activity will be launched in which to show the + * fragment. + * + * @param fragmentClass Full name of the class implementing the fragment. + * @param args Any desired arguments to supply to the fragment. + * @param titleRes Optional resource identifier of the title of this + * fragment. + * @param titleText Optional text of the title of this fragment. + * @param resultTo Optional fragment that result data should be sent to. + * If non-null, resultTo.onActivityResult() will be called when this + * preference panel is done. The launched panel must use + * {@link #finishPreferencePanel(Fragment, int, Intent)} when done. + * @param resultRequestCode If resultTo is non-null, this is the caller's + * request code to be received with the result. + */ + public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, + CharSequence titleText, Fragment resultTo, int resultRequestCode) { + String title = null; + if (titleRes < 0) { + if (titleText != null) { + title = titleText.toString(); + } else { + // There not much we can do in that case + title = ""; + } + } + Utils.startWithFragment(this, fragmentClass, args, resultTo, resultRequestCode, + titleRes, title, mIsShortcut); + } + + /** + * Start a new fragment in a new activity containing a preference panel for a given user. If the + * preferences are being displayed in multi-pane mode, the given fragment class will be + * instantiated and placed in the appropriate pane. If running in single-pane mode, a new + * activity will be launched in which to show the fragment. + * + * @param fragmentClass Full name of the class implementing the fragment. + * @param args Any desired arguments to supply to the fragment. + * @param titleRes Optional resource identifier of the title of this fragment. + * @param titleText Optional text of the title of this fragment. + * @param userHandle The user for which the panel has to be started. + */ + public void startPreferencePanelAsUser(String fragmentClass, Bundle args, int titleRes, + CharSequence titleText, UserHandle userHandle) { + String title = null; + if (titleRes < 0) { + if (titleText != null) { + title = titleText.toString(); + } else { + // There not much we can do in that case + title = ""; + } + } + Utils.startWithFragmentAsUser(this, fragmentClass, args, + titleRes, title, mIsShortcut, userHandle); + } + + /** + * Called by a preference panel fragment to finish itself. + * + * @param caller The fragment that is asking to be finished. + * @param resultCode Optional result code to send back to the original + * launching fragment. + * @param resultData Optional result data to send back to the original + * launching fragment. + */ + public void finishPreferencePanel(Fragment caller, int resultCode, Intent resultData) { + setResult(resultCode, resultData); + finish(); + } + + /** + * Start a new fragment. + * + * @param fragment The fragment to start + * @param push If true, the current fragment will be pushed onto the back stack. If false, + * the current fragment will be replaced. + */ + public void startPreferenceFragment(Fragment fragment, boolean push) { + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.main_content, fragment); + if (push) { + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + transaction.addToBackStack(BACK_STACK_PREFS); + } else { + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + } + transaction.commitAllowingStateLoss(); + } + + /** + * Switch to a specific Fragment with taking care of validation, Title and BackStack + */ + private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate, + boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) { + if (validate && !isValidFragment(fragmentName)) { + throw new IllegalArgumentException("Invalid fragment for this activity: " + + fragmentName); + } + Fragment f = Fragment.instantiate(this, fragmentName, args); + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.main_content, f); + if (withTransition) { + TransitionManager.beginDelayedTransition(mContent); + } + if (addToBackStack) { + transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS); + } + if (titleResId > 0) { + transaction.setBreadCrumbTitle(titleResId); + } else if (title != null) { + transaction.setBreadCrumbTitle(title); + } + transaction.commitAllowingStateLoss(); + getFragmentManager().executePendingTransactions(); + return f; + } + + /** + * Called when the activity needs its list of categories/tiles built. + * + * @param categories The list in which to place the tiles categories. + */ + private void buildDashboardCategories(List<DashboardCategory> categories) { + categories.clear(); + loadCategoriesFromResource(R.xml.dashboard_categories, categories); + updateTilesList(categories); + } + + /** + * Parse the given XML file as a categories description, adding each + * parsed categories and tiles into the target list. + * + * @param resid The XML resource to load and parse. + * @param target The list in which the parsed categories and tiles should be placed. + */ + private void loadCategoriesFromResource(int resid, List<DashboardCategory> target) { + XmlResourceParser parser = null; + try { + parser = getResources().getXml(resid); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Parse next until start tag is found + } + + String nodeName = parser.getName(); + if (!"dashboard-categories".equals(nodeName)) { + throw new RuntimeException( + "XML document must start with <preference-categories> tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + Bundle curBundle = null; + + final int outerDepth = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + nodeName = parser.getName(); + if ("dashboard-category".equals(nodeName)) { + DashboardCategory category = new DashboardCategory(); + + TypedArray sa = obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PreferenceHeader); + category.id = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_id, + (int)DashboardCategory.CAT_ID_UNDEFINED); + + TypedValue tv = sa.peekValue( + com.android.internal.R.styleable.PreferenceHeader_title); + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + category.titleRes = tv.resourceId; + } else { + category.title = tv.string; + } + } + sa.recycle(); + + final int innerDepth = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String innerNodeName = parser.getName(); + if (innerNodeName.equals("dashboard-tile")) { + DashboardTile tile = new DashboardTile(); + + sa = obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PreferenceHeader); + tile.id = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_id, + (int)TILE_ID_UNDEFINED); + tv = sa.peekValue( + com.android.internal.R.styleable.PreferenceHeader_title); + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + tile.titleRes = tv.resourceId; + } else { + tile.title = tv.string; + } + } + tv = sa.peekValue( + com.android.internal.R.styleable.PreferenceHeader_summary); + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + tile.summaryRes = tv.resourceId; + } else { + tile.summary = tv.string; + } + } + tile.iconRes = sa.getResourceId( + com.android.internal.R.styleable.PreferenceHeader_icon, 0); + tile.fragment = sa.getString( + com.android.internal.R.styleable.PreferenceHeader_fragment); + sa.recycle(); + + if (curBundle == null) { + curBundle = new Bundle(); + } + + final int innerDepth2 = parser.getDepth(); + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth2)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String innerNodeName2 = parser.getName(); + if (innerNodeName2.equals("extra")) { + getResources().parseBundleExtra("extra", attrs, curBundle); + XmlUtils.skipCurrentTag(parser); + + } else if (innerNodeName2.equals("intent")) { + tile.intent = Intent.parseIntent(getResources(), parser, attrs); + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + if (curBundle.size() > 0) { + tile.fragmentArguments = curBundle; + curBundle = null; + } + + // Show the SIM Cards setting if there are more than 2 SIMs installed. + if(tile.id != R.id.sim_settings || Utils.showSimCardTile(this)){ + category.addTile(tile); + } + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + target.add(category); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + } catch (XmlPullParserException e) { + throw new RuntimeException("Error parsing categories", e); + } catch (IOException e) { + throw new RuntimeException("Error parsing categories", e); + } finally { + if (parser != null) parser.close(); + } + } + + private void updateTilesList(List<DashboardCategory> target) { + final boolean showDev = mDevelopmentPreferences.getBoolean( + DevelopmentSettings.PREF_SHOW, + android.os.Build.TYPE.equals("eng")); + + final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); + + final int size = target.size(); + for (int i = 0; i < size; i++) { + + DashboardCategory category = target.get(i); + + // Ids are integers, so downcasting is ok + int id = (int) category.id; + int n = category.getTilesCount() - 1; + while (n >= 0) { + + DashboardTile tile = category.getTile(n); + boolean removeTile = false; + id = (int) tile.id; + if (id == R.id.operator_settings || id == R.id.manufacturer_settings) { + if (!Utils.updateTileToSpecificActivityFromMetaDataOrRemove(this, tile)) { + removeTile = true; + } + } else if (id == R.id.wifi_settings) { + // Remove WiFi Settings if WiFi service is not available. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { + removeTile = true; + } + } else if (id == R.id.bluetooth_settings) { + // Remove Bluetooth Settings if Bluetooth service is not available. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { + removeTile = true; + } + } else if (id == R.id.data_usage_settings) { + // Remove data usage when kernel module not enabled + final INetworkManagementService netManager = INetworkManagementService.Stub + .asInterface(ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); + try { + if (!netManager.isBandwidthControlEnabled()) { + removeTile = true; + } + } catch (RemoteException e) { + // ignored + } + } else if (id == R.id.battery_settings) { + // Remove battery settings when battery is not available. (e.g. TV) + + if (!mBatteryPresent) { + removeTile = true; + } + } else if (id == R.id.home_settings) { + if (!updateHomeSettingTiles(tile)) { + removeTile = true; + } + } else if (id == R.id.user_settings) { + boolean hasMultipleUsers = + ((UserManager) getSystemService(Context.USER_SERVICE)) + .getUserCount() > 1; + if (!UserHandle.MU_ENABLED + || (!UserManager.supportsMultipleUsers() + && !hasMultipleUsers) + || Utils.isMonkeyRunning()) { + removeTile = true; + } + } else if (id == R.id.nfc_payment_settings) { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)) { + removeTile = true; + } else { + // Only show if NFC is on and we have the HCE feature + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this); + if (adapter == null || !adapter.isEnabled() || + !getPackageManager().hasSystemFeature( + PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { + removeTile = true; + } + } + } else if (id == R.id.print_settings) { + boolean hasPrintingSupport = getPackageManager().hasSystemFeature( + PackageManager.FEATURE_PRINTING); + if (!hasPrintingSupport) { + removeTile = true; + } + } else if (id == R.id.development_settings) { + if (!showDev || um.hasUserRestriction( + UserManager.DISALLOW_DEBUGGING_FEATURES)) { + removeTile = true; + } + } + + if (UserHandle.MU_ENABLED && UserHandle.myUserId() != 0 + && !ArrayUtils.contains(SETTINGS_FOR_RESTRICTED, id)) { + removeTile = true; + } + + if (removeTile && n < category.getTilesCount()) { + category.removeTile(n); + } + n--; + } + } + } + + private boolean updateHomeSettingTiles(DashboardTile tile) { + // Once we decide to show Home settings, keep showing it forever + SharedPreferences sp = getSharedPreferences(HomeSettings.HOME_PREFS, Context.MODE_PRIVATE); + if (sp.getBoolean(HomeSettings.HOME_PREFS_DO_SHOW, false)) { + return true; + } + + try { + mHomeActivitiesCount = getHomeActivitiesCount(); + if (mHomeActivitiesCount < 2) { + // When there's only one available home app, omit this settings + // category entirely at the top level UI. If the user just + // uninstalled the penultimate home app candidiate, we also + // now tell them about why they aren't seeing 'Home' in the list. + if (sShowNoHomeNotice) { + sShowNoHomeNotice = false; + NoHomeDialogFragment.show(this); + } + return false; + } else { + // Okay, we're allowing the Home settings category. Tell it, when + // invoked via this front door, that we'll need to be told about the + // case when the user uninstalls all but one home app. + if (tile.fragmentArguments == null) { + tile.fragmentArguments = new Bundle(); + } + tile.fragmentArguments.putBoolean(HomeSettings.HOME_SHOW_NOTICE, true); + } + } catch (Exception e) { + // Can't look up the home activity; bail on configuring the icon + Log.w(LOG_TAG, "Problem looking up home activity!", e); + } + + sp.edit().putBoolean(HomeSettings.HOME_PREFS_DO_SHOW, true).apply(); + return true; + } + + private void getMetaData() { + try { + ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(), + PackageManager.GET_META_DATA); + if (ai == null || ai.metaData == null) return; + mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS); + } catch (NameNotFoundException nnfe) { + // No recovery + Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString()); + } + } + + // give subclasses access to the Next button + public boolean hasNextButton() { + return mNextButton != null; + } + + public Button getNextButton() { + return mNextButton; + } + + @Override + public boolean shouldUpRecreateTask(Intent targetIntent) { + return super.shouldUpRecreateTask(new Intent(this, SettingsActivity.class)); + } + + public static void requestHomeNotice() { + sShowNoHomeNotice = true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + switchToSearchResultsFragmentIfNeeded(); + mSearchQuery = query; + return mSearchResultsFragment.onQueryTextSubmit(query); + } + + @Override + public boolean onQueryTextChange(String newText) { + mSearchQuery = newText; + if (mSearchResultsFragment == null) { + return false; + } + return mSearchResultsFragment.onQueryTextChange(newText); + } + + @Override + public boolean onClose() { + return false; + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + if (item.getItemId() == mSearchMenuItem.getItemId()) { + switchToSearchResultsFragmentIfNeeded(); + } + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if (item.getItemId() == mSearchMenuItem.getItemId()) { + if (mSearchMenuItemExpanded) { + revertToInitialFragment(); + } + } + return true; + } + + private void switchToSearchResultsFragmentIfNeeded() { + if (mSearchResultsFragment != null) { + return; + } + Fragment current = getFragmentManager().findFragmentById(R.id.main_content); + if (current != null && current instanceof SearchResultsSummary) { + mSearchResultsFragment = (SearchResultsSummary) current; + } else { + mSearchResultsFragment = (SearchResultsSummary) switchToFragment( + SearchResultsSummary.class.getName(), null, false, true, + R.string.search_results_title, null, true); + } + mSearchResultsFragment.setSearchView(mSearchView); + mSearchMenuItemExpanded = true; + } + + public void needToRevertToInitialFragment() { + mNeedToRevertToInitialFragment = true; + } + + private void revertToInitialFragment() { + mNeedToRevertToInitialFragment = false; + mSearchResultsFragment = null; + mSearchMenuItemExpanded = false; + getFragmentManager().popBackStackImmediate(SettingsActivity.BACK_STACK_PREFS, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + if (mSearchMenuItem != null) { + mSearchMenuItem.collapseActionView(); + } + } + + public Intent getResultIntentData() { + return mResultIntentData; + } + + public void setResultIntentData(Intent resultIntentData) { + mResultIntentData = resultIntentData; + } +} diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index 0a382b5..e87f676 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -16,25 +16,32 @@ package com.android.settings; +import android.app.Activity; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.PackageManager; -import android.net.Uri; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroupAdapter; import android.text.TextUtils; 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.ViewGroup; import android.widget.Button; +import android.widget.ListAdapter; +import android.widget.ListView; /** * Base class for Settings fragments, with some helper functions and dialog management. @@ -44,6 +51,9 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di private static final String TAG = "SettingsPreferenceFragment"; private static final int MENU_HELP = Menu.FIRST + 100; + private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600; + + private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; private SettingsDialogFragment mDialogFragment; @@ -52,10 +62,34 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di // Cache the content resolver for async callbacks private ContentResolver mContentResolver; + private String mPreferenceKey; + private boolean mPreferenceHighlighted = false; + private Drawable mHighlightDrawable; + + private ListAdapter mCurrentRootAdapter; + private boolean mIsDataSetObserverRegistered = false; + private DataSetObserver mDataSetObserver = new DataSetObserver() { + @Override + public void onChanged() { + highlightPreferenceIfNeeded(); + } + + @Override + public void onInvalidated() { + highlightPreferenceIfNeeded(); + } + }; + + private ViewGroup mPinnedHeaderFrameLayout; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); + if (icicle != null) { + mPreferenceHighlighted = icicle.getBoolean(SAVE_HIGHLIGHTED_KEY); + } + // Prepare help url and enable menu if necessary int helpResource = getHelpResource(); if (helpResource != 0) { @@ -64,6 +98,31 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di } @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View root = super.onCreateView(inflater, container, savedInstanceState); + mPinnedHeaderFrameLayout = (ViewGroup) root.findViewById(R.id.pinned_header); + return root; + } + + public void setPinnedHeaderView(View pinnedHeader) { + mPinnedHeaderFrameLayout.addView(pinnedHeader); + mPinnedHeaderFrameLayout.setVisibility(View.VISIBLE); + } + + public void clearPinnedHeaderView() { + mPinnedHeaderFrameLayout.removeAllViews(); + mPinnedHeaderFrameLayout.setVisibility(View.GONE); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); + } + + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (!TextUtils.isEmpty(mHelpUrl)) { @@ -71,6 +130,137 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di } } + @Override + public void onResume() { + super.onResume(); + + final Bundle args = getArguments(); + if (args != null) { + mPreferenceKey = args.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY); + highlightPreferenceIfNeeded(); + } + } + + @Override + protected void onBindPreferences() { + registerObserverIfNeeded(); + } + + @Override + protected void onUnbindPreferences() { + unregisterObserverIfNeeded(); + } + + @Override + public void onStop() { + super.onStop(); + + unregisterObserverIfNeeded(); + } + + public void registerObserverIfNeeded() { + if (!mIsDataSetObserverRegistered) { + if (mCurrentRootAdapter != null) { + mCurrentRootAdapter.unregisterDataSetObserver(mDataSetObserver); + } + mCurrentRootAdapter = getPreferenceScreen().getRootAdapter(); + mCurrentRootAdapter.registerDataSetObserver(mDataSetObserver); + mIsDataSetObserverRegistered = true; + } + } + + public void unregisterObserverIfNeeded() { + if (mIsDataSetObserverRegistered) { + if (mCurrentRootAdapter != null) { + mCurrentRootAdapter.unregisterDataSetObserver(mDataSetObserver); + mCurrentRootAdapter = null; + } + mIsDataSetObserverRegistered = false; + } + } + + public void highlightPreferenceIfNeeded() { + if (isAdded() && !mPreferenceHighlighted &&!TextUtils.isEmpty(mPreferenceKey)) { + highlightPreference(mPreferenceKey); + } + } + + private Drawable getHighlightDrawable() { + if (mHighlightDrawable == null) { + mHighlightDrawable = getActivity().getDrawable(R.drawable.preference_highlight); + } + return mHighlightDrawable; + } + + /** + * Return a valid ListView position or -1 if none is found + */ + private int canUseListViewForHighLighting(String key) { + if (!hasListView()) { + return -1; + } + + ListView listView = getListView(); + ListAdapter adapter = listView.getAdapter(); + + if (adapter != null && adapter instanceof PreferenceGroupAdapter) { + return findListPositionFromKey(adapter, key); + } + + return -1; + } + + private void highlightPreference(String key) { + final Drawable highlight = getHighlightDrawable(); + + final int position = canUseListViewForHighLighting(key); + if (position >= 0) { + mPreferenceHighlighted = true; + + final ListView listView = getListView(); + final ListAdapter adapter = listView.getAdapter(); + + ((PreferenceGroupAdapter) adapter).setHighlightedDrawable(highlight); + ((PreferenceGroupAdapter) adapter).setHighlighted(position); + + listView.post(new Runnable() { + @Override + public void run() { + listView.setSelection(position); + listView.postDelayed(new Runnable() { + @Override + public void run() { + final int index = position - listView.getFirstVisiblePosition(); + if (index >= 0 && index < listView.getChildCount()) { + final View v = listView.getChildAt(index); + final int centerX = v.getWidth() / 2; + final int centerY = v.getHeight() / 2; + highlight.setHotspot(centerX, centerY); + v.setPressed(true); + v.setPressed(false); + } + } + }, DELAY_HIGHLIGHT_DURATION_MILLIS); + } + }); + } + } + + private int findListPositionFromKey(ListAdapter adapter, String key) { + final int count = adapter.getCount(); + for (int n = 0; n < count; n++) { + final Object item = adapter.getItem(n); + if (item instanceof Preference) { + Preference preference = (Preference) item; + final String preferenceKey = preference.getKey(); + if (preferenceKey != null && preferenceKey.equals(key)) { + return n; + } + } + } + return -1; + } + protected void removePreference(String key) { Preference pref = findPreference(key); if (pref != null) { @@ -147,7 +337,7 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di Log.e(TAG, "Old dialog fragment not null!"); } mDialogFragment = new SettingsDialogFragment(this, dialogId); - mDialogFragment.show(getActivity().getFragmentManager(), Integer.toString(dialogId)); + mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId)); } public Dialog onCreateDialog(int dialogId) { @@ -236,17 +426,18 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di public Dialog onCreateDialog(Bundle savedInstanceState) { if (savedInstanceState != null) { mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0); + mParentFragment = getParentFragment(); int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1); - if (mParentFragmentId > -1) { + if (mParentFragment == null) { mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId); - if (!(mParentFragment instanceof DialogCreatable)) { - throw new IllegalArgumentException( - (mParentFragment != null - ? mParentFragment.getClass().getName() - : mParentFragmentId) - + " must implement " - + DialogCreatable.class.getName()); - } + } + if (!(mParentFragment instanceof DialogCreatable)) { + throw new IllegalArgumentException( + (mParentFragment != null + ? mParentFragment.getClass().getName() + : mParentFragmentId) + + " must implement " + + DialogCreatable.class.getName()); } // This dialog fragment could be created from non-SettingsPreferenceFragment if (mParentFragment instanceof SettingsPreferenceFragment) { @@ -303,19 +494,23 @@ public class SettingsPreferenceFragment extends PreferenceFragment implements Di getActivity().onBackPressed(); } - public boolean startFragment( - Fragment caller, String fragmentClass, int requestCode, Bundle extras) { - if (getActivity() instanceof PreferenceActivity) { - PreferenceActivity preferenceActivity = (PreferenceActivity)getActivity(); - preferenceActivity.startPreferencePanel(fragmentClass, extras, - R.string.lock_settings_picker_title, null, caller, requestCode); + public boolean startFragment(Fragment caller, String fragmentClass, int titleRes, + int requestCode, Bundle extras) { + final Activity activity = getActivity(); + if (activity instanceof SettingsActivity) { + SettingsActivity sa = (SettingsActivity) activity; + sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, requestCode); + return true; + } else if (activity instanceof PreferenceActivity) { + PreferenceActivity sa = (PreferenceActivity) activity; + sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, 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 - + ")"); + Log.w(TAG, + "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to " + + "launch the given Fragment (name: " + fragmentClass + + ", requestCode: " + requestCode + ")"); return false; } } - } diff --git a/src/com/android/settings/SmsDefaultDialog.java b/src/com/android/settings/SmsDefaultDialog.java index d9a6c5f..3a3848b 100644 --- a/src/com/android/settings/SmsDefaultDialog.java +++ b/src/com/android/settings/SmsDefaultDialog.java @@ -64,7 +64,7 @@ public final class SmsDefaultDialog extends AlertActivity implements private boolean buildDialog(String packageName) { TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); - if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) { + if (!tm.isSmsCapable()) { // No phone, no SMS return false; } diff --git a/src/com/android/settings/SmsListPreference.java b/src/com/android/settings/SmsListPreference.java deleted file mode 100644 index 15df776..0000000 --- a/src/com/android/settings/SmsListPreference.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2013 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.AlertDialog.Builder; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.preference.ListPreference; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.CheckedTextView; -import android.widget.ImageView; -import android.widget.ListAdapter; - -/** - * Extends ListPreference to allow us to show the icons for the available SMS applications. We do - * this because the names of SMS applications are very similar and the user may not be able to - * determine what app they are selecting without an icon. - */ -public class SmsListPreference extends ListPreference { - private Drawable[] mEntryDrawables; - - public class SmsArrayAdapter extends ArrayAdapter<CharSequence> { - private Drawable[] mImageDrawables = null; - private int mSelectedIndex = 0; - - public SmsArrayAdapter(Context context, int textViewResourceId, - CharSequence[] objects, Drawable[] imageDrawables, int selectedIndex) { - super(context, textViewResourceId, objects); - mSelectedIndex = selectedIndex; - mImageDrawables = imageDrawables; - } - - public View getView(int position, View convertView, ViewGroup parent) { - LayoutInflater inflater = ((Activity)getContext()).getLayoutInflater(); - View view = inflater.inflate(R.layout.sms_preference_item, parent, false); - CheckedTextView checkedTextView = (CheckedTextView)view.findViewById(R.id.sms_text); - checkedTextView.setText(getItem(position)); - if (position == mSelectedIndex) { - checkedTextView.setChecked(true); - } - ImageView imageView = (ImageView)view.findViewById(R.id.sms_image); - imageView.setImageDrawable(mImageDrawables[position]); - return view; - } - } - - public SmsListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setEntryDrawables(Drawable[] entries) { - mEntryDrawables = entries; - } - - public Drawable[] getEntryDrawables() { - return mEntryDrawables; - } - - @Override - protected void onPrepareDialogBuilder(Builder builder) { - int selectedIndex = findIndexOfValue(getValue()); - ListAdapter adapter = new SmsArrayAdapter(getContext(), - R.layout.sms_preference_item, getEntries(), mEntryDrawables, selectedIndex); - builder.setAdapter(adapter, this); - super.onPrepareDialogBuilder(builder); - } -}
\ No newline at end of file diff --git a/src/com/android/settings/SoundSettings.java b/src/com/android/settings/SoundSettings.java deleted file mode 100644 index 28d93f1..0000000 --- a/src/com/android/settings/SoundSettings.java +++ /dev/null @@ -1,445 +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; - -import com.android.settings.bluetooth.DockEventReceiver; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -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.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.telephony.TelephonyManager; -import android.util.Log; - -import java.util.List; - -public class SoundSettings extends SettingsPreferenceFragment implements - Preference.OnPreferenceChangeListener { - private static final String TAG = "SoundSettings"; - - private static final int DIALOG_NOT_DOCKED = 1; - - /** If there is no setting in the provider, use this. */ - private static final int FALLBACK_EMERGENCY_TONE_VALUE = 0; - - private static final String KEY_VIBRATE = "vibrate_when_ringing"; - 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"; - private static final String KEY_EMERGENCY_TONE = "emergency_tone"; - private static final String KEY_SOUND_SETTINGS = "sound_settings"; - private static final String KEY_LOCK_SOUNDS = "lock_sounds"; - 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_and_notification"; - private static final String KEY_DOCK_CATEGORY = "dock_category"; - private static final String KEY_DOCK_AUDIO_SETTINGS = "dock_audio"; - private static final String KEY_DOCK_SOUNDS = "dock_sounds"; - private static final String KEY_DOCK_AUDIO_MEDIA_ENABLED = "dock_audio_media_enabled"; - - private static final String[] NEED_VOICE_CAPABILITY = { - KEY_RINGTONE, KEY_DTMF_TONE, KEY_CATEGORY_CALLS, - KEY_EMERGENCY_TONE, KEY_VIBRATE - }; - - private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1; - private static final int MSG_UPDATE_NOTIFICATION_SUMMARY = 2; - - private CheckBoxPreference mVibrateWhenRinging; - private CheckBoxPreference mDtmfTone; - private CheckBoxPreference mSoundEffects; - private CheckBoxPreference mHapticFeedback; - private Preference mMusicFx; - private CheckBoxPreference mLockSounds; - private Preference mRingtonePreference; - private Preference mNotificationPreference; - - private Runnable mRingtoneLookupRunnable; - - private AudioManager mAudioManager; - - private Preference mDockAudioSettings; - private CheckBoxPreference mDockSounds; - private Intent mDockIntent; - private CheckBoxPreference mDockAudioMediaEnabled; - - 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 final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(Intent.ACTION_DOCK_EVENT)) { - handleDockChange(intent); - } - } - }; - - private PreferenceGroup mSoundSettings; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ContentResolver resolver = getContentResolver(); - int activePhoneType = TelephonyManager.getDefault().getCurrentPhoneType(); - - mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - - addPreferencesFromResource(R.xml.sound_settings); - - if (TelephonyManager.PHONE_TYPE_CDMA != activePhoneType) { - // device is not CDMA, do not display CDMA emergency_tone - getPreferenceScreen().removePreference(findPreference(KEY_EMERGENCY_TONE)); - } - - if (!getResources().getBoolean(R.bool.has_silent_mode)) { - findPreference(KEY_RING_VOLUME).setDependency(null); - } - - if (getResources().getBoolean(com.android.internal.R.bool.config_useFixedVolume)) { - // device with fixed volume policy, do not display volumes submenu - getPreferenceScreen().removePreference(findPreference(KEY_RING_VOLUME)); - } - - mVibrateWhenRinging = (CheckBoxPreference) findPreference(KEY_VIBRATE); - mVibrateWhenRinging.setPersistent(false); - mVibrateWhenRinging.setChecked(Settings.System.getInt(resolver, - Settings.System.VIBRATE_WHEN_RINGING, 0) != 0); - - mDtmfTone = (CheckBoxPreference) findPreference(KEY_DTMF_TONE); - mDtmfTone.setPersistent(false); - mDtmfTone.setChecked(Settings.System.getInt(resolver, - Settings.System.DTMF_TONE_WHEN_DIALING, 1) != 0); - mSoundEffects = (CheckBoxPreference) findPreference(KEY_SOUND_EFFECTS); - mSoundEffects.setPersistent(false); - mSoundEffects.setChecked(Settings.System.getInt(resolver, - 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, 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); - - Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - if (vibrator == null || !vibrator.hasVibrator()) { - removePreference(KEY_VIBRATE); - removePreference(KEY_HAPTIC_FEEDBACK); - } - - if (TelephonyManager.PHONE_TYPE_CDMA == activePhoneType) { - ListPreference emergencyTonePreference = - (ListPreference) findPreference(KEY_EMERGENCY_TONE); - emergencyTonePreference.setValue(String.valueOf(Settings.Global.getInt( - resolver, Settings.Global.EMERGENCY_TONE, FALLBACK_EMERGENCY_TONE_VALUE))); - emergencyTonePreference.setOnPreferenceChangeListener(this); - } - - mSoundSettings = (PreferenceGroup) findPreference(KEY_SOUND_SETTINGS); - - 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); - if (pref != null) { - getPreferenceScreen().removePreference(pref); - } - } - } - - 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); - } - } - }; - - initDockSettings(); - } - - @Override - public void onResume() { - super.onResume(); - - lookupRingtoneNames(); - - IntentFilter filter = new IntentFilter(Intent.ACTION_DOCK_EVENT); - getActivity().registerReceiver(mReceiver, filter); - } - - @Override - public void onPause() { - super.onPause(); - - getActivity().unregisterReceiver(mReceiver); - } - - 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 != null) { - if (cursor.moveToFirst()) { - summary = cursor.getString(0); - } - cursor.close(); - } - } catch (SQLiteException sqle) { - // Unknown title for the ringtone - } - } - mHandler.sendMessage(mHandler.obtainMessage(msg, summary)); - } - - private void lookupRingtoneNames() { - new Thread(mRingtoneLookupRunnable).start(); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (preference == mVibrateWhenRinging) { - Settings.System.putInt(getContentResolver(), Settings.System.VIBRATE_WHEN_RINGING, - mVibrateWhenRinging.isChecked() ? 1 : 0); - } else if (preference == mDtmfTone) { - Settings.System.putInt(getContentResolver(), Settings.System.DTMF_TONE_WHEN_DIALING, - mDtmfTone.isChecked() ? 1 : 0); - - } else if (preference == mSoundEffects) { - if (mSoundEffects.isChecked()) { - mAudioManager.loadSoundEffects(); - } else { - mAudioManager.unloadSoundEffects(); - } - Settings.System.putInt(getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, - mSoundEffects.isChecked() ? 1 : 0); - - } else if (preference == mHapticFeedback) { - Settings.System.putInt(getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, - mHapticFeedback.isChecked() ? 1 : 0); - - } else if (preference == mLockSounds) { - Settings.System.putInt(getContentResolver(), Settings.System.LOCKSCREEN_SOUNDS_ENABLED, - mLockSounds.isChecked() ? 1 : 0); - - } else if (preference == mMusicFx) { - // let the framework fire off the intent - return false; - } else if (preference == mDockAudioSettings) { - int dockState = mDockIntent != null - ? mDockIntent.getIntExtra(Intent.EXTRA_DOCK_STATE, 0) - : Intent.EXTRA_DOCK_STATE_UNDOCKED; - - if (dockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) { - showDialog(DIALOG_NOT_DOCKED); - } else { - boolean isBluetooth = mDockIntent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) != null; - - if (isBluetooth) { - Intent i = new Intent(mDockIntent); - i.setAction(DockEventReceiver.ACTION_DOCK_SHOW_UI); - i.setClass(getActivity(), DockEventReceiver.class); - getActivity().sendBroadcast(i); - } else { - PreferenceScreen ps = (PreferenceScreen)mDockAudioSettings; - Bundle extras = ps.getExtras(); - extras.putBoolean("checked", - Settings.Global.getInt(getContentResolver(), - Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) == 1); - super.onPreferenceTreeClick(ps, ps); - } - } - } else if (preference == mDockSounds) { - Settings.Global.putInt(getContentResolver(), Settings.Global.DOCK_SOUNDS_ENABLED, - mDockSounds.isChecked() ? 1 : 0); - } else if (preference == mDockAudioMediaEnabled) { - Settings.Global.putInt(getContentResolver(), Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, - mDockAudioMediaEnabled.isChecked() ? 1 : 0); - } - return true; - } - - public boolean onPreferenceChange(Preference preference, Object objValue) { - final String key = preference.getKey(); - if (KEY_EMERGENCY_TONE.equals(key)) { - try { - int value = Integer.parseInt((String) objValue); - Settings.Global.putInt(getContentResolver(), - Settings.Global.EMERGENCY_TONE, value); - } catch (NumberFormatException e) { - Log.e(TAG, "could not persist emergency tone setting", e); - } - } - - return true; - } - - @Override - protected int getHelpResource() { - return R.string.help_url_sound; - } - - private boolean needsDockSettings() { - return getResources().getBoolean(R.bool.has_dock_settings); - } - - private void initDockSettings() { - ContentResolver resolver = getContentResolver(); - - if (needsDockSettings()) { - mDockSounds = (CheckBoxPreference) findPreference(KEY_DOCK_SOUNDS); - mDockSounds.setPersistent(false); - mDockSounds.setChecked(Settings.Global.getInt(resolver, - Settings.Global.DOCK_SOUNDS_ENABLED, 0) != 0); - mDockAudioSettings = findPreference(KEY_DOCK_AUDIO_SETTINGS); - mDockAudioSettings.setEnabled(false); - } else { - getPreferenceScreen().removePreference(findPreference(KEY_DOCK_CATEGORY)); - getPreferenceScreen().removePreference(findPreference(KEY_DOCK_AUDIO_SETTINGS)); - getPreferenceScreen().removePreference(findPreference(KEY_DOCK_SOUNDS)); - Settings.Global.putInt(resolver, Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 1); - } - } - - private void handleDockChange(Intent intent) { - if (mDockAudioSettings != null) { - int dockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, 0); - - boolean isBluetooth = - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) != null; - - mDockIntent = intent; - - if (dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { - // remove undocked dialog if currently showing. - try { - removeDialog(DIALOG_NOT_DOCKED); - } catch (IllegalArgumentException iae) { - // Maybe it was already dismissed - } - - if (isBluetooth) { - mDockAudioSettings.setEnabled(true); - } else { - if (dockState == Intent.EXTRA_DOCK_STATE_LE_DESK) { - ContentResolver resolver = getContentResolver(); - mDockAudioSettings.setEnabled(true); - if (Settings.Global.getInt(resolver, - Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, -1) == -1) { - Settings.Global.putInt(resolver, - Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0); - } - mDockAudioMediaEnabled = - (CheckBoxPreference) findPreference(KEY_DOCK_AUDIO_MEDIA_ENABLED); - mDockAudioMediaEnabled.setPersistent(false); - mDockAudioMediaEnabled.setChecked( - Settings.Global.getInt(resolver, - Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) != 0); - } else { - mDockAudioSettings.setEnabled(false); - } - } - } else { - mDockAudioSettings.setEnabled(false); - } - } - } - - @Override - public Dialog onCreateDialog(int id) { - if (id == DIALOG_NOT_DOCKED) { - return createUndockedMessage(); - } - return null; - } - - private Dialog createUndockedMessage() { - final AlertDialog.Builder ab = new AlertDialog.Builder(getActivity()); - ab.setTitle(R.string.dock_not_found_title); - ab.setMessage(R.string.dock_not_found_text); - ab.setPositiveButton(android.R.string.ok, null); - return ab.create(); - } -} - diff --git a/src/com/android/settings/SubSettings.java b/src/com/android/settings/SubSettings.java index 34e9ba3..04955b2 100644 --- a/src/com/android/settings/SubSettings.java +++ b/src/com/android/settings/SubSettings.java @@ -16,16 +16,13 @@ package com.android.settings; -import android.app.Fragment; import android.util.Log; -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; - /** * 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 { +public class SubSettings extends SettingsActivity { @Override public boolean onNavigateUp() { diff --git a/src/com/android/settings/TetherSettings.java b/src/com/android/settings/TetherSettings.java index 4e0933d..c611772 100644 --- a/src/com/android/settings/TetherSettings.java +++ b/src/com/android/settings/TetherSettings.java @@ -30,6 +30,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; @@ -40,13 +41,14 @@ import android.os.Environment; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; -import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; import android.text.TextUtils; import android.view.ViewGroup; import android.view.ViewParent; import android.webkit.WebView; +import android.widget.TextView; import java.io.InputStream; import java.util.ArrayList; @@ -63,16 +65,17 @@ public class TetherSettings extends SettingsPreferenceFragment private static final String USB_TETHER_SETTINGS = "usb_tether_settings"; private static final String ENABLE_WIFI_AP = "enable_wifi_ap"; private static final String ENABLE_BLUETOOTH_TETHERING = "enable_bluetooth_tethering"; + private static final String TETHER_CHOICE = "TETHER_TYPE"; private static final int DIALOG_AP_SETTINGS = 1; private WebView mView; - private CheckBoxPreference mUsbTether; + private SwitchPreference mUsbTether; private WifiApEnabler mWifiApEnabler; - private CheckBoxPreference mEnableWifiAp; + private SwitchPreference mEnableWifiAp; - private CheckBoxPreference mBluetoothTether; + private SwitchPreference mBluetoothTether; private BroadcastReceiver mTetherChangeReceiver; @@ -92,6 +95,7 @@ public class TetherSettings extends SettingsPreferenceFragment private WifiApDialog mDialog; private WifiManager mWifiManager; private WifiConfiguration mWifiConfig = null; + private UserManager mUm; private boolean mUsbConnected; private boolean mMassStorageActive; @@ -110,11 +114,25 @@ public class TetherSettings extends SettingsPreferenceFragment private String[] mProvisionApp; private static final int PROVISION_REQUEST = 0; + private boolean mUnavailable; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); + + if(icicle != null) { + mTetherChoice = icicle.getInt(TETHER_CHOICE); + } addPreferencesFromResource(R.xml.tether_prefs); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); + + if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING)) { + mUnavailable = true; + setPreferenceScreen(new PreferenceScreen(getActivity(), null)); + return; + } + final Activity activity = getActivity(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null) { @@ -123,10 +141,10 @@ public class TetherSettings extends SettingsPreferenceFragment } mEnableWifiAp = - (CheckBoxPreference) findPreference(ENABLE_WIFI_AP); + (SwitchPreference) findPreference(ENABLE_WIFI_AP); Preference wifiApSettings = findPreference(WIFI_AP_SSID_AND_SECURITY); - mUsbTether = (CheckBoxPreference) findPreference(USB_TETHER_SETTINGS); - mBluetoothTether = (CheckBoxPreference) findPreference(ENABLE_BLUETOOTH_TETHERING); + mUsbTether = (SwitchPreference) findPreference(USB_TETHER_SETTINGS); + mBluetoothTether = (SwitchPreference) findPreference(ENABLE_BLUETOOTH_TETHERING); ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); @@ -168,6 +186,12 @@ public class TetherSettings extends SettingsPreferenceFragment mView = new WebView(activity); } + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putInt(TETHER_CHOICE, mTetherChoice); + super.onSaveInstanceState(savedInstanceState); + } + private void initWifiTethering() { final Activity activity = getActivity(); mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); @@ -264,6 +288,15 @@ public class TetherSettings extends SettingsPreferenceFragment public void onStart() { super.onStart(); + if (mUnavailable) { + TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); + getListView().setEmptyView(emptyView); + if (emptyView != null) { + emptyView.setText(R.string.tethering_settings_not_available); + } + return; + } + final Activity activity = getActivity(); mMassStorageActive = Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState()); @@ -297,6 +330,10 @@ public class TetherSettings extends SettingsPreferenceFragment @Override public void onStop() { super.onStop(); + + if (mUnavailable) { + return; + } getActivity().unregisterReceiver(mTetherChangeReceiver); mTetherChangeReceiver = null; if (mWifiApEnabler != null) { @@ -435,18 +472,40 @@ public class TetherSettings extends SettingsPreferenceFragment return false; } - boolean isProvisioningNeeded() { - if (SystemProperties.getBoolean("net.tethering.noprovisioning", false)) { + public static boolean isProvisioningNeededButUnavailable(Context context) { + String[] provisionApp = context.getResources().getStringArray( + com.android.internal.R.array.config_mobile_hotspot_provision_app); + return (isProvisioningNeeded(provisionApp) + && !isIntentAvailable(context, provisionApp)); + } + + private static boolean isIntentAvailable(Context context, String[] provisionApp) { + if (provisionApp.length < 2) { + throw new IllegalArgumentException("provisionApp length should at least be 2"); + } + final PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(provisionApp[0], provisionApp[1]); + + return (packageManager.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY).size() > 0); + } + + + private static boolean isProvisioningNeeded(String[] provisionApp) { + if (SystemProperties.getBoolean("net.tethering.noprovisioning", false) + || provisionApp == null) { return false; } - return mProvisionApp.length == 2; + return (provisionApp.length == 2); } private void startProvisioningIfNecessary(int choice) { mTetherChoice = choice; - if (isProvisioningNeeded()) { + if (isProvisioningNeeded(mProvisionApp)) { Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClassName(mProvisionApp[0], mProvisionApp[1]); + intent.putExtra(TETHER_CHOICE, mTetherChoice); startActivityForResult(intent, PROVISION_REQUEST); } else { startTethering(); @@ -459,7 +518,7 @@ public class TetherSettings extends SettingsPreferenceFragment if (resultCode == Activity.RESULT_OK) { startTethering(); } else { - //BT and USB need checkbox turned off on failure + //BT and USB need switch turned off on failure //Wifi tethering is never turned on until afterwards switch (mTetherChoice) { case BLUETOOTH_TETHERING: diff --git a/src/com/android/settings/TrustAgentSettings.java b/src/com/android/settings/TrustAgentSettings.java new file mode 100644 index 0000000..54fef36 --- /dev/null +++ b/src/com/android/settings/TrustAgentSettings.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 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 java.util.List; + +import android.content.ComponentName; +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.preference.Preference; +import android.preference.PreferenceGroup; +import android.preference.SwitchPreference; +import android.service.trust.TrustAgentService; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.widget.LockPatternUtils; + +public class TrustAgentSettings extends SettingsPreferenceFragment implements + Preference.OnPreferenceChangeListener { + private static final String SERVICE_INTERFACE = TrustAgentService.SERVICE_INTERFACE; + private ArrayMap<ComponentName, AgentInfo> mAvailableAgents; + private final ArraySet<ComponentName> mActiveAgents = new ArraySet<ComponentName>(); + private LockPatternUtils mLockPatternUtils; + + public static final class AgentInfo { + CharSequence label; + ComponentName component; // service that implements ITrustAgent + SwitchPreference preference; + public Drawable icon; + + @Override + public boolean equals(Object other) { + if (other instanceof AgentInfo) { + return component.equals(((AgentInfo)other).component); + } + return true; + } + + public int compareTo(AgentInfo other) { + return component.compareTo(other.component); + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.trust_agent_settings); + } + + public void onResume() { + super.onResume(); + updateAgents(); + }; + + private void updateAgents() { + final Context context = getActivity(); + if (mAvailableAgents == null) { + mAvailableAgents = findAvailableTrustAgents(); + } + if (mLockPatternUtils == null) { + mLockPatternUtils = new LockPatternUtils(getActivity()); + } + loadActiveAgents(); + PreferenceGroup category = + (PreferenceGroup) getPreferenceScreen().findPreference("trust_agents"); + category.removeAll(); + final int count = mAvailableAgents.size(); + for (int i = 0; i < count; i++) { + AgentInfo agent = mAvailableAgents.valueAt(i); + final SwitchPreference preference = new SwitchPreference(context); + agent.preference = preference; + preference.setPersistent(false); + preference.setTitle(agent.label); + preference.setIcon(agent.icon); + preference.setPersistent(false); + preference.setOnPreferenceChangeListener(this); + preference.setChecked(mActiveAgents.contains(agent.component)); + category.addPreference(agent.preference); + } + } + + private void loadActiveAgents() { + List<ComponentName> activeTrustAgents = mLockPatternUtils.getEnabledTrustAgents(); + if (activeTrustAgents != null) { + mActiveAgents.addAll(activeTrustAgents); + } + } + + private void saveActiveAgents() { + mLockPatternUtils.setEnabledTrustAgents(mActiveAgents); + } + + ArrayMap<ComponentName, AgentInfo> findAvailableTrustAgents() { + PackageManager pm = getActivity().getPackageManager(); + Intent trustAgentIntent = new Intent(SERVICE_INTERFACE); + List<ResolveInfo> resolveInfos = pm.queryIntentServices(trustAgentIntent, + PackageManager.GET_META_DATA); + + ArrayMap<ComponentName, AgentInfo> agents = new ArrayMap<ComponentName, AgentInfo>(); + final int count = resolveInfos.size(); + agents.ensureCapacity(count); + for (int i = 0; i < count; i++ ) { + ResolveInfo resolveInfo = resolveInfos.get(i); + if (resolveInfo.serviceInfo == null) continue; + if (!TrustAgentUtils.checkProvidePermission(resolveInfo, pm)) continue; + ComponentName name = TrustAgentUtils.getComponentName(resolveInfo); + AgentInfo agentInfo = new AgentInfo(); + agentInfo.label = resolveInfo.loadLabel(pm); + agentInfo.icon = resolveInfo.loadIcon(pm); + agentInfo.component = name; + agents.put(name, agentInfo); + } + return agents; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference instanceof SwitchPreference) { + final int count = mAvailableAgents.size(); + for (int i = 0; i < count; i++) { + AgentInfo agent = mAvailableAgents.valueAt(i); + if (agent.preference == preference) { + if ((Boolean) newValue) { + if (!mActiveAgents.contains(agent.component)) { + mActiveAgents.add(agent.component); + } + } else { + mActiveAgents.remove(agent.component); + } + saveActiveAgents(); + return true; + } + } + } + return false; + } + +} diff --git a/src/com/android/settings/TrustAgentUtils.java b/src/com/android/settings/TrustAgentUtils.java new file mode 100644 index 0000000..31a073c --- /dev/null +++ b/src/com/android/settings/TrustAgentUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2014 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.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.service.trust.TrustAgentService; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +public class TrustAgentUtils { + static final String TAG = "TrustAgentUtils"; + + private static final String TRUST_AGENT_META_DATA = TrustAgentService.TRUST_AGENT_META_DATA; + private static final String PERMISSION_PROVIDE_AGENT = android.Manifest.permission.PROVIDE_TRUST_AGENT; + + /** + * @return true, if the service in resolveInfo has the permission to provide a trust agent. + */ + public static boolean checkProvidePermission(ResolveInfo resolveInfo, PackageManager pm) { + String packageName = resolveInfo.serviceInfo.packageName; + if (pm.checkPermission(PERMISSION_PROVIDE_AGENT, packageName) + != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "Skipping agent because package " + packageName + + " does not have permission " + PERMISSION_PROVIDE_AGENT + "."); + return false; + } + return true; + } + + public static class TrustAgentComponentInfo { + ComponentName componentName; + String title; + String summary; + } + + public static ComponentName getComponentName(ResolveInfo resolveInfo) { + if (resolveInfo == null || resolveInfo.serviceInfo == null) return null; + return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + } + + public static TrustAgentComponentInfo getSettingsComponent( + PackageManager pm, ResolveInfo resolveInfo) { + if (resolveInfo == null || resolveInfo.serviceInfo == null + || resolveInfo.serviceInfo.metaData == null) return null; + String cn = null; + TrustAgentComponentInfo trustAgentComponentInfo = new TrustAgentComponentInfo(); + XmlResourceParser parser = null; + Exception caughtException = null; + try { + parser = resolveInfo.serviceInfo.loadXmlMetaData(pm, TRUST_AGENT_META_DATA); + if (parser == null) { + Slog.w(TAG, "Can't find " + TRUST_AGENT_META_DATA + " meta-data"); + return null; + } + Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + String nodeName = parser.getName(); + if (!"trust-agent".equals(nodeName)) { + Slog.w(TAG, "Meta-data does not start with trust-agent tag"); + return null; + } + TypedArray sa = + res.obtainAttributes(attrs, com.android.internal.R.styleable.TrustAgent); + trustAgentComponentInfo.summary = + sa.getString(com.android.internal.R.styleable.TrustAgent_summary); + trustAgentComponentInfo.title = + sa.getString(com.android.internal.R.styleable.TrustAgent_title); + cn = sa.getString(com.android.internal.R.styleable.TrustAgent_settingsActivity); + sa.recycle(); + } catch (PackageManager.NameNotFoundException e) { + caughtException = e; + } catch (IOException e) { + caughtException = e; + } catch (XmlPullParserException e) { + caughtException = e; + } finally { + if (parser != null) parser.close(); + } + if (caughtException != null) { + Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException); + return null; + } + if (cn != null && cn.indexOf('/') < 0) { + cn = resolveInfo.serviceInfo.packageName + "/" + cn; + } + trustAgentComponentInfo.componentName = (cn == null) ? null : ComponentName.unflattenFromString(cn); + return trustAgentComponentInfo; + } +} diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java index cdb96cb..14c4936 100644 --- a/src/com/android/settings/TrustedCredentialsSettings.java +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -16,41 +16,49 @@ package com.android.settings; -import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; +import android.content.pm.UserInfo; +import android.content.res.TypedArray; import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; +import android.os.UserHandle; import android.os.UserManager; import android.security.IKeyChainService; import android.security.KeyChain; import android.security.KeyChain.KeyChainConnection; +import android.util.SparseArray; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; import android.widget.BaseAdapter; +import android.widget.BaseExpandableListAdapter; import android.widget.Button; import android.widget.CheckBox; -import android.widget.FrameLayout; +import android.widget.ExpandableListView; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; +import android.widget.Spinner; import android.widget.TabHost; import android.widget.TextView; + +import com.android.internal.util.ParcelableString; + 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 com.android.org.conscrypt.TrustedCertificateStore; public class TrustedCredentialsSettings extends Fragment { @@ -60,24 +68,20 @@ public class TrustedCredentialsSettings extends Fragment { private static final String USER_ACTION = "com.android.settings.TRUSTED_CREDENTIALS_USER"; - private static final int REQUEST_PIN_CHALLENGE = 12309; - // If the restriction PIN is entered correctly. - private boolean mChallengeSucceeded; - private boolean mChallengeRequested; - - private enum Tab { SYSTEM("system", R.string.trusted_credentials_system_tab, R.id.system_tab, R.id.system_progress, R.id.system_list, + R.id.system_expandable_list, true), USER("user", R.string.trusted_credentials_user_tab, R.id.user_tab, R.id.user_progress, R.id.user_list, + R.id.user_expandable_list, false); private final String mTag; @@ -85,28 +89,34 @@ public class TrustedCredentialsSettings extends Fragment { private final int mView; private final int mProgress; private final int mList; + private final int mExpandableList; private final boolean mCheckbox; - private Tab(String tag, int label, int view, int progress, int list, boolean checkbox) { + + private Tab(String tag, int label, int view, int progress, int list, int expandableList, + boolean checkbox) { mTag = tag; mLabel = label; mView = view; mProgress = progress; mList = list; + mExpandableList = expandableList; mCheckbox = checkbox; } - private Set<String> getAliases(TrustedCertificateStore store) { + + private List<ParcelableString> getAliases(IKeyChainService service) throws RemoteException { switch (this) { - case SYSTEM: - return store.allSystemAliases(); + case SYSTEM: { + return service.getSystemCaAliases().getList(); + } case USER: - return store.userAliases(); + return service.getUserCaAliases().getList(); } throw new AssertionError(); } - private boolean deleted(TrustedCertificateStore store, String alias) { + private boolean deleted(IKeyChainService service, String alias) throws RemoteException { switch (this) { case SYSTEM: - return !store.containsAlias(alias); + return !service.containsCaAlias(alias); case USER: return false; } @@ -141,7 +151,7 @@ public class TrustedCredentialsSettings extends Fragment { if (certHolder.mTab.mCheckbox) { certHolder.mDeleted = !certHolder.mDeleted; } else { - certHolder.mAdapter.mCertHolders.remove(certHolder); + certHolder.mAdapter.remove(certHolder); } certHolder.mAdapter.notifyDataSetChanged(); } else { @@ -151,10 +161,9 @@ public class TrustedCredentialsSettings extends Fragment { } } - // 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; + private final SparseArray<KeyChainConnection> + mKeyChainConnectionByProfileId = new SparseArray<KeyChainConnection>(); @Override public void onCreate(Bundle savedInstanceState) { @@ -176,6 +185,19 @@ public class TrustedCredentialsSettings extends Fragment { } return mTabHost; } + @Override + public void onDestroy() { + closeKeyChainConnections(); + super.onDestroy(); + } + + private void closeKeyChainConnections() { + final int n = mKeyChainConnectionByProfileId.size(); + for (int i = 0; i < n; ++i) { + mKeyChainConnectionByProfileId.valueAt(i).close(); + } + mKeyChainConnectionByProfileId.clear(); + } private void addTab(Tab tab) { TabHost.TabSpec systemSpec = mTabHost.newTabSpec(tab.mTag) @@ -183,86 +205,266 @@ public class TrustedCredentialsSettings extends Fragment { .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)); + if (mUserManager.getUserProfiles().size() > 1) { + ExpandableListView lv = (ExpandableListView) mTabHost.findViewById(tab.mExpandableList); + final TrustedCertificateExpandableAdapter adapter = + new TrustedCertificateExpandableAdapter(tab); + lv.setAdapter(adapter); + lv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { + @Override + public boolean onChildClick(ExpandableListView parent, View v, + int groupPosition, int childPosition, long id) { + showCertDialog(adapter.getChild(groupPosition, childPosition)); + return true; + } + }); + } else { + 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)); + } + }); + } + } + + /** + * Common interface for adapters of both expandable and non-expandable certificate lists. + */ + private interface TrustedCertificateAdapterCommons { + /** + * Remove a certificate from the list. + * @param certHolder the certificate to be removed. + */ + void remove(CertHolder certHolder); + /** + * Notify the adapter that the underlying data set has changed. + */ + void notifyDataSetChanged(); + /** + * Load the certificates. + */ + void load(); + /** + * Gets the identifier of the list view the adapter is connected to. + * @param tab the tab on which the list view resides. + * @return identifier of the list view. + */ + int getListViewId(Tab tab); + } + + /** + * Adapter for expandable list view of certificates. Groups in the view correspond to profiles + * whereas children correspond to certificates. + */ + private class TrustedCertificateExpandableAdapter extends BaseExpandableListAdapter implements + TrustedCertificateAdapterCommons { + private AdapterData mData; + + private TrustedCertificateExpandableAdapter(Tab tab) { + mData = new AdapterData(tab, this); + load(); + } + @Override + public void remove(CertHolder certHolder) { + mData.remove(certHolder); + } + @Override + public int getGroupCount() { + return mData.mCertHoldersByUserId.size(); + } + @Override + public int getChildrenCount(int groupPosition) { + List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(groupPosition); + if (certHolders != null) { + return certHolders.size(); } - }); + return 0; + } + @Override + public UserHandle getGroup(int groupPosition) { + return new UserHandle(mData.mCertHoldersByUserId.keyAt(groupPosition)); + } + @Override + public CertHolder getChild(int groupPosition, int childPosition) { + return mData.mCertHoldersByUserId.valueAt(groupPosition).get(childPosition); + } + @Override + public long getGroupId(int groupPosition) { + return mData.mCertHoldersByUserId.keyAt(groupPosition); + } + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + @Override + public boolean hasStableIds() { + return false; + } + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) getActivity() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflateCategoryHeader(inflater, parent); + } + + final TextView title = (TextView) convertView.findViewById(android.R.id.title); + final UserHandle profile = getGroup(groupPosition); + final UserInfo userInfo = mUserManager.getUserInfo(profile.getIdentifier()); + if (userInfo.isManagedProfile()) { + title.setText(R.string.category_work); + } else { + title.setText(R.string.category_personal); + } + title.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + + return convertView; + } + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + return getViewForCertificate(getChild(groupPosition, childPosition), mData.mTab, + convertView, parent); + } + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + @Override + public void load() { + mData.new AliasLoader().execute(); + } + @Override + public int getListViewId(Tab tab) { + return tab.mExpandableList; + } + private View inflateCategoryHeader(LayoutInflater inflater, ViewGroup parent) { + final TypedArray a = inflater.getContext().obtainStyledAttributes(null, + com.android.internal.R.styleable.Preference, + com.android.internal.R.attr.preferenceCategoryStyle, 0); + final int resId = a.getResourceId(com.android.internal.R.styleable.Preference_layout, + 0); + return inflater.inflate(resId, parent, false); + } + } - private class TrustedCertificateAdapter extends BaseAdapter { - private final List<CertHolder> mCertHolders = new ArrayList<CertHolder>(); - private final Tab mTab; + private class TrustedCertificateAdapter extends BaseAdapter implements + TrustedCertificateAdapterCommons { + private final AdapterData mData; private TrustedCertificateAdapter(Tab tab) { - mTab = tab; + mData = new AdapterData(tab, this); load(); } - private void load() { - new AliasLoader().execute(); + @Override + public void remove(CertHolder certHolder) { + mData.remove(certHolder); + } + @Override + public int getListViewId(Tab tab) { + return tab.mList; + } + @Override + public void load() { + mData.new AliasLoader().execute(); } @Override public int getCount() { - return mCertHolders.size(); + List<CertHolder> certHolders = mData.mCertHoldersByUserId.valueAt(0); + if (certHolders != null) { + return certHolders.size(); + } + return 0; } @Override public CertHolder getItem(int position) { - return mCertHolders.get(position); + return mData.mCertHoldersByUserId.valueAt(0).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; - }; + return getViewForCertificate(getItem(position), mData.mTab, view, parent); + } + } + + private class AdapterData { + private final SparseArray<List<CertHolder>> mCertHoldersByUserId = + new SparseArray<List<CertHolder>>(); + private final Tab mTab; + private final TrustedCertificateAdapterCommons mAdapter; + + private AdapterData(Tab tab, TrustedCertificateAdapterCommons adapter) { + mAdapter = adapter; + mTab = tab; + } + + private class AliasLoader extends AsyncTask<Void, Integer, SparseArray<List<CertHolder>>> { + private ProgressBar mProgressBar; + private View mList; - 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); + mList = content.findViewById(mAdapter.getListViewId(mTab)); 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); + @Override protected SparseArray<List<CertHolder>> doInBackground(Void... params) { + SparseArray<List<CertHolder>> certHoldersByProfile = + new SparseArray<List<CertHolder>>(); + try { + List<UserHandle> profiles = mUserManager.getUserProfiles(); + final int n = profiles.size(); + // First we get all aliases for all profiles in order to show progress + // correctly. Otherwise this could all be in a single loop. + SparseArray<List<ParcelableString>> aliasesByProfileId = new SparseArray< + List<ParcelableString>>(n); + int max = 0; + int progress = 0; + for (int i = 0; i < n; ++i) { + UserHandle profile = profiles.get(i); + int profileId = profile.getIdentifier(); + KeyChainConnection keyChainConnection = KeyChain.bindAsUser(getActivity(), + profile); + // Saving the connection for later use on the certificate dialog. + mKeyChainConnectionByProfileId.put(profileId, keyChainConnection); + IKeyChainService service = keyChainConnection.getService(); + List<ParcelableString> aliases = mTab.getAliases(service); + max += aliases.size(); + aliasesByProfileId.put(profileId, aliases); + } + for (int i = 0; i < n; ++i) { + UserHandle profile = profiles.get(i); + int profileId = profile.getIdentifier(); + List<ParcelableString> aliases = aliasesByProfileId.get(profileId); + IKeyChainService service = mKeyChainConnectionByProfileId.get(profileId) + .getService(); + List<CertHolder> certHolders = new ArrayList<CertHolder>(max); + final int aliasMax = aliases.size(); + for (int j = 0; j < aliasMax; ++j) { + String alias = aliases.get(j).string; + byte[] encodedCertificate = service.getEncodedCaCertificate(alias, + true); + X509Certificate cert = KeyChain.toCertificate(encodedCertificate); + certHolders.add(new CertHolder(service, mAdapter, + mTab, alias, cert, profileId)); + publishProgress(++progress, max); + } + Collections.sort(certHolders); + certHoldersByProfile.put(profileId, certHolders); + } + return certHoldersByProfile; + } catch (RemoteException e) { + Log.e(TAG, "Remote exception while loading aliases.", e); + return new SparseArray<List<CertHolder>>(); + } catch (InterruptedException e) { + Log.e(TAG, "InterruptedException while loading aliases.", e); + return new SparseArray<List<CertHolder>>(); } - Collections.sort(certHolders); - return certHolders; } @Override protected void onProgressUpdate(Integer... progressAndMax) { int progress = progressAndMax[0]; @@ -272,21 +474,33 @@ public class TrustedCredentialsSettings extends Fragment { } mProgressBar.setProgress(progress); } - @Override protected void onPostExecute(List<CertHolder> certHolders) { - mCertHolders.clear(); - mCertHolders.addAll(certHolders); - notifyDataSetChanged(); - View content = mTabHost.getTabContentView(); + @Override protected void onPostExecute(SparseArray<List<CertHolder>> certHolders) { + mCertHoldersByUserId.clear(); + final int n = certHolders.size(); + for (int i = 0; i < n; ++i) { + mCertHoldersByUserId.put(certHolders.keyAt(i), certHolders.valueAt(i)); + } + mAdapter.notifyDataSetChanged(); mProgressBar.setVisibility(View.GONE); mList.setVisibility(View.VISIBLE); mProgressBar.setProgress(0); } } + + public void remove(CertHolder certHolder) { + if (mCertHoldersByUserId != null) { + final List<CertHolder> certs = mCertHoldersByUserId.get(certHolder.mProfileId); + if (certs != null) { + certs.remove(certHolder); + } + } + } } private static class CertHolder implements Comparable<CertHolder> { - private final TrustedCertificateStore mStore; - private final TrustedCertificateAdapter mAdapter; + public int mProfileId; + private final IKeyChainService mService; + private final TrustedCertificateAdapterCommons mAdapter; private final Tab mTab; private final String mAlias; private final X509Certificate mX509Cert; @@ -296,12 +510,14 @@ public class TrustedCredentialsSettings extends Fragment { private final String mSubjectSecondary; private boolean mDeleted; - private CertHolder(TrustedCertificateStore store, - TrustedCertificateAdapter adapter, + private CertHolder(IKeyChainService service, + TrustedCertificateAdapterCommons adapter, Tab tab, String alias, - X509Certificate x509Cert) { - mStore = store; + X509Certificate x509Cert, + int profileId) { + mProfileId = profileId; + mService = service; mAdapter = adapter; mTab = tab; mAlias = alias; @@ -332,7 +548,13 @@ public class TrustedCredentialsSettings extends Fragment { mSubjectSecondary = ""; } } - mDeleted = mTab.deleted(mStore, mAlias); + try { + mDeleted = mTab.deleted(mService, mAlias); + } catch (RemoteException e) { + Log.e(TAG, "Remote exception while checking if alias " + mAlias + " is deleted.", + e); + mDeleted = false; + } } @Override public int compareTo(CertHolder o) { int primary = this.mSubjectPrimary.compareToIgnoreCase(o.mSubjectPrimary); @@ -353,6 +575,32 @@ public class TrustedCredentialsSettings extends Fragment { } } + private View getViewForCertificate(CertHolder certHolder, Tab mTab, View convertView, + ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + LayoutInflater inflater = LayoutInflater.from(getActivity()); + convertView = inflater.inflate(R.layout.trusted_credential, parent, false); + holder = new ViewHolder(); + holder.mSubjectPrimaryView = (TextView) + convertView.findViewById(R.id.trusted_credential_subject_primary); + holder.mSubjectSecondaryView = (TextView) + convertView.findViewById(R.id.trusted_credential_subject_secondary); + holder.mCheckBox = (CheckBox) convertView.findViewById( + R.id.trusted_credential_status); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + holder.mSubjectPrimaryView.setText(certHolder.mSubjectPrimary); + holder.mSubjectSecondaryView.setText(certHolder.mSubjectSecondary); + if (mTab.mCheckbox) { + holder.mCheckBox.setChecked(!certHolder.mDeleted); + holder.mCheckBox.setVisibility(View.VISIBLE); + } + return convertView; + } + private static class ViewHolder { private TextView mSubjectPrimaryView; private TextView mSubjectSecondaryView; @@ -360,10 +608,42 @@ public class TrustedCredentialsSettings extends Fragment { } 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); + + final ArrayList<View> views = new ArrayList<View>(); + final ArrayList<String> titles = new ArrayList<String>(); + addCertChain(certHolder, views, titles); + + ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(getActivity(), + android.R.layout.simple_spinner_item, + titles); + arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + Spinner spinner = new Spinner(getActivity()); + spinner.setAdapter(arrayAdapter); + spinner.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, + long id) { + for(int i = 0; i < views.size(); i++) { + views.get(i).setVisibility(i == position ? View.VISIBLE : View.GONE); + } + } + @Override + public void onNothingSelected(AdapterView<?> parent) { } + }); + + LinearLayout container = new LinearLayout(getActivity()); + container.setOrientation(LinearLayout.VERTICAL); + container.addView(spinner); + for (int i = 0; i < views.size(); ++i) { + View certificateView = views.get(i); + if (i != 0) { + certificateView.setVisibility(View.GONE); + } + container.addView(certificateView); + } + builder.setView(container); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); @@ -371,20 +651,17 @@ public class TrustedCredentialsSettings extends Fragment { }); final Dialog certDialog = builder.create(); - ViewGroup body = (ViewGroup) view.findViewById(com.android.internal.R.id.body); + ViewGroup body = (ViewGroup) container.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); + if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { + body.addView(removeButton); + } removeButton.setText(certHolder.mTab.getButtonLabel(certHolder)); removeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mUserManager.hasRestrictionsChallenge() && !mChallengeSucceeded) { - ensurePin(); - return; - } - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(certHolder.mTab.getButtonConfirmation(certHolder)); builder.setPositiveButton( @@ -409,68 +686,75 @@ public class TrustedCredentialsSettings extends Fragment { certDialog.show(); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_PIN_CHALLENGE) { - mChallengeRequested = false; - if (resultCode == Activity.RESULT_OK) { - mChallengeSucceeded = true; + private void addCertChain(final CertHolder certHolder, + final ArrayList<View> views, final ArrayList<String> titles) { + + List<X509Certificate> certificates = null; + try { + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + certHolder.mProfileId); + IKeyChainService service = keyChainConnection.getService(); + List<String> chain = service.getCaCertificateChainAliases(certHolder.mAlias, true); + final int n = chain.size(); + certificates = new ArrayList<X509Certificate>(n); + for (int i = 0; i < n; ++i) { + byte[] encodedCertificate = service.getEncodedCaCertificate(chain.get(i), true); + X509Certificate certificate = KeyChain.toCertificate(encodedCertificate); + certificates.add(certificate); } + } catch (RemoteException ex) { + Log.e(TAG, "RemoteException while retrieving certificate chain for root " + + certHolder.mAlias, ex); return; } - - super.onActivityResult(requestCode, resultCode, data); - } - - private void ensurePin() { - if (!mChallengeSucceeded) { - final UserManager um = UserManager.get(getActivity()); - if (!mChallengeRequested) { - if (um.hasRestrictionsChallenge()) { - Intent requestPin = - new Intent(Intent.ACTION_RESTRICTIONS_CHALLENGE); - startActivityForResult(requestPin, REQUEST_PIN_CHALLENGE); - mChallengeRequested = true; - } - } + for (X509Certificate certificate : certificates) { + addCertDetails(certificate, views, titles); } - mChallengeSucceeded = false; } + private void addCertDetails(X509Certificate certificate, final ArrayList<View> views, + final ArrayList<String> titles) { + SslCertificate sslCert = new SslCertificate(certificate); + views.add(sslCert.inflateCertificateView(getActivity())); + titles.add(sslCert.getIssuedTo().getCName()); + } private class AliasOperation extends AsyncTask<Void, Void, Boolean> { private final CertHolder mCertHolder; + private AliasOperation(CertHolder certHolder) { mCertHolder = certHolder; } - @Override protected Boolean doInBackground(Void... params) { + + @Override + protected Boolean doInBackground(Void... params) { try { - KeyChainConnection keyChainConnection = KeyChain.bind(getActivity()); + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + mCertHolder.mProfileId); 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(); + if (mCertHolder.mDeleted) { + byte[] bytes = mCertHolder.mX509Cert.getEncoded(); + service.installCaCertificate(bytes); + return true; + } else { + return service.deleteCaCertificate(mCertHolder.mAlias); } } catch (CertificateEncodingException e) { + Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, + e); return false; } catch (IllegalStateException e) { // used by installCaCertificate to report errors + Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, e); return false; } catch (RemoteException e) { - return false; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + Log.w(TAG, "Error while toggling alias " + mCertHolder.mAlias, e); return false; } } - @Override protected void onPostExecute(Boolean ok) { + + @Override + protected void onPostExecute(Boolean ok) { mCertHolder.mTab.postOperationUpdate(ok, mCertHolder); } } diff --git a/src/com/android/settings/UsageAccessSettings.java b/src/com/android/settings/UsageAccessSettings.java new file mode 100644 index 0000000..89e184e --- /dev/null +++ b/src/com/android/settings/UsageAccessSettings.java @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2014 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 com.android.internal.content.PackageMonitor; + +import android.Manifest; +import android.app.ActivityThread; +import android.app.AlertDialog; +import android.app.AppOpsManager; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.util.ArrayMap; +import android.util.Log; + +import java.util.List; + +public class UsageAccessSettings extends SettingsPreferenceFragment implements + Preference.OnPreferenceChangeListener { + + private static final String TAG = "UsageAccessSettings"; + + private static final String[] PM_USAGE_STATS_PERMISSION = new String[] { + Manifest.permission.PACKAGE_USAGE_STATS + }; + + private static final int[] APP_OPS_OP_CODES = new int[] { + AppOpsManager.OP_GET_USAGE_STATS + }; + + private static class PackageEntry { + public PackageEntry(String packageName) { + this.packageName = packageName; + this.appOpMode = AppOpsManager.MODE_DEFAULT; + } + + final String packageName; + PackageInfo packageInfo; + boolean permissionGranted; + int appOpMode; + + SwitchPreference preference; + } + + /** + * Fetches the list of Apps that are requesting access to the UsageStats API and updates + * the PreferenceScreen with the results when complete. + */ + private class AppsRequestingAccessFetcher extends + AsyncTask<Void, Void, ArrayMap<String, PackageEntry>> { + + private final Context mContext; + private final PackageManager mPackageManager; + private final IPackageManager mIPackageManager; + + public AppsRequestingAccessFetcher(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + mIPackageManager = ActivityThread.getPackageManager(); + } + + @Override + protected ArrayMap<String, PackageEntry> doInBackground(Void... params) { + final String[] packages; + try { + packages = mIPackageManager.getAppOpPermissionPackages( + Manifest.permission.PACKAGE_USAGE_STATS); + } catch (RemoteException e) { + Log.w(TAG, "PackageManager is dead. Can't get list of packages requesting " + + Manifest.permission.PACKAGE_USAGE_STATS); + return null; + } + + if (packages == null) { + // No packages are requesting permission to use the UsageStats API. + return null; + } + + ArrayMap<String, PackageEntry> entries = new ArrayMap<>(); + for (final String packageName : packages) { + if (!shouldIgnorePackage(packageName)) { + entries.put(packageName, new PackageEntry(packageName)); + } + } + + // Load the packages that have been granted the PACKAGE_USAGE_STATS permission. + final List<PackageInfo> packageInfos = mPackageManager.getPackagesHoldingPermissions( + PM_USAGE_STATS_PERMISSION, 0); + final int packageInfoCount = packageInfos != null ? packageInfos.size() : 0; + for (int i = 0; i < packageInfoCount; i++) { + final PackageInfo packageInfo = packageInfos.get(i); + final PackageEntry pe = entries.get(packageInfo.packageName); + if (pe != null) { + pe.packageInfo = packageInfo; + pe.permissionGranted = true; + } + } + + // Load the remaining packages that have requested but don't have the + // PACKAGE_USAGE_STATS permission. + int packageCount = entries.size(); + for (int i = 0; i < packageCount; i++) { + final PackageEntry pe = entries.valueAt(i); + if (pe.packageInfo == null) { + try { + pe.packageInfo = mPackageManager.getPackageInfo(pe.packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + // This package doesn't exist. This may occur when an app is uninstalled for + // one user, but it is not removed from the system. + entries.removeAt(i); + i--; + packageCount--; + } + } + } + + // Find out which packages have been granted permission from AppOps. + final List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps( + APP_OPS_OP_CODES); + final int packageOpsCount = packageOps != null ? packageOps.size() : 0; + for (int i = 0; i < packageOpsCount; i++) { + final AppOpsManager.PackageOps packageOp = packageOps.get(i); + final PackageEntry pe = entries.get(packageOp.getPackageName()); + if (pe == null) { + Log.w(TAG, "AppOp permission exists for package " + packageOp.getPackageName() + + " but package doesn't exist or did not request UsageStats access"); + continue; + } + + if (packageOp.getUid() != pe.packageInfo.applicationInfo.uid) { + // This AppOp does not belong to this user. + continue; + } + + if (packageOp.getOps().size() < 1) { + Log.w(TAG, "No AppOps permission exists for package " + + packageOp.getPackageName()); + continue; + } + + pe.appOpMode = packageOp.getOps().get(0).getMode(); + } + + return entries; + } + + @Override + protected void onPostExecute(ArrayMap<String, PackageEntry> newEntries) { + mLastFetcherTask = null; + + if (getActivity() == null) { + // We must have finished the Activity while we were processing in the background. + return; + } + + if (newEntries == null) { + mPackageEntryMap.clear(); + mPreferenceScreen.removeAll(); + return; + } + + // Find the deleted entries and remove them from the PreferenceScreen. + final int oldPackageCount = mPackageEntryMap.size(); + for (int i = 0; i < oldPackageCount; i++) { + final PackageEntry oldPackageEntry = mPackageEntryMap.valueAt(i); + final PackageEntry newPackageEntry = newEntries.get(oldPackageEntry.packageName); + if (newPackageEntry == null) { + // This package has been removed. + mPreferenceScreen.removePreference(oldPackageEntry.preference); + } else { + // This package already exists in the preference hierarchy, so reuse that + // Preference. + newPackageEntry.preference = oldPackageEntry.preference; + } + } + + // Now add new packages to the PreferenceScreen. + final int packageCount = newEntries.size(); + for (int i = 0; i < packageCount; i++) { + final PackageEntry packageEntry = newEntries.valueAt(i); + if (packageEntry.preference == null) { + packageEntry.preference = new SwitchPreference(mContext); + packageEntry.preference.setPersistent(false); + packageEntry.preference.setOnPreferenceChangeListener(UsageAccessSettings.this); + mPreferenceScreen.addPreference(packageEntry.preference); + } + updatePreference(packageEntry); + } + + mPackageEntryMap.clear(); + mPackageEntryMap = newEntries; + } + + private void updatePreference(PackageEntry pe) { + pe.preference.setIcon(pe.packageInfo.applicationInfo.loadIcon(mPackageManager)); + pe.preference.setTitle(pe.packageInfo.applicationInfo.loadLabel(mPackageManager)); + pe.preference.setKey(pe.packageName); + + boolean check = false; + if (pe.appOpMode == AppOpsManager.MODE_ALLOWED) { + check = true; + } else if (pe.appOpMode == AppOpsManager.MODE_DEFAULT) { + // If the default AppOps mode is set, then fall back to + // whether the app has been granted permission by PackageManager. + check = pe.permissionGranted; + } + + if (check != pe.preference.isChecked()) { + pe.preference.setChecked(check); + } + } + } + + static boolean shouldIgnorePackage(String packageName) { + return packageName.equals("android") || packageName.equals("com.android.settings"); + } + + private AppsRequestingAccessFetcher mLastFetcherTask; + ArrayMap<String, PackageEntry> mPackageEntryMap = new ArrayMap<>(); + AppOpsManager mAppOpsManager; + PreferenceScreen mPreferenceScreen; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.usage_access_settings); + mPreferenceScreen = getPreferenceScreen(); + mPreferenceScreen.setOrderingAsAdded(false); + mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); + } + + @Override + public void onResume() { + super.onResume(); + + updateInterestedApps(); + mPackageMonitor.register(getActivity(), Looper.getMainLooper(), false); + } + + @Override + public void onPause() { + super.onPause(); + + mPackageMonitor.unregister(); + if (mLastFetcherTask != null) { + mLastFetcherTask.cancel(true); + mLastFetcherTask = null; + } + } + + private void updateInterestedApps() { + if (mLastFetcherTask != null) { + // Canceling can only fail for some obscure reason since mLastFetcherTask would be + // null if the task has already completed. So we ignore the result of cancel and + // spawn a new task to get fresh data. AsyncTask executes tasks serially anyways, + // so we are safe from running two tasks at the same time. + mLastFetcherTask.cancel(true); + } + + mLastFetcherTask = new AppsRequestingAccessFetcher(getActivity()); + mLastFetcherTask.execute(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String packageName = preference.getKey(); + final PackageEntry pe = mPackageEntryMap.get(packageName); + if (pe == null) { + Log.w(TAG, "Preference change event for package " + packageName + + " but that package is no longer valid."); + return false; + } + + if (!(newValue instanceof Boolean)) { + Log.w(TAG, "Preference change event for package " + packageName + + " had non boolean value of type " + newValue.getClass().getName()); + return false; + } + + final int newMode = (Boolean) newValue ? + AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED; + + // Check if we need to do any work. + if (pe.appOpMode != newMode) { + if (newMode != AppOpsManager.MODE_ALLOWED) { + // Turning off the setting has no warning. + setNewMode(pe, newMode); + return true; + } + + // Turning on the setting has a Warning. + FragmentTransaction ft = getChildFragmentManager().beginTransaction(); + Fragment prev = getChildFragmentManager().findFragmentByTag("warning"); + if (prev != null) { + ft.remove(prev); + } + WarningDialogFragment.newInstance(pe.packageName).show(ft, "warning"); + return false; + } + return true; + } + + void setNewMode(PackageEntry pe, int newMode) { + mAppOpsManager.setMode(AppOpsManager.OP_GET_USAGE_STATS, + pe.packageInfo.applicationInfo.uid, pe.packageName, newMode); + pe.appOpMode = newMode; + } + + void allowAccess(String packageName) { + final PackageEntry entry = mPackageEntryMap.get(packageName); + if (entry == null) { + Log.w(TAG, "Unable to give access to package " + packageName + ": it does not exist."); + return; + } + + setNewMode(entry, AppOpsManager.MODE_ALLOWED); + entry.preference.setChecked(true); + } + + private final PackageMonitor mPackageMonitor = new PackageMonitor() { + @Override + public void onPackageAdded(String packageName, int uid) { + updateInterestedApps(); + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + updateInterestedApps(); + } + }; + + public static class WarningDialogFragment extends DialogFragment + implements DialogInterface.OnClickListener { + private static final String ARG_PACKAGE_NAME = "package"; + + public static WarningDialogFragment newInstance(String packageName) { + WarningDialogFragment dialog = new WarningDialogFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PACKAGE_NAME, packageName); + dialog.setArguments(args); + return dialog; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.allow_usage_access_title) + .setMessage(R.string.allow_usage_access_message) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setNegativeButton(R.string.cancel, this) + .setPositiveButton(android.R.string.ok, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + ((UsageAccessSettings) getParentFragment()).allowAccess( + getArguments().getString(ARG_PACKAGE_NAME)); + } else { + dialog.cancel(); + } + } + } +} diff --git a/src/com/android/settings/UsageStats.java b/src/com/android/settings/UsageStatsActivity.java index f67eeec..90aec5b 100755 --- a/src/com/android/settings/UsageStats.java +++ b/src/com/android/settings/UsageStatsActivity.java @@ -1,5 +1,3 @@ - - /** * Copyright (C) 2007 The Android Open Source Project * @@ -18,24 +16,25 @@ package com.android.settings; -import com.android.internal.app.IUsageStats; -import com.android.settings.R; import android.app.Activity; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import com.android.internal.os.PkgUsageStats; + +import java.text.DateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; +import android.text.format.DateUtils; +import android.util.ArrayMap; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -50,111 +49,121 @@ import android.widget.AdapterView.OnItemSelectedListener; /** * Activity to display package usage statistics. */ -public class UsageStats extends Activity implements OnItemSelectedListener { - private static final String TAG="UsageStatsActivity"; +public class UsageStatsActivity extends Activity implements OnItemSelectedListener { + private static final String TAG = "UsageStatsActivity"; private static final boolean localLOGV = false; - private Spinner mTypeSpinner; - private ListView mListView; - private IUsageStats mUsageStatsService; + private UsageStatsManager mUsageStatsManager; private LayoutInflater mInflater; private UsageStatsAdapter mAdapter; private PackageManager mPm; - - public static class AppNameComparator implements Comparator<PkgUsageStats> { - Map<String, CharSequence> mAppLabelList; - AppNameComparator(Map<String, CharSequence> appList) { + + public static class AppNameComparator implements Comparator<UsageStats> { + private Map<String, String> mAppLabelList; + + AppNameComparator(Map<String, String> appList) { mAppLabelList = appList; } - public final int compare(PkgUsageStats a, PkgUsageStats b) { - String alabel = mAppLabelList.get(a.packageName).toString(); - String blabel = mAppLabelList.get(b.packageName).toString(); + + @Override + public final int compare(UsageStats a, UsageStats b) { + String alabel = mAppLabelList.get(a.getPackageName()); + String blabel = mAppLabelList.get(b.getPackageName()); return alabel.compareTo(blabel); } } - - public static class LaunchCountComparator implements Comparator<PkgUsageStats> { - public final int compare(PkgUsageStats a, PkgUsageStats b) { + + public static class LastTimeUsedComparator implements Comparator<UsageStats> { + @Override + public final int compare(UsageStats a, UsageStats b) { // return by descending order - return b.launchCount - a.launchCount; + return (int)(b.getLastTimeUsed() - a.getLastTimeUsed()); } } - - public static class UsageTimeComparator implements Comparator<PkgUsageStats> { - public final int compare(PkgUsageStats a, PkgUsageStats b) { - long ret = a.usageTime-b.usageTime; - if (ret == 0) { - return 0; - } - if (ret < 0) { - return 1; - } - return -1; + + public static class UsageTimeComparator implements Comparator<UsageStats> { + @Override + public final int compare(UsageStats a, UsageStats b) { + return (int)(b.getTotalTimeInForeground() - a.getTotalTimeInForeground()); } } - - // View Holder used when displaying views + + // View Holder used when displaying views static class AppViewHolder { TextView pkgName; - TextView launchCount; + TextView lastTimeUsed; TextView usageTime; } - + class UsageStatsAdapter extends BaseAdapter { // Constants defining order for display order private static final int _DISPLAY_ORDER_USAGE_TIME = 0; - private static final int _DISPLAY_ORDER_LAUNCH_COUNT = 1; + private static final int _DISPLAY_ORDER_LAST_TIME_USED = 1; private static final int _DISPLAY_ORDER_APP_NAME = 2; - + private int mDisplayOrder = _DISPLAY_ORDER_USAGE_TIME; - private List<PkgUsageStats> mUsageStats; - private LaunchCountComparator mLaunchCountComparator; - private UsageTimeComparator mUsageTimeComparator; + private LastTimeUsedComparator mLastTimeUsedComparator = new LastTimeUsedComparator(); + private UsageTimeComparator mUsageTimeComparator = new UsageTimeComparator(); private AppNameComparator mAppLabelComparator; - private HashMap<String, CharSequence> mAppLabelMap; - + private final ArrayMap<String, String> mAppLabelMap = new ArrayMap<>(); + private final ArrayList<UsageStats> mPackageStats = new ArrayList<>(); + UsageStatsAdapter() { - mUsageStats = new ArrayList<PkgUsageStats>(); - mAppLabelMap = new HashMap<String, CharSequence>(); - PkgUsageStats[] stats; - try { - stats = mUsageStatsService.getAllPkgUsageStats(); - } catch (RemoteException e) { - Log.e(TAG, "Failed initializing usage stats service"); + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_YEAR, -5); + + final List<UsageStats> stats = + mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, + cal.getTimeInMillis(), System.currentTimeMillis()); + if (stats == null) { return; } - if (stats == null) { - return; - } - for (PkgUsageStats ps : stats) { - mUsageStats.add(ps); - // load application labels for each application - CharSequence label; - try { - ApplicationInfo appInfo = mPm.getApplicationInfo(ps.packageName, 0); - label = appInfo.loadLabel(mPm); + + ArrayMap<String, UsageStats> map = new ArrayMap<>(); + final int statCount = stats.size(); + for (int i = 0; i < statCount; i++) { + final android.app.usage.UsageStats pkgStats = stats.get(i); + + // load application labels for each application + try { + ApplicationInfo appInfo = mPm.getApplicationInfo(pkgStats.getPackageName(), 0); + String label = appInfo.loadLabel(mPm).toString(); + mAppLabelMap.put(pkgStats.getPackageName(), label); + + UsageStats existingStats = + map.get(pkgStats.getPackageName()); + if (existingStats == null) { + map.put(pkgStats.getPackageName(), pkgStats); + } else { + existingStats.add(pkgStats); + } + } catch (NameNotFoundException e) { - label = ps.packageName; + // This package may be gone. } - mAppLabelMap.put(ps.packageName, label); - } - // Sort list - mLaunchCountComparator = new LaunchCountComparator(); - mUsageTimeComparator = new UsageTimeComparator(); - mAppLabelComparator = new AppNameComparator(mAppLabelMap); - sortList(); + } + mPackageStats.addAll(map.values()); + + // Sort list + mAppLabelComparator = new AppNameComparator(mAppLabelMap); + sortList(); } + + @Override public int getCount() { - return mUsageStats.size(); + return mPackageStats.size(); } + @Override public Object getItem(int position) { - return mUsageStats.get(position); + return mPackageStats.get(position); } + @Override public long getItemId(int position) { return position; } + @Override public View getView(int position, View convertView, ViewGroup parent) { // A ViewHolder keeps references to children views to avoid unneccessary calls // to findViewById() on each row. @@ -170,7 +179,7 @@ public class UsageStats extends Activity implements OnItemSelectedListener { // we want to bind data to. holder = new AppViewHolder(); holder.pkgName = (TextView) convertView.findViewById(R.id.package_name); - holder.launchCount = (TextView) convertView.findViewById(R.id.launch_count); + holder.lastTimeUsed = (TextView) convertView.findViewById(R.id.last_time_used); holder.usageTime = (TextView) convertView.findViewById(R.id.usage_time); convertView.setTag(holder); } else { @@ -180,18 +189,20 @@ public class UsageStats extends Activity implements OnItemSelectedListener { } // Bind the data efficiently with the holder - PkgUsageStats pkgStats = mUsageStats.get(position); + UsageStats pkgStats = mPackageStats.get(position); if (pkgStats != null) { - CharSequence label = mAppLabelMap.get(pkgStats.packageName); + String label = mAppLabelMap.get(pkgStats.getPackageName()); holder.pkgName.setText(label); - holder.launchCount.setText(String.valueOf(pkgStats.launchCount)); - holder.usageTime.setText(String.valueOf(pkgStats.usageTime)+" ms"); + holder.lastTimeUsed.setText(DateUtils.formatSameDayTime(pkgStats.getLastTimeUsed(), + System.currentTimeMillis(), DateFormat.MEDIUM, DateFormat.MEDIUM)); + holder.usageTime.setText( + DateUtils.formatElapsedTime(pkgStats.getTotalTimeInForeground() / 1000)); } else { Log.w(TAG, "No usage stats info for package:" + position); } return convertView; } - + void sortList(int sortOrder) { if (mDisplayOrder == sortOrder) { // do nothing @@ -203,47 +214,43 @@ public class UsageStats extends Activity implements OnItemSelectedListener { private void sortList() { if (mDisplayOrder == _DISPLAY_ORDER_USAGE_TIME) { if (localLOGV) Log.i(TAG, "Sorting by usage time"); - Collections.sort(mUsageStats, mUsageTimeComparator); - } else if (mDisplayOrder == _DISPLAY_ORDER_LAUNCH_COUNT) { - if (localLOGV) Log.i(TAG, "Sorting launch count"); - Collections.sort(mUsageStats, mLaunchCountComparator); + Collections.sort(mPackageStats, mUsageTimeComparator); + } else if (mDisplayOrder == _DISPLAY_ORDER_LAST_TIME_USED) { + if (localLOGV) Log.i(TAG, "Sorting by last time used"); + Collections.sort(mPackageStats, mLastTimeUsedComparator); } else if (mDisplayOrder == _DISPLAY_ORDER_APP_NAME) { if (localLOGV) Log.i(TAG, "Sorting by application name"); - Collections.sort(mUsageStats, mAppLabelComparator); + Collections.sort(mPackageStats, mAppLabelComparator); } notifyDataSetChanged(); } } /** Called when the activity is first created. */ + @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - mUsageStatsService = IUsageStats.Stub.asInterface(ServiceManager.getService("usagestats")); - if (mUsageStatsService == null) { - Log.e(TAG, "Failed to retrieve usagestats service"); - return; - } + setContentView(R.layout.usage_stats); + + mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); mPm = getPackageManager(); - - setContentView(R.layout.usage_stats); - mTypeSpinner = (Spinner) findViewById(R.id.typeSpinner); - mTypeSpinner.setOnItemSelectedListener(this); - - mListView = (ListView) findViewById(R.id.pkg_list); - // Initialize the inflater - + + Spinner typeSpinner = (Spinner) findViewById(R.id.typeSpinner); + typeSpinner.setOnItemSelectedListener(this); + + ListView listView = (ListView) findViewById(R.id.pkg_list); mAdapter = new UsageStatsAdapter(); - mListView.setAdapter(mAdapter); + listView.setAdapter(mAdapter); } - public void onItemSelected(AdapterView<?> parent, View view, int position, - long id) { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { mAdapter.sortList(position); } + @Override public void onNothingSelected(AdapterView<?> parent) { // do nothing } } - diff --git a/src/com/android/settings/UserDictionarySettings.java b/src/com/android/settings/UserDictionarySettings.java index da12004..1e9fd0a 100644 --- a/src/com/android/settings/UserDictionarySettings.java +++ b/src/com/android/settings/UserDictionarySettings.java @@ -192,9 +192,8 @@ public class UserDictionarySettings extends ListFragment { args.putString(UserDictionaryAddWordContents.EXTRA_WORD, editingWord); args.putString(UserDictionaryAddWordContents.EXTRA_SHORTCUT, editingShortcut); args.putString(UserDictionaryAddWordContents.EXTRA_LOCALE, mLocale); - android.preference.PreferenceActivity pa = - (android.preference.PreferenceActivity)getActivity(); - pa.startPreferencePanel( + SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel( com.android.settings.inputmethod.UserDictionaryAddWordFragment.class.getName(), args, R.string.user_dict_settings_add_dialog_title, null, null, 0); } diff --git a/src/com/android/settings/UserSpinnerAdapter.java b/src/com/android/settings/UserSpinnerAdapter.java new file mode 100644 index 0000000..001dfc4 --- /dev/null +++ b/src/com/android/settings/UserSpinnerAdapter.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2014 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.pm.UserInfo; +import android.content.res.Resources; +import android.database.DataSetObserver; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +import com.android.internal.util.UserIcons; + +import java.util.ArrayList; + +/** + * Adapter for a spinner that shows a list of users. + */ +public class UserSpinnerAdapter implements SpinnerAdapter { + // TODO: Update UI. See: http://b/16518801 + /** Holder for user details */ + public static class UserDetails { + private final UserHandle mUserHandle; + private final String name; + private final Drawable icon; + + public UserDetails(UserHandle userHandle, UserManager um, Context context) { + mUserHandle = userHandle; + UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); + if (userInfo.isManagedProfile()) { + name = context.getString(R.string.managed_user_title); + icon = Resources.getSystem().getDrawable( + com.android.internal.R.drawable.ic_corp_icon); + } else { + name = userInfo.name; + final int userId = userInfo.id; + if (um.getUserIcon(userId) != null) { + icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); + } else { + icon = UserIcons.getDefaultUserIcon(userId, /* light= */ false); + } + } + } + } + private ArrayList<UserDetails> data; + private final LayoutInflater mInflater; + + public UserSpinnerAdapter(Context context, ArrayList<UserDetails> users) { + if (users == null) { + throw new IllegalArgumentException("A list of user details must be provided"); + } + this.data = users; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public UserHandle getUserHandle(int position) { + if (position < 0 || position >= data.size()) { + return null; + } + return data.get(position).mUserHandle; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + final View row = convertView != null ? convertView : createUser(parent); + + UserDetails user = data.get(position); + ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.icon); + ((TextView) row.findViewById(android.R.id.title)).setText(user.name); + return row; + } + + private View createUser(ViewGroup parent) { + return mInflater.inflate(R.layout.user_preference, parent, false); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public UserDetails getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).mUserHandle.getIdentifier(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getDropDownView(position, convertView, parent); + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index b9e729c..08cfc58 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -16,18 +16,25 @@ package com.android.settings; -import android.app.Activity; +import static android.content.Intent.EXTRA_USER; + +import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityManagerNative; import android.app.AlertDialog; import android.app.Dialog; +import android.app.Fragment; +import android.app.IActivityManager; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.content.pm.Signature; import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -40,21 +47,22 @@ import android.net.LinkProperties; import android.net.Uri; import android.os.BatteryManager; import android.os.Bundle; -import android.os.ParcelFileDescriptor; +import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity.Header; import android.preference.PreferenceFrameLayout; import android.preference.PreferenceGroup; import android.provider.ContactsContract.CommonDataKinds; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Profile; import android.provider.ContactsContract.RawContacts; +import android.service.persistentdata.PersistentDataBlockManager; import android.telephony.TelephonyManager; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -62,17 +70,24 @@ import android.view.ViewGroup; import android.widget.ListView; import android.widget.TabWidget; -import com.android.settings.users.ProfileUpdateReceiver; +import com.android.internal.util.ImageUtils; +import com.android.internal.util.UserIcons; +import com.android.settings.UserSpinnerAdapter.UserDetails; +import com.android.settings.dashboard.DashboardCategory; +import com.android.settings.dashboard.DashboardTile; +import com.android.settings.drawable.CircleFramedDrawable; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; +import java.text.NumberFormat; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; -public class Utils { +public final class Utils { + private static final String TAG = "Settings"; /** * Set the preference's title to the matching activity's label. @@ -85,6 +100,15 @@ public class Utils { public static final float DISABLED_ALPHA = 0.4f; /** + * Color spectrum to use to indicate badness. 0 is completely transparent (no data), + * 1 is most bad (red), the last value is least bad (green). + */ + public static final int[] BADNESS_COLORS = new int[] { + 0x00000000, 0xffc43828, 0xffe54918, 0xfff47b00, + 0xfffabf2c, 0xff679e37, 0xff0a7f42 + }; + + /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the icon that should be displayed for the preference. */ @@ -102,6 +126,12 @@ public class Utils { */ private static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"; + private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 60 * 60; + private static final int SECONDS_PER_DAY = 24 * 60 * 60; + /** * Finds a matching activity for a preference's intent. If a matching * activity is not found, it will remove the preference. @@ -157,98 +187,10 @@ public class Utils { return false; } - /** - * Finds a matching activity for a preference's intent. If a matching - * activity is not found, it will remove the preference. The icon, title and - * summary of the preference will also be updated with the values retrieved - * from the activity's meta-data elements. If no meta-data elements are - * specified then the preference title will be set to match the label of the - * activity, an icon and summary text will not be displayed. - * - * @param context The context. - * @param parentPreferenceGroup The preference group that contains the - * preference whose intent is being resolved. - * @param preferenceKey The key of the preference whose intent is being - * resolved. - * - * @return Whether an activity was found. If false, the preference was - * removed. - * - * @see {@link #META_DATA_PREFERENCE_ICON} - * {@link #META_DATA_PREFERENCE_TITLE} - * {@link #META_DATA_PREFERENCE_SUMMARY} - */ - public static boolean updatePreferenceToSpecificActivityFromMetaDataOrRemove(Context context, - PreferenceGroup parentPreferenceGroup, String preferenceKey) { - - IconPreferenceScreen preference = (IconPreferenceScreen)parentPreferenceGroup - .findPreference(preferenceKey); - if (preference == null) { - return false; - } + public static boolean updateTileToSpecificActivityFromMetaDataOrRemove(Context context, + DashboardTile tile) { - Intent intent = preference.getIntent(); - if (intent != null) { - // Find the activity that is in the system image - PackageManager pm = context.getPackageManager(); - List<ResolveInfo> list = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); - int listSize = list.size(); - for (int i = 0; i < listSize; i++) { - ResolveInfo resolveInfo = list.get(i); - if ((resolveInfo.activityInfo.applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 0) { - Drawable icon = null; - String title = null; - String summary = null; - - // Get the activity's meta-data - try { - Resources res = pm - .getResourcesForApplication(resolveInfo.activityInfo.packageName); - Bundle metaData = resolveInfo.activityInfo.metaData; - - if (res != null && metaData != null) { - icon = res.getDrawable(metaData.getInt(META_DATA_PREFERENCE_ICON)); - title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE)); - summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY)); - } - } catch (NameNotFoundException e) { - // Ignore - } catch (NotFoundException e) { - // Ignore - } - - // Set the preference title to the activity's label if no - // meta-data is found - if (TextUtils.isEmpty(title)) { - title = resolveInfo.loadLabel(pm).toString(); - } - - // Set icon, title and summary for the preference - preference.setIcon(icon); - preference.setTitle(title); - preference.setSummary(summary); - - // Replace the intent with this specific activity - preference.setIntent(new Intent().setClassName( - resolveInfo.activityInfo.packageName, - resolveInfo.activityInfo.name)); - - return true; - } - } - } - - // Did not find a matching activity, so remove the preference - parentPreferenceGroup.removePreference(preference); - - return false; - } - - public static boolean updateHeaderToSpecificActivityFromMetaDataOrRemove(Context context, - List<Header> target, Header header) { - - Intent intent = header.intent; + Intent intent = tile.intent; if (intent != null) { // Find the activity that is in the system image PackageManager pm = context.getPackageManager(); @@ -287,11 +229,11 @@ public class Utils { // Set icon, title and summary for the preference // TODO: - //header.icon = icon; - header.title = title; - header.summary = summary; + //tile.icon = icon; + tile.title = title; + tile.summary = summary; // Replace the intent with this specific activity - header.intent = new Intent().setClassName(resolveInfo.activityInfo.packageName, + tile.intent = new Intent().setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); return true; @@ -299,9 +241,6 @@ public class Utils { } } - // Did not find a matching activity, so remove the preference - target.remove(header); - return false; } @@ -384,14 +323,34 @@ public class Utils { } } + /** Formats the ratio of amount/total as a percentage. */ + public static String formatPercentage(long amount, long total) { + return formatPercentage(((double) amount) / total); + } + + /** Formats an integer from 0..100 as a percentage. */ + public static String formatPercentage(int percentage) { + return formatPercentage(((double) percentage) / 100.0); + } + + /** Formats a double from 0.0..1.0 as a percentage. */ + private static String formatPercentage(double percentage) { + BidiFormatter bf = BidiFormatter.getInstance(); + return bf.unicodeWrap(NumberFormat.getPercentInstance().format(percentage)); + } + public static boolean isBatteryPresent(Intent batteryChangedIntent) { return batteryChangedIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); } public static String getBatteryPercentage(Intent batteryChangedIntent) { + return formatPercentage(getBatteryLevel(batteryChangedIntent)); + } + + public static int getBatteryLevel(Intent batteryChangedIntent) { int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); - return String.valueOf(level * 100 / scale) + "%"; + return (level * 100) / scale; } public static String getBatteryStatus(Resources res, Intent batteryChangedIntent) { @@ -402,18 +361,17 @@ public class Utils { BatteryManager.BATTERY_STATUS_UNKNOWN); String statusString; if (status == BatteryManager.BATTERY_STATUS_CHARGING) { - statusString = res.getString(R.string.battery_info_status_charging); - if (plugType > 0) { - int resId; - if (plugType == BatteryManager.BATTERY_PLUGGED_AC) { - resId = R.string.battery_info_status_charging_ac; - } else if (plugType == BatteryManager.BATTERY_PLUGGED_USB) { - resId = R.string.battery_info_status_charging_usb; - } else { - resId = R.string.battery_info_status_charging_wireless; - } - statusString = statusString + " " + res.getString(resId); + int resId; + if (plugType == BatteryManager.BATTERY_PLUGGED_AC) { + resId = R.string.battery_info_status_charging_ac; + } else if (plugType == BatteryManager.BATTERY_PLUGGED_USB) { + resId = R.string.battery_info_status_charging_usb; + } else if (plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) { + resId = R.string.battery_info_status_charging_wireless; + } else { + resId = R.string.battery_info_status_charging; } + statusString = res.getString(resId); } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { statusString = res.getString(R.string.battery_info_status_discharging); } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { @@ -442,19 +400,35 @@ public class Utils { public static void prepareCustomPreferencesList( ViewGroup parent, View child, View list, boolean ignoreSidePadding) { final boolean movePadding = list.getScrollBarStyle() == View.SCROLLBARS_OUTSIDE_OVERLAY; - if (movePadding && parent instanceof PreferenceFrameLayout) { - ((PreferenceFrameLayout.LayoutParams) child.getLayoutParams()).removeBorders = true; - + if (movePadding) { final Resources res = list.getResources(); final int paddingSide = res.getDimensionPixelSize(R.dimen.settings_side_margin); final int paddingBottom = res.getDimensionPixelSize( com.android.internal.R.dimen.preference_fragment_padding_bottom); - final int effectivePaddingSide = ignoreSidePadding ? 0 : paddingSide; - list.setPaddingRelative(effectivePaddingSide, 0, effectivePaddingSide, paddingBottom); + if (parent instanceof PreferenceFrameLayout) { + ((PreferenceFrameLayout.LayoutParams) child.getLayoutParams()).removeBorders = true; + + final int effectivePaddingSide = ignoreSidePadding ? 0 : paddingSide; + list.setPaddingRelative(effectivePaddingSide, 0, effectivePaddingSide, paddingBottom); + } else { + list.setPaddingRelative(paddingSide, 0, paddingSide, paddingBottom); + } } } + public static void forceCustomPadding(View view, boolean additive) { + final Resources res = view.getResources(); + final int paddingSide = res.getDimensionPixelSize(R.dimen.settings_side_margin); + + final int paddingStart = paddingSide + (additive ? view.getPaddingStart() : 0); + final int paddingEnd = paddingSide + (additive ? view.getPaddingEnd() : 0); + final int paddingBottom = res.getDimensionPixelSize( + com.android.internal.R.dimen.preference_fragment_padding_bottom); + + view.setPaddingRelative(paddingStart, 0, paddingEnd, paddingBottom); + } + /** * Return string resource that best describes combination of tethering * options available on this device. @@ -601,4 +575,393 @@ public class Utils { return ((UserManager) context.getSystemService(Context.USER_SERVICE)) .getUsers().size() > 1; } + + /** + * Start a new instance of the activity, showing only the given fragment. + * When launched in this mode, the given preference fragment will be instantiated and fill the + * entire activity. + * + * @param context The context. + * @param fragmentName The name of the fragment to display. + * @param args Optional arguments to supply to the fragment. + * @param resultTo Option fragment that should receive the result of the activity launch. + * @param resultRequestCode If resultTo is non-null, this is the request code in which + * to report the result. + * @param titleResId resource id for the String to display for the title of this set + * of preferences. + * @param title String to display for the title of this set of preferences. + */ + public static void startWithFragment(Context context, String fragmentName, Bundle args, + Fragment resultTo, int resultRequestCode, int titleResId, CharSequence title) { + startWithFragment(context, fragmentName, args, resultTo, resultRequestCode, + titleResId, title, false /* not a shortcut */); + } + + public static void startWithFragment(Context context, String fragmentName, Bundle args, + Fragment resultTo, int resultRequestCode, int titleResId, CharSequence title, + boolean isShortcut) { + Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResId, + title, isShortcut); + if (resultTo == null) { + context.startActivity(intent); + } else { + resultTo.startActivityForResult(intent, resultRequestCode); + } + } + + public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args, + int titleResId, CharSequence title, boolean isShortcut, UserHandle userHandle) { + Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResId, + title, isShortcut); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivityAsUser(intent, userHandle); + } + + /** + * Build an Intent to launch a new activity showing the selected fragment. + * The implementation constructs an Intent that re-launches the current activity with the + * appropriate arguments to display the fragment. + * + * + * @param context The Context. + * @param fragmentName The name of the fragment to display. + * @param args Optional arguments to supply to the fragment. + * @param titleResId Optional title resource id to show for this item. + * @param title Optional title to show for this item. + * @param isShortcut tell if this is a Launcher Shortcut or not + * @return Returns an Intent that can be launched to display the given + * fragment. + */ + public static Intent onBuildStartFragmentIntent(Context context, String fragmentName, + Bundle args, int titleResId, CharSequence title, boolean isShortcut) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(context, SubSettings.class); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, fragmentName); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, titleResId); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, isShortcut); + return intent; + } + + /** + * Returns the managed profile of the current user or null if none found. + */ + public static UserHandle getManagedProfile(UserManager userManager) { + List<UserHandle> userProfiles = userManager.getUserProfiles(); + final int count = userProfiles.size(); + for (int i = 0; i < count; i++) { + final UserHandle profile = userProfiles.get(i); + if (profile.getIdentifier() == userManager.getUserHandle()) { + continue; + } + final UserInfo userInfo = userManager.getUserInfo(profile.getIdentifier()); + if (userInfo.isManagedProfile()) { + return profile; + } + } + return null; + } + + /** + * Returns true if the current profile is a managed one. + */ + public static boolean isManagedProfile(UserManager userManager) { + UserInfo currentUser = userManager.getUserInfo(userManager.getUserHandle()); + return currentUser.isManagedProfile(); + } + + /** + * Creates a {@link UserSpinnerAdapter} if there is more than one profile on the device. + * + * <p> The adapter can be used to populate a spinner that switches between the Settings + * app on the different profiles. + * + * @return a {@link UserSpinnerAdapter} or null if there is only one profile. + */ + public static UserSpinnerAdapter createUserSpinnerAdapter(UserManager userManager, + Context context) { + List<UserHandle> userProfiles = userManager.getUserProfiles(); + if (userProfiles.size() < 2) { + return null; + } + + UserHandle myUserHandle = new UserHandle(UserHandle.myUserId()); + // The first option should be the current profile + userProfiles.remove(myUserHandle); + userProfiles.add(0, myUserHandle); + + ArrayList<UserDetails> userDetails = new ArrayList<UserDetails>(userProfiles.size()); + final int count = userProfiles.size(); + for (int i = 0; i < count; i++) { + userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); + } + return new UserSpinnerAdapter(context, userDetails); + } + + /** + * Returns the target user for a Settings activity. + * + * The target user can be either the current user, the user that launched this activity or + * the user contained as an extra in the arguments or intent extras. + * + * Note: This is secure in the sense that it only returns a target user different to the current + * one if the app launching this activity is the Settings app itself, running in the same user + * or in one that is in the same profile group, or if the user id is provided by the system. + */ + public static UserHandle getSecureTargetUser(IBinder activityToken, + UserManager um, @Nullable Bundle arguments, @Nullable Bundle intentExtras) { + UserHandle currentUser = new UserHandle(UserHandle.myUserId()); + IActivityManager am = ActivityManagerNative.getDefault(); + try { + String launchedFromPackage = am.getLaunchedFromPackage(activityToken); + boolean launchedFromSettingsApp = SETTINGS_PACKAGE_NAME.equals(launchedFromPackage); + + UserHandle launchedFromUser = new UserHandle(UserHandle.getUserId( + am.getLaunchedFromUid(activityToken))); + if (launchedFromUser != null && !launchedFromUser.equals(currentUser)) { + // Check it's secure + if (isProfileOf(um, launchedFromUser)) { + return launchedFromUser; + } + } + UserHandle extrasUser = intentExtras != null + ? (UserHandle) intentExtras.getParcelable(EXTRA_USER) : null; + if (extrasUser != null && !extrasUser.equals(currentUser)) { + // Check it's secure + if (launchedFromSettingsApp && isProfileOf(um, extrasUser)) { + return extrasUser; + } + } + UserHandle argumentsUser = arguments != null + ? (UserHandle) arguments.getParcelable(EXTRA_USER) : null; + if (argumentsUser != null && !argumentsUser.equals(currentUser)) { + // Check it's secure + if (launchedFromSettingsApp && isProfileOf(um, argumentsUser)) { + return argumentsUser; + } + } + } catch (RemoteException e) { + // Should not happen + Log.v(TAG, "Could not talk to activity manager.", e); + } + return currentUser; + } + + /** + * Returns the target user for a Settings activity. + * + * The target user can be either the current user, the user that launched this activity or + * the user contained as an extra in the arguments or intent extras. + * + * You should use {@link #getSecureTargetUser(IBinder, UserManager, Bundle, Bundle)} if + * possible. + * + * @see #getInsecureTargetUser(IBinder, Bundle, Bundle) + */ + public static UserHandle getInsecureTargetUser(IBinder activityToken, @Nullable Bundle arguments, + @Nullable Bundle intentExtras) { + UserHandle currentUser = new UserHandle(UserHandle.myUserId()); + IActivityManager am = ActivityManagerNative.getDefault(); + try { + UserHandle launchedFromUser = new UserHandle(UserHandle.getUserId( + am.getLaunchedFromUid(activityToken))); + if (launchedFromUser != null && !launchedFromUser.equals(currentUser)) { + return launchedFromUser; + } + UserHandle extrasUser = intentExtras != null + ? (UserHandle) intentExtras.getParcelable(EXTRA_USER) : null; + if (extrasUser != null && !extrasUser.equals(currentUser)) { + return extrasUser; + } + UserHandle argumentsUser = arguments != null + ? (UserHandle) arguments.getParcelable(EXTRA_USER) : null; + if (argumentsUser != null && !argumentsUser.equals(currentUser)) { + return argumentsUser; + } + } catch (RemoteException e) { + // Should not happen + Log.v(TAG, "Could not talk to activity manager.", e); + return null; + } + return currentUser; + } + + /** + * Returns true if the user provided is in the same profiles group as the current user. + */ + private static boolean isProfileOf(UserManager um, UserHandle otherUser) { + if (um == null || otherUser == null) return false; + return (UserHandle.myUserId() == otherUser.getIdentifier()) + || um.getUserProfiles().contains(otherUser); + } + + /** + * Creates a dialog to confirm with the user if it's ok to remove the user + * and delete all the data. + * + * @param context a Context object + * @param removingUserId The userId of the user to remove + * @param onConfirmListener Callback object for positive action + * @return the created Dialog + */ + public static Dialog createRemoveConfirmationDialog(Context context, int removingUserId, + DialogInterface.OnClickListener onConfirmListener) { + UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + UserInfo userInfo = um.getUserInfo(removingUserId); + int titleResId; + int messageResId; + if (UserHandle.myUserId() == removingUserId) { + titleResId = R.string.user_confirm_remove_self_title; + messageResId = R.string.user_confirm_remove_self_message; + } else if (userInfo.isRestricted()) { + titleResId = R.string.user_profile_confirm_remove_title; + messageResId = R.string.user_profile_confirm_remove_message; + } else if (userInfo.isManagedProfile()) { + titleResId = R.string.work_profile_confirm_remove_title; + messageResId = R.string.work_profile_confirm_remove_message; + } else { + titleResId = R.string.user_confirm_remove_title; + messageResId = R.string.user_confirm_remove_message; + } + Dialog dlg = new AlertDialog.Builder(context) + .setTitle(titleResId) + .setMessage(messageResId) + .setPositiveButton(R.string.user_delete_button, + onConfirmListener) + .setNegativeButton(android.R.string.cancel, null) + .create(); + return dlg; + } + + /** + * Returns whether or not this device is able to be OEM unlocked. + */ + static boolean isOemUnlockEnabled(Context context) { + PersistentDataBlockManager manager =(PersistentDataBlockManager) + context.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE); + return manager.getOemUnlockEnabled(); + } + + /** + * Allows enabling or disabling OEM unlock on this device. OEM unlocked + * devices allow users to flash other OSes to them. + */ + static void setOemUnlockEnabled(Context context, boolean enabled) { + PersistentDataBlockManager manager =(PersistentDataBlockManager) + context.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE); + manager.setOemUnlockEnabled(enabled); + } + + /** + * Returns a circular icon for a user. + */ + public static Drawable getUserIcon(Context context, UserManager um, UserInfo user) { + if (user.iconPath != null) { + Bitmap icon = um.getUserIcon(user.id); + if (icon != null) { + return CircleFramedDrawable.getInstance(context, icon); + } + } + return UserIcons.getDefaultUserIcon(user.id, /* light= */ false); + } + + /** + * Return whether or not the user should have a SIM Cards option in Settings. + * TODO: Change back to returning true if count is greater than one after testing. + * TODO: See bug 16533525. + */ + public static boolean showSimCardTile(Context context) { + final TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + // TODO: Uncomment to re-enable SimSettings. + // return tm.getSimCount() > 0; + return false; + } + + /** + * Determine whether a package is a "system package", in which case certain things (like + * disabling notifications or disabling the package altogether) should be disallowed. + */ + public static boolean isSystemPackage(PackageManager pm, PackageInfo pkg) { + if (sSystemSignature == null) { + sSystemSignature = new Signature[]{ getSystemSignature(pm) }; + } + return sSystemSignature[0] != null && sSystemSignature[0].equals(getFirstSignature(pkg)); + } + + private static Signature[] sSystemSignature; + + private static Signature getFirstSignature(PackageInfo pkg) { + if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) { + return pkg.signatures[0]; + } + return null; + } + + private static Signature getSystemSignature(PackageManager pm) { + try { + final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES); + return getFirstSignature(sys); + } catch (NameNotFoundException e) { + } + return null; + } + + /** + * Returns elapsed time for the given millis, in the following format: + * 2d 5h 40m 29s + * @param context the application context + * @param millis the elapsed time in milli seconds + * @param withSeconds include seconds? + * @return the formatted elapsed time + */ + public static String formatElapsedTime(Context context, double millis, boolean withSeconds) { + StringBuilder sb = new StringBuilder(); + int seconds = (int) Math.floor(millis / 1000); + if (!withSeconds) { + // Round up. + seconds += 30; + } + + int days = 0, hours = 0, minutes = 0; + if (seconds >= SECONDS_PER_DAY) { + days = seconds / SECONDS_PER_DAY; + seconds -= days * SECONDS_PER_DAY; + } + if (seconds >= SECONDS_PER_HOUR) { + hours = seconds / SECONDS_PER_HOUR; + seconds -= hours * SECONDS_PER_HOUR; + } + if (seconds >= SECONDS_PER_MINUTE) { + minutes = seconds / SECONDS_PER_MINUTE; + seconds -= minutes * SECONDS_PER_MINUTE; + } + if (withSeconds) { + if (days > 0) { + sb.append(context.getString(R.string.battery_history_days, + days, hours, minutes, seconds)); + } else if (hours > 0) { + sb.append(context.getString(R.string.battery_history_hours, + hours, minutes, seconds)); + } else if (minutes > 0) { + sb.append(context.getString(R.string.battery_history_minutes, minutes, seconds)); + } else { + sb.append(context.getString(R.string.battery_history_seconds, seconds)); + } + } else { + if (days > 0) { + sb.append(context.getString(R.string.battery_history_days_no_seconds, + days, hours, minutes)); + } else if (hours > 0) { + sb.append(context.getString(R.string.battery_history_hours_no_seconds, + hours, minutes)); + } else { + sb.append(context.getString(R.string.battery_history_minutes_no_seconds, minutes)); + } + } + return sb.toString(); + } } diff --git a/src/com/android/settings/VoiceInputOutputSettings.java b/src/com/android/settings/VoiceInputOutputSettings.java index b499ded..e052f8e 100644 --- a/src/com/android/settings/VoiceInputOutputSettings.java +++ b/src/com/android/settings/VoiceInputOutputSettings.java @@ -16,58 +16,31 @@ package com.android.settings; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceGroup; -import android.preference.PreferenceScreen; -import android.preference.Preference.OnPreferenceChangeListener; -import android.provider.Settings; -import android.speech.RecognitionService; import android.speech.tts.TtsEngines; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Xml; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; +import com.android.settings.voice.VoiceInputHelper; /** * Settings screen for voice input/output. */ -public class VoiceInputOutputSettings implements OnPreferenceChangeListener { +public class VoiceInputOutputSettings { private static final String TAG = "VoiceInputOutputSettings"; private static final String KEY_VOICE_CATEGORY = "voice_category"; - private static final String KEY_RECOGNIZER = "recognizer"; - private static final String KEY_RECOGNIZER_SETTINGS = "recognizer_settings"; + private static final String KEY_VOICE_INPUT_SETTINGS = "voice_input_settings"; private static final String KEY_TTS_SETTINGS = "tts_settings"; private PreferenceGroup mParent; private PreferenceCategory mVoiceCategory; - private ListPreference mRecognizerPref; - private Preference mRecognizerSettingsPref; + private Preference mVoiceInputSettingsPref; private Preference mTtsSettingsPref; - private PreferenceScreen mSettingsPref; private final SettingsPreferenceFragment mFragment; private final TtsEngines mTtsEngines; - private HashMap<String, ResolveInfo> mAvailableRecognizersMap; - public VoiceInputOutputSettings(SettingsPreferenceFragment fragment) { mFragment = fragment; mTtsEngines = new TtsEngines(fragment.getPreferenceScreen().getContext()); @@ -77,22 +50,16 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener { mParent = mFragment.getPreferenceScreen(); mVoiceCategory = (PreferenceCategory) mParent.findPreference(KEY_VOICE_CATEGORY); - mRecognizerPref = (ListPreference) mVoiceCategory.findPreference(KEY_RECOGNIZER); - mRecognizerSettingsPref = mVoiceCategory.findPreference(KEY_RECOGNIZER_SETTINGS); + mVoiceInputSettingsPref = mVoiceCategory.findPreference(KEY_VOICE_INPUT_SETTINGS); mTtsSettingsPref = mVoiceCategory.findPreference(KEY_TTS_SETTINGS); - mRecognizerPref.setOnPreferenceChangeListener(this); - mSettingsPref = (PreferenceScreen) - mVoiceCategory.findPreference(KEY_RECOGNIZER_SETTINGS); - - mAvailableRecognizersMap = new HashMap<String, ResolveInfo>(); populateOrRemovePreferences(); } private void populateOrRemovePreferences() { - boolean hasRecognizerPrefs = populateOrRemoveRecognizerPrefs(); + boolean hasVoiceInputPrefs = populateOrRemoveVoiceInputPrefs(); boolean hasTtsPrefs = populateOrRemoveTtsPrefs(); - if (!hasRecognizerPrefs && !hasTtsPrefs) { + if (!hasVoiceInputPrefs && !hasTtsPrefs) { // There were no TTS settings and no recognizer settings, // so it should be safe to hide the preference category // entirely. @@ -100,42 +67,13 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener { } } - private boolean populateOrRemoveRecognizerPrefs() { - List<ResolveInfo> availableRecognitionServices = - mFragment.getPackageManager().queryIntentServices( - new Intent(RecognitionService.SERVICE_INTERFACE), - PackageManager.GET_META_DATA); - int numAvailable = availableRecognitionServices.size(); - - if (numAvailable == 0) { - mVoiceCategory.removePreference(mRecognizerPref); - mVoiceCategory.removePreference(mRecognizerSettingsPref); + private boolean populateOrRemoveVoiceInputPrefs() { + VoiceInputHelper helper = new VoiceInputHelper(mFragment.getActivity()); + if (!helper.hasItems()) { + mVoiceCategory.removePreference(mVoiceInputSettingsPref); return false; } - if (numAvailable == 1) { - // Only one recognizer available, so don't show the list of choices, but do - // set up the link to settings for the available recognizer. - mVoiceCategory.removePreference(mRecognizerPref); - - // But first set up the available recognizers map with just the one recognizer. - ResolveInfo resolveInfo = availableRecognitionServices.get(0); - String recognizerComponent = - new ComponentName(resolveInfo.serviceInfo.packageName, - resolveInfo.serviceInfo.name).flattenToShortString(); - - mAvailableRecognizersMap.put(recognizerComponent, resolveInfo); - - String currentSetting = Settings.Secure.getString( - mFragment.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE); - updateSettingsLink(currentSetting); - } else { - // Multiple recognizers available, so show the full list of choices. - populateRecognizerPreference(availableRecognitionServices); - } - - // In this case, there was at least one available recognizer so - // we populated the settings. return true; } @@ -147,111 +85,4 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener { return true; } - - private void populateRecognizerPreference(List<ResolveInfo> recognizers) { - int size = recognizers.size(); - CharSequence[] entries = new CharSequence[size]; - CharSequence[] values = new CharSequence[size]; - - // Get the current value from the secure setting. - String currentSetting = Settings.Secure.getString( - mFragment.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE); - - // Iterate through all the available recognizers and load up their info to show - // in the preference. Also build up a map of recognizer component names to their - // ResolveInfos - we'll need that a little later. - for (int i = 0; i < size; i++) { - ResolveInfo resolveInfo = recognizers.get(i); - String recognizerComponent = - new ComponentName(resolveInfo.serviceInfo.packageName, - resolveInfo.serviceInfo.name).flattenToShortString(); - - mAvailableRecognizersMap.put(recognizerComponent, resolveInfo); - - entries[i] = resolveInfo.loadLabel(mFragment.getPackageManager()); - values[i] = recognizerComponent; - } - - mRecognizerPref.setEntries(entries); - mRecognizerPref.setEntryValues(values); - - mRecognizerPref.setDefaultValue(currentSetting); - mRecognizerPref.setValue(currentSetting); - - updateSettingsLink(currentSetting); - } - - private void updateSettingsLink(String currentSetting) { - ResolveInfo currentRecognizer = mAvailableRecognizersMap.get(currentSetting); - if (currentRecognizer == null) return; - - ServiceInfo si = currentRecognizer.serviceInfo; - XmlResourceParser parser = null; - String settingsActivity = null; - try { - parser = si.loadXmlMetaData(mFragment.getPackageManager(), - RecognitionService.SERVICE_META_DATA); - if (parser == null) { - throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA + - " meta-data for " + si.packageName); - } - - Resources res = mFragment.getPackageManager().getResourcesForApplication( - si.applicationInfo); - - AttributeSet attrs = Xml.asAttributeSet(parser); - - int type; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - } - - String nodeName = parser.getName(); - if (!"recognition-service".equals(nodeName)) { - throw new XmlPullParserException( - "Meta-data does not start with recognition-service tag"); - } - - TypedArray array = res.obtainAttributes(attrs, - com.android.internal.R.styleable.RecognitionService); - settingsActivity = array.getString( - com.android.internal.R.styleable.RecognitionService_settingsActivity); - array.recycle(); - } catch (XmlPullParserException e) { - Log.e(TAG, "error parsing recognition service meta-data", e); - } catch (IOException e) { - Log.e(TAG, "error parsing recognition service meta-data", e); - } catch (NameNotFoundException e) { - Log.e(TAG, "error parsing recognition service meta-data", e); - } finally { - if (parser != null) parser.close(); - } - - if (settingsActivity == null) { - // No settings preference available - hide the preference. - Log.w(TAG, "no recognizer settings available for " + si.packageName); - mSettingsPref.setIntent(null); - mVoiceCategory.removePreference(mSettingsPref); - } else { - Intent i = new Intent(Intent.ACTION_MAIN); - i.setComponent(new ComponentName(si.packageName, settingsActivity)); - mSettingsPref.setIntent(i); - mRecognizerPref.setSummary(currentRecognizer.loadLabel(mFragment.getPackageManager())); - } - } - - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == mRecognizerPref) { - String setting = (String) newValue; - - // Put the new value back into secure settings. - Settings.Secure.putString(mFragment.getContentResolver(), - Settings.Secure.VOICE_RECOGNITION_SERVICE, - setting); - - // Update the settings item so it points to the right settings. - updateSettingsLink(setting); - } - return true; - } } diff --git a/src/com/android/settings/VoiceSettingsActivity.java b/src/com/android/settings/VoiceSettingsActivity.java new file mode 100644 index 0000000..b5e8ede --- /dev/null +++ b/src/com/android/settings/VoiceSettingsActivity.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 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.Intent; +import android.os.Bundle; +import android.util.Log; + +/** + * Activity for modifying a setting using the Voice Interaction API. This activity + * MUST only modify the setting if the intent was sent using + * {@link android.service.voice.VoiceInteractionSession#startVoiceActivity startVoiceActivity}. + */ +abstract public class VoiceSettingsActivity extends Activity { + + private static final String TAG = "VoiceSettingsActivity"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (isVoiceInteraction()) { + // Only permit if this is a voice interaction. + onVoiceSettingInteraction(getIntent()); + } else { + Log.v(TAG, "Cannot modify settings without voice interaction"); + } + finish(); + } + + /** + * Modify the setting as a voice interaction. The activity will finish + * after this method is called. + */ + abstract protected void onVoiceSettingInteraction(Intent intent); +} diff --git a/src/com/android/settings/WallpaperTypeSettings.java b/src/com/android/settings/WallpaperTypeSettings.java index fa5f0ac..7b2dcbf 100644 --- a/src/com/android/settings/WallpaperTypeSettings.java +++ b/src/com/android/settings/WallpaperTypeSettings.java @@ -16,18 +16,23 @@ package com.android.settings; -import android.app.Activity; import android.content.ComponentName; +import android.content.Context; 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 com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import java.util.ArrayList; import java.util.List; -public class WallpaperTypeSettings extends SettingsPreferenceFragment { +public class WallpaperTypeSettings extends SettingsPreferenceFragment implements Indexable { + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -38,9 +43,9 @@ public class WallpaperTypeSettings extends SettingsPreferenceFragment { private void populateWallpaperTypes() { // Search for activities that satisfy the ACTION_SET_WALLPAPER action - Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); final PackageManager pm = getPackageManager(); - List<ResolveInfo> rList = pm.queryIntentActivities(intent, + final List<ResolveInfo> rList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); final PreferenceScreen parent = getPreferenceScreen(); @@ -58,4 +63,34 @@ public class WallpaperTypeSettings extends SettingsPreferenceFragment { parent.addPreference(pref); } } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + final PackageManager pm = context.getPackageManager(); + final List<ResolveInfo> rList = pm.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + + // Add indexable data for each of the matching activities + for (ResolveInfo info : rList) { + CharSequence label = info.loadLabel(pm); + if (label == null) label = info.activityInfo.packageName; + + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = label.toString(); + data.screenTitle = context.getResources().getString( + R.string.wallpaper_settings_fragment_title); + data.intentAction = Intent.ACTION_SET_WALLPAPER; + data.intentTargetPackage = info.activityInfo.packageName; + data.intentTargetClass = info.activityInfo.name; + result.add(data); + } + + return result; + } + }; } diff --git a/src/com/android/settings/WirelessSettings.java b/src/com/android/settings/WirelessSettings.java index 65127b5..09d4a54 100644 --- a/src/com/android/settings/WirelessSettings.java +++ b/src/com/android/settings/WirelessSettings.java @@ -21,24 +21,28 @@ import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; 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.NetworkInfo; +import android.net.Uri; import android.nfc.NfcAdapter; +import android.nfc.NfcManager; import android.os.Bundle; import android.os.SystemProperties; import android.os.UserHandle; +import android.os.UserManager; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; @@ -49,11 +53,16 @@ import com.android.internal.telephony.SmsApplication.SmsApplicationData; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.TelephonyProperties; import com.android.settings.nfc.NfcEnabler; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.List; -public class WirelessSettings extends RestrictedSettingsFragment - implements OnPreferenceChangeListener { +public class WirelessSettings extends SettingsPreferenceFragment + implements OnPreferenceChangeListener, Indexable { private static final String TAG = "WirelessSettings"; private static final String KEY_TOGGLE_AIRPLANE = "toggle_airplane"; @@ -73,32 +82,28 @@ public class WirelessSettings extends RestrictedSettingsFragment public static final int REQUEST_CODE_EXIT_ECM = 1; private AirplaneModeEnabler mAirplaneModeEnabler; - private CheckBoxPreference mAirplaneModePreference; + private SwitchPreference mAirplaneModePreference; private NfcEnabler mNfcEnabler; private NfcAdapter mNfcAdapter; private NsdEnabler mNsdEnabler; private ConnectivityManager mCm; private TelephonyManager mTm; + private PackageManager mPm; + private UserManager mUm; private static final int MANAGE_MOBILE_PLAN_DIALOG_ID = 1; private static final String SAVED_MANAGE_MOBILE_PLAN_MSG = "mManageMobilePlanMessage"; - private SmsListPreference mSmsApplicationPreference; + private AppListPreference mSmsApplicationPreference; - public WirelessSettings() { - super(null); - } /** * Invoked on each preference click in this hierarchy, overrides - * PreferenceActivity's implementation. Used to make sure we track the + * PreferenceFragment's implementation. Used to make sure we track the * preference click events. */ @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - if (ensurePinRestrictedPreference(preference)) { - return true; - } log("onPreferenceTreeClick: preference=" + preference); if (preference == mAirplaneModePreference && Boolean.parseBoolean( SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) { @@ -115,8 +120,6 @@ public class WirelessSettings extends RestrictedSettingsFragment } private String mManageMobilePlanMessage; - private static final String CONNECTED_TO_PROVISIONING_NETWORK_ACTION - = "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION"; public void onManageMobilePlanClick() { log("onManageMobilePlanClick:"); mManageMobilePlanMessage = null; @@ -124,14 +127,32 @@ public class WirelessSettings extends RestrictedSettingsFragment NetworkInfo ni = mCm.getProvisioningOrActiveNetworkInfo(); if (mTm.hasIccCard() && (ni != null)) { + // Check for carrier apps that can handle provisioning first + Intent provisioningIntent = new Intent(TelephonyIntents.ACTION_CARRIER_SETUP); + List<String> carrierPackages = + mTm.getCarrierPackageNamesForIntent(provisioningIntent); + if (carrierPackages != null && !carrierPackages.isEmpty()) { + if (carrierPackages.size() != 1) { + Log.w(TAG, "Multiple matching carrier apps found, launching the first."); + } + provisioningIntent.setPackage(carrierPackages.get(0)); + startActivity(provisioningIntent); + return; + } + // Get provisioning URL String url = mCm.getMobileProvisioningUrl(); if (!TextUtils.isEmpty(url)) { - Intent intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION); - intent.putExtra("EXTRA_URL", url); - Context context = getActivity().getBaseContext(); - context.sendBroadcast(intent); - mManageMobilePlanMessage = null; + Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, + Intent.CATEGORY_APP_BROWSER); + intent.setData(Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "onManageMobilePlanClick: startActivity failed" + e); + } } else { // No provisioning URL String operatorName = mTm.getSimOperatorName(); @@ -164,23 +185,6 @@ public class WirelessSettings extends RestrictedSettingsFragment } } - private void updateSmsApplicationSetting() { - log("updateSmsApplicationSetting:"); - ComponentName appName = SmsApplication.getDefaultSmsApplication(getActivity(), true); - if (appName != null) { - String packageName = appName.getPackageName(); - - CharSequence[] values = mSmsApplicationPreference.getEntryValues(); - for (int i = 0; i < values.length; i++) { - if (packageName.contentEquals(values[i])) { - mSmsApplicationPreference.setValueIndex(i); - mSmsApplicationPreference.setSummary(mSmsApplicationPreference.getEntries()[i]); - break; - } - } - } - } - private void initSmsApplicationSetting() { log("initSmsApplicationSetting:"); Collection<SmsApplicationData> smsApplications = @@ -188,26 +192,18 @@ public class WirelessSettings extends RestrictedSettingsFragment // If the list is empty the dialog will be empty, but we will not crash. int count = smsApplications.size(); - CharSequence[] entries = new CharSequence[count]; - CharSequence[] entryValues = new CharSequence[count]; - Drawable[] entryImages = new Drawable[count]; - - PackageManager packageManager = getPackageManager(); + String[] packageNames = new String[count]; int i = 0; for (SmsApplicationData smsApplicationData : smsApplications) { - entries[i] = smsApplicationData.mApplicationName; - entryValues[i] = smsApplicationData.mPackageName; - try { - entryImages[i] = packageManager.getApplicationIcon(smsApplicationData.mPackageName); - } catch (NameNotFoundException e) { - entryImages[i] = packageManager.getDefaultActivityIcon(); - } + packageNames[i] = smsApplicationData.mPackageName; i++; } - mSmsApplicationPreference.setEntries(entries); - mSmsApplicationPreference.setEntryValues(entryValues); - mSmsApplicationPreference.setEntryDrawables(entryImages); - updateSmsApplicationSetting(); + String defaultPackageName = null; + ComponentName appName = SmsApplication.getDefaultSmsApplication(getActivity(), true); + if (appName != null) { + defaultPackageName = appName.getPackageName(); + } + mSmsApplicationPreference.setPackageNames(packageNames, defaultPackageName); } @Override @@ -247,7 +243,7 @@ public class WirelessSettings extends RestrictedSettingsFragment private boolean isSmsSupported() { // Some tablet has sim card but could not do telephony operations. Skip those. - return (mTm.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE); + return mTm.isSmsCapable(); } @Override @@ -260,21 +256,23 @@ public class WirelessSettings extends RestrictedSettingsFragment mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); mTm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + mPm = getPackageManager(); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); addPreferencesFromResource(R.xml.wireless_settings); final boolean isSecondaryUser = UserHandle.myUserId() != UserHandle.USER_OWNER; final Activity activity = getActivity(); - mAirplaneModePreference = (CheckBoxPreference) findPreference(KEY_TOGGLE_AIRPLANE); - CheckBoxPreference nfc = (CheckBoxPreference) findPreference(KEY_TOGGLE_NFC); + mAirplaneModePreference = (SwitchPreference) findPreference(KEY_TOGGLE_AIRPLANE); + SwitchPreference nfc = (SwitchPreference) findPreference(KEY_TOGGLE_NFC); PreferenceScreen androidBeam = (PreferenceScreen) findPreference(KEY_ANDROID_BEAM_SETTINGS); CheckBoxPreference nsd = (CheckBoxPreference) findPreference(KEY_TOGGLE_NSD); mAirplaneModeEnabler = new AirplaneModeEnabler(activity, mAirplaneModePreference); mNfcEnabler = new NfcEnabler(activity, nfc, androidBeam); - mSmsApplicationPreference = (SmsListPreference) findPreference(KEY_SMS_APPLICATION); + mSmsApplicationPreference = (AppListPreference) findPreference(KEY_SMS_APPLICATION); mSmsApplicationPreference.setOnPreferenceChangeListener(this); initSmsApplicationSetting(); @@ -286,9 +284,10 @@ public class WirelessSettings extends RestrictedSettingsFragment Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); //enable/disable wimax depending on the value in config.xml - boolean isWimaxEnabled = !isSecondaryUser && this.getResources().getBoolean( + final boolean isWimaxEnabled = !isSecondaryUser && this.getResources().getBoolean( com.android.internal.R.bool.config_wimaxEnabled); - if (!isWimaxEnabled) { + if (!isWimaxEnabled + || mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { PreferenceScreen root = getPreferenceScreen(); Preference ps = (Preference) findPreference(KEY_WIMAX_SETTINGS); if (ps != null) root.removePreference(ps); @@ -299,16 +298,16 @@ public class WirelessSettings extends RestrictedSettingsFragment ps.setDependency(KEY_TOGGLE_AIRPLANE); } } - protectByRestrictions(KEY_WIMAX_SETTINGS); // Manually set dependencies for Wifi when not toggleable. if (toggleable == null || !toggleable.contains(Settings.Global.RADIO_WIFI)) { findPreference(KEY_VPN_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); } - if (isSecondaryUser) { // Disable VPN + // Disable VPN. + if (isSecondaryUser || mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { removePreference(KEY_VPN_SETTINGS); } - protectByRestrictions(KEY_VPN_SETTINGS); + // Manually set dependencies for Bluetooth when not toggleable. if (toggleable == null || !toggleable.contains(Settings.Global.RADIO_BLUETOOTH)) { // No bluetooth-dependent items in the list. Code kept in case one is added later. @@ -320,7 +319,7 @@ public class WirelessSettings extends RestrictedSettingsFragment findPreference(KEY_ANDROID_BEAM_SETTINGS).setDependency(KEY_TOGGLE_AIRPLANE); } - // Remove NFC if its not available + // Remove NFC if not available mNfcAdapter = NfcAdapter.getDefaultAdapter(activity); if (mNfcAdapter == null) { getPreferenceScreen().removePreference(nfc); @@ -328,14 +327,16 @@ public class WirelessSettings extends RestrictedSettingsFragment mNfcEnabler = null; } - // Remove Mobile Network Settings and Manage Mobile Plan if it's a wifi-only device. - if (isSecondaryUser || Utils.isWifiOnly(getActivity())) { + // Remove Mobile Network Settings and Manage Mobile Plan for secondary users, + // if it's a wifi-only device, or if the settings are restricted. + if (isSecondaryUser || Utils.isWifiOnly(getActivity()) + || mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { removePreference(KEY_MOBILE_NETWORK_SETTINGS); removePreference(KEY_MANAGE_MOBILE_PLAN); } // Remove Mobile Network Settings and Manage Mobile Plan // if config_show_mobile_plan sets false. - boolean isMobilePlanEnabled = this.getResources().getBoolean( + final boolean isMobilePlanEnabled = this.getResources().getBoolean( R.bool.config_show_mobile_plan); if (!isMobilePlanEnabled) { Preference pref = findPreference(KEY_MANAGE_MOBILE_PLAN); @@ -343,8 +344,6 @@ public class WirelessSettings extends RestrictedSettingsFragment removePreference(KEY_MANAGE_MOBILE_PLAN); } } - protectByRestrictions(KEY_MOBILE_NETWORK_SETTINGS); - protectByRestrictions(KEY_MANAGE_MOBILE_PLAN); // Remove SMS Application if the device does not support SMS if (!isSmsSupported()) { @@ -352,36 +351,39 @@ public class WirelessSettings extends RestrictedSettingsFragment } // Remove Airplane Mode settings if it's a stationary device such as a TV. - if (getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) { + if (mPm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) { removePreference(KEY_TOGGLE_AIRPLANE); } // Enable Proxy selector settings if allowed. Preference mGlobalProxy = findPreference(KEY_PROXY_SETTINGS); - DevicePolicyManager mDPM = (DevicePolicyManager) + final DevicePolicyManager mDPM = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE); // proxy UI disabled until we have better app support getPreferenceScreen().removePreference(mGlobalProxy); mGlobalProxy.setEnabled(mDPM.getGlobalProxyAdmin() == null); // Disable Tethering if it's not allowed or if it's a wifi-only device - ConnectivityManager cm = + final ConnectivityManager cm = (ConnectivityManager) activity.getSystemService(Context.CONNECTIVITY_SERVICE); - if (isSecondaryUser || !cm.isTetheringSupported()) { + if (isSecondaryUser || !cm.isTetheringSupported() + || mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING)) { getPreferenceScreen().removePreference(findPreference(KEY_TETHER_SETTINGS)); } else { Preference p = findPreference(KEY_TETHER_SETTINGS); p.setTitle(Utils.getTetheringLabel(cm)); + + // Grey out if provisioning is not available. + p.setEnabled(!TetherSettings + .isProvisioningNeededButUnavailable(getActivity())); } - protectByRestrictions(KEY_TETHER_SETTINGS); // Enable link to CMAS app settings depending on the value in config.xml. boolean isCellBroadcastAppLinkEnabled = this.getResources().getBoolean( com.android.internal.R.bool.config_cellBroadcastAppLinks); try { if (isCellBroadcastAppLinkEnabled) { - PackageManager pm = getPackageManager(); - if (pm.getApplicationEnabledSetting("com.android.cellbroadcastreceiver") + if (mPm.getApplicationEnabledSetting("com.android.cellbroadcastreceiver") == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { isCellBroadcastAppLinkEnabled = false; // CMAS app disabled } @@ -389,12 +391,12 @@ public class WirelessSettings extends RestrictedSettingsFragment } catch (IllegalArgumentException ignored) { isCellBroadcastAppLinkEnabled = false; // CMAS app not installed } - if (isSecondaryUser || !isCellBroadcastAppLinkEnabled) { + if (isSecondaryUser || !isCellBroadcastAppLinkEnabled + || mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_CELL_BROADCASTS)) { PreferenceScreen root = getPreferenceScreen(); Preference ps = findPreference(KEY_CELL_BROADCAST_SETTINGS); if (ps != null) root.removePreference(ps); } - protectByRestrictions(KEY_CELL_BROADCAST_SETTINGS); } @Override @@ -459,9 +461,109 @@ public class WirelessSettings extends RestrictedSettingsFragment public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference == mSmsApplicationPreference && newValue != null) { SmsApplication.setDefaultApplication(newValue.toString(), getActivity()); - updateSmsApplicationSetting(); return true; } return false; } + + /** + * For Search. + */ + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.wireless_settings; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final ArrayList<String> result = new ArrayList<String>(); + + result.add(KEY_TOGGLE_NSD); + + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + final boolean isSecondaryUser = UserHandle.myUserId() != UserHandle.USER_OWNER; + final boolean isWimaxEnabled = !isSecondaryUser && context.getResources().getBoolean( + com.android.internal.R.bool.config_wimaxEnabled); + if (!isWimaxEnabled + || um.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) { + result.add(KEY_WIMAX_SETTINGS); + } + + if (isSecondaryUser) { // Disable VPN + result.add(KEY_VPN_SETTINGS); + } + + // Remove NFC if not available + final NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); + if (manager != null) { + NfcAdapter adapter = manager.getDefaultAdapter(); + if (adapter == null) { + result.add(KEY_TOGGLE_NFC); + result.add(KEY_ANDROID_BEAM_SETTINGS); + } + } + + // Remove Mobile Network Settings and Manage Mobile Plan if it's a wifi-only device. + if (isSecondaryUser || Utils.isWifiOnly(context)) { + result.add(KEY_MOBILE_NETWORK_SETTINGS); + result.add(KEY_MANAGE_MOBILE_PLAN); + } + + // Remove Mobile Network Settings and Manage Mobile Plan + // if config_show_mobile_plan sets false. + final boolean isMobilePlanEnabled = context.getResources().getBoolean( + R.bool.config_show_mobile_plan); + if (!isMobilePlanEnabled) { + result.add(KEY_MANAGE_MOBILE_PLAN); + } + + // Remove SMS Application if the device does not support SMS + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (!tm.isSmsCapable()) { + result.add(KEY_SMS_APPLICATION); + } + + final PackageManager pm = context.getPackageManager(); + + // Remove Airplane Mode settings if it's a stationary device such as a TV. + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) { + result.add(KEY_TOGGLE_AIRPLANE); + } + + // proxy UI disabled until we have better app support + result.add(KEY_PROXY_SETTINGS); + + // Disable Tethering if it's not allowed or if it's a wifi-only device + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (isSecondaryUser || !cm.isTetheringSupported()) { + result.add(KEY_TETHER_SETTINGS); + } + + // Enable link to CMAS app settings depending on the value in config.xml. + boolean isCellBroadcastAppLinkEnabled = context.getResources().getBoolean( + com.android.internal.R.bool.config_cellBroadcastAppLinks); + try { + if (isCellBroadcastAppLinkEnabled) { + if (pm.getApplicationEnabledSetting("com.android.cellbroadcastreceiver") + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { + isCellBroadcastAppLinkEnabled = false; // CMAS app disabled + } + } + } catch (IllegalArgumentException ignored) { + isCellBroadcastAppLinkEnabled = false; // CMAS app not installed + } + if (isSecondaryUser || !isCellBroadcastAppLinkEnabled) { + result.add(KEY_CELL_BROADCAST_SETTINGS); + } + + return result; + } + }; } diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 4d24d09..92c478e 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -18,14 +18,10 @@ package com.android.settings.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; import android.app.ActivityManagerNative; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.ActivityNotFoundException; +import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; @@ -33,16 +29,17 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; -import android.os.SystemProperties; +import android.os.UserHandle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; -import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; @@ -56,7 +53,11 @@ import com.android.settings.DialogCreatable; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -67,21 +68,12 @@ import java.util.Set; * Activity with the accessibility settings. */ public class AccessibilitySettings extends SettingsPreferenceFragment implements DialogCreatable, - Preference.OnPreferenceChangeListener { - private static final String LOG_TAG = "AccessibilitySettings"; - - private static final String DEFAULT_SCREENREADER_MARKET_LINK = - "market://search?q=pname:com.google.android.marvin.talkback"; + Preference.OnPreferenceChangeListener, Indexable { private static final float LARGE_FONT_SCALE = 1.3f; - private static final String SYSTEM_PROPERTY_MARKET_URL = "ro.screenreader.market"; - static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':'; - private static final String KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE = - "key_install_accessibility_service_offered_once"; - // Preference categories private static final String SERVICES_CATEGORY = "services_category"; private static final String SYSTEM_CATEGORY = "system_category"; @@ -89,6 +81,10 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // Preferences private static final String TOGGLE_LARGE_TEXT_PREFERENCE = "toggle_large_text_preference"; + private static final String TOGGLE_HIGH_TEXT_CONTRAST_PREFERENCE = + "toggle_high_text_contrast_preference"; + private static final String TOGGLE_INVERSION_PREFERENCE = + "toggle_inversion_preference"; private static final String TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE = "toggle_power_button_ends_call_preference"; private static final String TOGGLE_LOCK_SCREEN_ROTATION_PREFERENCE = @@ -103,6 +99,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements "captioning_preference_screen"; private static final String DISPLAY_MAGNIFICATION_PREFERENCE_SCREEN = "screen_magnification_preference_screen"; + private static final String DISPLAY_DALTONIZER_PREFERENCE_SCREEN = + "daltonizer_preference_screen"; // Extras passed to sub-fragments. static final String EXTRA_PREFERENCE_KEY = "preference_key"; @@ -119,9 +117,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // presentation. private static final long DELAY_UPDATE_SERVICES_MILLIS = 1000; - // Dialog IDs. - private static final int DIALOG_ID_NO_ACCESSIBILITY_SERVICES = 1; - // Auxiliary members. final static SimpleStringSplitter sStringColonSplitter = new SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR); @@ -190,6 +185,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements private PreferenceCategory mSystemsCategory; private CheckBoxPreference mToggleLargeTextPreference; + private CheckBoxPreference mToggleHighTextContrastPreference; private CheckBoxPreference mTogglePowerButtonEndsCallPreference; private CheckBoxPreference mToggleLockScreenRotationPreference; private CheckBoxPreference mToggleSpeakPasswordPreference; @@ -198,14 +194,20 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements private PreferenceScreen mCaptioningPreferenceScreen; private PreferenceScreen mDisplayMagnificationPreferenceScreen; private PreferenceScreen mGlobalGesturePreferenceScreen; + private PreferenceScreen mDisplayDaltonizerPreferenceScreen; + private SwitchPreference mToggleInversionPreference; private int mLongPressTimeoutDefault; + private DevicePolicyManager mDpm; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.accessibility_settings); initializeAllPreferences(); + mDpm = (DevicePolicyManager) (getActivity() + .getSystemService(Context.DEVICE_POLICY_SERVICE)); } @Override @@ -214,8 +216,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements loadInstalledServices(); updateAllPreferences(); - offerInstallAccessibilitySerivceOnce(); - mSettingsPackageMonitor.register(getActivity(), getActivity().getMainLooper(), false); mSettingsContentObserver.register(getContentResolver()); if (RotationPolicy.isRotationSupported(getActivity())) { @@ -237,22 +237,36 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements @Override 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)); + if (mSelectLongPressTimeoutPreference == preference) { + handleLongPressTimeoutPreferenceChange((String) newValue); + return true; + } else if (mToggleInversionPreference == preference) { + handleToggleInversionPreferenceChange((Boolean) newValue); return true; } return false; } + private void handleLongPressTimeoutPreferenceChange(String stringValue) { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LONG_PRESS_TIMEOUT, Integer.parseInt(stringValue)); + mSelectLongPressTimeoutPreference.setSummary( + mLongPressTimeoutValuetoTitleMap.get(stringValue)); + } + + private void handleToggleInversionPreferenceChange(boolean checked) { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, (checked ? 1 : 0)); + } + @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { if (mToggleLargeTextPreference == preference) { handleToggleLargeTextPreferenceClick(); return true; + } else if (mToggleHighTextContrastPreference == preference) { + handleToggleTextContrastPreferenceClick(); + return true; } else if (mTogglePowerButtonEndsCallPreference == preference) { handleTogglePowerButtonEndsCallPreferenceClick(); return true; @@ -263,7 +277,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements handleToggleSpeakPasswordPreferenceClick(); return true; } else if (mGlobalGesturePreferenceScreen == preference) { - handleTogglEnableAccessibilityGesturePreferenceClick(); + handleToggleEnableAccessibilityGesturePreferenceClick(); return true; } else if (mDisplayMagnificationPreferenceScreen == preference) { handleDisplayMagnificationPreferenceScreenClick(); @@ -281,6 +295,12 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } + private void handleToggleTextContrastPreferenceClick() { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, + (mToggleHighTextContrastPreference.isChecked() ? 1 : 0)); + } + private void handleTogglePowerButtonEndsCallPreferenceClick() { Settings.Secure.putInt(getContentResolver(), Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, @@ -300,7 +320,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mToggleSpeakPasswordPreference.isChecked() ? 1 : 0); } - private void handleTogglEnableAccessibilityGesturePreferenceClick() { + private void handleToggleEnableAccessibilityGesturePreferenceClick() { Bundle extras = mGlobalGesturePreferenceScreen.getExtras(); extras.putString(EXTRA_TITLE, getString( R.string.accessibility_global_gesture_preference_title)); @@ -332,6 +352,14 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mToggleLargeTextPreference = (CheckBoxPreference) findPreference(TOGGLE_LARGE_TEXT_PREFERENCE); + // Text contrast. + mToggleHighTextContrastPreference = + (CheckBoxPreference) findPreference(TOGGLE_HIGH_TEXT_CONTRAST_PREFERENCE); + + // Display inversion. + mToggleInversionPreference = (SwitchPreference) findPreference(TOGGLE_INVERSION_PREFERENCE); + mToggleInversionPreference.setOnPreferenceChangeListener(this); + // Power button ends calls. mTogglePowerButtonEndsCallPreference = (CheckBoxPreference) findPreference(TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE); @@ -375,6 +403,10 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mDisplayMagnificationPreferenceScreen = (PreferenceScreen) findPreference( DISPLAY_MAGNIFICATION_PREFERENCE_SCREEN); + // Display color adjustments. + mDisplayDaltonizerPreferenceScreen = (PreferenceScreen) findPreference( + DISPLAY_DALTONIZER_PREFERENCE_SCREEN); + // Global gesture. mGlobalGesturePreferenceScreen = (PreferenceScreen) findPreference(ENABLE_ACCESSIBILITY_GESTURE_PREFERENCE_SCREEN); @@ -408,7 +440,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements accessibilityManager.getInstalledAccessibilityServiceList(); Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings( getActivity()); - + List<String> permittedServices = mDpm.getPermittedAccessibilityServices( + UserHandle.myUserId()); final boolean accessibilityEnabled = Settings.Secure.getInt(getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1; @@ -428,12 +461,27 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements preference.setTitle(title); final boolean serviceEnabled = accessibilityEnabled && enabledServices.contains(componentName); + String serviceEnabledString; if (serviceEnabled) { - preference.setSummary(getString(R.string.accessibility_feature_state_on)); + serviceEnabledString = getString(R.string.accessibility_feature_state_on); } else { - preference.setSummary(getString(R.string.accessibility_feature_state_off)); + serviceEnabledString = getString(R.string.accessibility_feature_state_off); } + // Disable all accessibility services that are not permitted. + String packageName = serviceInfo.packageName; + boolean serviceAllowed = + permittedServices == null || permittedServices.contains(packageName); + preference.setEnabled(serviceAllowed || serviceEnabled); + + String summaryString; + if (serviceAllowed) { + summaryString = serviceEnabledString; + } else { + summaryString = getString(R.string.accessibility_feature_or_input_method_not_allowed); + } + preference.setSummary(summaryString); + preference.setOrder(i); preference.setFragment(ToggleAccessibilityServicePreferenceFragment.class.getName()); preference.setPersistent(true); @@ -465,19 +513,13 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements if (mServicesCategory.getPreferenceCount() == 0) { if (mNoServicesMessagePreference == null) { - mNoServicesMessagePreference = new Preference(getActivity()) { - @Override - protected void onBindView(View view) { - super.onBindView(view); - TextView summaryView = (TextView) view.findViewById(R.id.summary); - String title = getString(R.string.accessibility_no_services_installed); - summaryView.setText(title); - } - }; + mNoServicesMessagePreference = new Preference(getActivity()); mNoServicesMessagePreference.setPersistent(false); mNoServicesMessagePreference.setLayoutResource( R.layout.text_description_preference); mNoServicesMessagePreference.setSelectable(false); + mNoServicesMessagePreference.setSummary( + getString(R.string.accessibility_no_services_installed)); } mServicesCategory.addPreference(mNoServicesMessagePreference); } @@ -492,6 +534,14 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } mToggleLargeTextPreference.setChecked(mCurConfig.fontScale == LARGE_FONT_SCALE); + mToggleHighTextContrastPreference.setChecked( + Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0) == 1); + + // If the quick setting is enabled, the preference MUST be enabled. + mToggleInversionPreference.setChecked(Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0) == 1); + // Power button ends calls. if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_POWER) && Utils.isVoiceCapable(getActivity())) { @@ -518,25 +568,12 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mSelectLongPressTimeoutPreference.setValue(value); mSelectLongPressTimeoutPreference.setSummary(mLongPressTimeoutValuetoTitleMap.get(value)); - // Captioning. - final boolean captioningEnabled = Settings.Secure.getInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, 0) == 1; - if (captioningEnabled) { - mCaptioningPreferenceScreen.setSummary(R.string.accessibility_feature_state_on); - } else { - mCaptioningPreferenceScreen.setSummary(R.string.accessibility_feature_state_off); - } - - // Screen magnification. - final boolean magnificationEnabled = Settings.Secure.getInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, 0) == 1; - if (magnificationEnabled) { - mDisplayMagnificationPreferenceScreen.setSummary( - R.string.accessibility_feature_state_on); - } else { - mDisplayMagnificationPreferenceScreen.setSummary( - R.string.accessibility_feature_state_off); - } + updateFeatureSummary(Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, + mCaptioningPreferenceScreen); + updateFeatureSummary(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + mDisplayMagnificationPreferenceScreen); + updateFeatureSummary(Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + mDisplayDaltonizerPreferenceScreen); // Global gesture final boolean globalGestureEnabled = Settings.Global.getInt(getContentResolver(), @@ -550,6 +587,12 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } + private void updateFeatureSummary(String prefKey, Preference pref) { + final boolean enabled = Settings.Secure.getInt(getContentResolver(), prefKey, 0) == 1; + pref.setSummary(enabled ? R.string.accessibility_feature_state_on + : R.string.accessibility_feature_state_off); + } + private void updateLockScreenRotationCheckbox() { Context context = getActivity(); if (context != null) { @@ -558,72 +601,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } - private void offerInstallAccessibilitySerivceOnce() { - // There is always one preference - if no services it is just a message. - if (mServicesCategory.getPreference(0) != mNoServicesMessagePreference) { - return; - } - SharedPreferences preferences = getActivity().getPreferences(Context.MODE_PRIVATE); - final boolean offerInstallService = !preferences.getBoolean( - KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE, false); - if (offerInstallService) { - String screenreaderMarketLink = SystemProperties.get( - SYSTEM_PROPERTY_MARKET_URL, - DEFAULT_SCREENREADER_MARKET_LINK); - Uri marketUri = Uri.parse(screenreaderMarketLink); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, marketUri); - - if (getPackageManager().resolveActivity(marketIntent, 0) == null) { - // Don't show the dialog if no market app is found/installed. - return; - } - - 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); - } - } - - @Override - public Dialog onCreateDialog(int dialogId) { - switch (dialogId) { - case DIALOG_ID_NO_ACCESSIBILITY_SERVICES: - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.accessibility_service_no_apps_title) - .setMessage(R.string.accessibility_service_no_apps_message) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // dismiss the dialog before launching - // the activity otherwise the dialog - // removal occurs after - // onSaveInstanceState which triggers an - // exception - removeDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES); - String screenreaderMarketLink = SystemProperties.get( - SYSTEM_PROPERTY_MARKET_URL, - DEFAULT_SCREENREADER_MARKET_LINK); - Uri marketUri = Uri.parse(screenreaderMarketLink); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, - marketUri); - try { - startActivity(marketIntent); - } catch (ActivityNotFoundException anfe) { - Log.w(LOG_TAG, "Couldn't start play store activity", - anfe); - } - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - default: - return null; - } - } - private void loadInstalledServices() { Set<ComponentName> installedServices = sInstalledServices; installedServices.clear(); @@ -644,4 +621,54 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements installedServices.add(installedService); } } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>(); + + PackageManager packageManager = context.getPackageManager(); + AccessibilityManager accessibilityManager = (AccessibilityManager) + context.getSystemService(Context.ACCESSIBILITY_SERVICE); + + String screenTitle = context.getResources().getString( + R.string.accessibility_services_title); + + // Indexing all services, regardless if enabled. + List<AccessibilityServiceInfo> services = accessibilityManager + .getInstalledAccessibilityServiceList(); + final int serviceCount = services.size(); + for (int i = 0; i < serviceCount; i++) { + AccessibilityServiceInfo service = services.get(i); + if (service == null || service.getResolveInfo() == null) { + continue; + } + + ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + ComponentName componentName = new ComponentName(serviceInfo.packageName, + serviceInfo.name); + + SearchIndexableRaw indexable = new SearchIndexableRaw(context); + indexable.key = componentName.flattenToString(); + indexable.title = service.getResolveInfo().loadLabel(packageManager).toString(); + indexable.summaryOn = context.getString(R.string.accessibility_feature_state_on); + indexable.summaryOff = context.getString(R.string.accessibility_feature_state_off); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + return indexables; + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>(); + SearchIndexableResource indexable = new SearchIndexableResource(context); + indexable.xmlResId = R.xml.accessibility_settings; + indexables.add(indexable); + return indexables; + } + }; } diff --git a/src/com/android/settings/accessibility/CaptionPropertiesFragment.java b/src/com/android/settings/accessibility/CaptionPropertiesFragment.java index 8a08d90..9822fc3 100644 --- a/src/com/android/settings/accessibility/CaptionPropertiesFragment.java +++ b/src/com/android/settings/accessibility/CaptionPropertiesFragment.java @@ -16,8 +16,6 @@ package com.android.settings.accessibility; -import android.app.ActionBar; -import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; @@ -29,9 +27,9 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceFrameLayout; import android.preference.Preference.OnPreferenceChangeListener; import android.provider.Settings; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.accessibility.CaptioningManager; @@ -39,9 +37,12 @@ import android.view.accessibility.CaptioningManager.CaptionStyle; import com.android.internal.widget.SubtitleView; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.accessibility.ListDialogPreference.OnValueChangedListener; -import com.android.settings.accessibility.ToggleSwitch.OnBeforeCheckedChangeListener; +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.ToggleSwitch; +import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener; import java.util.Locale; @@ -54,6 +55,8 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment private static final String PREF_BACKGROUND_OPACITY = "captioning_background_opacity"; private static final String PREF_FOREGROUND_COLOR = "captioning_foreground_color"; private static final String PREF_FOREGROUND_OPACITY = "captioning_foreground_opacity"; + private static final String PREF_WINDOW_COLOR = "captioning_window_color"; + private static final String PREF_WINDOW_OPACITY = "captioning_window_opacity"; private static final String PREF_EDGE_COLOR = "captioning_edge_color"; private static final String PREF_EDGE_TYPE = "captioning_edge_type"; private static final String PREF_FONT_SIZE = "captioning_font_size"; @@ -62,10 +65,15 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment private static final String PREF_PRESET = "captioning_preset"; private static final String PREF_CUSTOM = "custom"; - private static final float DEFAULT_FONT_SIZE = 48f; + /** WebVtt specifies line height as 5.3% of the viewport height. */ + private static final float LINE_HEIGHT_RATIO = 0.0533f; private CaptioningManager mCaptioningManager; private SubtitleView mPreviewText; + private View mPreviewWindow; + private View mPreviewViewport; + private SwitchBar mSwitchBar; + private ToggleSwitch mToggleSwitch; // Standard options. private LocalePreference mLocale; @@ -80,6 +88,8 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment private ColorPreference mEdgeColor; private ColorPreference mBackgroundColor; private ColorPreference mBackgroundOpacity; + private ColorPreference mWindowColor; + private ColorPreference mWindowOpacity; private PreferenceCategory mCustom; private boolean mShowingCustom; @@ -119,10 +129,42 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + final boolean enabled = mCaptioningManager.isEnabled(); mPreviewText = (SubtitleView) view.findViewById(R.id.preview_text); + mPreviewText.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + + mPreviewWindow = view.findViewById(R.id.preview_window); + mPreviewViewport = view.findViewById(R.id.preview_viewport); + mPreviewViewport.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + refreshPreviewText(); + } + }); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final boolean enabled = mCaptioningManager.isEnabled(); + SettingsActivity activity = (SettingsActivity) getActivity(); + mSwitchBar = activity.getSwitchBar(); + mSwitchBar.setCheckedInternal(enabled); + mToggleSwitch = mSwitchBar.getSwitch(); + + getPreferenceScreen().setEnabled(enabled); - installActionBarToggleSwitch(); refreshPreviewText(); + + installSwitchBarToggleSwitch(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + removeSwitchBarToggleSwitch(); } private void refreshPreviewText() { @@ -135,7 +177,7 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment final SubtitleView preview = mPreviewText; if (preview != null) { final int styleId = mCaptioningManager.getRawUserStyle(); - applyCaptionProperties(mCaptioningManager, preview, styleId); + applyCaptionProperties(mCaptioningManager, preview, mPreviewViewport, styleId); final Locale locale = mCaptioningManager.getLocale(); if (locale != null) { @@ -145,17 +187,34 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment } else { preview.setText(R.string.captioning_preview_text); } + + final CaptionStyle style = mCaptioningManager.getUserStyle(); + if (style.hasWindowColor()) { + mPreviewWindow.setBackgroundColor(style.windowColor); + } else { + final CaptionStyle defStyle = CaptionStyle.DEFAULT; + mPreviewWindow.setBackgroundColor(defStyle.windowColor); + } } } - public static void applyCaptionProperties( - CaptioningManager manager, SubtitleView previewText, int styleId) { + public static void applyCaptionProperties(CaptioningManager manager, SubtitleView previewText, + View previewWindow, int styleId) { previewText.setStyle(styleId); final Context context = previewText.getContext(); final ContentResolver cr = context.getContentResolver(); final float fontScale = manager.getFontScale(); - previewText.setTextSize(fontScale * DEFAULT_FONT_SIZE); + if (previewWindow != null) { + // Assume the viewport is clipped with a 16:9 aspect ratio. + final float virtualHeight = Math.max(9 * previewWindow.getWidth(), + 16 * previewWindow.getHeight()) / 16.0f; + previewText.setTextSize(virtualHeight * LINE_HEIGHT_RATIO * fontScale); + } else { + final float textSize = context.getResources().getDimension( + R.dimen.caption_preview_text_size); + previewText.setTextSize(textSize * fontScale); + } final Locale locale = manager.getLocale(); if (locale != null) { @@ -167,39 +226,32 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment } } - private void installActionBarToggleSwitch() { - final Activity activity = getActivity(); - final ToggleSwitch toggleSwitch = new ToggleSwitch(activity); - - final int padding = getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - toggleSwitch.setPaddingRelative(0, 0, padding, 0); - - final ActionBar actionBar = activity.getActionBar(); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM); - - final ActionBar.LayoutParams params = new ActionBar.LayoutParams( - ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_VERTICAL | Gravity.END); - actionBar.setCustomView(toggleSwitch, params); - - final boolean enabled = mCaptioningManager.isEnabled(); - getPreferenceScreen().setEnabled(enabled); - mPreviewText.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); - toggleSwitch.setCheckedInternal(enabled); - toggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { + protected void onInstallSwitchBarToggleSwitch() { + mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { - toggleSwitch.setCheckedInternal(checked); + mSwitchBar.setCheckedInternal(checked); Settings.Secure.putInt(getActivity().getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, checked ? 1 : 0); getPreferenceScreen().setEnabled(checked); - mPreviewText.setVisibility(checked ? View.VISIBLE : View.INVISIBLE); + if (mPreviewText != null) { + mPreviewText.setVisibility(checked ? View.VISIBLE : View.INVISIBLE); + } return false; } }); } + private void installSwitchBarToggleSwitch() { + onInstallSwitchBarToggleSwitch(); + mSwitchBar.show(); + } + + private void removeSwitchBarToggleSwitch() { + mSwitchBar.hide(); + mToggleSwitch.setOnBeforeCheckedChangeListener(null); + } + private void initializeAllPreferences() { mLocale = (LocalePreference) findPreference(PREF_LOCALE); mFontSize = (ListPreference) findPreference(PREF_FONT_SIZE); @@ -246,6 +298,14 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment mBackgroundOpacity.setTitles(opacityTitles); mBackgroundOpacity.setValues(opacityValues); + mWindowColor = (ColorPreference) mCustom.findPreference(PREF_WINDOW_COLOR); + mWindowColor.setTitles(bgColorTitles); + mWindowColor.setValues(bgColorValues); + + mWindowOpacity = (ColorPreference) mCustom.findPreference(PREF_WINDOW_OPACITY); + mWindowOpacity.setTitles(opacityTitles); + mWindowOpacity.setValues(opacityValues); + mEdgeType = (EdgeTypePreference) mCustom.findPreference(PREF_EDGE_TYPE); mTypeface = (ListPreference) mCustom.findPreference(PREF_TYPEFACE); } @@ -257,6 +317,8 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment mEdgeColor.setOnValueChangedListener(this); mBackgroundColor.setOnValueChangedListener(this); mBackgroundOpacity.setOnValueChangedListener(this); + mWindowColor.setOnValueChangedListener(this); + mWindowOpacity.setOnValueChangedListener(this); mEdgeType.setOnValueChangedListener(this); mTypeface.setOnPreferenceChangeListener(this); @@ -278,6 +340,7 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment parseColorOpacity(mForegroundColor, mForegroundOpacity, attrs.foregroundColor); parseColorOpacity(mBackgroundColor, mBackgroundOpacity, attrs.backgroundColor); + parseColorOpacity(mWindowColor, mWindowOpacity, attrs.windowColor); final String rawTypeface = attrs.mRawTypeface; mTypeface.setValue(rawTypeface == null ? "" : rawTypeface); @@ -305,7 +368,7 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment final int opacityValue = opacity.getValue(); final int value; if (Color.alpha(colorValue) == 0) { - value = Color.alpha(opacityValue); + value = colorValue & 0x00FFFF00 | Color.alpha(opacityValue); } else { value = colorValue & 0x00FFFFFF | opacityValue & 0xFF000000; } @@ -334,6 +397,10 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment final int merged = mergeColorOpacity(mBackgroundColor, mBackgroundOpacity); Settings.Secure.putInt( cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, merged); + } else if (mWindowColor == preference || mWindowOpacity == preference) { + final int merged = mergeColorOpacity(mWindowColor, mWindowOpacity); + Settings.Secure.putInt( + cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, merged); } else if (mEdgeColor == preference) { Settings.Secure.putInt(cr, Settings.Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, value); } else if (mPreset == preference) { diff --git a/src/com/android/settings/accessibility/ColorPreference.java b/src/com/android/settings/accessibility/ColorPreference.java index f4a5ba7..39e555a 100644 --- a/src/com/android/settings/accessibility/ColorPreference.java +++ b/src/com/android/settings/accessibility/ColorPreference.java @@ -60,7 +60,7 @@ public class ColorPreference extends ListDialogPreference { @Override public boolean shouldDisableDependents() { - return getValue() == Color.TRANSPARENT || super.shouldDisableDependents(); + return Color.alpha(getValue()) == 0 || super.shouldDisableDependents(); } @Override diff --git a/src/com/android/settings/accessibility/EdgeTypePreference.java b/src/com/android/settings/accessibility/EdgeTypePreference.java index ad71a76..3bff704 100644 --- a/src/com/android/settings/accessibility/EdgeTypePreference.java +++ b/src/com/android/settings/accessibility/EdgeTypePreference.java @@ -34,7 +34,7 @@ public class EdgeTypePreference extends ListDialogPreference { private static final int DEFAULT_FOREGROUND_COLOR = Color.WHITE; private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT; private static final int DEFAULT_EDGE_COLOR = Color.BLACK; - private static final float DEFAULT_FONT_SIZE = 96f; + private static final float DEFAULT_FONT_SIZE = 32f; public EdgeTypePreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -57,7 +57,9 @@ public class EdgeTypePreference extends ListDialogPreference { preview.setForegroundColor(DEFAULT_FOREGROUND_COLOR); preview.setBackgroundColor(DEFAULT_BACKGROUND_COLOR); - preview.setTextSize(DEFAULT_FONT_SIZE); + + final float density = getContext().getResources().getDisplayMetrics().density; + preview.setTextSize(DEFAULT_FONT_SIZE * density); final int value = getValueAt(index); preview.setEdgeType(value); diff --git a/src/com/android/settings/accessibility/ListDialogPreference.java b/src/com/android/settings/accessibility/ListDialogPreference.java index a252454..2140d91 100644 --- a/src/com/android/settings/accessibility/ListDialogPreference.java +++ b/src/com/android/settings/accessibility/ListDialogPreference.java @@ -82,6 +82,10 @@ public abstract class ListDialogPreference extends DialogPreference { */ public void setValues(int[] values) { mEntryValues = values; + + if (mValueSet && mValueIndex == AbsListView.INVALID_POSITION) { + mValueIndex = getIndexForValue(mValue); + } } /** @@ -172,10 +176,12 @@ public abstract class ListDialogPreference extends DialogPreference { */ protected int getIndexForValue(int value) { final int[] values = mEntryValues; - final int count = values.length; - for (int i = 0; i < count; i++) { - if (values[i] == value) { - return i; + if (values != null) { + final int count = values.length; + for (int i = 0; i < count; i++) { + if (values[i] == value) { + return i; + } } } diff --git a/src/com/android/settings/accessibility/LocalePreference.java b/src/com/android/settings/accessibility/LocalePreference.java index 41cb1e5..0a94817 100644 --- a/src/com/android/settings/accessibility/LocalePreference.java +++ b/src/com/android/settings/accessibility/LocalePreference.java @@ -21,10 +21,12 @@ import android.content.res.Resources; import android.preference.ListPreference; import android.util.AttributeSet; +import com.android.internal.app.LocalePicker; import com.android.settings.R; import java.text.Collator; import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -43,108 +45,22 @@ public class LocalePreference extends ListPreference { } public void init(Context context) { - final String[] systemLocales = Resources.getSystem().getAssets().getLocales(); - Arrays.sort(systemLocales); - - final Resources resources = context.getResources(); - final String[] specialLocaleCodes = resources.getStringArray( - com.android.internal.R.array.special_locale_codes); - final String[] specialLocaleNames = resources.getStringArray( - com.android.internal.R.array.special_locale_names); - - int finalSize = 0; - - final int origSize = systemLocales.length; - final LocaleInfo[] localeInfos = new LocaleInfo[origSize]; - for (int i = 0; i < origSize; i++) { - final String localeStr = systemLocales[i]; - final int len = localeStr.length(); - if (len != 5) { - continue; - } - - final String language = localeStr.substring(0, 2); - final String country = localeStr.substring(3, 5); - final Locale l = new Locale(language, country); - - if (finalSize == 0) { - localeInfos[finalSize++] = new LocaleInfo(l.getDisplayLanguage(l), l); - } else { - // check previous entry: - // same lang and a country -> upgrade to full name and - // insert ours with full name - // diff lang -> insert ours with lang-only name - final LocaleInfo previous = localeInfos[finalSize - 1]; - if (previous.locale.getLanguage().equals(language) - && !previous.locale.getLanguage().equals("zz")) { - previous.label = getDisplayName( - localeInfos[finalSize - 1].locale, specialLocaleCodes, - specialLocaleNames); - localeInfos[finalSize++] = new LocaleInfo(getDisplayName(l, - specialLocaleCodes, specialLocaleNames), l); - } else { - final String displayName; - if (localeStr.equals("zz_ZZ")) { - displayName = "[Developer] Accented English"; - } else if (localeStr.equals("zz_ZY")) { - displayName = "[Developer] Fake Bi-Directional"; - } else { - displayName = l.getDisplayLanguage(l); - } - localeInfos[finalSize++] = new LocaleInfo(displayName, l); - } - } - } + List<LocalePicker.LocaleInfo> locales = LocalePicker.getAllAssetLocales(context, + false /* in developer mode */); + final int finalSize = locales.size(); final CharSequence[] entries = new CharSequence[finalSize + 1]; final CharSequence[] entryValues = new CharSequence[finalSize + 1]; - Arrays.sort(localeInfos, 0, finalSize); - - entries[0] = resources.getString(R.string.locale_default); + entries[0] = context.getResources().getString(R.string.locale_default); entryValues[0] = ""; for (int i = 0; i < finalSize; i++) { - final LocaleInfo info = localeInfos[i]; + final LocalePicker.LocaleInfo info = locales.get(i); entries[i + 1] = info.toString(); - entryValues[i + 1] = info.locale.toString(); + entryValues[i + 1] = info.getLocale().toString(); } setEntries(entries); setEntryValues(entryValues); } - - private static String getDisplayName( - Locale l, String[] specialLocaleCodes, String[] specialLocaleNames) { - String code = l.toString(); - - for (int i = 0; i < specialLocaleCodes.length; i++) { - if (specialLocaleCodes[i].equals(code)) { - return specialLocaleNames[i]; - } - } - - return l.getDisplayName(l); - } - - private static class LocaleInfo implements Comparable<LocaleInfo> { - private static final Collator sCollator = Collator.getInstance(); - - public String label; - public Locale locale; - - public LocaleInfo(String label, Locale locale) { - this.label = label; - this.locale = locale; - } - - @Override - public String toString() { - return label; - } - - @Override - public int compareTo(LocaleInfo another) { - return sCollator.compare(this.label, another.label); - } - } } diff --git a/src/com/android/settings/accessibility/PresetPreference.java b/src/com/android/settings/accessibility/PresetPreference.java index 84aba6c..fe5ca68 100644 --- a/src/com/android/settings/accessibility/PresetPreference.java +++ b/src/com/android/settings/accessibility/PresetPreference.java @@ -27,7 +27,7 @@ import com.android.internal.widget.SubtitleView; import com.android.settings.R; public class PresetPreference extends ListDialogPreference { - private static final float DEFAULT_FONT_SIZE = 96f; + private static final float DEFAULT_FONT_SIZE = 32f; private final CaptioningManager mCaptioningManager; @@ -49,12 +49,14 @@ public class PresetPreference extends ListDialogPreference { @Override protected void onBindListItem(View view, int index) { + final View previewViewport = view.findViewById(R.id.preview_viewport); final SubtitleView previewText = (SubtitleView) view.findViewById(R.id.preview); final int value = getValueAt(index); CaptionPropertiesFragment.applyCaptionProperties( - mCaptioningManager, previewText, value); + mCaptioningManager, previewText, previewViewport, value); - previewText.setTextSize(DEFAULT_FONT_SIZE); + final float density = getContext().getResources().getDisplayMetrics().density; + previewText.setTextSize(DEFAULT_FONT_SIZE * density); final CharSequence title = getTitleAt(index); if (title != null) { diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index 0c568f0..08fba67 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -17,8 +17,10 @@ package com.android.settings.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; +import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -36,8 +38,11 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.ConfirmDeviceCredentialActivity; import com.android.settings.R; -import com.android.settings.accessibility.ToggleSwitch.OnBeforeCheckedChangeListener; +import com.android.settings.widget.ToggleSwitch; +import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener; import java.util.Collections; import java.util.HashSet; @@ -50,6 +55,10 @@ public class ToggleAccessibilityServicePreferenceFragment private static final int DIALOG_ID_ENABLE_WARNING = 1; private static final int DIALOG_ID_DISABLE_WARNING = 2; + public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1; + + private LockPatternUtils mLockPatternUtils; + private final SettingsContentObserver mSettingsContentObserver = new SettingsContentObserver(new Handler()) { @Override @@ -57,7 +66,7 @@ public class ToggleAccessibilityServicePreferenceFragment String settingValue = Settings.Secure.getString(getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); final boolean enabled = settingValue.contains(mComponentName.flattenToString()); - mToggleSwitch.setCheckedInternal(enabled); + mSwitchBar.setCheckedInternal(enabled); } }; @@ -66,6 +75,12 @@ public class ToggleAccessibilityServicePreferenceFragment private int mShownDialogId; @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mLockPatternUtils = new LockPatternUtils(getActivity()); + } + + @Override public void onResume() { mSettingsContentObserver.register(getContentResolver()); super.onResume(); @@ -154,41 +169,42 @@ public class ToggleAccessibilityServicePreferenceFragment public Dialog onCreateDialog(int dialogId) { switch (dialogId) { case DIALOG_ID_ENABLE_WARNING: { - mShownDialogId = DIALOG_ID_ENABLE_WARNING; - AccessibilityServiceInfo info = getAccessibilityServiceInfo(); - if (info == null) { - return null; + mShownDialogId = DIALOG_ID_ENABLE_WARNING; + AccessibilityServiceInfo info = getAccessibilityServiceInfo(); + if (info == null) { + return null; + } + AlertDialog ad = new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.enable_service_title, + info.getResolveInfo().loadLabel(getPackageManager()))) + .setView(createEnableDialogContentView(info)) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + ad.create(); + ad.getButton(AlertDialog.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + return ad; } - return new AlertDialog.Builder(getActivity()) - .setTitle(getString(R.string.enable_service_title, - info.getResolveInfo().loadLabel(getPackageManager()))) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setView(createEnableDialogContentView(info)) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - } case DIALOG_ID_DISABLE_WARNING: { - mShownDialogId = DIALOG_ID_DISABLE_WARNING; - AccessibilityServiceInfo info = getAccessibilityServiceInfo(); - if (info == null) { - return null; + mShownDialogId = DIALOG_ID_DISABLE_WARNING; + AccessibilityServiceInfo info = getAccessibilityServiceInfo(); + if (info == null) { + return null; + } + return new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.disable_service_title, + info.getResolveInfo().loadLabel(getPackageManager()))) + .setMessage(getString(R.string.disable_service_message, + info.getResolveInfo().loadLabel(getPackageManager()))) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); } - return new AlertDialog.Builder(getActivity()) - .setTitle(getString(R.string.disable_service_title, - info.getResolveInfo().loadLabel(getPackageManager()))) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setMessage(getString(R.string.disable_service_message, - info.getResolveInfo().loadLabel(getPackageManager()))) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - } default: { - throw new IllegalArgumentException(); - } + throw new IllegalArgumentException(); + } } } @@ -199,6 +215,17 @@ public class ToggleAccessibilityServicePreferenceFragment View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content, null); + TextView encryptionWarningView = (TextView) content.findViewById( + R.id.encryption_warning); + if (LockPatternUtils.isDeviceEncrypted()) { + String text = getString(R.string.enable_service_encryption_warning, + info.getResolveInfo().loadLabel(getPackageManager())); + encryptionWarningView.setText(text); + encryptionWarningView.setVisibility(View.VISIBLE); + } else { + encryptionWarningView.setVisibility(View.GONE); + } + TextView capabilitiesHeaderView = (TextView) content.findViewById( R.id.capabilities_header); capabilitiesHeaderView.setText(getString(R.string.capabilities_list_title, @@ -256,38 +283,84 @@ public class ToggleAccessibilityServicePreferenceFragment } @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) { + if (resultCode == Activity.RESULT_OK) { + handleConfirmServiceEnabled(true); + // The user confirmed that they accept weaker encryption when + // enabling the accessibility service, so change encryption. + // Since we came here asynchronously, check encryption again. + if (LockPatternUtils.isDeviceEncrypted()) { + mLockPatternUtils.clearEncryptionPassword(); + Settings.Global.putInt(getContentResolver(), + Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0); + } + } else { + handleConfirmServiceEnabled(false); + } + } + } + + @Override public void onClick(DialogInterface dialog, int which) { final boolean checked; switch (which) { case DialogInterface.BUTTON_POSITIVE: - checked = (mShownDialogId == DIALOG_ID_ENABLE_WARNING); - mToggleSwitch.setCheckedInternal(checked); - getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); - onPreferenceToggled(mPreferenceKey, checked); + if (mShownDialogId == DIALOG_ID_ENABLE_WARNING) { + if (LockPatternUtils.isDeviceEncrypted()) { + String title = createConfirmCredentialReasonMessage(); + Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); + startActivityForResult(intent, + ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); + } else { + handleConfirmServiceEnabled(true); + } + } else { + handleConfirmServiceEnabled(false); + } break; case DialogInterface.BUTTON_NEGATIVE: checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING); - mToggleSwitch.setCheckedInternal(checked); - getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); - onPreferenceToggled(mPreferenceKey, checked); + handleConfirmServiceEnabled(checked); break; default: throw new IllegalArgumentException(); } } + private void handleConfirmServiceEnabled(boolean confirmed) { + mSwitchBar.setCheckedInternal(confirmed); + getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); + onPreferenceToggled(mPreferenceKey, confirmed); + } + + private String createConfirmCredentialReasonMessage() { + int resId = R.string.enable_service_password_reason; + switch (mLockPatternUtils.getKeyguardStoredPasswordQuality()) { + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: { + resId = R.string.enable_service_pattern_reason; + } break; + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: { + resId = R.string.enable_service_pin_reason; + } break; + } + return getString(resId, getAccessibilityServiceInfo().getResolveInfo() + .loadLabel(getPackageManager())); + } + @Override - protected void onInstallActionBarToggleSwitch() { - super.onInstallActionBarToggleSwitch(); + protected void onInstallSwitchBarToggleSwitch() { + super.onInstallSwitchBarToggleSwitch(); mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { if (checked) { - toggleSwitch.setCheckedInternal(false); + mSwitchBar.setCheckedInternal(false); getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, false); showDialog(DIALOG_ID_ENABLE_WARNING); } else { - toggleSwitch.setCheckedInternal(true); + mSwitchBar.setCheckedInternal(true); getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, true); showDialog(DIALOG_ID_DISABLE_WARNING); } diff --git a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java new file mode 100644 index 0000000..1f7ecf7 --- /dev/null +++ b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 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.accessibility; + +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.provider.Settings; +import android.view.View; +import android.view.accessibility.AccessibilityManager; + +import android.widget.Switch; +import com.android.settings.R; +import com.android.settings.widget.SwitchBar; + +public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceFragment + implements Preference.OnPreferenceChangeListener, SwitchBar.OnSwitchChangeListener { + private static final String ENABLED = Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED; + private static final String TYPE = Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER; + private static final int DEFAULT_TYPE = AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY; + + private ListPreference mType; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.accessibility_daltonizer_settings); + + mType = (ListPreference) findPreference("type"); + + initPreferences(); + } + + @Override + protected void onPreferenceToggled(String preferenceKey, boolean enabled) { + Settings.Secure.putInt(getContentResolver(), ENABLED, enabled ? 1 : 0); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mType) { + Settings.Secure.putInt(getContentResolver(), TYPE, Integer.parseInt((String) newValue)); + preference.setSummary("%s"); + } + + return true; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setTitle(getString(R.string.accessibility_display_daltonizer_preference_title)); + } + + @Override + protected void onInstallSwitchBarToggleSwitch() { + super.onInstallSwitchBarToggleSwitch(); + + mSwitchBar.setCheckedInternal( + Settings.Secure.getInt(getContentResolver(), ENABLED, 0) == 1); + mSwitchBar.addOnSwitchChangeListener(this); + } + + @Override + protected void onRemoveSwitchBarToggleSwitch() { + super.onRemoveSwitchBarToggleSwitch(); + mSwitchBar.removeOnSwitchChangeListener(this); + } + + private void initPreferences() { + final String value = Integer.toString( + Settings.Secure.getInt(getContentResolver(), TYPE, DEFAULT_TYPE)); + mType.setValue(value); + mType.setOnPreferenceChangeListener(this); + final int index = mType.findIndexOfValue(value); + if (index < 0) { + // We're using a mode controlled by developer preferences. + mType.setSummary(getString(R.string.daltonizer_type_overridden, + getString(R.string.simulate_color_space))); + } + } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + onPreferenceToggled(mPreferenceKey, isChecked); + } +} diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index 2fbbabd..b23035b 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -16,16 +16,12 @@ package com.android.settings.accessibility; -import android.app.ActionBar; -import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; -import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -35,11 +31,15 @@ import android.view.accessibility.AccessibilityManager; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.ToggleSwitch; public abstract class ToggleFeaturePreferenceFragment extends SettingsPreferenceFragment { + protected SwitchBar mSwitchBar; protected ToggleSwitch mToggleSwitch; protected String mPreferenceKey; @@ -55,10 +55,10 @@ public abstract class ToggleFeaturePreferenceFragment getActivity()); setPreferenceScreen(preferenceScreen); mSummaryPreference = new Preference(getActivity()) { - @Override + @Override protected void onBindView(View view) { super.onBindView(view); - TextView summaryView = (TextView) view.findViewById(R.id.summary); + final TextView summaryView = (TextView) view.findViewById(android.R.id.summary); summaryView.setText(getSummary()); sendAccessibilityEvent(summaryView); } @@ -86,18 +86,26 @@ public abstract class ToggleFeaturePreferenceFragment @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - onInstallActionBarToggleSwitch(); + + SettingsActivity activity = (SettingsActivity) getActivity(); + mSwitchBar = activity.getSwitchBar(); + mToggleSwitch = mSwitchBar.getSwitch(); + onProcessArguments(getArguments()); - // Set a transparent drawable to prevent use of the default one. - getListView().setSelector(new ColorDrawable(Color.TRANSPARENT)); - getListView().setDivider(null); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + installActionBarToggleSwitch(); } @Override public void onDestroyView() { - getActivity().getActionBar().setCustomView(null); - mToggleSwitch.setOnBeforeCheckedChangeListener(null); super.onDestroyView(); + + removeActionBarToggleSwitch(); } protected abstract void onPreferenceToggled(String preferenceKey, boolean enabled); @@ -110,38 +118,60 @@ public abstract class ToggleFeaturePreferenceFragment menuItem.setIntent(mSettingsIntent); } - protected void onInstallActionBarToggleSwitch() { - mToggleSwitch = createAndAddActionBarToggleSwitch(getActivity()); + protected void onInstallSwitchBarToggleSwitch() { + // Implement this to set a checked listener. + } + + protected void onRemoveSwitchBarToggleSwitch() { + // Implement this to reset a checked listener. + } + + private void installActionBarToggleSwitch() { + mSwitchBar.show(); + onInstallSwitchBarToggleSwitch(); + } + + private void removeActionBarToggleSwitch() { + mToggleSwitch.setOnBeforeCheckedChangeListener(null); + onRemoveSwitchBarToggleSwitch(); + mSwitchBar.hide(); } - private ToggleSwitch createAndAddActionBarToggleSwitch(Activity activity) { - ToggleSwitch toggleSwitch = new ToggleSwitch(activity); - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - toggleSwitch.setPaddingRelative(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.END)); - return toggleSwitch; + public void setTitle(String title) { + getActivity().setTitle(title); } protected void onProcessArguments(Bundle arguments) { + if (arguments == null) { + getPreferenceScreen().removePreference(mSummaryPreference); + return; + } + // Key. mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY); + // Enabled. - final boolean enabled = arguments.getBoolean(AccessibilitySettings.EXTRA_CHECKED); - mToggleSwitch.setCheckedInternal(enabled); + if (arguments.containsKey(AccessibilitySettings.EXTRA_CHECKED)) { + final boolean enabled = arguments.getBoolean(AccessibilitySettings.EXTRA_CHECKED); + mSwitchBar.setCheckedInternal(enabled); + } + // Title. - PreferenceActivity activity = (PreferenceActivity) getActivity(); - if (!activity.onIsMultiPane() || activity.onIsHidingHeaders()) { - String title = arguments.getString(AccessibilitySettings.EXTRA_TITLE); - getActivity().setTitle(title); + if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) { + setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE)); } + // Summary. - CharSequence summary = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY); - mSummaryPreference.setSummary(summary); + if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) { + final CharSequence summary = arguments.getCharSequence( + AccessibilitySettings.EXTRA_SUMMARY); + mSummaryPreference.setSummary(summary); + + // Set a transparent drawable to prevent use of the default one. + getListView().setSelector(new ColorDrawable(Color.TRANSPARENT)); + getListView().setDivider(null); + } else { + getPreferenceScreen().removePreference(mSummaryPreference); + } } } diff --git a/src/com/android/settings/accessibility/ToggleGlobalGesturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleGlobalGesturePreferenceFragment.java index f4ac2cc..7fb1625 100644 --- a/src/com/android/settings/accessibility/ToggleGlobalGesturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleGlobalGesturePreferenceFragment.java @@ -18,7 +18,8 @@ package com.android.settings.accessibility; import android.provider.Settings; -import com.android.settings.accessibility.ToggleSwitch.OnBeforeCheckedChangeListener; +import com.android.settings.widget.ToggleSwitch; +import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener; public class ToggleGlobalGesturePreferenceFragment extends ToggleFeaturePreferenceFragment { @@ -29,12 +30,12 @@ public class ToggleGlobalGesturePreferenceFragment } @Override - protected void onInstallActionBarToggleSwitch() { - super.onInstallActionBarToggleSwitch(); + protected void onInstallSwitchBarToggleSwitch() { + super.onInstallSwitchBarToggleSwitch(); mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { - toggleSwitch.setCheckedInternal(checked); + mSwitchBar.setCheckedInternal(checked); getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); onPreferenceToggled(mPreferenceKey, checked); return false; diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 27d07d2..4650c06 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -18,7 +18,8 @@ package com.android.settings.accessibility; import android.provider.Settings; -import com.android.settings.accessibility.ToggleSwitch.OnBeforeCheckedChangeListener; +import com.android.settings.widget.ToggleSwitch; +import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener; public class ToggleScreenMagnificationPreferenceFragment extends ToggleFeaturePreferenceFragment { @@ -29,12 +30,12 @@ public class ToggleScreenMagnificationPreferenceFragment } @Override - protected void onInstallActionBarToggleSwitch() { - super.onInstallActionBarToggleSwitch(); + protected void onInstallSwitchBarToggleSwitch() { + super.onInstallSwitchBarToggleSwitch(); mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { - toggleSwitch.setCheckedInternal(checked); + mSwitchBar.setCheckedInternal(checked); getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); onPreferenceToggled(mPreferenceKey, checked); return false; diff --git a/src/com/android/settings/accounts/AccountPreferenceBase.java b/src/com/android/settings/accounts/AccountPreferenceBase.java index 2759a8f..bb60871 100644 --- a/src/com/android/settings/accounts/AccountPreferenceBase.java +++ b/src/com/android/settings/accounts/AccountPreferenceBase.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,50 +17,63 @@ package com.android.settings.accounts; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import com.android.settings.SettingsPreferenceFragment; -import com.google.android.collect.Maps; - -import android.accounts.Account; -import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; -import android.accounts.OnAccountsUpdateListener; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; -import android.content.SyncAdapterType; import android.content.SyncStatusObserver; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; -import android.preference.PreferenceActivity; +import android.os.UserHandle; +import android.os.UserManager; import android.preference.PreferenceScreen; import android.text.format.DateFormat; import android.util.Log; +import android.view.ContextThemeWrapper; + +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; + +import java.util.ArrayList; +import java.util.Date; class AccountPreferenceBase extends SettingsPreferenceFragment - implements OnAccountsUpdateListener { + implements AuthenticatorHelper.OnAccountsUpdateListener { protected static final String TAG = "AccountSettings"; + public static final String AUTHORITIES_FILTER_KEY = "authorities"; public static final String ACCOUNT_TYPES_FILTER_KEY = "account_types"; + private final Handler mHandler = new Handler(); + + private UserManager mUm; private Object mStatusChangeListenerHandle; - private HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = null; - private AuthenticatorHelper mAuthenticatorHelper = new AuthenticatorHelper(); + protected AuthenticatorHelper mAuthenticatorHelper; + protected UserHandle mUserHandle; + private java.text.DateFormat mDateFormat; private java.text.DateFormat mTimeFormat; + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); + final Activity activity = getActivity(); + mUserHandle = Utils.getSecureTargetUser(activity.getActivityToken(), mUm, getArguments(), + activity.getIntent().getExtras()); + mAuthenticatorHelper = new AuthenticatorHelper(activity, mUserHandle, mUm, this); + } + /** * Overload to handle account updates. */ - public void onAccountsUpdated(Account[] accounts) { + @Override + public void onAccountsUpdate(UserHandle userHandle) { } @@ -115,24 +129,7 @@ class AccountPreferenceBase extends SettingsPreferenceFragment }; public ArrayList<String> getAuthoritiesForAccountType(String type) { - if (mAccountTypeToAuthorities == null) { - mAccountTypeToAuthorities = Maps.newHashMap(); - SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); - for (int i = 0, n = syncAdapters.length; i < n; i++) { - final SyncAdapterType sa = syncAdapters[i]; - ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType); - if (authorities == null) { - authorities = new ArrayList<String>(); - mAccountTypeToAuthorities.put(sa.accountType, authorities); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "added authority " + sa.authority + " to accountType " - + sa.accountType); - } - authorities.add(sa.authority); - } - } - return mAccountTypeToAuthorities.get(type); + return mAuthenticatorHelper.getAuthoritiesForAccountType(type); } /** @@ -148,8 +145,19 @@ class AccountPreferenceBase extends SettingsPreferenceFragment try { desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); if (desc != null && desc.accountPreferencesId != 0) { - Context authContext = getActivity().createPackageContext(desc.packageName, 0); - prefs = getPreferenceManager().inflateFromResource(authContext, + // Load the context of the target package, then apply the + // base Settings theme (no references to local resources) + // and create a context theme wrapper so that we get the + // correct text colors. Control colors will still be wrong, + // but there's not much we can do about it since we can't + // reference local color resources. + final Context targetCtx = getActivity().createPackageContextAsUser( + desc.packageName, 0, mUserHandle); + final Theme baseTheme = getResources().newTheme(); + baseTheme.applyStyle(com.android.settings.R.style.Theme_SettingsBase, true); + final Context themedCtx = new ContextThemeWrapper(targetCtx, 0); + themedCtx.getTheme().setTo(baseTheme); + prefs = getPreferenceManager().inflateFromResource(themedCtx, desc.accountPreferencesId, parent); } } catch (PackageManager.NameNotFoundException e) { diff --git a/src/com/android/settings/accounts/AccountSettings.java b/src/com/android/settings/accounts/AccountSettings.java new file mode 100644 index 0000000..8a183da --- /dev/null +++ b/src/com/android/settings/accounts/AccountSettings.java @@ -0,0 +1,588 @@ +/* + * Copyright (C) 2014 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.accounts; + + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.Process; +import android.util.Log; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceGroup; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static android.content.Intent.EXTRA_USER; +import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS; +import static android.provider.Settings.EXTRA_AUTHORITIES; + +/** + * Settings screen for the account types on the device. + * This shows all account types available for personal and work profiles. + * + * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for + * which the action needs to be performed is different to the one the Settings App will run in. + */ +public class AccountSettings extends SettingsPreferenceFragment + implements AuthenticatorHelper.OnAccountsUpdateListener, + OnPreferenceClickListener { + public static final String TAG = "AccountSettings"; + + private static final String KEY_ACCOUNT = "account"; + + private static final String ADD_ACCOUNT_ACTION = "android.settings.ADD_ACCOUNT_SETTINGS"; + private static final String TAG_CONFIRM_AUTO_SYNC_CHANGE = "confirmAutoSyncChange"; + + private static final int ORDER_LAST = 1001; + private static final int ORDER_NEXT_TO_LAST = 1000; + + private UserManager mUm; + private SparseArray<ProfileData> mProfiles = new SparseArray<ProfileData>(); + private ManagedProfileBroadcastReceiver mManagedProfileBroadcastReceiver + = new ManagedProfileBroadcastReceiver(); + private Preference mProfileNotAvailablePreference; + private String[] mAuthorities; + private int mAuthoritiesCount = 0; + + /** + * Holds data related to the accounts belonging to one profile. + */ + private static class ProfileData { + /** + * The preference that displays the accounts. + */ + public PreferenceGroup preferenceGroup; + /** + * The preference that displays the add account button. + */ + public Preference addAccountPreference; + /** + * The preference that displays the button to remove the managed profile + */ + public Preference removeWorkProfilePreference; + /** + * The {@link AuthenticatorHelper} that holds accounts data for this profile. + */ + public AuthenticatorHelper authenticatorHelper; + /** + * The {@link UserInfo} of the profile. + */ + public UserInfo userInfo; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); + mProfileNotAvailablePreference = new Preference(getActivity()); + mAuthorities = getActivity().getIntent().getStringArrayExtra(EXTRA_AUTHORITIES); + if (mAuthorities != null) { + mAuthoritiesCount = mAuthorities.length; + } + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.account_settings, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + final UserHandle currentProfile = Process.myUserHandle(); + if (mProfiles.size() == 1) { + menu.findItem(R.id.account_settings_menu_auto_sync) + .setVisible(true) + .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile)) + .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( + currentProfile.getIdentifier())); + menu.findItem(R.id.account_settings_menu_auto_sync_personal).setVisible(false); + menu.findItem(R.id.account_settings_menu_auto_sync_work).setVisible(false); + } else if (mProfiles.size() > 1) { + // We assume there's only one managed profile, otherwise UI needs to change + final UserHandle managedProfile = mProfiles.valueAt(1).userInfo.getUserHandle(); + + menu.findItem(R.id.account_settings_menu_auto_sync_personal) + .setVisible(true) + .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile)) + .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( + currentProfile.getIdentifier())); + menu.findItem(R.id.account_settings_menu_auto_sync_work) + .setVisible(true) + .setOnMenuItemClickListener(new MasterSyncStateClickListener(managedProfile)) + .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser( + managedProfile.getIdentifier())); + menu.findItem(R.id.account_settings_menu_auto_sync).setVisible(false); + } else { + Log.w(TAG, "Method onPrepareOptionsMenu called before mProfiles was initialized"); + } + } + + @Override + public void onResume() { + super.onResume(); + updateUi(); + mManagedProfileBroadcastReceiver.register(getActivity()); + listenToAccountUpdates(); + } + + @Override + public void onPause() { + super.onPause(); + stopListeningToAccountUpdates(); + mManagedProfileBroadcastReceiver.unregister(getActivity()); + cleanUpPreferences(); + } + + @Override + public void onAccountsUpdate(UserHandle userHandle) { + final ProfileData profileData = mProfiles.get(userHandle.getIdentifier()); + if (profileData != null) { + updateAccountTypes(profileData); + } else { + Log.w(TAG, "Missing Settings screen for: " + userHandle.getIdentifier()); + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + // Check the preference + final int count = mProfiles.size(); + for (int i = 0; i < count; i++) { + ProfileData profileData = mProfiles.valueAt(i); + if (preference == profileData.addAccountPreference) { + Intent intent = new Intent(ADD_ACCOUNT_ACTION); + intent.putExtra(EXTRA_USER, profileData.userInfo.getUserHandle()); + intent.putExtra(EXTRA_AUTHORITIES, mAuthorities); + startActivity(intent); + return true; + } + if (preference == profileData.removeWorkProfilePreference) { + final int userId = profileData.userInfo.id; + Utils.createRemoveConfirmationDialog(getActivity(), userId, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mUm.removeUser(userId); + } + } + ).show(); + return true; + } + } + return false; + } + + void updateUi() { + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.account_settings); + + if (Utils.isManagedProfile(mUm)) { + // This should not happen + Log.e(TAG, "We should not be showing settings for a managed profile"); + finish(); + return; + } + + final PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(KEY_ACCOUNT); + if(mUm.isLinkedUser()) { + // Restricted user or similar + UserInfo userInfo = mUm.getUserInfo(UserHandle.myUserId()); + updateProfileUi(userInfo, false /* no category needed */, preferenceScreen); + } else { + List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); + final int profilesCount = profiles.size(); + final boolean addCategory = profilesCount > 1; + for (int i = 0; i < profilesCount; i++) { + updateProfileUi(profiles.get(i), addCategory, preferenceScreen); + } + } + + // Add all preferences, starting with one for the primary profile. + // Note that we're relying on the ordering given by the SparseArray keys, and on the + // value of UserHandle.USER_OWNER being smaller than all the rest. + final int profilesCount = mProfiles.size(); + for (int i = 0; i < profilesCount; i++) { + ProfileData profileData = mProfiles.valueAt(i); + if (!profileData.preferenceGroup.equals(preferenceScreen)) { + preferenceScreen.addPreference(profileData.preferenceGroup); + } + updateAccountTypes(profileData); + } + } + + private void updateProfileUi(final UserInfo userInfo, boolean addCategory, + PreferenceScreen parent) { + final Context context = getActivity(); + final ProfileData profileData = new ProfileData(); + profileData.userInfo = userInfo; + if (addCategory) { + profileData.preferenceGroup = new PreferenceCategory(context); + profileData.preferenceGroup.setTitle(userInfo.isManagedProfile() + ? R.string.category_work : R.string.category_personal); + parent.addPreference(profileData.preferenceGroup); + } else { + profileData.preferenceGroup = parent; + } + if (userInfo.isEnabled()) { + profileData.authenticatorHelper = new AuthenticatorHelper(context, + userInfo.getUserHandle(), mUm, this); + if (!mUm.hasUserRestriction(DISALLOW_MODIFY_ACCOUNTS, userInfo.getUserHandle())) { + profileData.addAccountPreference = newAddAccountPreference(context); + } + } + if (userInfo.isManagedProfile()) { + profileData.removeWorkProfilePreference = newRemoveWorkProfilePreference(context); + } + mProfiles.put(userInfo.id, profileData); + } + + private Preference newAddAccountPreference(Context context) { + Preference preference = new Preference(context); + preference.setTitle(R.string.add_account_label); + preference.setIcon(R.drawable.ic_menu_add_dark); + preference.setOnPreferenceClickListener(this); + preference.setOrder(ORDER_NEXT_TO_LAST); + return preference; + } + + private Preference newRemoveWorkProfilePreference(Context context) { + Preference preference = new Preference(context); + preference.setTitle(R.string.remove_managed_profile_label); + preference.setIcon(R.drawable.ic_menu_delete); + preference.setOnPreferenceClickListener(this); + preference.setOrder(ORDER_LAST); + return preference; + } + + private void cleanUpPreferences() { + PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.removeAll(); + } + mProfiles.clear(); + } + + private void listenToAccountUpdates() { + final int count = mProfiles.size(); + for (int i = 0; i < count; i++) { + AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; + if (authenticatorHelper != null) { + authenticatorHelper.listenToAccountUpdates(); + } + } + } + + private void stopListeningToAccountUpdates() { + final int count = mProfiles.size(); + for (int i = 0; i < count; i++) { + AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper; + if (authenticatorHelper != null) { + authenticatorHelper.stopListeningToAccountUpdates(); + } + } + } + + private void updateAccountTypes(ProfileData profileData) { + profileData.preferenceGroup.removeAll(); + if (profileData.userInfo.isEnabled()) { + final ArrayList<AccountPreference> preferences = getAccountTypePreferences( + profileData.authenticatorHelper, profileData.userInfo.getUserHandle()); + final int count = preferences.size(); + for (int i = 0; i < count; i++) { + profileData.preferenceGroup.addPreference(preferences.get(i)); + } + if (profileData.addAccountPreference != null) { + profileData.preferenceGroup.addPreference(profileData.addAccountPreference); + } + } else { + // Put a label instead of the accounts list + mProfileNotAvailablePreference.setEnabled(false); + mProfileNotAvailablePreference.setIcon(R.drawable.empty_icon); + mProfileNotAvailablePreference.setTitle(null); + mProfileNotAvailablePreference.setSummary( + R.string.managed_profile_not_available_label); + profileData.preferenceGroup.addPreference(mProfileNotAvailablePreference); + } + if (profileData.removeWorkProfilePreference != null) { + profileData.preferenceGroup.addPreference(profileData.removeWorkProfilePreference); + } + } + + private ArrayList<AccountPreference> getAccountTypePreferences(AuthenticatorHelper helper, + UserHandle userHandle) { + final String[] accountTypes = helper.getEnabledAccountTypes(); + final ArrayList<AccountPreference> accountTypePreferences = + new ArrayList<AccountPreference>(accountTypes.length); + + for (int i = 0; i < accountTypes.length; i++) { + final String accountType = accountTypes[i]; + // Skip showing any account that does not have any of the requested authorities + if (!accountTypeHasAnyRequestedAuthorities(helper, accountType)) { + continue; + } + final CharSequence label = helper.getLabelForType(getActivity(), accountType); + if (label == null) { + continue; + } + + final Account[] accounts = AccountManager.get(getActivity()) + .getAccountsByTypeAsUser(accountType, userHandle); + final boolean skipToAccount = accounts.length == 1 + && !helper.hasAccountPreferences(accountType); + + if (skipToAccount) { + final Bundle fragmentArguments = new Bundle(); + fragmentArguments.putParcelable(AccountSyncSettings.ACCOUNT_KEY, + accounts[0]); + fragmentArguments.putParcelable(EXTRA_USER, userHandle); + + accountTypePreferences.add(new AccountPreference(getActivity(), label, + AccountSyncSettings.class.getName(), fragmentArguments, + helper.getDrawableForType(getActivity(), accountType))); + } else { + final Bundle fragmentArguments = new Bundle(); + fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType); + fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_LABEL, + label.toString()); + fragmentArguments.putParcelable(EXTRA_USER, userHandle); + + accountTypePreferences.add(new AccountPreference(getActivity(), label, + ManageAccountsSettings.class.getName(), fragmentArguments, + helper.getDrawableForType(getActivity(), accountType))); + } + helper.preloadDrawableForType(getActivity(), accountType); + } + // Sort by label + Collections.sort(accountTypePreferences, new Comparator<AccountPreference>() { + @Override + public int compare(AccountPreference t1, AccountPreference t2) { + return t1.mTitle.toString().compareTo(t2.mTitle.toString()); + } + }); + return accountTypePreferences; + } + + private boolean accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper, + String accountType) { + if (mAuthoritiesCount == 0) { + // No authorities required + return true; + } + final ArrayList<String> authoritiesForType = helper.getAuthoritiesForAccountType( + accountType); + if (authoritiesForType == null) { + Log.d(TAG, "No sync authorities for account type: " + accountType); + return false; + } + for (int j = 0; j < mAuthoritiesCount; j++) { + if (authoritiesForType.contains(mAuthorities[j])) { + return true; + } + } + return false; + } + + private class AccountPreference extends Preference implements OnPreferenceClickListener { + /** + * Title of the tile that is shown to the user. + * @attr ref android.R.styleable#PreferenceHeader_title + */ + private final CharSequence mTitle; + + /** + * Full class name of the fragment to display when this tile is + * selected. + * @attr ref android.R.styleable#PreferenceHeader_fragment + */ + private final String mFragment; + + /** + * Optional arguments to supply to the fragment when it is + * instantiated. + */ + private final Bundle mFragmentArguments; + + public AccountPreference(Context context, CharSequence title, String fragment, + Bundle fragmentArguments, Drawable icon) { + super(context); + mTitle = title; + mFragment = fragment; + mFragmentArguments = fragmentArguments; + setWidgetLayoutResource(R.layout.account_type_preference); + + setTitle(title); + setIcon(icon); + + setOnPreferenceClickListener(this); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mFragment != null) { + Utils.startWithFragment( + getContext(), mFragment, mFragmentArguments, null, 0, 0, mTitle); + return true; + } + return false; + } + } + + private class ManagedProfileBroadcastReceiver extends BroadcastReceiver { + private boolean listeningToManagedProfileEvents; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_REMOVED) + || intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { + Log.v(TAG, "Received broadcast: " + intent.getAction()); + // Clean old state + stopListeningToAccountUpdates(); + cleanUpPreferences(); + // Build new state + updateUi(); + listenToAccountUpdates(); + // Force the menu to update. Note that #onPrepareOptionsMenu uses data built by + // #updateUi so we must call this later + getActivity().invalidateOptionsMenu(); + return; + } + Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); + } + + public void register(Context context) { + if (!listeningToManagedProfileEvents) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); + intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); + context.registerReceiver(this, intentFilter); + listeningToManagedProfileEvents = true; + } + } + + public void unregister(Context context) { + if (listeningToManagedProfileEvents) { + context.unregisterReceiver(this); + listeningToManagedProfileEvents = false; + } + } + } + + private class MasterSyncStateClickListener implements MenuItem.OnMenuItemClickListener { + private final UserHandle mUserHandle; + + public MasterSyncStateClickListener(UserHandle userHandle) { + mUserHandle = userHandle; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (ActivityManager.isUserAMonkey()) { + Log.d(TAG, "ignoring monkey's attempt to flip sync state"); + } else { + ConfirmAutoSyncChangeFragment.show(AccountSettings.this, !item.isChecked(), + mUserHandle); + } + return true; + } + } + + /** + * Dialog to inform user about changing auto-sync setting + */ + public static class ConfirmAutoSyncChangeFragment extends DialogFragment { + private static final String SAVE_ENABLING = "enabling"; + private boolean mEnabling; + private UserHandle mUserHandle; + + public static void show(AccountSettings parent, boolean enabling, UserHandle userHandle) { + if (!parent.isAdded()) return; + + final ConfirmAutoSyncChangeFragment dialog = new ConfirmAutoSyncChangeFragment(); + dialog.mEnabling = enabling; + dialog.mUserHandle = userHandle; + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_AUTO_SYNC_CHANGE); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + if (savedInstanceState != null) { + mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING); + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + if (!mEnabling) { + builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title); + builder.setMessage(R.string.data_usage_auto_sync_off_dialog); + } else { + builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title); + builder.setMessage(R.string.data_usage_auto_sync_on_dialog); + } + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ContentResolver.setMasterSyncAutomaticallyAsUser(mEnabling, + mUserHandle.getIdentifier()); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + return builder.create(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(SAVE_ENABLING, mEnabling); + } + } + // TODO Implement a {@link SearchIndexProvider} to allow Indexing and Search of account types + // See http://b/15403806 +} diff --git a/src/com/android/settings/accounts/AccountSyncSettings.java b/src/com/android/settings/accounts/AccountSyncSettings.java index c14dbbe..f0896c9 100644 --- a/src/com/android/settings/accounts/AccountSyncSettings.java +++ b/src/com/android/settings/accounts/AccountSyncSettings.java @@ -16,13 +16,14 @@ package com.android.settings.accounts; +import com.google.android.collect.Lists; + import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; -import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; @@ -34,6 +35,7 @@ import android.content.SyncStatusInfo; import android.content.pm.ProviderInfo; import android.net.ConnectivityManager; import android.os.Bundle; +import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceScreen; @@ -51,14 +53,11 @@ import android.widget.TextView; import com.android.settings.R; import com.android.settings.Utils; -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; public class AccountSyncSettings extends AccountPreferenceBase { @@ -75,9 +74,6 @@ public class AccountSyncSettings extends AccountPreferenceBase { private ImageView mProviderIcon; private TextView mErrorInfoView; private Account mAccount; - // List of all accounts, updated when accounts are added/removed - // We need to re-scan the accounts on sync events, in case sync state changes. - private Account[] mAccounts; private ArrayList<SyncStateCheckBoxPreference> mCheckBoxes = new ArrayList<SyncStateCheckBoxPreference>(); private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList(); @@ -94,8 +90,10 @@ public class AccountSyncSettings extends AccountPreferenceBase { new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { + // TODO: We need an API to remove an account from a different user. + // See: http://b/15466880 AccountManager.get(AccountSyncSettings.this.getActivity()) - .removeAccount(mAccount, + .removeAccountAsUser(mAccount, new AccountManagerCallback<Boolean>() { @Override public void run(AccountManagerFuture<Boolean> future) { @@ -122,7 +120,7 @@ public class AccountSyncSettings extends AccountPreferenceBase { finish(); } } - }, null); + }, null, mUserHandle); } }) .create(); @@ -180,23 +178,27 @@ public class AccountSyncSettings extends AccountPreferenceBase { Bundle arguments = getArguments(); if (arguments == null) { Log.e(TAG, "No arguments provided when starting intent. ACCOUNT_KEY needed."); + finish(); 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); - mProviderId.setText(mAccount.type); + if (!accountExists(mAccount)) { + Log.e(TAG, "Account provided does not exist: " + mAccount); + finish(); + return; } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Got account: " + mAccount); + } + mUserId.setText(mAccount.name); + mProviderId.setText(mAccount.type); } @Override public void onResume() { - final Activity activity = getActivity(); - AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, false); + mAuthenticatorHelper.listenToAccountUpdates(); updateAuthDescriptions(); - onAccountsUpdated(AccountManager.get(activity).getAccounts()); + onAccountsUpdate(UserHandle.getCallingUserHandle()); super.onResume(); } @@ -204,14 +206,15 @@ public class AccountSyncSettings extends AccountPreferenceBase { @Override public void onPause() { super.onPause(); - AccountManager.get(getActivity()).removeOnAccountsUpdatedListener(this); + mAuthenticatorHelper.stopListeningToAccountUpdates(); } private void addSyncStateCheckBox(Account account, String authority) { SyncStateCheckBoxPreference item = new SyncStateCheckBoxPreference(getActivity(), account, authority); item.setPersistent(false); - final ProviderInfo providerInfo = getPackageManager().resolveContentProvider(authority, 0); + final ProviderInfo providerInfo = getPackageManager().resolveContentProviderAsUser( + authority, 0, mUserHandle.getIdentifier()); if (providerInfo == null) { return; } @@ -235,12 +238,11 @@ public class AccountSyncSettings extends AccountPreferenceBase { MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0, getString(R.string.sync_menu_sync_cancel)) .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel); - final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); - if (!um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) { + if (!um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS, mUserHandle)) { MenuItem removeAccount = menu.add(0, MENU_REMOVE_ACCOUNT_ID, 0, getString(R.string.remove_account_label)) - .setIcon(R.drawable.ic_menu_delete_holo_dark); + .setIcon(R.drawable.ic_menu_delete); removeAccount.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @@ -255,7 +257,9 @@ public class AccountSyncSettings extends AccountPreferenceBase { @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - boolean syncActive = ContentResolver.getCurrentSync() != null; + // Note that this also counts accounts that are not currently displayed + boolean syncActive = ContentResolver.getCurrentSyncsAsUser( + mUserHandle.getIdentifier()).isEmpty(); menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive); menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive); } @@ -282,7 +286,9 @@ public class AccountSyncSettings extends AccountPreferenceBase { SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) preference; String authority = syncPref.getAuthority(); Account account = syncPref.getAccount(); - boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority); + final int userId = mUserHandle.getIdentifier(); + boolean syncAutomatically = ContentResolver.getSyncAutomaticallyAsUser(account, + authority, userId); if (syncPref.isOneTimeSyncMode()) { requestOrCancelSync(account, authority, true); } else { @@ -290,11 +296,11 @@ public class AccountSyncSettings extends AccountPreferenceBase { boolean oldSyncState = syncAutomatically; if (syncOn != oldSyncState) { // if we're enabling sync, this will request a sync as well - ContentResolver.setSyncAutomatically(account, authority, syncOn); + ContentResolver.setSyncAutomaticallyAsUser(account, authority, syncOn, userId); // if the master sync switch is off, the request above will // get dropped. when the user clicks on this toggle, // we want to force the sync, however. - if (!ContentResolver.getMasterSyncAutomatically() || !syncOn) { + if (!ContentResolver.getMasterSyncAutomaticallyAsUser(userId) || !syncOn) { requestOrCancelSync(account, authority, syncOn); } } @@ -332,10 +338,7 @@ public class AccountSyncSettings extends AccountPreferenceBase { // plus whatever the system needs to sync, e.g., invisible sync adapters if (mAccount != null) { for (SyncAdapterType syncAdapter : mInvisibleAdapters) { - // invisible sync adapters' account type should be same as current account type - if (syncAdapter.accountType.equals(mAccount.type)) { - requestOrCancelSync(mAccount, syncAdapter.authority, startSync); - } + requestOrCancelSync(mAccount, syncAdapter.authority, startSync); } } } @@ -344,9 +347,10 @@ public class AccountSyncSettings extends AccountPreferenceBase { if (flag) { Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - ContentResolver.requestSync(account, authority, extras); + ContentResolver.requestSyncAsUser(account, authority, mUserHandle.getIdentifier(), + extras); } else { - ContentResolver.cancelSync(account, authority); + ContentResolver.cancelSyncAsUser(account, authority, mUserHandle.getIdentifier()); } } @@ -368,11 +372,12 @@ public class AccountSyncSettings extends AccountPreferenceBase { private void setFeedsState() { // iterate over all the preferences, setting the state properly for each Date date = new Date(); - List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncs(); + final int userId = mUserHandle.getIdentifier(); + List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId); boolean syncIsFailing = false; // Refresh the sync status checkboxes - some syncs may have become active. - updateAccountCheckboxes(mAccounts); + updateAccountCheckboxes(); for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) { Preference pref = getPreferenceScreen().getPreference(i); @@ -384,8 +389,9 @@ public class AccountSyncSettings extends AccountPreferenceBase { String authority = syncPref.getAuthority(); Account account = syncPref.getAccount(); - SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority); - boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); + SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, userId); + boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority, + userId); boolean authorityIsPending = status == null ? false : status.pending; boolean initialSync = status == null ? false : status.initialize; @@ -415,7 +421,7 @@ public class AccountSyncSettings extends AccountPreferenceBase { } else { syncPref.setSummary(""); } - int syncState = ContentResolver.getIsSyncable(account, authority); + int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId); syncPref.setActive(activelySyncing && (syncState >= 0) && !initialSync); @@ -425,7 +431,8 @@ public class AccountSyncSettings extends AccountPreferenceBase { syncPref.setFailed(lastSyncFailed); ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); + final boolean masterSyncAutomatically = + ContentResolver.getMasterSyncAutomaticallyAsUser(userId); final boolean backgroundDataEnabled = connManager.getBackgroundDataSetting(); final boolean oneTimeSyncMode = !masterSyncAutomatically || !backgroundDataEnabled; syncPref.setOneTimeSyncMode(oneTimeSyncMode); @@ -436,29 +443,44 @@ public class AccountSyncSettings extends AccountPreferenceBase { } @Override - public void onAccountsUpdated(Account[] accounts) { - super.onAccountsUpdated(accounts); - mAccounts = accounts; - updateAccountCheckboxes(accounts); + public void onAccountsUpdate(final UserHandle userHandle) { + super.onAccountsUpdate(userHandle); + if (!accountExists(mAccount)) { + // The account was deleted + finish(); + return; + } + updateAccountCheckboxes(); onSyncStateUpdated(); } - private void updateAccountCheckboxes(Account[] accounts) { + private boolean accountExists(Account account) { + if (account == null) return false; + + Account[] accounts = AccountManager.get(getActivity()).getAccountsByTypeAsUser( + account.type, mUserHandle); + final int count = accounts.length; + for (int i = 0; i < count; i++) { + if (accounts[i].equals(account)) { + return true; + } + } + return false; + } + + private void updateAccountCheckboxes() { mInvisibleAdapters.clear(); - SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); - HashMap<String, ArrayList<String>> accountTypeToAuthorities = - Maps.newHashMap(); + SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( + mUserHandle.getIdentifier()); + ArrayList<String> authorities = new ArrayList<String>(); for (int i = 0, n = syncAdapters.length; i < n; i++) { final SyncAdapterType sa = syncAdapters[i]; + // Only keep track of sync adapters for this account + if (!sa.accountType.equals(mAccount.type)) continue; if (sa.isUserVisible()) { - ArrayList<String> authorities = accountTypeToAuthorities.get(sa.accountType); - if (authorities == null) { - authorities = new ArrayList<String>(); - accountTypeToAuthorities.put(sa.accountType, authorities); - } if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "onAccountUpdated: added authority " + sa.authority + Log.d(TAG, "updateAccountCheckboxes: added authority " + sa.authority + " to accountType " + sa.accountType); } authorities.add(sa.authority); @@ -474,24 +496,19 @@ public class AccountSyncSettings extends AccountPreferenceBase { } mCheckBoxes.clear(); - for (int i = 0, n = accounts.length; i < n; i++) { - final Account account = accounts[i]; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "looking for sync adapters that match account " + mAccount); + } + for (int j = 0, m = authorities.size(); j < m; j++) { + final String authority = authorities.get(j); + // We could check services here.... + int syncState = ContentResolver.getIsSyncableAsUser(mAccount, authority, + mUserHandle.getIdentifier()); if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "looking for sync adapters that match account " + account); + Log.d(TAG, " found authority " + authority + " " + syncState); } - final ArrayList<String> authorities = accountTypeToAuthorities.get(account.type); - if (authorities != null && (mAccount == null || mAccount.equals(account))) { - for (int j = 0, m = authorities.size(); j < m; j++) { - final String authority = authorities.get(j); - // We could check services here.... - int syncState = ContentResolver.getIsSyncable(account, authority); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, " found authority " + authority + " " + syncState); - } - if (syncState > 0) { - addSyncStateCheckBox(account, authority); - } - } + if (syncState > 0) { + addSyncStateCheckBox(mAccount, authority); } } diff --git a/src/com/android/settings/accounts/AddAccountSettings.java b/src/com/android/settings/accounts/AddAccountSettings.java index add3f86..3af28b2 100644 --- a/src/com/android/settings/accounts/AddAccountSettings.java +++ b/src/com/android/settings/accounts/AddAccountSettings.java @@ -27,6 +27,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.widget.Toast; @@ -36,8 +37,10 @@ import com.android.settings.Utils; import java.io.IOException; +import static android.content.Intent.EXTRA_USER; + /** - * Entry point Actiivty for account setup. Works as follows + * Entry point Activity for account setup. Works as follows * * 1) When the other Activities launch this Activity, it launches {@link ChooseAccountActivity} * without showing anything. @@ -50,6 +53,9 @@ import java.io.IOException; * currently delegate the work to the other Activity. When we let this Activity do that work, users * would see the list of account types when leaving this Activity, since the UI is already ready * when returning from each account setup, which doesn't look good. + * + * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for + * which the action needs to be performed is different to the one the Settings App will run in. */ public class AddAccountSettings extends Activity { /** @@ -91,8 +97,9 @@ public class AddAccountSettings extends Activity { addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent); addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(AddAccountSettings.this)); + addAccountOptions.putParcelable(EXTRA_USER, mUserHandle); intent.putExtras(addAccountOptions); - startActivityForResult(intent, ADD_ACCOUNT_REQUEST); + startActivityForResultAsUser(intent, ADD_ACCOUNT_REQUEST, mUserHandle); } else { setResult(RESULT_OK); if (mPendingIntent != null) { @@ -117,6 +124,7 @@ public class AddAccountSettings extends Activity { }; private boolean mAddAccountCalled = false; + private UserHandle mUserHandle; @Override public void onCreate(Bundle savedInstanceState) { @@ -128,7 +136,9 @@ public class AddAccountSettings extends Activity { } final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); - if (um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) { + mUserHandle = Utils.getSecureTargetUser(getActivityToken(), um, null /* arguments */, + getIntent().getExtras()); + if (um.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS, mUserHandle)) { // We aren't allowed to add an account. Toast.makeText(this, R.string.user_cannot_add_accounts_message, Toast.LENGTH_LONG) .show(); @@ -151,6 +161,7 @@ public class AddAccountSettings extends Activity { if (accountTypes != null) { intent.putExtra(AccountPreferenceBase.ACCOUNT_TYPES_FILTER_KEY, accountTypes); } + intent.putExtra(EXTRA_USER, mUserHandle); startActivityForResult(intent, CHOOSE_ACCOUNT_REQUEST); } @@ -203,14 +214,15 @@ public class AddAccountSettings extends Activity { mPendingIntent = PendingIntent.getBroadcast(this, 0, identityIntent, 0); addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent); addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(this)); - AccountManager.get(this).addAccount( + AccountManager.get(this).addAccountAsUser( accountType, null, /* authTokenType */ null, /* requiredFeatures */ addAccountOptions, null, mCallback, - null /* handler */); + null /* handler */, + mUserHandle); mAddAccountCalled = true; } } diff --git a/src/com/android/settings/accounts/AuthenticatorHelper.java b/src/com/android/settings/accounts/AuthenticatorHelper.java index a164b8b..cc8a6d5 100644 --- a/src/com/android/settings/accounts/AuthenticatorHelper.java +++ b/src/com/android/settings/accounts/AuthenticatorHelper.java @@ -16,30 +16,67 @@ package com.android.settings.accounts; +import com.google.android.collect.Maps; + import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SyncAdapterType; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.AsyncTask; +import android.os.UserHandle; +import android.os.UserManager; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -public class AuthenticatorHelper { - +/** + * Helper class for monitoring accounts on the device for a given user. + * + * Classes using this helper should implement {@link OnAccountsUpdateListener}. + * {@link OnAccountsUpdateListener#onAccountsUpdate(UserHandle)} will then be + * called once accounts get updated. For setting up listening for account + * updates, {@link #listenToAccountUpdates()} and + * {@link #stopListeningToAccountUpdates()} should be used. + */ +final public class AuthenticatorHelper extends BroadcastReceiver { private static final String TAG = "AuthenticatorHelper"; + private Map<String, AuthenticatorDescription> mTypeToAuthDescription = new HashMap<String, AuthenticatorDescription>(); private AuthenticatorDescription[] mAuthDescs; private ArrayList<String> mEnabledAccountTypes = new ArrayList<String>(); private Map<String, Drawable> mAccTypeIconCache = new HashMap<String, Drawable>(); + private HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = Maps.newHashMap(); + + private final UserHandle mUserHandle; + private final UserManager mUm; + private final Context mContext; + private final OnAccountsUpdateListener mListener; + private boolean mListeningToAccountUpdates; - public AuthenticatorHelper() { + public interface OnAccountsUpdateListener { + void onAccountsUpdate(UserHandle userHandle); + } + + public AuthenticatorHelper(Context context, UserHandle userHandle, UserManager userManager, + OnAccountsUpdateListener listener) { + mContext = context; + mUm = userManager; + mUserHandle = userHandle; + mListener = listener; + // This guarantees that the helper is ready to use once constructed: the account types and + // authorities are initialized + onAccountsUpdated(null); } public String[] getEnabledAccountTypes() { @@ -71,8 +108,10 @@ public class AuthenticatorHelper { if (mTypeToAuthDescription.containsKey(accountType)) { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = context.createPackageContext(desc.packageName, 0); - icon = authContext.getResources().getDrawable(desc.iconId); + Context authContext = context.createPackageContextAsUser(desc.packageName, 0, + mUserHandle); + icon = mContext.getPackageManager().getUserBadgedIcon( + authContext.getResources().getDrawable(desc.iconId), mUserHandle); synchronized (mAccTypeIconCache) { mAccTypeIconCache.put(accountType, icon); } @@ -96,7 +135,8 @@ public class AuthenticatorHelper { if (mTypeToAuthDescription.containsKey(accountType)) { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = context.createPackageContext(desc.packageName, 0); + Context authContext = context.createPackageContextAsUser(desc.packageName, 0, + mUserHandle); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No label name for account type " + accountType); @@ -112,25 +152,13 @@ public class AuthenticatorHelper { * and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated(). */ public void updateAuthDescriptions(Context context) { - mAuthDescs = AccountManager.get(context).getAuthenticatorTypes(); + mAuthDescs = AccountManager.get(context) + .getAuthenticatorTypesAsUser(mUserHandle.getIdentifier()); for (int i = 0; i < mAuthDescs.length; i++) { mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]); } } - public void onAccountsUpdated(Context context, Account[] accounts) { - if (accounts == null) { - accounts = AccountManager.get(context).getAccounts(); - } - mEnabledAccountTypes.clear(); - mAccTypeIconCache.clear(); - for (Account account: accounts) { - if (!mEnabledAccountTypes.contains(account.type)) { - mEnabledAccountTypes.add(account.type); - } - } - } - public boolean containsAccountType(String accountType) { return mTypeToAuthDescription.containsKey(accountType); } @@ -148,4 +176,73 @@ public class AuthenticatorHelper { } return false; } + + void onAccountsUpdated(Account[] accounts) { + updateAuthDescriptions(mContext); + if (accounts == null) { + accounts = AccountManager.get(mContext).getAccountsAsUser(mUserHandle.getIdentifier()); + } + mEnabledAccountTypes.clear(); + mAccTypeIconCache.clear(); + for (int i = 0; i < accounts.length; i++) { + final Account account = accounts[i]; + if (!mEnabledAccountTypes.contains(account.type)) { + mEnabledAccountTypes.add(account.type); + } + } + buildAccountTypeToAuthoritiesMap(); + if (mListeningToAccountUpdates) { + mListener.onAccountsUpdate(mUserHandle); + } + } + + @Override + public void onReceive(final Context context, final Intent intent) { + // TODO: watch for package upgrades to invalidate cache; see http://b/7206643 + final Account[] accounts = AccountManager.get(mContext) + .getAccountsAsUser(mUserHandle.getIdentifier()); + onAccountsUpdated(accounts); + } + + public void listenToAccountUpdates() { + if (!mListeningToAccountUpdates) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); + // At disk full, certain actions are blocked (such as writing the accounts to storage). + // It is useful to also listen for recovery from disk full to avoid bugs. + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + mContext.registerReceiverAsUser(this, mUserHandle, intentFilter, null, null); + mListeningToAccountUpdates = true; + } + } + + public void stopListeningToAccountUpdates() { + if (mListeningToAccountUpdates) { + mContext.unregisterReceiver(this); + mListeningToAccountUpdates = false; + } + } + + public ArrayList<String> getAuthoritiesForAccountType(String type) { + return mAccountTypeToAuthorities.get(type); + } + + private void buildAccountTypeToAuthoritiesMap() { + mAccountTypeToAuthorities.clear(); + SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( + mUserHandle.getIdentifier()); + for (int i = 0, n = syncAdapters.length; i < n; i++) { + final SyncAdapterType sa = syncAdapters[i]; + ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType); + if (authorities == null) { + authorities = new ArrayList<String>(); + mAccountTypeToAuthorities.put(sa.accountType, authorities); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "Added authority " + sa.authority + " to accountType " + + sa.accountType); + } + authorities.add(sa.authority); + } + } } diff --git a/src/com/android/settings/accounts/ChooseAccountActivity.java b/src/com/android/settings/accounts/ChooseAccountActivity.java index 631fe47..e52d640 100644 --- a/src/com/android/settings/accounts/ChooseAccountActivity.java +++ b/src/com/android/settings/accounts/ChooseAccountActivity.java @@ -26,13 +26,18 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.util.Log; + import com.android.internal.util.CharSequences; import com.android.settings.R; +import com.android.settings.Utils; + import com.google.android.collect.Maps; import java.util.ArrayList; @@ -41,8 +46,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import static android.content.Intent.EXTRA_USER; + /** * Activity asking a user to select an account to be set up. + * + * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for + * which the action needs to be performed is different to the one the Settings App will run in. */ public class ChooseAccountActivity extends PreferenceActivity { @@ -55,7 +65,10 @@ public class ChooseAccountActivity extends PreferenceActivity { private HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = null; private Map<String, AuthenticatorDescription> mTypeToAuthDescription = new HashMap<String, AuthenticatorDescription>(); - + // The UserHandle of the user we are choosing an account for + private UserHandle mUserHandle; + private UserManager mUm; + private static class ProviderEntry implements Comparable<ProviderEntry> { private final CharSequence name; private final String type; @@ -92,6 +105,9 @@ public class ChooseAccountActivity extends PreferenceActivity { } } mAddAccountGroup = getPreferenceScreen(); + mUm = UserManager.get(this); + mUserHandle = Utils.getSecureTargetUser(getActivityToken(), mUm, null /* arguments */, + getIntent().getExtras()); updateAuthDescriptions(); } @@ -100,7 +116,8 @@ public class ChooseAccountActivity extends PreferenceActivity { * and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated(). */ private void updateAuthDescriptions() { - mAuthDescs = AccountManager.get(this).getAuthenticatorTypes(); + mAuthDescs = AccountManager.get(this).getAuthenticatorTypesAsUser( + mUserHandle.getIdentifier()); for (int i = 0; i < mAuthDescs.length; i++) { mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]); } @@ -168,7 +185,8 @@ public class ChooseAccountActivity extends PreferenceActivity { public ArrayList<String> getAuthoritiesForAccountType(String type) { if (mAccountTypeToAuthorities == null) { mAccountTypeToAuthorities = Maps.newHashMap(); - SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); + SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( + mUserHandle.getIdentifier()); for (int i = 0, n = syncAdapters.length; i < n; i++) { final SyncAdapterType sa = syncAdapters[i]; ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType); @@ -196,8 +214,9 @@ public class ChooseAccountActivity extends PreferenceActivity { if (mTypeToAuthDescription.containsKey(accountType)) { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = createPackageContext(desc.packageName, 0); - icon = authContext.getResources().getDrawable(desc.iconId); + Context authContext = createPackageContextAsUser(desc.packageName, 0, mUserHandle); + icon = getPackageManager().getUserBadgedIcon( + authContext.getResources().getDrawable(desc.iconId), mUserHandle); } catch (PackageManager.NameNotFoundException e) { // TODO: place holder icon for missing account icons? Log.w(TAG, "No icon name for account type " + accountType); @@ -219,7 +238,7 @@ public class ChooseAccountActivity extends PreferenceActivity { if (mTypeToAuthDescription.containsKey(accountType)) { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = createPackageContext(desc.packageName, 0); + Context authContext = createPackageContextAsUser(desc.packageName, 0, mUserHandle); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No label name for account type " + accountType); @@ -245,6 +264,7 @@ public class ChooseAccountActivity extends PreferenceActivity { private void finishWithAccountType(String accountType) { Intent intent = new Intent(); intent.putExtra(AddAccountSettings.EXTRA_SELECTED_ACCOUNT, accountType); + intent.putExtra(EXTRA_USER, mUserHandle); setResult(RESULT_OK, intent); finish(); } diff --git a/src/com/android/settings/accounts/ManageAccountsSettings.java b/src/com/android/settings/accounts/ManageAccountsSettings.java index 184f680..074176b 100644 --- a/src/com/android/settings/accounts/ManageAccountsSettings.java +++ b/src/com/android/settings/accounts/ManageAccountsSettings.java @@ -18,7 +18,7 @@ package com.android.settings.accounts; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; +import android.accounts.AuthenticatorDescription; import android.app.ActionBar; import android.app.Activity; import android.content.ContentResolver; @@ -26,12 +26,17 @@ import android.content.Intent; import android.content.SyncAdapterType; import android.content.SyncInfo; import android.content.SyncStatusInfo; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.UserHandle; import android.preference.Preference; -import android.preference.PreferenceActivity; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; import android.util.Log; import android.view.LayoutInflater; @@ -45,16 +50,20 @@ import android.widget.TextView; import com.android.settings.AccountPreference; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.location.LocationSettings; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; +import java.util.List; + +import static android.content.Intent.EXTRA_USER; /** Manages settings for Google Account. */ public class ManageAccountsSettings extends AccountPreferenceBase - implements OnAccountsUpdateListener { + implements AuthenticatorHelper.OnAccountsUpdateListener { private static final String ACCOUNT_KEY = "account"; // to pass to auth settings public static final String KEY_ACCOUNT_TYPE = "account_type"; public static final String KEY_ACCOUNT_LABEL = "account_label"; @@ -92,8 +101,9 @@ public class ManageAccountsSettings extends AccountPreferenceBase @Override public void onStart() { super.onStart(); - Activity activity = getActivity(); - AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, true); + mAuthenticatorHelper.listenToAccountUpdates(); + updateAuthDescriptions(); + showAccountsIfNeeded(); } @Override @@ -121,14 +131,13 @@ public class ManageAccountsSettings extends AccountPreferenceBase if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) { getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL)); } - updateAuthDescriptions(); } @Override public void onStop() { super.onStop(); final Activity activity = getActivity(); - AccountManager.get(activity).removeOnAccountsUpdatedListener(this); + mAuthenticatorHelper.stopListeningToAccountUpdates(); activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM); activity.getActionBar().setCustomView(null); } @@ -146,7 +155,8 @@ public class ManageAccountsSettings extends AccountPreferenceBase private void startAccountSettings(AccountPreference acctPref) { Bundle args = new Bundle(); args.putParcelable(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount()); - ((PreferenceActivity) getActivity()).startPreferencePanel( + args.putParcelable(EXTRA_USER, mUserHandle); + ((SettingsActivity) getActivity()).startPreferencePanel( AccountSyncSettings.class.getCanonicalName(), args, R.string.account_sync_settings_title, acctPref.getAccount().name, this, REQUEST_SHOW_SYNC_SETTINGS); @@ -166,7 +176,8 @@ public class ManageAccountsSettings extends AccountPreferenceBase @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - boolean syncActive = ContentResolver.getCurrentSync() != null; + boolean syncActive = ContentResolver.getCurrentSyncsAsUser( + mUserHandle.getIdentifier()).isEmpty(); menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive && mFirstAccount != null); menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive && mFirstAccount != null); } @@ -185,7 +196,8 @@ public class ManageAccountsSettings extends AccountPreferenceBase } private void requestOrCancelSyncForAccounts(boolean sync) { - SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); + final int userId = mUserHandle.getIdentifier(); + SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId); Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); int count = getPreferenceScreen().getPreferenceCount(); @@ -198,11 +210,13 @@ public class ManageAccountsSettings extends AccountPreferenceBase for (int j = 0; j < syncAdapters.length; j++) { SyncAdapterType sa = syncAdapters[j]; if (syncAdapters[j].accountType.equals(mAccountType) - && ContentResolver.getSyncAutomatically(account, sa.authority)) { + && ContentResolver.getSyncAutomaticallyAsUser(account, sa.authority, + userId)) { if (sync) { - ContentResolver.requestSync(account, sa.authority, extras); + ContentResolver.requestSyncAsUser(account, sa.authority, userId, + extras); } else { - ContentResolver.cancelSync(account, sa.authority); + ContentResolver.cancelSyncAsUser(account, sa.authority, userId); } } } @@ -212,17 +226,23 @@ public class ManageAccountsSettings extends AccountPreferenceBase @Override protected void onSyncStateUpdated() { + showSyncState(); + } + + private void showSyncState() { // Catch any delayed delivery of update messages if (getActivity() == null) return; + final int userId = mUserHandle.getIdentifier(); + // iterate over all the preferences, setting the state properly for each - SyncInfo currentSync = ContentResolver.getCurrentSync(); + List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId); boolean anySyncFailed = false; // true if sync on any account failed Date date = new Date(); // only track userfacing sync adapters when deciding if account is synced or not - final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); + final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId); HashSet<String> userFacing = new HashSet<String>(); for (int k = 0, n = syncAdapters.length; k < n; k++) { final SyncAdapterType sa = syncAdapters[k]; @@ -245,15 +265,11 @@ public class ManageAccountsSettings extends AccountPreferenceBase boolean syncingNow = false; if (authorities != null) { for (String authority : authorities) { - SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority); - boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority) - && ContentResolver.getMasterSyncAutomatically() - && (ContentResolver.getIsSyncable(account, authority) > 0); + SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, + userId); + boolean syncEnabled = isSyncEnabled(userId, account, authority); boolean authorityIsPending = ContentResolver.isSyncPending(account, authority); - boolean activelySyncing = currentSync != null - && currentSync.authority.equals(authority) - && new Account(currentSync.account.name, currentSync.account.type) - .equals(account); + boolean activelySyncing = isSyncing(currentSyncs, account, authority); boolean lastSyncFailed = status != null && syncEnabled && status.lastFailureTime != 0 @@ -299,9 +315,34 @@ public class ManageAccountsSettings extends AccountPreferenceBase mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE); } + + private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { + final int count = currentSyncs.size(); + for (int i = 0; i < count; i++) { + SyncInfo syncInfo = currentSyncs.get(i); + if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { + return true; + } + } + return false; + } + + private boolean isSyncEnabled(int userId, Account account, String authority) { + return ContentResolver.getSyncAutomaticallyAsUser(account, authority, userId) + && ContentResolver.getMasterSyncAutomaticallyAsUser(userId) + && (ContentResolver.getIsSyncableAsUser(account, authority, userId) > 0); + } + @Override - public void onAccountsUpdated(Account[] accounts) { + public void onAccountsUpdate(UserHandle userHandle) { + showAccountsIfNeeded(); + onSyncStateUpdated(); + } + + private void showAccountsIfNeeded() { if (getActivity() == null) return; + Account[] accounts = AccountManager.get(getActivity()).getAccountsAsUser( + mUserHandle.getIdentifier()); getPreferenceScreen().removeAll(); mFirstAccount = null; addPreferencesFromResource(R.xml.manage_accounts_settings); @@ -341,7 +382,6 @@ public class ManageAccountsSettings extends AccountPreferenceBase settingsTop.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); getActivity().startActivity(settingsTop); } - onSyncStateUpdated(); } private void addAuthenticatorSettings() { @@ -368,7 +408,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase @Override public boolean onPreferenceClick(Preference preference) { - ((PreferenceActivity) getActivity()).startPreferencePanel( + ((SettingsActivity) getActivity()).startPreferencePanel( mClass, null, mTitleRes, null, null, 0); // Hack: announce that the Google account preferences page is launching the location // settings @@ -388,7 +428,7 @@ public class ManageAccountsSettings extends AccountPreferenceBase * intent, and hack the location settings to start it as a fragment. */ private void updatePreferenceIntents(PreferenceScreen prefs) { - PackageManager pm = getActivity().getPackageManager(); + final PackageManager pm = getActivity().getPackageManager(); for (int i = 0; i < prefs.getPreferenceCount();) { Preference pref = prefs.getPreference(i); Intent intent = pref.getIntent(); @@ -415,13 +455,36 @@ public class ManageAccountsSettings extends AccountPreferenceBase LocationSettings.class.getName(), R.string.location_settings_title)); } else { - ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + ResolveInfo ri = pm.resolveActivityAsUser(intent, + PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); if (ri == null) { prefs.removePreference(pref); continue; } else { intent.putExtra(ACCOUNT_KEY, mFirstAccount); intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent prefIntent = preference.getIntent(); + /* + * Check the intent to see if it resolves to a exported=false + * activity that doesn't share a uid with the authenticator. + * + * Otherwise the intent is considered unsafe in that it will be + * exploiting the fact that settings has system privileges. + */ + if (isSafeIntent(pm, prefIntent)) { + getActivity().startActivityAsUser(prefIntent, mUserHandle); + } else { + Log.e(TAG, + "Refusing to launch authenticator intent because" + + "it exploits Settings permissions: " + + prefIntent); + } + return true; + } + }); } } } @@ -429,6 +492,32 @@ public class ManageAccountsSettings extends AccountPreferenceBase } } + /** + * Determines if the supplied Intent is safe. A safe intent is one that is + * will launch a exported=true activity or owned by the same uid as the + * authenticator supplying the intent. + */ + private boolean isSafeIntent(PackageManager pm, Intent intent) { + AuthenticatorDescription authDesc = + mAuthenticatorHelper.getAccountTypeDescription(mAccountType); + ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); + if (resolveInfo == null) { + return false; + } + ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; + ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; + try { + ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); + return resolvedActivityInfo.exported + || resolvedAppInfo.uid == authenticatorAppInf.uid; + } catch (NameNotFoundException e) { + Log.e(TAG, + "Intent considered unsafe due to exception.", + e); + return false; + } + } + @Override protected void onAuthDescriptionsUpdated() { // Update account icons for all account preference items diff --git a/src/com/android/settings/accounts/SyncSettings.java b/src/com/android/settings/accounts/SyncSettings.java deleted file mode 100644 index 3248113..0000000 --- a/src/com/android/settings/accounts/SyncSettings.java +++ /dev/null @@ -1,179 +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.accounts; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.app.Activity; -import android.app.ActivityManager; -import android.content.ContentResolver; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceScreen; -import android.util.Log; - -import com.android.settings.AccountPreference; -import com.android.settings.DialogCreatable; -import com.android.settings.R; - -import java.util.ArrayList; - -public class SyncSettings extends AccountPreferenceBase - implements OnAccountsUpdateListener, DialogCreatable { - - private static final String KEY_SYNC_SWITCH = "sync_switch"; - - private String[] mAuthorities; - - private SettingsDialogFragment mDialogFragment; - private CheckBoxPreference mAutoSyncPreference; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - addPreferencesFromResource(R.xml.sync_settings); - mAutoSyncPreference = - (CheckBoxPreference) getPreferenceScreen().findPreference(KEY_SYNC_SWITCH); - mAutoSyncPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (ActivityManager.isUserAMonkey()) { - Log.d("SyncSettings", "ignoring monkey's attempt to flip sync state"); - } else { - ContentResolver.setMasterSyncAutomatically((Boolean) newValue); - } - return true; - } - }); - - setHasOptionsMenu(true); - } - - @Override - public void onStart() { - super.onStart(); - Activity activity = getActivity(); - AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, true); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final Activity activity = getActivity(); - mAutoSyncPreference.setChecked(ContentResolver.getMasterSyncAutomatically()); - mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY); - - updateAuthDescriptions(); - } - - @Override - public void onStop() { - super.onStop(); - final Activity activity = getActivity(); - AccountManager.get(activity).removeOnAccountsUpdatedListener(this); - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) { - if (preference instanceof AccountPreference) { - startAccountSettings((AccountPreference) preference); - } else { - return false; - } - return true; - } - - private void startAccountSettings(AccountPreference acctPref) { - Intent intent = new Intent("android.settings.ACCOUNT_SYNC_SETTINGS"); - intent.putExtra(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finish(); - } - - @Override - public void showDialog(int dialogId) { - if (mDialogFragment != null) { - Log.e(TAG, "Old dialog fragment not null!"); - } - mDialogFragment = new SettingsDialogFragment(this, dialogId); - mDialogFragment.show(getActivity().getFragmentManager(), Integer.toString(dialogId)); - } - - private void removeAccountPreferences() { - PreferenceScreen parent = getPreferenceScreen(); - for (int i = 0; i < parent.getPreferenceCount(); ) { - if (parent.getPreference(i) instanceof AccountPreference) { - parent.removePreference(parent.getPreference(i)); - } else { - i++; - } - } - } - - @Override - public void onAccountsUpdated(Account[] accounts) { - if (getActivity() == null) return; - - removeAccountPreferences(); - for (int i = 0, n = accounts.length; i < n; i++) { - final Account account = accounts[i]; - final ArrayList<String> auths = getAuthoritiesForAccountType(account.type); - - boolean showAccount = true; - if (mAuthorities != null && auths != null) { - showAccount = false; - for (String requestedAuthority : mAuthorities) { - if (auths.contains(requestedAuthority)) { - showAccount = true; - break; - } - } - } - - if (showAccount) { - final Drawable icon = getDrawableForType(account.type); - final AccountPreference preference = - new AccountPreference(getActivity(), account, icon, auths, true); - getPreferenceScreen().addPreference(preference); - preference.setSummary(getLabelForType(account.type)); - } - } - onSyncStateUpdated(); - } - - @Override - protected void onAuthDescriptionsUpdated() { - // Update account icons for all account preference items - for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { - Preference pref = getPreferenceScreen().getPreference(i); - if (pref instanceof AccountPreference) { - AccountPreference accPref = (AccountPreference) - getPreferenceScreen().getPreference(i); - accPref.setIcon(getDrawableForType(accPref.getAccount().type)); - accPref.setSummary(getLabelForType(accPref.getAccount().type)); - } - } - } -} diff --git a/src/com/android/settings/accounts/SyncSettingsActivity.java b/src/com/android/settings/accounts/SyncSettingsActivity.java deleted file mode 100644 index 96f16d6..0000000 --- a/src/com/android/settings/accounts/SyncSettingsActivity.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2012 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.accounts; - -import android.app.Fragment; -import android.content.Intent; -import android.preference.PreferenceActivity; - -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; - -/** - * Launcher activity for the SyncSettings fragment. - * - */ -public class SyncSettingsActivity extends PreferenceActivity { - @Override - public Intent getIntent() { - Intent modIntent = new Intent(super.getIntent()); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, SyncSettings.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); - return modIntent; - } - - @Override - protected boolean isValidFragment(String fragmentName) { - if (SyncSettings.class.getName().equals(fragmentName)) return true; - return false; - } -}
\ No newline at end of file diff --git a/src/com/android/settings/applications/AppOpsCategory.java b/src/com/android/settings/applications/AppOpsCategory.java index 125a43b..03ebb9e 100644 --- a/src/com/android/settings/applications/AppOpsCategory.java +++ b/src/com/android/settings/applications/AppOpsCategory.java @@ -28,7 +28,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -40,6 +39,7 @@ import android.widget.TextView; import java.util.List; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.applications.AppOpsState.AppOpEntry; public class AppOpsCategory extends ListFragment implements @@ -333,8 +333,8 @@ public class AppOpsCategory extends ListFragment implements Bundle args = new Bundle(); args.putString(AppOpsDetails.ARG_PACKAGE_NAME, mCurrentPkgName); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.startPreferencePanel(AppOpsDetails.class.getName(), args, + SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(AppOpsDetails.class.getName(), args, R.string.app_ops_settings, null, this, RESULT_APP_DETAILS); } diff --git a/src/com/android/settings/applications/AppOpsDetails.java b/src/com/android/settings/applications/AppOpsDetails.java index 1e2ac7d..d9dec19 100644 --- a/src/com/android/settings/applications/AppOpsDetails.java +++ b/src/com/android/settings/applications/AppOpsDetails.java @@ -28,7 +28,6 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -40,6 +39,7 @@ import android.widget.Switch; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.Utils; import java.util.List; @@ -161,8 +161,8 @@ public class AppOpsDetails extends Fragment { private void setIntentAndFinish(boolean finish, boolean appChanged) { Intent intent = new Intent(); intent.putExtra(ManageApplications.APP_CHG, appChanged); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.finishPreferencePanel(this, Activity.RESULT_OK, intent); + SettingsActivity sa = (SettingsActivity)getActivity(); + sa.finishPreferencePanel(this, Activity.RESULT_OK, intent); } /** Called when the activity is first created. */ diff --git a/src/com/android/settings/applications/AppOpsState.java b/src/com/android/settings/applications/AppOpsState.java index c396479..580c44e 100644 --- a/src/com/android/settings/applications/AppOpsState.java +++ b/src/com/android/settings/applications/AppOpsState.java @@ -103,14 +103,14 @@ public class AppOpsState { AppOpsManager.OP_WIFI_SCAN, AppOpsManager.OP_NEIGHBORING_CELLS, AppOpsManager.OP_MONITOR_LOCATION, - AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION}, + AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION }, new boolean[] { true, true, false, false, false, false, - false} + false } ); public static final OpsTemplate PERSONAL_TEMPLATE = new OpsTemplate( @@ -166,7 +166,8 @@ public class AppOpsState { AppOpsManager.OP_AUDIO_MEDIA_VOLUME, AppOpsManager.OP_AUDIO_ALARM_VOLUME, AppOpsManager.OP_AUDIO_NOTIFICATION_VOLUME, - AppOpsManager.OP_AUDIO_BLUETOOTH_VOLUME, }, + AppOpsManager.OP_AUDIO_BLUETOOTH_VOLUME, + AppOpsManager.OP_MUTE_MICROPHONE}, new boolean[] { false, true, true, @@ -188,13 +189,17 @@ public class AppOpsState { AppOpsManager.OP_CALL_PHONE, AppOpsManager.OP_WRITE_SETTINGS, AppOpsManager.OP_SYSTEM_ALERT_WINDOW, - AppOpsManager.OP_WAKE_LOCK }, + AppOpsManager.OP_WAKE_LOCK, + AppOpsManager.OP_PROJECT_MEDIA, + AppOpsManager.OP_ACTIVATE_VPN, }, new boolean[] { false, true, true, true, true, - true, } + true, + false, + false, } ); public static final OpsTemplate[] ALL_TEMPLATES = new OpsTemplate[] { diff --git a/src/com/android/settings/applications/AppOpsSummary.java b/src/com/android/settings/applications/AppOpsSummary.java index 4cee8e5..3401c99 100644 --- a/src/com/android/settings/applications/AppOpsSummary.java +++ b/src/com/android/settings/applications/AppOpsSummary.java @@ -16,7 +16,6 @@ package com.android.settings.applications; -import android.app.AppOpsManager; import android.app.Fragment; import android.app.FragmentManager; import android.os.Bundle; @@ -104,7 +103,7 @@ public class AppOpsSummary extends Fragment { mViewPager.setAdapter(adapter); mViewPager.setOnPageChangeListener(adapter); PagerTabStrip tabs = (PagerTabStrip) rootView.findViewById(R.id.tabs); - tabs.setTabIndicatorColorResource(android.R.color.holo_blue_light); + tabs.setTabIndicatorColorResource(R.color.theme_accent); // We have to do this now because PreferenceFrameLayout looks at it // only when the view is added. diff --git a/src/com/android/settings/applications/InstalledAppDetails.java b/src/com/android/settings/applications/InstalledAppDetails.java index cbb06a0..4b1bc10 100644..100755 --- a/src/com/android/settings/applications/InstalledAppDetails.java +++ b/src/com/android/settings/applications/InstalledAppDetails.java @@ -19,6 +19,7 @@ package com.android.settings.applications; import com.android.internal.telephony.ISms; import com.android.internal.telephony.SmsUsageMonitor; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.applications.ApplicationsState.AppEntry; @@ -57,7 +58,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; -import android.preference.PreferenceActivity; import android.text.SpannableString; import android.text.TextUtils; import android.text.format.Formatter; @@ -126,6 +126,7 @@ public class InstalledAppDetails extends Fragment private CheckBox mAskCompatibilityCB; private CheckBox mEnableCompatibilityCB; private boolean mCanClearData = true; + private boolean mAppControlRestricted = false; private TextView mAppVersion; private TextView mTotalSize; private TextView mAppSize; @@ -264,6 +265,10 @@ public class InstalledAppDetails extends Fragment } mClearDataButton.setOnClickListener(this); } + + if (mAppControlRestricted) { + mClearDataButton.setEnabled(false); + } } private CharSequence getMoveErrMsg(int errCode) { @@ -303,7 +308,7 @@ public class InstalledAppDetails extends Fragment mCanBeOnSdCardChecker.init(); moveDisable = !mCanBeOnSdCardChecker.check(mAppEntry.info); } - if (moveDisable) { + if (moveDisable || mAppControlRestricted) { mMoveAppButton.setEnabled(false); } else { mMoveAppButton.setOnClickListener(this); @@ -311,22 +316,13 @@ public class InstalledAppDetails extends Fragment } } - private boolean isThisASystemPackage() { - try { - PackageInfo sys = mPm.getPackageInfo("android", PackageManager.GET_SIGNATURES); - return (mPackageInfo != null && mPackageInfo.signatures != null && - sys.signatures[0].equals(mPackageInfo.signatures[0])); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - private boolean handleDisableable(Button button) { boolean disableable = false; // Try to prevent the user from bricking their phone // by not allowing disabling of apps signed with the // system cert and any launcher app in the system. - if (mHomePackages.contains(mAppEntry.info.packageName) || isThisASystemPackage()) { + if (mHomePackages.contains(mAppEntry.info.packageName) + || Utils.isSystemPackage(mPm, mPackageInfo)) { // Disable button for core system applications. button.setText(R.string.disable_text); } else if (mAppEntry.info.enabled) { @@ -342,18 +338,22 @@ public class InstalledAppDetails extends Fragment private void initUninstallButtons() { mUpdatedSysApp = (mAppEntry.info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; + final boolean isBundled = (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; boolean enabled = true; if (mUpdatedSysApp) { mUninstallButton.setText(R.string.app_factory_reset); - boolean specialDisable = false; - if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - specialDisable = handleDisableable(mSpecialDisableButton); + boolean showSpecialDisable = false; + if (isBundled) { + showSpecialDisable = handleDisableable(mSpecialDisableButton); mSpecialDisableButton.setOnClickListener(this); } - mMoreControlButtons.setVisibility(specialDisable ? View.VISIBLE : View.GONE); + if (mAppControlRestricted) { + showSpecialDisable = false; + } + mMoreControlButtons.setVisibility(showSpecialDisable ? View.VISIBLE : View.GONE); } else { mMoreControlButtons.setVisibility(View.GONE); - if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + if (isBundled) { enabled = handleDisableable(mUninstallButton); } else if ((mPackageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0 @@ -372,22 +372,34 @@ public class InstalledAppDetails extends Fragment enabled = false; } - // If this is the default (or only) home app, suppress uninstall (even if - // we still think it should be allowed for other reasons) + // Home apps need special handling. Bundled ones we don't risk downgrading + // because that can interfere with home-key resolution. Furthermore, we + // can't allow uninstallation of the only home app, and we don't want to + // allow uninstallation of an explicitly preferred one -- the user can go + // to Home settings and pick a different one, after which we'll permit + // uninstallation of the now-not-default one. if (enabled && mHomePackages.contains(mPackageInfo.packageName)) { - ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); - ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); - if (currentDefaultHome == null) { - // No preferred default, so permit uninstall only when - // there is more than one candidate - enabled = (mHomePackages.size() > 1); + if (isBundled) { + enabled = false; } else { - // There is an explicit default home app -- forbid uninstall of - // that one, but permit it for installed-but-inactive ones. - enabled = !mPackageInfo.packageName.equals(currentDefaultHome.getPackageName()); + ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); + ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); + if (currentDefaultHome == null) { + // No preferred default, so permit uninstall only when + // there is more than one candidate + enabled = (mHomePackages.size() > 1); + } else { + // There is an explicit default home app -- forbid uninstall of + // that one, but permit it for installed-but-inactive ones. + enabled = !mPackageInfo.packageName.equals(currentDefaultHome.getPackageName()); + } } } + if (mAppControlRestricted) { + enabled = false; + } + mUninstallButton.setEnabled(enabled); if (enabled) { // Register listener @@ -406,7 +418,7 @@ public class InstalledAppDetails extends Fragment // this does not bode well } mNotificationSwitch.setChecked(enabled); - if (isThisASystemPackage()) { + if (Utils.isSystemPackage(mPm, mPackageInfo)) { mNotificationSwitch.setEnabled(false); } else { mNotificationSwitch.setEnabled(true); @@ -443,17 +455,19 @@ public class InstalledAppDetails extends Fragment public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.installed_app_details, container, false); - Utils.prepareCustomPreferencesList(container, view, view, false); + + final ViewGroup allDetails = (ViewGroup) view.findViewById(R.id.all_details); + Utils.forceCustomPadding(allDetails, true /* additive padding */); mRootView = view; mComputingStr = getActivity().getText(R.string.computing_size); // Set default values on sizes - 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); - mExternalCodeSize = (TextView)view.findViewById(R.id.external_code_size_text); - mExternalDataSize = (TextView)view.findViewById(R.id.external_data_size_text); + 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); + mExternalCodeSize = (TextView) view.findViewById(R.id.external_code_size_text); + mExternalDataSize = (TextView) view.findViewById(R.id.external_data_size_text); if (Environment.isExternalStorageEmulated()) { ((View)mExternalCodeSize.getParent()).setVisibility(View.GONE); @@ -464,13 +478,13 @@ public class InstalledAppDetails extends Fragment View btnPanel = view.findViewById(R.id.control_buttons_panel); mForceStopButton = (Button) btnPanel.findViewById(R.id.left_button); mForceStopButton.setText(R.string.force_stop); - mUninstallButton = (Button)btnPanel.findViewById(R.id.right_button); + mUninstallButton = (Button) btnPanel.findViewById(R.id.right_button); mForceStopButton.setEnabled(false); // Get More Control button panel mMoreControlButtons = view.findViewById(R.id.more_control_buttons_panel); mMoreControlButtons.findViewById(R.id.left_button).setVisibility(View.INVISIBLE); - mSpecialDisableButton = (Button)mMoreControlButtons.findViewById(R.id.right_button); + mSpecialDisableButton = (Button) mMoreControlButtons.findViewById(R.id.right_button); mMoreControlButtons.setVisibility(View.GONE); // Initialize clear data and move install location buttons @@ -482,12 +496,12 @@ public class InstalledAppDetails extends Fragment mCacheSize = (TextView) view.findViewById(R.id.cache_size_text); mClearCacheButton = (Button) view.findViewById(R.id.clear_cache_button); - mActivitiesButton = (Button)view.findViewById(R.id.clear_activities_button); + mActivitiesButton = (Button) view.findViewById(R.id.clear_activities_button); // Screen compatibility control mScreenCompatSection = view.findViewById(R.id.screen_compatibility_section); - mAskCompatibilityCB = (CheckBox)view.findViewById(R.id.ask_compatibility_cb); - mEnableCompatibilityCB = (CheckBox)view.findViewById(R.id.enable_compatibility_cb); + mAskCompatibilityCB = (CheckBox) view.findViewById(R.id.ask_compatibility_cb); + mEnableCompatibilityCB = (CheckBox) view.findViewById(R.id.enable_compatibility_cb); mNotificationSwitch = (CompoundButton) view.findViewById(R.id.notification_switch); @@ -580,6 +594,7 @@ public class InstalledAppDetails extends Fragment public void onResume() { super.onResume(); + mAppControlRestricted = mUserManager.hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL); mSession.resume(); if (!refreshUi()) { setIntentAndFinish(true, true); @@ -593,6 +608,12 @@ public class InstalledAppDetails extends Fragment } @Override + public void onDestroyView() { + super.onDestroyView(); + mSession.release(); + } + + @Override public void onAllSizesComputed() { } @@ -937,8 +958,8 @@ public class InstalledAppDetails extends Fragment if(localLOGV) Log.i(TAG, "appChanged="+appChanged); Intent intent = new Intent(); intent.putExtra(ManageApplications.APP_CHG, appChanged); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.finishPreferencePanel(this, Activity.RESULT_OK, intent); + SettingsActivity sa = (SettingsActivity)getActivity(); + sa.finishPreferencePanel(this, Activity.RESULT_OK, intent); } private void refreshSizeInfo() { @@ -1002,6 +1023,10 @@ public class InstalledAppDetails extends Fragment mClearCacheButton.setOnClickListener(this); } } + if (mAppControlRestricted) { + mClearCacheButton.setEnabled(false); + mClearDataButton.setEnabled(false); + } } /* @@ -1103,7 +1128,6 @@ public class InstalledAppDetails extends Fragment case DLG_CLEAR_DATA: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.clear_data_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.clear_data_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1117,7 +1141,6 @@ public class InstalledAppDetails extends Fragment case DLG_FACTORY_RESET: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.app_factory_reset_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.app_factory_reset_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1132,7 +1155,6 @@ public class InstalledAppDetails extends Fragment case DLG_APP_NOT_FOUND: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.app_not_found_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.app_not_found_dlg_title)) .setNeutralButton(getActivity().getText(R.string.dlg_ok), new DialogInterface.OnClickListener() { @@ -1145,7 +1167,6 @@ public class InstalledAppDetails extends Fragment case DLG_CANNOT_CLEAR_DATA: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.clear_failed_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.clear_failed_dlg_text)) .setNeutralButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1159,7 +1180,6 @@ public class InstalledAppDetails extends Fragment case DLG_FORCE_STOP: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.force_stop_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.force_stop_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1175,14 +1195,12 @@ public class InstalledAppDetails extends Fragment getOwner().getMoveErrMsg(moveErrorCode)); return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.move_app_failed_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .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)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.app_disable_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1198,7 +1216,6 @@ public class InstalledAppDetails extends Fragment case DLG_DISABLE_NOTIFICATIONS: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.app_disable_notifications_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.app_disable_notifications_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1218,7 +1235,6 @@ public class InstalledAppDetails extends Fragment case DLG_SPECIAL_DISABLE: return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getText(R.string.app_special_disable_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getText(R.string.app_special_disable_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { @@ -1264,8 +1280,12 @@ public class InstalledAppDetails extends Fragment }; private void updateForceStopButton(boolean enabled) { - mForceStopButton.setEnabled(enabled); - mForceStopButton.setOnClickListener(InstalledAppDetails.this); + if (mAppControlRestricted) { + mForceStopButton.setEnabled(false); + } else { + mForceStopButton.setEnabled(enabled); + mForceStopButton.setOnClickListener(InstalledAppDetails.this); + } } private void checkForceStop() { diff --git a/src/com/android/settings/applications/InstalledAppDetailsTop.java b/src/com/android/settings/applications/InstalledAppDetailsTop.java index 44a88fb..e078729 100644 --- a/src/com/android/settings/applications/InstalledAppDetailsTop.java +++ b/src/com/android/settings/applications/InstalledAppDetailsTop.java @@ -1,18 +1,30 @@ +/* + * 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.applications; -import android.app.Fragment; import android.content.Intent; -import android.preference.PreferenceActivity; - -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; +import com.android.settings.SettingsActivity; -public class InstalledAppDetailsTop extends PreferenceActivity { +public class InstalledAppDetailsTop extends SettingsActivity { @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, InstalledAppDetails.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } @@ -21,5 +33,4 @@ public class InstalledAppDetailsTop extends PreferenceActivity { if (InstalledAppDetails.class.getName().equals(fragmentName)) return true; return false; } - } diff --git a/src/com/android/settings/applications/LinearColorBar.java b/src/com/android/settings/applications/LinearColorBar.java index f374c29..158a625 100644 --- a/src/com/android/settings/applications/LinearColorBar.java +++ b/src/com/android/settings/applications/LinearColorBar.java @@ -16,9 +16,9 @@ import android.view.MotionEvent; import android.widget.LinearLayout; public class LinearColorBar extends LinearLayout { - static final int LEFT_COLOR = 0xff0099cc; - static final int MIDDLE_COLOR = 0xff0099cc; - static final int RIGHT_COLOR = 0xff888888; + static final int LEFT_COLOR = 0xff009688; + static final int MIDDLE_COLOR = 0xff009688; + static final int RIGHT_COLOR = 0xffced7db; static final int GRAY_COLOR = 0xff555555; static final int WHITE_COLOR = 0xffffffff; diff --git a/src/com/android/settings/applications/LinearColorPreference.java b/src/com/android/settings/applications/LinearColorPreference.java index 8d9fb72..b5f707e 100644 --- a/src/com/android/settings/applications/LinearColorPreference.java +++ b/src/com/android/settings/applications/LinearColorPreference.java @@ -25,6 +25,9 @@ public class LinearColorPreference extends Preference { float mRedRatio; float mYellowRatio; float mGreenRatio; + int mRedColor = 0xffaa5030; + int mYellowColor = 0xffaaaa30; + int mGreenColor = 0xff30aa50; int mColoredRegions = LinearColorBar.REGION_ALL; LinearColorBar.OnRegionTappedListener mOnRegionTappedListener; @@ -40,6 +43,13 @@ public class LinearColorPreference extends Preference { notifyChanged(); } + public void setColors(int red, int yellow, int green) { + mRedColor = red; + mYellowColor = yellow; + mGreenColor = green; + notifyChanged(); + } + public void setOnRegionTappedListener(LinearColorBar.OnRegionTappedListener listener) { mOnRegionTappedListener = listener; notifyChanged(); @@ -57,7 +67,7 @@ public class LinearColorPreference extends Preference { LinearColorBar colors = (LinearColorBar)view.findViewById( R.id.linear_color_bar); colors.setShowIndicator(false); - colors.setColors(0xffaa5030, 0xffaaaa30, 0xff30aa50); + colors.setColors(mRedColor, mYellowColor, mGreenColor); colors.setRatios(mRedRatio, mYellowRatio, mGreenRatio); colors.setColoredRegions(mColoredRegions); colors.setOnRegionTappedListener(mOnRegionTappedListener); diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index 0a1dcb1..e64e56e 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -29,7 +29,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; @@ -44,14 +43,12 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.preference.PreferenceActivity; +import android.os.UserManager; import android.preference.PreferenceFrameLayout; import android.provider.Settings; import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerTabStrip; import android.support.v4.view.ViewPager; -import android.text.BidiFormatter; -import android.text.format.Formatter; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -63,15 +60,18 @@ import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.ListView; -import android.widget.TextView; +import android.widget.Spinner; import com.android.internal.app.IMediaContainerService; import com.android.internal.content.PackageHelper; import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.UserSpinnerAdapter; import com.android.settings.Settings.RunningServicesActivity; import com.android.settings.Settings.StorageUseActivity; import com.android.settings.applications.ApplicationsState.AppEntry; @@ -136,11 +136,12 @@ interface AppClickListener { */ public class ManageApplications extends Fragment implements AppClickListener, DialogInterface.OnClickListener, - DialogInterface.OnDismissListener { + DialogInterface.OnDismissListener, OnItemSelectedListener { static final String TAG = "ManageApplications"; static final boolean DEBUG = false; + private static final String EXTRA_LIST_TYPE = "currentListType"; private static final String EXTRA_SORT_ORDER = "sortOrder"; private static final String EXTRA_SHOW_BACKGROUND = "showBackground"; private static final String EXTRA_DEFAULT_LIST_TYPE = "defaultListType"; @@ -196,15 +197,17 @@ public class ManageApplications extends Fragment implements private View mListContainer; + private ViewGroup mPinnedHeader; + // ListView used to display list private ListView mListView; // Custom view used to display running processes private RunningProcessesView mRunningProcessesView; - private LinearColorBar mColorBar; - private TextView mStorageChartLabel; - private TextView mUsedStorageText; - private TextView mFreeStorageText; + //private LinearColorBar mColorBar; + //private TextView mStorageChartLabel; + //private TextView mUsedStorageText; + //private TextView mFreeStorageText; private long mFreeStorage = 0, mAppStorage = 0, mTotalStorage = 0; private long mLastUsedStorage, mLastAppStorage, mLastFreeStorage; @@ -247,6 +250,14 @@ public class ManageApplications extends Fragment implements mRootView = inflater.inflate(mListType == LIST_TYPE_RUNNING ? R.layout.manage_applications_running : R.layout.manage_applications_apps, null); + mPinnedHeader = (ViewGroup) mRootView.findViewById(R.id.pinned_header); + if (mOwner.mProfileSpinnerAdapter != null) { + Spinner spinner = (Spinner) inflater.inflate(R.layout.spinner_view, null); + spinner.setAdapter(mOwner.mProfileSpinnerAdapter); + spinner.setOnItemSelectedListener(mOwner); + mPinnedHeader.addView(spinner); + mPinnedHeader.setVisibility(View.VISIBLE); + } mLoadingContainer = mRootView.findViewById(R.id.loading_container); mLoadingContainer.setVisibility(View.VISIBLE); mListContainer = mRootView.findViewById(R.id.list_container); @@ -265,17 +276,17 @@ public class ManageApplications extends Fragment implements mApplications = new ApplicationsAdapter(mApplicationsState, this, mFilter); mListView.setAdapter(mApplications); mListView.setRecyclerListener(mApplications); - mColorBar = (LinearColorBar)mListContainer.findViewById(R.id.storage_color_bar); - mStorageChartLabel = (TextView)mListContainer.findViewById(R.id.storageChartLabel); - mUsedStorageText = (TextView)mListContainer.findViewById(R.id.usedStorageText); - mFreeStorageText = (TextView)mListContainer.findViewById(R.id.freeStorageText); + //mColorBar = (LinearColorBar)mListContainer.findViewById(R.id.storage_color_bar); + //mStorageChartLabel = (TextView)mListContainer.findViewById(R.id.storageChartLabel); + //mUsedStorageText = (TextView)mListContainer.findViewById(R.id.usedStorageText); + //mFreeStorageText = (TextView)mListContainer.findViewById(R.id.freeStorageText); Utils.prepareCustomPreferencesList(contentParent, contentChild, mListView, false); if (mFilter == FILTER_APPS_SDCARD) { - mStorageChartLabel.setText(mOwner.getActivity().getText( - R.string.sd_card_storage)); + //mStorageChartLabel.setText(mOwner.getActivity().getText( + // R.string.sd_card_storage)); } else { - mStorageChartLabel.setText(mOwner.getActivity().getText( - R.string.internal_storage)); + //mStorageChartLabel.setText(mOwner.getActivity().getText( + // R.string.internal_storage)); } applyCurrentStorage(); } @@ -321,6 +332,12 @@ public class ManageApplications extends Fragment implements } } + public void release() { + if (mApplications != null) { + mApplications.release(); + } + } + void updateStorageUsage() { // Make sure a callback didn't come at an inopportune time. if (mOwner.getActivity() == null) return; @@ -385,6 +402,7 @@ public class ManageApplications extends Fragment implements if (mRootView == null) { return; } + /* if (mTotalStorage > 0) { BidiFormatter bidiFormatter = BidiFormatter.getInstance(); mColorBar.setRatios((mTotalStorage-mFreeStorage-mAppStorage)/(float)mTotalStorage, @@ -415,6 +433,7 @@ public class ManageApplications extends Fragment implements mFreeStorageText.setText(""); } } + */ } @Override @@ -448,7 +467,8 @@ public class ManageApplications extends Fragment implements // These are for keeping track of activity and spinner switch state. private boolean mActivityResumed; - + + private static final int LIST_TYPE_MISSING = -1; static final int LIST_TYPE_DOWNLOADED = 0; static final int LIST_TYPE_RUNNING = 1; static final int LIST_TYPE_SDCARD = 2; @@ -462,6 +482,8 @@ public class ManageApplications extends Fragment implements private ViewGroup mContentContainer; private View mRootView; private ViewPager mViewPager; + private UserSpinnerAdapter mProfileSpinnerAdapter; + private Context mContext; AlertDialog mResetDialog; @@ -593,6 +615,10 @@ public class ManageApplications extends Fragment implements } } + public void release() { + mSession.release(); + } + public void rebuild(int sort) { if (sort == mLastSortMode) { return; @@ -820,13 +846,14 @@ public class ManageApplications extends Fragment implements mActive.remove(view); } } - + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + mContext = getActivity(); mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication()); Intent intent = getActivity().getIntent(); String action = intent.getAction(); @@ -844,7 +871,7 @@ public class ManageApplications extends Fragment implements || className.endsWith(".StorageUse")) { mSortOrder = SORT_ORDER_SIZE; defaultListType = LIST_TYPE_ALL; - } else if (Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS.equals(action)) { + } else if (android.provider.Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS.equals(action)) { // Select the all-apps list, with the default sorting defaultListType = LIST_TYPE_ALL; } @@ -893,6 +920,9 @@ public class ManageApplications extends Fragment implements mTabs.add(tab); mNumTabs = mTabs.size(); + + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext); } @@ -911,7 +941,7 @@ public class ManageApplications extends Fragment implements mViewPager.setAdapter(adapter); mViewPager.setOnPageChangeListener(adapter); PagerTabStrip tabs = (PagerTabStrip) rootView.findViewById(R.id.tabs); - tabs.setTabIndicatorColorResource(android.R.color.holo_blue_light); + tabs.setTabIndicatorColorResource(R.color.theme_accent); // We have to do this now because PreferenceFrameLayout looks at it // only when the view is added. @@ -925,9 +955,13 @@ public class ManageApplications extends Fragment implements if (savedInstanceState == null) { // First time init: make sure view pager is showing the correct tab. - for (int i = 0; i < mTabs.size(); i++) { + int extraCurrentListType = getActivity().getIntent().getIntExtra(EXTRA_LIST_TYPE, + LIST_TYPE_MISSING); + int currentListType = (extraCurrentListType != LIST_TYPE_MISSING) + ? extraCurrentListType : mDefaultListType; + for (int i = 0; i < mNumTabs; i++) { TabInfo tab = mTabs.get(i); - if (tab.mListType == mDefaultListType) { + if (tab.mListType == currentListType) { mViewPager.setCurrentItem(i); break; } @@ -990,6 +1024,7 @@ public class ManageApplications extends Fragment implements // are no longer attached to their view hierarchy. for (int i=0; i<mTabs.size(); i++) { mTabs.get(i).detachView(); + mTabs.get(i).release(); } } @@ -1000,6 +1035,24 @@ public class ManageApplications extends Fragment implements } } + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); + if (selectedUser.getIdentifier() != UserHandle.myUserId()) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + int currentTab = mViewPager.getCurrentItem(); + intent.putExtra(EXTRA_LIST_TYPE, mTabs.get(currentTab).mListType); + mContext.startActivityAsUser(intent, selectedUser); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + // Nothing to do + } + private void updateNumTabs() { int newNum = mApplicationsState.haveDisabledApps() ? mTabs.size() : (mTabs.size()-1); if (newNum != mNumTabs) { @@ -1026,8 +1079,8 @@ public class ManageApplications extends Fragment implements Bundle args = new Bundle(); args.putString(InstalledAppDetails.ARG_PACKAGE_NAME, mCurrentPkgName); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.startPreferencePanel(InstalledAppDetails.class.getName(), args, + SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(InstalledAppDetails.class.getName(), args, R.string.application_info_label, null, this, INSTALLED_APP_DETAILS); } diff --git a/src/com/android/settings/applications/ProcStatsEntry.java b/src/com/android/settings/applications/ProcStatsEntry.java index 0821ced..8702478 100644 --- a/src/com/android/settings/applications/ProcStatsEntry.java +++ b/src/com/android/settings/applications/ProcStatsEntry.java @@ -23,6 +23,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.util.Log; +import android.util.SparseArray; import com.android.internal.app.ProcessStats; import java.util.ArrayList; @@ -109,22 +110,26 @@ public final class ProcStatsEntry implements Parcelable { // See if there is one significant package that was running here. ArrayList<ProcStatsEntry> subProcs = new ArrayList<ProcStatsEntry>(); for (int ipkg=0; ipkg<mPackages.size(); ipkg++) { - ProcessStats.PackageState pkgState = stats.mPackages.get(mPackages.get(ipkg), mUid); - if (DEBUG) Log.d(TAG, "Eval pkg of " + mName + ", pkg " - + mPackages.get(ipkg) + ":"); - if (pkgState == null) { - Log.w(TAG, "No package state found for " + mPackages.get(ipkg) + "/" - + mUid + " in process " + mName); - continue; - } - ProcessStats.ProcessState pkgProc = pkgState.mProcesses.get(mName); - if (pkgProc == null) { - Log.w(TAG, "No process " + mName + " found in package state " - + mPackages.get(ipkg) + "/" + mUid); - continue; + SparseArray<ProcessStats.PackageState> vpkgs + = stats.mPackages.get(mPackages.get(ipkg), mUid); + for (int ivers=0; ivers<vpkgs.size(); ivers++) { + ProcessStats.PackageState pkgState = vpkgs.valueAt(ivers); + if (DEBUG) Log.d(TAG, "Eval pkg of " + mName + ", pkg " + + pkgState + ":"); + if (pkgState == null) { + Log.w(TAG, "No package state found for " + mPackages.get(ipkg) + "/" + + mUid + " in process " + mName); + continue; + } + ProcessStats.ProcessState pkgProc = pkgState.mProcesses.get(mName); + if (pkgProc == null) { + Log.w(TAG, "No process " + mName + " found in package state " + + mPackages.get(ipkg) + "/" + mUid); + continue; + } + subProcs.add(new ProcStatsEntry(pkgProc, pkgState.mPackageName, totals, useUss, + weightWithTime)); } - subProcs.add(new ProcStatsEntry(pkgProc, pkgState.mPackageName, totals, useUss, - weightWithTime)); } if (subProcs.size() > 1) { Collections.sort(subProcs, compare); diff --git a/src/com/android/settings/applications/ProcessStatsDetail.java b/src/com/android/settings/applications/ProcessStatsDetail.java index 326ca7b..30f6b52 100644 --- a/src/com/android/settings/applications/ProcessStatsDetail.java +++ b/src/com/android/settings/applications/ProcessStatsDetail.java @@ -30,7 +30,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; -import android.preference.PreferenceActivity; import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.View; @@ -40,6 +39,7 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.Utils; import java.util.ArrayList; import java.util.Collections; @@ -73,11 +73,6 @@ public class ProcessStatsDetail extends Fragment implements Button.OnClickListen private ViewGroup mDetailsParent; private ViewGroup mServicesParent; - public static String makePercentString(Resources res, long amount, long total) { - final double percent = (((double)amount) / total) * 100; - return res.getString(R.string.percentage, (int) Math.round(percent)); - } - @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -117,7 +112,7 @@ public class ProcessStatsDetail extends Fragment implements Button.OnClickListen final double percentOfWeight = (((double)mEntry.mWeight) / mMaxWeight) * 100; int appLevel = (int) Math.ceil(percentOfWeight); - String appLevelText = makePercentString(getResources(), mEntry.mDuration, mTotalTime); + String appLevelText = Utils.formatPercentage(mEntry.mDuration, mTotalTime); // Set all values in the header. final TextView summary = (TextView) mRootView.findViewById(android.R.id.summary); @@ -161,7 +156,6 @@ public class ProcessStatsDetail extends Fragment implements Button.OnClickListen } private void doAction(int action) { - PreferenceActivity pa = (PreferenceActivity)getActivity(); switch (action) { case ACTION_FORCE_STOP: killProcesses(); @@ -205,7 +199,7 @@ public class ProcessStatsDetail extends Fragment implements Button.OnClickListen Formatter.formatShortFileSize(getActivity(), (mUseUss ? mEntry.mMaxUss : mEntry.mMaxPss) * 1024)); addDetailsItem(mDetailsParent, getResources().getText(R.string.process_stats_run_time), - makePercentString(getResources(), mEntry.mDuration, mTotalTime)); + Utils.formatPercentage(mEntry.mDuration, mTotalTime)); } final static Comparator<ProcStatsEntry.Service> sServiceCompare @@ -267,10 +261,8 @@ public class ProcessStatsDetail extends Fragment implements Button.OnClickListen if (tail >= 0 && tail < (label.length()-1)) { label = label.substring(tail+1); } - long duration = service.mDuration; - final double percentOfTime = (((double)duration) / mTotalTime) * 100; - addDetailsItem(mServicesParent, label, getActivity().getResources().getString( - R.string.percentage, (int) Math.ceil(percentOfTime))); + String percentage = Utils.formatPercentage(service.mDuration, mTotalTime); + addDetailsItem(mServicesParent, label, percentage); } } } diff --git a/src/com/android/settings/applications/ProcessStatsMemDetail.java b/src/com/android/settings/applications/ProcessStatsMemDetail.java new file mode 100644 index 0000000..ab7dbdb --- /dev/null +++ b/src/com/android/settings/applications/ProcessStatsMemDetail.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2014 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.applications; + +import android.app.Fragment; +import android.os.Bundle; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.android.internal.app.ProcessStats; +import com.android.settings.R; + +import static com.android.settings.Utils.prepareCustomPreferencesList; + +public class ProcessStatsMemDetail extends Fragment { + public static final String EXTRA_MEM_TIMES = "mem_times"; + public static final String EXTRA_MEM_STATE_WEIGHTS = "mem_state_weights"; + public static final String EXTRA_MEM_CACHED_WEIGHT = "mem_cached_weight"; + public static final String EXTRA_MEM_FREE_WEIGHT = "mem_free_weight"; + public static final String EXTRA_MEM_ZRAM_WEIGHT = "mem_zram_weight"; + public static final String EXTRA_MEM_KERNEL_WEIGHT = "mem_kernel_weight"; + public static final String EXTRA_MEM_NATIVE_WEIGHT = "mem_native_weight"; + public static final String EXTRA_MEM_TOTAL_WEIGHT = "mem_total_weight"; + public static final String EXTRA_USE_USS = "use_uss"; + public static final String EXTRA_TOTAL_TIME = "total_time"; + + long[] mMemTimes; + double[] mMemStateWeights; + double mMemCachedWeight; + double mMemFreeWeight; + double mMemZRamWeight; + double mMemKernelWeight; + double mMemNativeWeight; + double mMemTotalWeight; + boolean mUseUss; + long mTotalTime; + + private View mRootView; + private ViewGroup mMemStateParent; + private ViewGroup mMemUseParent; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + final Bundle args = getArguments(); + mMemTimes = args.getLongArray(EXTRA_MEM_TIMES); + mMemStateWeights = args.getDoubleArray(EXTRA_MEM_STATE_WEIGHTS); + mMemCachedWeight = args.getDouble(EXTRA_MEM_CACHED_WEIGHT); + mMemFreeWeight = args.getDouble(EXTRA_MEM_FREE_WEIGHT); + mMemZRamWeight = args.getDouble(EXTRA_MEM_ZRAM_WEIGHT); + mMemKernelWeight = args.getDouble(EXTRA_MEM_KERNEL_WEIGHT); + mMemNativeWeight = args.getDouble(EXTRA_MEM_NATIVE_WEIGHT); + mMemTotalWeight = args.getDouble(EXTRA_MEM_TOTAL_WEIGHT); + mUseUss = args.getBoolean(EXTRA_USE_USS); + mTotalTime = args.getLong(EXTRA_TOTAL_TIME); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.process_stats_mem_details, container, false); + prepareCustomPreferencesList(container, view, view, false); + + mRootView = view; + createDetails(); + return view; + } + + @Override + public void onPause() { + super.onPause(); + } + + private void createDetails() { + mMemStateParent = (ViewGroup)mRootView.findViewById(R.id.mem_state); + mMemUseParent = (ViewGroup)mRootView.findViewById(R.id.mem_use); + + fillMemStateSection(); + fillMemUseSection(); + } + + private void addDetailsItem(ViewGroup parent, CharSequence title, + float level, CharSequence value) { + LayoutInflater inflater = getActivity().getLayoutInflater(); + ViewGroup item = (ViewGroup) inflater.inflate(R.layout.app_percentage_item, + null); + parent.addView(item); + item.findViewById(android.R.id.icon).setVisibility(View.GONE); + TextView titleView = (TextView) item.findViewById(android.R.id.title); + TextView valueView = (TextView) item.findViewById(android.R.id.text1); + titleView.setText(title); + valueView.setText(value); + ProgressBar progress = (ProgressBar) item.findViewById(android.R.id.progress); + progress.setProgress(Math.round(level*100)); + } + + private void fillMemStateSection() { + CharSequence[] labels = getResources().getTextArray(R.array.proc_stats_memory_states); + for (int i=0; i<ProcessStats.ADJ_MEM_FACTOR_COUNT; i++) { + if (mMemTimes[i] > 0) { + float level = ((float)mMemTimes[i])/mTotalTime; + addDetailsItem(mMemStateParent, labels[i], level, + Formatter.formatShortElapsedTime(getActivity(), mMemTimes[i])); + } + } + } + + private void addMemUseDetailsItem(ViewGroup parent, CharSequence title, double weight) { + if (weight > 0) { + float level = (float)(weight/mMemTotalWeight); + String value = Formatter.formatShortFileSize(getActivity(), + (long)((weight * 1024) / mTotalTime)); + addDetailsItem(parent, title, level, value); + } + } + + private void fillMemUseSection() { + CharSequence[] labels = getResources().getTextArray(R.array.proc_stats_process_states); + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_kernel_type), mMemKernelWeight); + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_zram_type), mMemZRamWeight); + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_native_type), mMemNativeWeight); + for (int i=0; i<ProcessStats.STATE_COUNT; i++) { + addMemUseDetailsItem(mMemUseParent, labels[i], mMemStateWeights[i]); + } + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_kernel_cache_type), mMemCachedWeight); + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_free_type), mMemFreeWeight); + addMemUseDetailsItem(mMemUseParent, + getResources().getText(R.string.mem_use_total), mMemTotalWeight); + } +} diff --git a/src/com/android/settings/applications/ProcessStatsPreference.java b/src/com/android/settings/applications/ProcessStatsPreference.java index bf2676d..adf80e5 100644 --- a/src/com/android/settings/applications/ProcessStatsPreference.java +++ b/src/com/android/settings/applications/ProcessStatsPreference.java @@ -21,20 +21,38 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.preference.Preference; import android.text.format.Formatter; +import android.util.AttributeSet; import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.Utils; public class ProcessStatsPreference extends Preference { - private final ProcStatsEntry mEntry; + private ProcStatsEntry mEntry; private int mProgress; private CharSequence mProgressText; - public ProcessStatsPreference(Context context, Drawable icon, ProcStatsEntry entry) { - super(context); + public ProcessStatsPreference(Context context) { + this(context, null); + } + + public ProcessStatsPreference(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ProcessStatsPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ProcessStatsPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setLayoutResource(R.layout.preference_app_percentage); + } + + public void init(Drawable icon, ProcStatsEntry entry) { mEntry = entry; - setLayoutResource(R.layout.app_percentage_item); setIcon(icon != null ? icon : new ColorDrawable(0)); } @@ -44,8 +62,7 @@ public class ProcessStatsPreference extends Preference { public void setPercent(double percentOfWeight, double percentOfTime) { mProgress = (int) Math.ceil(percentOfWeight); - mProgressText = getContext().getResources().getString( - R.string.percentage, (int) Math.round(percentOfTime)); + mProgressText = Utils.formatPercentage((int) percentOfTime); notifyChanged(); } diff --git a/src/com/android/settings/applications/ProcessStatsUi.java b/src/com/android/settings/applications/ProcessStatsUi.java index 8322ea3..30a8817 100644 --- a/src/com/android/settings/applications/ProcessStatsUi.java +++ b/src/com/android/settings/applications/ProcessStatsUi.java @@ -16,6 +16,7 @@ package com.android.settings.applications; +import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; @@ -25,10 +26,10 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.text.format.Formatter; import android.util.Log; import android.util.SparseArray; import android.util.TimeUtils; @@ -39,8 +40,10 @@ import android.view.SubMenu; import com.android.internal.app.IProcessStats; import com.android.internal.app.ProcessMap; import com.android.internal.app.ProcessStats; +import com.android.internal.util.MemInfoReader; import com.android.settings.R; -import com.android.settings.fuelgauge.Utils; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import java.io.IOException; import java.io.InputStream; @@ -112,6 +115,15 @@ public class ProcessStatsUi extends PreferenceFragment long mMaxWeight; long mTotalTime; + long[] mMemTimes = new long[ProcessStats.ADJ_MEM_FACTOR_COUNT]; + double[] mMemStateWeights = new double[ProcessStats.STATE_COUNT]; + double mMemCachedWeight; + double mMemFreeWeight; + double mMemZRamWeight; + double mMemKernelWeight; + double mMemNativeWeight; + double mMemTotalWeight; + // The actual duration value to use for each duration option. Note these // are lower than the actual duration, since our durations are computed in // batches of 3 hours so we want to allow the time we use to be slightly @@ -182,6 +194,24 @@ public class ProcessStatsUi extends PreferenceFragment @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference instanceof LinearColorPreference) { + Bundle args = new Bundle(); + args.putLongArray(ProcessStatsMemDetail.EXTRA_MEM_TIMES, mMemTimes); + args.putDoubleArray(ProcessStatsMemDetail.EXTRA_MEM_STATE_WEIGHTS, mMemStateWeights); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_CACHED_WEIGHT, mMemCachedWeight); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_FREE_WEIGHT, mMemFreeWeight); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_ZRAM_WEIGHT, mMemZRamWeight); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_KERNEL_WEIGHT, mMemKernelWeight); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_NATIVE_WEIGHT, mMemNativeWeight); + args.putDouble(ProcessStatsMemDetail.EXTRA_MEM_TOTAL_WEIGHT, mMemTotalWeight); + args.putBoolean(ProcessStatsMemDetail.EXTRA_USE_USS, mUseUss); + args.putLong(ProcessStatsMemDetail.EXTRA_TOTAL_TIME, mTotalTime); + ((SettingsActivity) getActivity()).startPreferencePanel( + ProcessStatsMemDetail.class.getName(), args, R.string.mem_details_title, + null, null, 0); + return true; + } + if (!(preference instanceof ProcessStatsPreference)) { return false; } @@ -192,7 +222,7 @@ public class ProcessStatsUi extends PreferenceFragment args.putBoolean(ProcessStatsDetail.EXTRA_USE_USS, mUseUss); args.putLong(ProcessStatsDetail.EXTRA_MAX_WEIGHT, mMaxWeight); args.putLong(ProcessStatsDetail.EXTRA_TOTAL_TIME, mTotalTime); - ((PreferenceActivity) getActivity()).startPreferencePanel( + ((SettingsActivity) getActivity()).startPreferencePanel( ProcessStatsDetail.class.getName(), args, R.string.details_title, null, null, 0); return super.onPreferenceTreeClick(preferenceScreen, preference); @@ -374,10 +404,11 @@ public class ProcessStatsUi extends PreferenceFragment mAppListGroup.removeAll(); mAppListGroup.setOrderingAsAdded(false); + final long elapsedTime = mStats.mTimePeriodEndRealtime-mStats.mTimePeriodStartRealtime; + mMemStatusPref.setOrder(-2); mAppListGroup.addPreference(mMemStatusPref); - String durationString = Utils.formatElapsedTime(getActivity(), - mStats.mTimePeriodEndRealtime-mStats.mTimePeriodStartRealtime, false); + String durationString = Utils.formatElapsedTime(getActivity(), elapsedTime, false); CharSequence memString; CharSequence[] memStatesStr = getResources().getTextArray(R.array.ram_states); if (mMemState >= 0 && mMemState < memStatesStr.length) { @@ -408,11 +439,13 @@ public class ProcessStatsUi extends PreferenceFragment mStats.mMemFactor, mStats.mStartTime, now); if (DEBUG) Log.d(TAG, "Total time of stats: " + makeDuration(mTotalTime)); - long[] memTimes = new long[ProcessStats.ADJ_MEM_FACTOR_COUNT]; + for (int i=0; i<mMemTimes.length; i++) { + mMemTimes[i] = 0; + } for (int iscreen=0; iscreen<ProcessStats.ADJ_COUNT; iscreen+=ProcessStats.ADJ_SCREEN_MOD) { for (int imem=0; imem<ProcessStats.ADJ_MEM_FACTOR_COUNT; imem++) { int state = imem+iscreen; - memTimes[imem] += mStats.mMemFactorDurations[state]; + mMemTimes[imem] += mStats.mMemFactorDurations[state]; } } @@ -421,31 +454,155 @@ public class ProcessStatsUi extends PreferenceFragment LinearColorPreference colors = new LinearColorPreference(getActivity()); colors.setOrder(-1); - colors.setOnRegionTappedListener(this); switch (mMemRegion) { case LinearColorBar.REGION_RED: - colors.setColoredRegions(LinearColorBar.REGION_RED); - memTotalTime = memTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL]; + memTotalTime = mMemTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL]; memStates = RED_MEM_STATES; break; case LinearColorBar.REGION_YELLOW: - colors.setColoredRegions(LinearColorBar.REGION_RED - | LinearColorBar.REGION_YELLOW); - memTotalTime = memTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL] - + memTimes[ProcessStats.ADJ_MEM_FACTOR_LOW] - + memTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]; + memTotalTime = mMemTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL] + + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_LOW] + + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]; memStates = YELLOW_MEM_STATES; break; default: - colors.setColoredRegions(LinearColorBar.REGION_ALL); memTotalTime = mTotalTime; memStates = ProcessStats.ALL_MEM_ADJ; break; } - colors.setRatios(memTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL] / (float)mTotalTime, - (memTimes[ProcessStats.ADJ_MEM_FACTOR_LOW] - + memTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]) / (float)mTotalTime, - memTimes[ProcessStats.ADJ_MEM_FACTOR_NORMAL] / (float)mTotalTime); + colors.setColoredRegions(LinearColorBar.REGION_RED); + + // Compute memory badness for chart color. + int[] badColors = com.android.settings.Utils.BADNESS_COLORS; + long timeGood = mMemTimes[ProcessStats.ADJ_MEM_FACTOR_NORMAL]; + timeGood += (mMemTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]*2)/3; + timeGood += mMemTimes[ProcessStats.ADJ_MEM_FACTOR_LOW]/3; + float memBadness = ((float)timeGood)/mTotalTime; + int badnessColor = badColors[1 + Math.round(memBadness*(badColors.length-2))]; + colors.setColors(badnessColor, badnessColor, badnessColor); + + // We are now going to scale the mMemTimes to match the total elapsed time. + // These are in uptime, so they will often be smaller than the elapsed time, + // but if the user taps on the bar we want to show the times to them. It is confusing + // to see them be smaller than what we told them the measured duration is, so just + // scaling them up with make things look reasonable with them none the wiser. + for (int i=0; i<ProcessStats.ADJ_MEM_FACTOR_COUNT; i++) { + mMemTimes[i] = (long)((mMemTimes[i]*(double)elapsedTime)/mTotalTime); + } + + ProcessStats.TotalMemoryUseCollection totalMem = new ProcessStats.TotalMemoryUseCollection( + ProcessStats.ALL_SCREEN_ADJ, memStates); + mStats.computeTotalMemoryUse(totalMem, now); + double freeWeight = totalMem.sysMemFreeWeight + totalMem.sysMemCachedWeight; + double usedWeight = totalMem.sysMemKernelWeight + totalMem.sysMemNativeWeight + + totalMem.sysMemZRamWeight; + double backgroundWeight = 0, persBackgroundWeight = 0; + mMemCachedWeight = totalMem.sysMemCachedWeight; + mMemFreeWeight = totalMem.sysMemFreeWeight; + mMemZRamWeight = totalMem.sysMemZRamWeight; + mMemKernelWeight = totalMem.sysMemKernelWeight; + mMemNativeWeight = totalMem.sysMemNativeWeight; + for (int i=0; i<ProcessStats.STATE_COUNT; i++) { + if (i == ProcessStats.STATE_SERVICE_RESTARTING) { + // These don't really run. + mMemStateWeights[i] = 0; + } else { + mMemStateWeights[i] = totalMem.processStateWeight[i]; + if (i >= ProcessStats.STATE_HOME) { + freeWeight += totalMem.processStateWeight[i]; + } else { + usedWeight += totalMem.processStateWeight[i]; + } + if (i >= ProcessStats.STATE_IMPORTANT_FOREGROUND) { + backgroundWeight += totalMem.processStateWeight[i]; + persBackgroundWeight += totalMem.processStateWeight[i]; + } + if (i == ProcessStats.STATE_PERSISTENT) { + persBackgroundWeight += totalMem.processStateWeight[i]; + } + } + } + if (DEBUG) { + Log.i(TAG, "Used RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)((usedWeight * 1024) / memTotalTime))); + Log.i(TAG, "Free RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)((freeWeight * 1024) / memTotalTime))); + Log.i(TAG, "Total RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)(((freeWeight+usedWeight) * 1024) / memTotalTime))); + Log.i(TAG, "Background+Cached RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)((backgroundWeight * 1024) / memTotalTime))); + } + mMemTotalWeight = freeWeight + usedWeight; + + // For computing the ratio to show, we want to count the baseline cached RAM we + // need (at which point we start killing processes) as used RAM, so that if we + // reach the point of thrashing due to no RAM for any background processes we + // report that as RAM being full. To do this, we need to first convert the weights + // back to actual RAM... and since the RAM values we compute here won't exactly + // match the real physical RAM, scale those to the actual physical RAM. No problem! + double usedRam = (usedWeight*1024)/memTotalTime; + double freeRam = (freeWeight*1024)/memTotalTime; + double totalRam = usedRam + freeRam; + MemInfoReader memReader = new MemInfoReader(); + memReader.readMemInfo(); + double realTotalRam = memReader.getTotalSize(); + double totalScale = realTotalRam / totalRam; + double realUsedRam = usedRam * totalScale; + double realFreeRam = freeRam * totalScale; + if (DEBUG) { + Log.i(TAG, "Scaled Used RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)realUsedRam)); + Log.i(TAG, "Scaled Free RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)realFreeRam)); + } + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + ((ActivityManager)getActivity().getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo( + memInfo); + if (memInfo.hiddenAppThreshold >= realFreeRam) { + realUsedRam = realFreeRam; + realFreeRam = 0; + } else { + realUsedRam += memInfo.hiddenAppThreshold; + realFreeRam -= memInfo.hiddenAppThreshold; + } + if (DEBUG) { + Log.i(TAG, "Adj Scaled Used RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)realUsedRam)); + Log.i(TAG, "Adj Scaled Free RAM: " + Formatter.formatShortFileSize(getActivity(), + (long)realFreeRam)); + } + + float usedRatio = (float)(realUsedRam/(realFreeRam+realUsedRam)); + colors.setRatios(usedRatio, 0, 1-usedRatio); + + if (false) { + colors.setOnRegionTappedListener(this); + switch (mMemRegion) { + case LinearColorBar.REGION_RED: + colors.setColoredRegions(LinearColorBar.REGION_RED); + memTotalTime = mMemTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL]; + memStates = RED_MEM_STATES; + break; + case LinearColorBar.REGION_YELLOW: + colors.setColoredRegions(LinearColorBar.REGION_RED + | LinearColorBar.REGION_YELLOW); + memTotalTime = mMemTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL] + + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_LOW] + + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]; + memStates = YELLOW_MEM_STATES; + break; + default: + colors.setColoredRegions(LinearColorBar.REGION_ALL); + memTotalTime = mTotalTime; + memStates = ProcessStats.ALL_MEM_ADJ; + break; + } + colors.setRatios(mMemTimes[ProcessStats.ADJ_MEM_FACTOR_CRITICAL] / (float)mTotalTime, + (mMemTimes[ProcessStats.ADJ_MEM_FACTOR_LOW] + + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_MODERATE]) / (float)mTotalTime, + mMemTimes[ProcessStats.ADJ_MEM_FACTOR_NORMAL] / (float)mTotalTime); + } + mAppListGroup.addPreference(colors); ProcessStats.ProcessDataCollection totals = new ProcessStats.ProcessDataCollection( @@ -466,33 +623,36 @@ public class ProcessStatsUi extends PreferenceFragment final ProcessMap<ProcStatsEntry> entriesMap = new ProcessMap<ProcStatsEntry>(); for (int ipkg=0, N=mStats.mPackages.getMap().size(); ipkg<N; ipkg++) { - final SparseArray<ProcessStats.PackageState> pkgUids + final SparseArray<SparseArray<ProcessStats.PackageState>> pkgUids = mStats.mPackages.getMap().valueAt(ipkg); for (int iu=0; iu<pkgUids.size(); iu++) { - final ProcessStats.PackageState st = pkgUids.valueAt(iu); - for (int iproc=0; iproc<st.mProcesses.size(); iproc++) { - final ProcessStats.ProcessState pkgProc = st.mProcesses.valueAt(iproc); - final ProcessStats.ProcessState proc = mStats.mProcesses.get(pkgProc.mName, - pkgProc.mUid); - if (proc == null) { - Log.w(TAG, "No process found for pkg " + st.mPackageName - + "/" + st.mUid + " proc name " + pkgProc.mName); - continue; - } - ProcStatsEntry ent = entriesMap.get(proc.mName, proc.mUid); - if (ent == null) { - ent = new ProcStatsEntry(proc, st.mPackageName, totals, mUseUss, - mStatsType == MENU_TYPE_BACKGROUND); - if (ent.mDuration > 0) { - if (DEBUG) Log.d(TAG, "Adding proc " + proc.mName + "/" - + proc.mUid + ": time=" + makeDuration(ent.mDuration) + " (" - + ((((double)ent.mDuration) / memTotalTime) * 100) + "%)" - + " pss=" + ent.mAvgPss); - entriesMap.put(proc.mName, proc.mUid, ent); - entries.add(ent); + final SparseArray<ProcessStats.PackageState> vpkgs = pkgUids.valueAt(iu); + for (int iv=0; iv<vpkgs.size(); iv++) { + final ProcessStats.PackageState st = vpkgs.valueAt(iv); + for (int iproc=0; iproc<st.mProcesses.size(); iproc++) { + final ProcessStats.ProcessState pkgProc = st.mProcesses.valueAt(iproc); + final ProcessStats.ProcessState proc = mStats.mProcesses.get(pkgProc.mName, + pkgProc.mUid); + if (proc == null) { + Log.w(TAG, "No process found for pkg " + st.mPackageName + + "/" + st.mUid + " proc name " + pkgProc.mName); + continue; + } + ProcStatsEntry ent = entriesMap.get(proc.mName, proc.mUid); + if (ent == null) { + ent = new ProcStatsEntry(proc, st.mPackageName, totals, mUseUss, + mStatsType == MENU_TYPE_BACKGROUND); + if (ent.mDuration > 0) { + if (DEBUG) Log.d(TAG, "Adding proc " + proc.mName + "/" + + proc.mUid + ": time=" + makeDuration(ent.mDuration) + " (" + + ((((double)ent.mDuration) / memTotalTime) * 100) + "%)" + + " pss=" + ent.mAvgPss); + entriesMap.put(proc.mName, proc.mUid, ent); + entries.add(ent); + } + } else { + ent.addPackage(st.mPackageName); } - } else { - ent.addPackage(st.mPackageName); } } } @@ -503,21 +663,25 @@ public class ProcessStatsUi extends PreferenceFragment // Add in service info. if (mStatsType == MENU_TYPE_BACKGROUND) { for (int ip=0, N=mStats.mPackages.getMap().size(); ip<N; ip++) { - SparseArray<ProcessStats.PackageState> uids = mStats.mPackages.getMap().valueAt(ip); + SparseArray<SparseArray<ProcessStats.PackageState>> uids + = mStats.mPackages.getMap().valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { - ProcessStats.PackageState ps = uids.valueAt(iu); - for (int is=0, NS=ps.mServices.size(); is<NS; is++) { - ProcessStats.ServiceState ss = ps.mServices.valueAt(is); - if (ss.mProcessName != null) { - ProcStatsEntry ent = entriesMap.get(ss.mProcessName, uids.keyAt(iu)); - if (ent != null) { - if (DEBUG) Log.d(TAG, "Adding service " + ps.mPackageName - + "/" + ss.mName + "/" + uids.keyAt(iu) + " to proc " - + ss.mProcessName); - ent.addService(ss); - } else { - Log.w(TAG, "No process " + ss.mProcessName + "/" + uids.keyAt(iu) - + " for service " + ss.mName); + SparseArray<ProcessStats.PackageState> vpkgs = uids.valueAt(iu); + for (int iv=0; iv<vpkgs.size(); iv++) { + ProcessStats.PackageState ps = vpkgs.valueAt(iv); + for (int is=0, NS=ps.mServices.size(); is<NS; is++) { + ProcessStats.ServiceState ss = ps.mServices.valueAt(is); + if (ss.mProcessName != null) { + ProcStatsEntry ent = entriesMap.get(ss.mProcessName, uids.keyAt(iu)); + if (ent != null) { + if (DEBUG) Log.d(TAG, "Adding service " + ps.mPackageName + + "/" + ss.mName + "/" + uids.keyAt(iu) + " to proc " + + ss.mProcessName); + ent.addService(ss); + } else { + Log.w(TAG, "No process " + ss.mProcessName + "/" + uids.keyAt(iu) + + " for service " + ss.mName); + } } } } @@ -559,20 +723,39 @@ public class ProcessStatsUi extends PreferenceFragment maxWeight = proc.mWeight; } } - mMaxWeight = maxWeight; + if (mStatsType == MENU_TYPE_BACKGROUND) { + mMaxWeight = (long)(mShowSystem ? persBackgroundWeight : backgroundWeight); + if (mMaxWeight < maxWeight) { + mMaxWeight = maxWeight; + } + if (DEBUG) { + Log.i(TAG, "Bar max RAM: " + Formatter.formatShortFileSize(getActivity(), + (mMaxWeight * 1024) / memTotalTime)); + } + } else { + mMaxWeight = maxWeight; + } if (DEBUG) Log.d(TAG, "-------------------- BUILDING UI"); - for (int i=0, N=(entries != null ? entries.size() : 0); i<N; i++) { - ProcStatsEntry proc = entries.get(i); - final double percentOfWeight = (((double)proc.mWeight) / maxWeight) * 100; + // Find where we should stop. Because we have two properties we are looking at, + // we need to go from the back looking for the first place either holds. + int end = entries != null ? entries.size()-1 : -1; + while (end >= 0) { + ProcStatsEntry proc = entries.get(end); + final double percentOfWeight = (((double)proc.mWeight) / mMaxWeight) * 100; final double percentOfTime = (((double)proc.mDuration) / memTotalTime) * 100; - if (percentOfWeight < 1 && percentOfTime < 33) { - if (DEBUG) Log.d(TAG, "Skipping " + proc.mName + " weight=" + percentOfWeight - + " time=" + percentOfTime); - continue; + if (percentOfWeight >= 1 || percentOfTime >= 25) { + break; } - ProcessStatsPreference pref = new ProcessStatsPreference(getActivity(), null, proc); + end--; + } + for (int i=0; i<=end; i++) { + ProcStatsEntry proc = entries.get(i); + final double percentOfWeight = (((double)proc.mWeight) / mMaxWeight) * 100; + final double percentOfTime = (((double)proc.mDuration) / memTotalTime) * 100; + ProcessStatsPreference pref = new ProcessStatsPreference(getActivity()); + pref.init(null, proc); proc.evaluateTargetPackage(pm, mStats, totals, sEntryCompare, mUseUss, mStatsType == MENU_TYPE_BACKGROUND); proc.retrieveUiData(pm); @@ -583,6 +766,16 @@ public class ProcessStatsUi extends PreferenceFragment pref.setOrder(i); pref.setPercent(percentOfWeight, percentOfTime); mAppListGroup.addPreference(pref); + if (mStatsType == MENU_TYPE_BACKGROUND) { + if (DEBUG) { + Log.i(TAG, "App " + proc.mUiLabel + ": weightedRam=" + + Formatter.formatShortFileSize(getActivity(), + (proc.mWeight * 1024) / memTotalTime) + + ", avgRam=" + Formatter.formatShortFileSize(getActivity(), + (proc.mAvgPss*1024))); + } + + } if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST+1)) { if (DEBUG) Log.d(TAG, "Done with UI, hit item limit!"); break; diff --git a/src/com/android/settings/applications/RunningProcessesView.java b/src/com/android/settings/applications/RunningProcessesView.java index 8eb0496..13d9655 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 android.content.res.Resources; import android.text.BidiFormatter; import com.android.internal.util.MemInfoReader; import com.android.settings.R; @@ -28,7 +29,6 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.SystemClock; import android.os.UserHandle; -import android.preference.PreferenceActivity; import android.text.format.DateUtils; import android.text.format.Formatter; import android.util.AttributeSet; @@ -42,6 +42,7 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.AbsListView.RecyclerListener; +import com.android.settings.SettingsActivity; import java.util.ArrayList; import java.util.Collections; @@ -71,19 +72,22 @@ public class RunningProcessesView extends FrameLayout RunningState.BaseItem mCurSelected; ListView mListView; + View mHeader; ServiceListAdapter mAdapter; LinearColorBar mColorBar; + TextView mBackgroundProcessPrefix; + TextView mAppsProcessPrefix; + TextView mForegroundProcessPrefix; TextView mBackgroundProcessText; + TextView mAppsProcessText; TextView mForegroundProcessText; - - int mLastNumBackgroundProcesses = -1; - int mLastNumForegroundProcesses = -1; - int mLastNumServiceProcesses = -1; - long mLastBackgroundProcessMemory = -1; - long mLastForegroundProcessMemory = -1; - long mLastServiceProcessMemory = -1; - long mLastAvailMemory = -1; - + + long mCurTotalRam = -1; + long mCurHighRam = -1; // "System" or "Used" + long mCurMedRam = -1; // "Apps" or "Cached" + long mCurLowRam = -1; // "Free" + boolean mCurShowCached = false; + Dialog mCurDialog; MemInfoReader mMemInfoReader = new MemInfoReader(); @@ -95,7 +99,7 @@ public class RunningProcessesView extends FrameLayout ViewHolder mHolder; long mFirstRunTime; boolean mSetBackground; - + void updateTime(Context context, StringBuilder builder) { TextView uptimeView = null; @@ -123,7 +127,7 @@ public class RunningProcessesView extends FrameLayout uptimeView = mHolder.uptime; } } - + if (uptimeView != null) { mSetBackground = false; if (mFirstRunTime >= 0) { @@ -225,8 +229,7 @@ public class RunningProcessesView extends FrameLayout mShowBackground = showBackground; mState.setWatchingBackgroundItems(showBackground); refreshItems(); - notifyDataSetChanged(); - mColorBar.setShowingGreen(mShowBackground); + refreshUi(true); } } @@ -316,7 +319,7 @@ public class RunningProcessesView extends FrameLayout void refreshUi(boolean dataChanged) { if (dataChanged) { - ServiceListAdapter adapter = (ServiceListAdapter)(mListView.getAdapter()); + ServiceListAdapter adapter = mAdapter; adapter.refreshItems(); adapter.notifyDataSetChanged(); } @@ -326,56 +329,71 @@ public class RunningProcessesView extends FrameLayout mDataAvail = null; } + mMemInfoReader.readMemInfo(); + + /* // This is the amount of available memory until we start killing // background services. - 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 - || mLastAvailMemory != availMem) { - mLastNumBackgroundProcesses = mState.mNumBackgroundProcesses; - mLastBackgroundProcessMemory = mState.mBackgroundProcessMemory; - mLastAvailMemory = availMem; - long freeMem = mLastAvailMemory + mLastBackgroundProcessMemory; + if (mCurShowCached != mAdapter.mShowBackground) { + mCurShowCached = mAdapter.mShowBackground; + if (mCurShowCached) { + mForegroundProcessPrefix.setText(getResources().getText( + R.string.running_processes_header_used_prefix)); + mAppsProcessPrefix.setText(getResources().getText( + R.string.running_processes_header_cached_prefix)); + } else { + mForegroundProcessPrefix.setText(getResources().getText( + R.string.running_processes_header_system_prefix)); + mAppsProcessPrefix.setText(getResources().getText( + R.string.running_processes_header_apps_prefix)); + } + } + + final long totalRam = mMemInfoReader.getTotalSize(); + final long medRam; + final long lowRam; + if (mCurShowCached) { + lowRam = mMemInfoReader.getFreeSize() + mMemInfoReader.getCachedSize(); + medRam = mState.mBackgroundProcessMemory; + } else { + lowRam = mMemInfoReader.getFreeSize() + mMemInfoReader.getCachedSize() + + mState.mBackgroundProcessMemory; + medRam = mState.mServiceProcessMemory; + + } + final long highRam = totalRam - medRam - lowRam; + + if (mCurTotalRam != totalRam || mCurHighRam != highRam || mCurMedRam != medRam + || mCurLowRam != lowRam) { + mCurTotalRam = totalRam; + mCurHighRam = highRam; + mCurMedRam = medRam; + mCurLowRam = lowRam; BidiFormatter bidiFormatter = BidiFormatter.getInstance(); String sizeStr = bidiFormatter.unicodeWrap( - Formatter.formatShortFileSize(getContext(), freeMem)); + Formatter.formatShortFileSize(getContext(), lowRam)); mBackgroundProcessText.setText(getResources().getString( - R.string.service_background_processes, sizeStr)); + R.string.running_processes_header_ram, sizeStr)); sizeStr = bidiFormatter.unicodeWrap( - Formatter.formatShortFileSize(getContext(), - mMemInfoReader.getTotalSize() - freeMem)); - mForegroundProcessText.setText(getResources().getString( - R.string.service_foreground_processes, sizeStr)); - } - if (mLastNumForegroundProcesses != mState.mNumForegroundProcesses - || mLastForegroundProcessMemory != mState.mForegroundProcessMemory - || mLastNumServiceProcesses != mState.mNumServiceProcesses - || mLastServiceProcessMemory != mState.mServiceProcessMemory) { - mLastNumForegroundProcesses = mState.mNumForegroundProcesses; - mLastForegroundProcessMemory = mState.mForegroundProcessMemory; - mLastNumServiceProcesses = mState.mNumServiceProcesses; - mLastServiceProcessMemory = mState.mServiceProcessMemory; - /* - String sizeStr = Formatter.formatShortFileSize(getContext(), - mLastForegroundProcessMemory + mLastServiceProcessMemory); + Formatter.formatShortFileSize(getContext(), medRam)); + mAppsProcessText.setText(getResources().getString( + R.string.running_processes_header_ram, sizeStr)); + sizeStr = bidiFormatter.unicodeWrap( + Formatter.formatShortFileSize(getContext(), highRam)); mForegroundProcessText.setText(getResources().getString( - R.string.service_foreground_processes, sizeStr)); - */ + R.string.running_processes_header_ram, sizeStr)); + mColorBar.setRatios(highRam/(float)totalRam, + medRam/(float)totalRam, + lowRam/(float)totalRam); } - - float totalMem = mMemInfoReader.getTotalSize(); - float totalShownMem = availMem + mLastBackgroundProcessMemory - + mLastServiceProcessMemory; - mColorBar.setRatios((totalMem-totalShownMem)/totalMem, - mLastServiceProcessMemory/totalMem, - mLastBackgroundProcessMemory/totalMem); } } @@ -397,9 +415,9 @@ public class RunningProcessesView extends FrameLayout } args.putInt(RunningServiceDetails.KEY_USER_ID, mi.mUserId); args.putBoolean(RunningServiceDetails.KEY_BACKGROUND, mAdapter.mShowBackground); - - PreferenceActivity pa = (PreferenceActivity)mOwner.getActivity(); - pa.startPreferencePanel(RunningServiceDetails.class.getName(), args, + + SettingsActivity sa = (SettingsActivity) mOwner.getActivity(); + sa.startPreferencePanel(RunningServiceDetails.class.getName(), args, R.string.runningservicedetails_settings_title, null, null, 0); } } @@ -428,27 +446,19 @@ public class RunningProcessesView extends FrameLayout mListView.setRecyclerListener(this); mAdapter = new ServiceListAdapter(mState); mListView.setAdapter(mAdapter); - mColorBar = (LinearColorBar)findViewById(R.id.color_bar); - mBackgroundProcessText = (TextView)findViewById(R.id.backgroundText); - mBackgroundProcessText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.setShowBackground(true); - if (mOwner != null) { - mOwner.getActivity().invalidateOptionsMenu(); - } - } - }); - mForegroundProcessText = (TextView)findViewById(R.id.foregroundText); - mForegroundProcessText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.setShowBackground(false); - if (mOwner != null) { - mOwner.getActivity().invalidateOptionsMenu(); - } - } - }); + mHeader = inflater.inflate(R.layout.running_processes_header, null); + mListView.addHeaderView(mHeader, null, false /* set as not selectable */); + mColorBar = (LinearColorBar)mHeader.findViewById(R.id.color_bar); + Resources res = getResources(); + mColorBar.setColors(res.getColor(R.color.running_processes_system_ram), + res.getColor(R.color.running_processes_apps_ram), + res.getColor(R.color.running_processes_free_ram)); + mBackgroundProcessPrefix = (TextView)mHeader.findViewById(R.id.freeSizePrefix); + mAppsProcessPrefix = (TextView)mHeader.findViewById(R.id.appsSizePrefix); + mForegroundProcessPrefix = (TextView)mHeader.findViewById(R.id.systemSizePrefix); + mBackgroundProcessText = (TextView)mHeader.findViewById(R.id.freeSize); + mAppsProcessText = (TextView)mHeader.findViewById(R.id.appsSize); + mForegroundProcessText = (TextView)mHeader.findViewById(R.id.systemSize); ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); mAm.getMemoryInfo(memInfo); diff --git a/src/com/android/settings/applications/RunningServiceDetails.java b/src/com/android/settings/applications/RunningServiceDetails.java index 73547f1..45cad3d 100644 --- a/src/com/android/settings/applications/RunningServiceDetails.java +++ b/src/com/android/settings/applications/RunningServiceDetails.java @@ -583,7 +583,6 @@ public class RunningServiceDetails extends Fragment return new AlertDialog.Builder(getActivity()) .setTitle(getActivity().getString(R.string.runningservicedetails_stop_dlg_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getActivity().getString(R.string.runningservicedetails_stop_dlg_text)) .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { diff --git a/src/com/android/settings/applications/RunningState.java b/src/com/android/settings/applications/RunningState.java index 94ab11d..1019abc 100644 --- a/src/com/android/settings/applications/RunningState.java +++ b/src/com/android/settings/applications/RunningState.java @@ -17,7 +17,7 @@ package com.android.settings.applications; import com.android.settings.R; -import com.android.settings.users.UserUtils; +import com.android.settings.Utils; import android.app.ActivityManager; import android.app.ActivityManagerNative; @@ -837,12 +837,13 @@ public class RunningState { UserInfo info = mUm.getUserInfo(newItem.mUserId); userItem.mUser.mInfo = info; if (info != null) { - userItem.mUser.mIcon = UserUtils.getUserIcon(context, mUm, - info, context.getResources()); + userItem.mUser.mIcon = Utils.getUserIcon(context, mUm, info); } String name = info != null ? info.name : null; - if (name == null) { + if (name == null && info != null) { name = Integer.toString(info.id); + } else if (info == null) { + name = context.getString(R.string.unknown); } userItem.mUser.mLabel = context.getResources().getString( R.string.running_process_item_user_label, name); diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 80a3d1f..5d6b17c 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -35,6 +35,8 @@ import android.view.View.OnClickListener; import android.widget.ImageView; import com.android.settings.R; +import com.android.settings.search.Index; +import com.android.settings.search.SearchIndexableRaw; import java.util.List; @@ -65,6 +67,8 @@ public final class BluetoothDevicePreference extends Preference implements mCachedDevice = cachedDevice; + setLayoutResource(R.layout.preference_bt_icon); + if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); if (! um.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH)) { @@ -131,10 +135,10 @@ public final class BluetoothDevicePreference extends Preference implements 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); } } @@ -209,6 +213,17 @@ public final class BluetoothDevicePreference extends Preference implements if (!mCachedDevice.startPairing()) { Utils.showError(getContext(), mCachedDevice.getName(), R.string.bluetooth_pairing_error_message); + } else { + final Context context = getContext(); + + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.className = BluetoothSettings.class.getName(); + data.title = mCachedDevice.getName(); + data.screenTitle = context.getResources().getString(R.string.bluetooth_settings); + data.iconResId = R.drawable.ic_settings_bluetooth2; + data.enabled = true; + + Index.getInstance(context).updateFromSearchIndexableData(data); } } @@ -232,7 +247,7 @@ public final class BluetoothDevicePreference extends Preference implements break; case BluetoothProfile.STATE_DISCONNECTED: - if (profile.isProfileReady() && profile.isPreferred(cachedDevice.getDevice())) { + if (profile.isProfileReady()) { if (profile instanceof A2dpProfile) { a2dpNotConnected = true; } else if (profile instanceof HeadsetProfile) { @@ -305,6 +320,6 @@ public final class BluetoothDevicePreference extends Preference implements return R.drawable.ic_bt_headset_hfp; } } - return 0; + return R.drawable.ic_settings_bluetooth2; } } diff --git a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java index d687136..17da0a7 100755 --- a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java @@ -60,7 +60,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES; - private final Context mContext; + private Context mContext; private final Handler mUiHandler; private final Preference mDiscoveryPreference; @@ -92,9 +92,8 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick } }; - BluetoothDiscoverableEnabler(Context context, LocalBluetoothAdapter adapter, + BluetoothDiscoverableEnabler(LocalBluetoothAdapter adapter, Preference discoveryPreference) { - mContext = context; mUiHandler = new Handler(); mLocalAdapter = adapter; mDiscoveryPreference = discoveryPreference; @@ -102,11 +101,15 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick discoveryPreference.setPersistent(false); } - public void resume() { + public void resume(Context context) { if (mLocalAdapter == null) { return; } + if (mContext != context) { + mContext = context; + } + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); mContext.registerReceiver(mReceiver, filter); mDiscoveryPreference.setOnPreferenceClickListener(this); @@ -143,7 +146,10 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick if (timeout > 0) { BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(mContext, endTimestamp); + } else { + BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext); } + } else { mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext); diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java index df13eef..b006c65 100644 --- a/src/com/android/settings/bluetooth/BluetoothEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -21,6 +21,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; import android.provider.Settings; import android.widget.CompoundButton; import android.widget.Switch; @@ -28,19 +30,38 @@ import android.widget.Toast; import com.android.settings.R; import com.android.settings.WirelessSettings; +import com.android.settings.search.Index; +import com.android.settings.widget.SwitchBar; /** * 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 CompoundButton.OnCheckedChangeListener { - private final Context mContext; +public final class BluetoothEnabler implements SwitchBar.OnSwitchChangeListener { + private Context mContext; private Switch mSwitch; + private SwitchBar mSwitchBar; private boolean mValidListener; private final LocalBluetoothAdapter mLocalAdapter; private final IntentFilter mIntentFilter; + private static final String EVENT_DATA_IS_BT_ON = "is_bluetooth_on"; + private static final int EVENT_UPDATE_INDEX = 0; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_UPDATE_INDEX: + final boolean isBluetoothOn = msg.getData().getBoolean(EVENT_DATA_IS_BT_ON); + Index.getInstance(mContext).updateFromClassNameResource( + BluetoothSettings.class.getName(), true, isBluetoothOn); + break; + } + } + }; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -51,9 +72,10 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis } }; - public BluetoothEnabler(Context context, Switch switch_) { + public BluetoothEnabler(Context context, SwitchBar switchBar) { mContext = context; - mSwitch = switch_; + mSwitchBar = switchBar; + mSwitch = switchBar.getSwitch(); mValidListener = false; LocalBluetoothManager manager = LocalBluetoothManager.getInstance(context); @@ -67,17 +89,29 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); } - public void resume() { + public void setupSwitchBar() { + mSwitchBar.show(); + } + + public void teardownSwitchBar() { + mSwitchBar.hide(); + } + + public void resume(Context context) { if (mLocalAdapter == null) { mSwitch.setEnabled(false); return; } + if (mContext != context) { + mContext = context; + } + // Bluetooth state is not sticky, so set it manually handleStateChanged(mLocalAdapter.getBluetoothState()); + mSwitchBar.addOnSwitchChangeListener(this); mContext.registerReceiver(mReceiver, mIntentFilter); - mSwitch.setOnCheckedChangeListener(this); mValidListener = true; } @@ -86,40 +120,11 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis return; } + mSwitchBar.removeOnSwitchChangeListener(this); mContext.unregisterReceiver(mReceiver); - mSwitch.setOnCheckedChangeListener(null); mValidListener = false; } - public void setSwitch(Switch switch_) { - if (mSwitch == switch_) return; - mSwitch.setOnCheckedChangeListener(null); - mSwitch = switch_; - mSwitch.setOnCheckedChangeListener(mValidListener ? this : null); - - int bluetoothState = BluetoothAdapter.STATE_OFF; - if (mLocalAdapter != null) bluetoothState = mLocalAdapter.getBluetoothState(); - boolean isOn = bluetoothState == BluetoothAdapter.STATE_ON; - boolean isOff = bluetoothState == BluetoothAdapter.STATE_OFF; - 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 (isChecked && - !WirelessSettings.isRadioAllowed(mContext, Settings.Global.RADIO_BLUETOOTH)) { - Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show(); - // Reset switch to off - buttonView.setChecked(false); - } - - if (mLocalAdapter != null) { - mLocalAdapter.setBluetoothEnabled(isChecked); - } - mSwitch.setEnabled(false); - } - void handleStateChanged(int state) { switch (state) { case BluetoothAdapter.STATE_TURNING_ON: @@ -128,6 +133,7 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis case BluetoothAdapter.STATE_ON: setChecked(true); mSwitch.setEnabled(true); + updateSearchIndex(true); break; case BluetoothAdapter.STATE_TURNING_OFF: mSwitch.setEnabled(false); @@ -135,10 +141,12 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis case BluetoothAdapter.STATE_OFF: setChecked(false); mSwitch.setEnabled(true); + updateSearchIndex(false); break; default: setChecked(false); mSwitch.setEnabled(true); + updateSearchIndex(false); } } @@ -147,12 +155,37 @@ public final class BluetoothEnabler implements CompoundButton.OnCheckedChangeLis // set listener to null, so onCheckedChanged won't be called // if the checked status on Switch isn't changed by user click if (mValidListener) { - mSwitch.setOnCheckedChangeListener(null); + mSwitchBar.removeOnSwitchChangeListener(this); } mSwitch.setChecked(isChecked); if (mValidListener) { - mSwitch.setOnCheckedChangeListener(this); + mSwitchBar.addOnSwitchChangeListener(this); } } } + + private void updateSearchIndex(boolean isBluetoothOn) { + mHandler.removeMessages(EVENT_UPDATE_INDEX); + + Message msg = new Message(); + msg.what = EVENT_UPDATE_INDEX; + msg.getData().putBoolean(EVENT_DATA_IS_BT_ON, isBluetoothOn); + mHandler.sendMessage(msg); + } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + // Show toast message if Bluetooth is not allowed in airplane mode + if (isChecked && + !WirelessSettings.isRadioAllowed(mContext, Settings.Global.RADIO_BLUETOOTH)) { + Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show(); + // Reset switch to off + switchView.setChecked(false); + } + + if (mLocalAdapter != null) { + mLocalAdapter.setBluetoothEnabled(isChecked); + } + mSwitch.setEnabled(false); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothEventManager.java b/src/com/android/settings/bluetooth/BluetoothEventManager.java index 0eead85..bf7606e 100755 --- a/src/com/android/settings/bluetooth/BluetoothEventManager.java +++ b/src/com/android/settings/bluetooth/BluetoothEventManager.java @@ -206,7 +206,7 @@ final class BluetoothEventManager { } cachedDevice.setRssi(rssi); cachedDevice.setBtClass(btClass); - cachedDevice.setName(name); + cachedDevice.setNewName(name); cachedDevice.setVisible(true); } } diff --git a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java index 0af9c4e..4466aea 100644 --- a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java @@ -26,7 +26,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; @@ -93,7 +92,6 @@ public final class BluetoothNameDialogFragment extends DialogFragment implements mDeviceNameEdited = savedInstanceState.getBoolean(KEY_NAME_EDITED, false); } mAlertDialog = new AlertDialog.Builder(getActivity()) - .setIcon(android.R.drawable.ic_dialog_info) .setTitle(R.string.bluetooth_rename_device) .setView(createDialogView(deviceName)) .setPositiveButton(R.string.bluetooth_rename_button, @@ -180,8 +178,6 @@ public final class BluetoothNameDialogFragment extends DialogFragment implements mDeviceNameEdited = false; mDeviceNameView.setText(mLocalAdapter.getName()); } - PreferenceActivity activity = (PreferenceActivity)getActivity(); - activity.showBreadCrumbs(mLocalAdapter.getName(), ""); } public void afterTextChanged(Editable s) { diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java index 9b2a3e8..fd8489b 100755 --- a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java @@ -55,6 +55,9 @@ public final class BluetoothPairingDialog extends AlertActivity implements private static final int BLUETOOTH_PIN_MAX_LENGTH = 16; private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6; + + private LocalBluetoothManager mBluetoothManager; + private CachedBluetoothDeviceManager mCachedDeviceManager; private BluetoothDevice mDevice; private int mType; private String mPairingKey; @@ -98,13 +101,13 @@ public final class BluetoothPairingDialog extends AlertActivity implements return; } - LocalBluetoothManager manager = LocalBluetoothManager.getInstance(this); - if (manager == null) { + mBluetoothManager = LocalBluetoothManager.getInstance(this); + if (mBluetoothManager == null) { Log.e(TAG, "Error: BluetoothAdapter not supported by system"); finish(); return; } - CachedBluetoothDeviceManager deviceManager = manager.getCachedDeviceManager(); + mCachedDeviceManager = mBluetoothManager.getCachedDeviceManager(); mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); @@ -112,7 +115,7 @@ public final class BluetoothPairingDialog extends AlertActivity implements switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN: case BluetoothDevice.PAIRING_VARIANT_PASSKEY: - createUserEntryDialog(deviceManager); + createUserEntryDialog(); break; case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: @@ -123,12 +126,12 @@ public final class BluetoothPairingDialog extends AlertActivity implements return; } mPairingKey = String.format(Locale.US, "%06d", passkey); - createConfirmationDialog(deviceManager); + createConfirmationDialog(); break; case BluetoothDevice.PAIRING_VARIANT_CONSENT: case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: - createConsentDialog(deviceManager); + createConsentDialog(); break; case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: @@ -144,7 +147,7 @@ public final class BluetoothPairingDialog extends AlertActivity implements } else { mPairingKey = String.format("%04d", pairingKey); } - createDisplayPasskeyOrPinDialog(deviceManager); + createDisplayPasskeyOrPinDialog(); break; default: @@ -159,11 +162,10 @@ public final class BluetoothPairingDialog extends AlertActivity implements registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); } - private void createUserEntryDialog(CachedBluetoothDeviceManager deviceManager) { + private void createUserEntryDialog() { final AlertController.AlertParams p = mAlertParams; - p.mIconId = android.R.drawable.ic_dialog_info; p.mTitle = getString(R.string.bluetooth_pairing_request); - p.mView = createPinEntryView(deviceManager.getName(mDevice)); + p.mView = createPinEntryView(); p.mPositiveButtonText = getString(android.R.string.ok); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(android.R.string.cancel); @@ -174,9 +176,10 @@ public final class BluetoothPairingDialog extends AlertActivity implements mOkButton.setEnabled(false); } - private View createPinEntryView(String deviceName) { + private View createPinEntryView() { View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); - TextView messageView = (TextView) view.findViewById(R.id.message); + TextView messageViewCaption = (TextView) view.findViewById(R.id.message_caption); + TextView messageViewContent = (TextView) view.findViewById(R.id.message_subhead); 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); @@ -195,7 +198,7 @@ public final class BluetoothPairingDialog extends AlertActivity implements break; case BluetoothDevice.PAIRING_VARIANT_PASSKEY: - messageId1 = R.string.bluetooth_enter_passkey_msg; + messageId1 = R.string.bluetooth_enter_pin_msg; messageId2 = R.string.bluetooth_enter_passkey_other_device; // Maximum of 6 digits for passkey maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH; @@ -207,9 +210,8 @@ public final class BluetoothPairingDialog extends AlertActivity implements return null; } - // Format the message string, then parse HTML style tags - String messageText = getString(messageId1, deviceName); - messageView.setText(Html.fromHtml(messageText)); + messageViewCaption.setText(messageId1); + messageViewContent.setText(mCachedDeviceManager.getName(mDevice)); messageView2.setText(messageId2); mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); mPairingView.setFilters(new InputFilter[] { @@ -218,42 +220,56 @@ public final class BluetoothPairingDialog extends AlertActivity implements return view; } - private View createView(CachedBluetoothDeviceManager deviceManager) { + private View createView() { 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 + // Escape device name to avoid HTML injection. + String name = Html.escapeHtml(mCachedDeviceManager.getName(mDevice)); + TextView messageViewCaption = (TextView) view.findViewById(R.id.message_caption); + TextView messageViewContent = (TextView) view.findViewById(R.id.message_subhead); + TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption); + TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead); + TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message); + + String messageCaption = null; + String pairingContent = null; switch (mType) { + case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: + case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: + messagePairing.setVisibility(View.VISIBLE); case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: - messageText = getString(R.string.bluetooth_confirm_passkey_msg, - name, mPairingKey); + messageCaption = getString(R.string.bluetooth_enter_pin_msg); + pairingContent = mPairingKey; break; case BluetoothDevice.PAIRING_VARIANT_CONSENT: 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: - messageText = getString(R.string.bluetooth_display_passkey_pin_msg, name, - mPairingKey); + messagePairing.setVisibility(View.VISIBLE); + messageCaption = getString(R.string.bluetooth_enter_pin_msg); break; default: Log.e(TAG, "Incorrect pairing type received, not creating view"); return null; } - messageView.setText(Html.fromHtml(messageText)); + + if (messageViewCaption != null) { + messageViewCaption.setText(messageCaption); + messageViewContent.setText(name); + } + + if (pairingContent != null) { + pairingViewCaption.setVisibility(View.VISIBLE); + pairingViewContent.setVisibility(View.VISIBLE); + pairingViewContent.setText(pairingContent); + } + return view; } - private void createConfirmationDialog(CachedBluetoothDeviceManager deviceManager) { + private void createConfirmationDialog() { 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 = createView(); p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline); @@ -261,11 +277,10 @@ public final class BluetoothPairingDialog extends AlertActivity implements setupAlert(); } - private void createConsentDialog(CachedBluetoothDeviceManager deviceManager) { + private void createConsentDialog() { 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 = createView(); p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline); @@ -273,12 +288,10 @@ public final class BluetoothPairingDialog extends AlertActivity implements setupAlert(); } - private void createDisplayPasskeyOrPinDialog( - CachedBluetoothDeviceManager deviceManager) { + private void createDisplayPasskeyOrPinDialog() { 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 = createView(); p.mNegativeButtonText = getString(android.R.string.cancel); p.mNegativeButtonListener = this; setupAlert(); @@ -305,7 +318,20 @@ public final class BluetoothPairingDialog extends AlertActivity implements } } + private void allowPhonebookAccess() { + CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(mDevice); + if (cachedDevice == null) { + cachedDevice = mCachedDeviceManager.addDevice( + mBluetoothManager.getBluetoothAdapter(), + mBluetoothManager.getProfileManager(), + mDevice); + } + cachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); + } + private void onPair(String value) { + allowPhonebookAccess(); + switch (mType) { case BluetoothDevice.PAIRING_VARIANT_PIN: byte[] pinBytes = BluetoothDevice.convertPinToBytes(value); diff --git a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java index 838e7b1..44198d3 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java @@ -90,7 +90,9 @@ public final class BluetoothPairingRequest extends BroadcastReceiver { .setContentText(res.getString(R.string.bluetooth_notif_message, name)) .setContentIntent(pending) .setAutoCancel(true) - .setDefaults(Notification.DEFAULT_SOUND); + .setDefaults(Notification.DEFAULT_SOUND) + .setColor(res.getColor( + com.android.internal.R.color.system_notification_accent_color)); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); @@ -103,6 +105,19 @@ public final class BluetoothPairingRequest extends BroadcastReceiver { NotificationManager manager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(NOTIFICATION_ID); + + } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { + int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.ERROR); + int oldState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, + BluetoothDevice.ERROR); + if((oldState == BluetoothDevice.BOND_BONDING) && + (bondState == BluetoothDevice.BOND_NONE)) { + // Remove the notification + NotificationManager manager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(NOTIFICATION_ID); + } } } } diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java index 9f071cc..f43d176 100755 --- a/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java @@ -26,11 +26,8 @@ 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.TextView; import android.widget.Button; -import android.widget.CompoundButton.OnCheckedChangeListener; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; @@ -62,7 +59,7 @@ public class BluetoothPermissionActivity extends AlertActivity implements String action = intent.getAction(); if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL)) { int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, - BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); + BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS); if (requestType != mRequestType) return; BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (mDevice.equals(device)) dismissDialog(); @@ -117,7 +114,6 @@ public class BluetoothPermissionActivity extends AlertActivity implements private void showDialog(String title, int requestType) { final AlertController.AlertParams p = mAlertParams; - p.mIconId = android.R.drawable.ic_dialog_info; p.mTitle = title; if(DEBUG) Log.i(TAG, "showDialog() Request type: " + mRequestType + " this: " + this); switch(requestType) @@ -187,40 +183,43 @@ public class BluetoothPermissionActivity extends AlertActivity implements private void onPositive() { if (DEBUG) Log.d(TAG, "onPositive"); - savePermissionChoice(mRequestType, CachedBluetoothDevice.ACCESS_ALLOWED); - // TODO(edjee): Now that we always save the user's choice, - // we can get rid of BluetoothDevice#EXTRA_ALWAYS_ALLOWED. - sendIntentToReceiver(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY, true, - BluetoothDevice.EXTRA_ALWAYS_ALLOWED, true); + sendReplyIntentToReceiver(true, true); finish(); } private void onNegative() { if (DEBUG) Log.d(TAG, "onNegative"); - savePermissionChoice(mRequestType, CachedBluetoothDevice.ACCESS_REJECTED); - sendIntentToReceiver(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY, false, - null, false // dummy value, no effect since last param is null - ); - finish(); + + boolean always = true; + if (mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { + LocalBluetoothManager bluetoothManager = LocalBluetoothManager.getInstance(this); + CachedBluetoothDeviceManager cachedDeviceManager = + bluetoothManager.getCachedDeviceManager(); + CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(mDevice); + if (cachedDevice == null) { + cachedDevice = cachedDeviceManager.addDevice(bluetoothManager.getBluetoothAdapter(), + bluetoothManager.getProfileManager(), + mDevice); + } + always = cachedDevice.checkAndIncreaseMessageRejectionCount(); + } + + sendReplyIntentToReceiver(false, always); } - private void sendIntentToReceiver(final String intentName, final boolean allowed, - final String extraName, final boolean extraValue) { - Intent intent = new Intent(intentName); + private void sendReplyIntentToReceiver(final boolean allowed, final boolean always) { + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); if (mReturnPackage != null && mReturnClass != null) { intent.setClassName(mReturnPackage, mReturnClass); } - if(DEBUG) Log.i(TAG, "sendIntentToReceiver() Request type: " + mRequestType + + if (DEBUG) Log.i(TAG, "sendReplyIntentToReceiver() Request type: " + mRequestType + " mReturnPackage" + mReturnPackage + " mReturnClass" + mReturnClass); intent.putExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, - allowed ? BluetoothDevice.CONNECTION_ACCESS_YES : - BluetoothDevice.CONNECTION_ACCESS_NO); - - if (extraName != null) { - intent.putExtra(extraName, extraValue); - } + allowed ? BluetoothDevice.CONNECTION_ACCESS_YES + : BluetoothDevice.CONNECTION_ACCESS_NO); + intent.putExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, always); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, mRequestType); sendBroadcast(intent, android.Manifest.permission.BLUETOOTH_ADMIN); @@ -252,23 +251,4 @@ public class BluetoothPermissionActivity extends AlertActivity implements public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } - - private void savePermissionChoice(int permissionType, int permissionChoice) { - LocalBluetoothManager bluetoothManager = LocalBluetoothManager.getInstance(this); - CachedBluetoothDeviceManager cachedDeviceManager = - bluetoothManager.getCachedDeviceManager(); - CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(mDevice); - if (DEBUG) Log.d(TAG, "savePermissionChoice permissionType: " + permissionType); - if (cachedDevice == null ) { - cachedDevice = cachedDeviceManager.addDevice(bluetoothManager.getBluetoothAdapter(), - bluetoothManager.getProfileManager(), - mDevice); - } - if(permissionType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS){ - cachedDevice.setPhonebookPermissionChoice(permissionChoice); - }else if (permissionType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS){ - cachedDevice.setMessagePermissionChoice(permissionChoice); - } - } - } diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java index c3b93be..bcd4d77 100644 --- a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java @@ -66,20 +66,30 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { if (DEBUG) Log.d(TAG, "onReceive request type: " + mRequestType + " return " + mReturnPackage + "," + mReturnClass); - // Check if user had made decisions on accepting or rejecting the phonebook access - // request. If there is, reply the request and return, no need to start permission - // activity dialog or notification. + // Even if the user has already made the choice, Bluetooth still may not know that if + // the user preference data have not been migrated from Settings app's shared + // preferences to Bluetooth app's. In that case, Bluetooth app broadcasts an + // ACTION_CONNECTION_ACCESS_REQUEST intent to ask to Settings app. + // + // If that happens, 'checkUserChoice()' here will do migration because it finds or + // creates a 'CachedBluetoothDevice' object for the device. + // + // After migration is done, 'checkUserChoice()' replies to the request by sending an + // ACTION_CONNECTION_ACCESS_REPLY intent. And we don't need to start permission activity + // dialog or notification. if (checkUserChoice()) { return; } Intent connectionAccessIntent = new Intent(action); connectionAccessIntent.setClass(context, BluetoothPermissionActivity.class); - // We use the FLAG_ACTIVITY_MULTIPLE_TASK since we can have multiple concurrent access requests - connectionAccessIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - connectionAccessIntent.setType(Integer.toString(mRequestType)); /* This is needed to create two pending - intents to the same activity. - The value is not used in the activity. */ + // We use the FLAG_ACTIVITY_MULTIPLE_TASK since we can have multiple concurrent access + // requests. + connectionAccessIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + // This is needed to create two pending intents to the same activity. The value is not + // used in the activity. + connectionAccessIntent.setType(Integer.toString(mRequestType)); connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, mRequestType); connectionAccessIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); @@ -92,8 +102,9 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - if (powerManager.isScreenOn() && - LocalBluetoothPreferences.shouldShowDialogInForeground(context, deviceAddress) ) { + if (powerManager.isScreenOn() + && LocalBluetoothPreferences.shouldShowDialogInForeground( + context, deviceAddress)) { context.startActivity(connectionAccessIntent); } else { // Put up a notification that leads to the dialog @@ -110,36 +121,43 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { switch (mRequestType) { case BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS: title = context.getString(R.string.bluetooth_phonebook_request); - message = context.getString(R.string.bluetooth_pb_acceptance_dialog_text, deviceName, deviceName); + message = context.getString(R.string.bluetooth_pb_acceptance_dialog_text, + deviceName, deviceName); break; case BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS: title = context.getString(R.string.bluetooth_map_request); - message = context.getString(R.string.bluetooth_map_acceptance_dialog_text, deviceName, deviceName); + message = context.getString(R.string.bluetooth_map_acceptance_dialog_text, + deviceName, deviceName); break; default: title = context.getString(R.string.bluetooth_connection_permission_request); - message = context.getString(R.string.bluetooth_connection_dialog_text, deviceName, deviceName); + message = context.getString(R.string.bluetooth_connection_dialog_text, + deviceName, deviceName); break; } Notification notification = new Notification.Builder(context) - .setContentTitle(title) - .setTicker(message) - .setContentText(message) - .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) - .setAutoCancel(true) - .setPriority(Notification.PRIORITY_MAX) - .setOnlyAlertOnce(false) - .setDefaults(Notification.DEFAULT_ALL) - .setContentIntent(PendingIntent.getActivity(context, 0, connectionAccessIntent, 0)) - .setDeleteIntent(PendingIntent.getBroadcast(context, 0, deleteIntent, 0)) - .build(); - - notification.flags |= Notification.FLAG_NO_CLEAR; /* cannot be set with the builder */ + .setContentTitle(title) + .setTicker(message) + .setContentText(message) + .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) + .setAutoCancel(true) + .setPriority(Notification.PRIORITY_MAX) + .setOnlyAlertOnce(false) + .setDefaults(Notification.DEFAULT_ALL) + .setContentIntent(PendingIntent.getActivity(context, 0, + connectionAccessIntent, 0)) + .setDeleteIntent(PendingIntent.getBroadcast(context, 0, deleteIntent, 0)) + .setColor(context.getResources().getColor( + com.android.internal.R.color.system_notification_accent_color)) + .build(); + + notification.flags |= Notification.FLAG_NO_CLEAR; // Cannot be set with the builder. NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(getNotificationTag(mRequestType),NOTIFICATION_ID, notification); + notificationManager.notify(getNotificationTag(mRequestType), NOTIFICATION_ID, + notification); } } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL)) { // Remove the notification @@ -171,7 +189,7 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { // ignore if it is something else than phonebook/message settings it wants us to remember if (mRequestType != BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS && mRequestType != BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { - if (DEBUG) Log.d(TAG, "Unknown RequestType: " + mRequestType); + if (DEBUG) Log.d(TAG, "checkUserChoice(): Unknown RequestType " + mRequestType); return processed; } @@ -179,71 +197,56 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { CachedBluetoothDeviceManager cachedDeviceManager = bluetoothManager.getCachedDeviceManager(); CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(mDevice); - if (cachedDevice == null) { cachedDevice = cachedDeviceManager.addDevice(bluetoothManager.getBluetoothAdapter(), bluetoothManager.getProfileManager(), mDevice); } - if(mRequestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) { + String intentName = BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY; + if (mRequestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) { int phonebookPermission = cachedDevice.getPhonebookPermissionChoice(); if (phonebookPermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { - return processed; - } - - String intentName = BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY; - if (phonebookPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { - sendIntentToReceiver(intentName, true, BluetoothDevice.EXTRA_ALWAYS_ALLOWED, true); + // Leave 'processed' as false. + } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + sendReplyIntentToReceiver(true); processed = true; } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_REJECTED) { - sendIntentToReceiver(intentName, false, - null, false ); // dummy value, no effect since previous param is null + sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad phonebookPermission: " + phonebookPermission); } - - } else if(mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { - + } else if (mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { int messagePermission = cachedDevice.getMessagePermissionChoice(); if (messagePermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { - return processed; - } - - String intentName = BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY; - if (messagePermission == CachedBluetoothDevice.ACCESS_ALLOWED) { - sendIntentToReceiver(intentName, true, BluetoothDevice.EXTRA_ALWAYS_ALLOWED, true); + // Leave 'processed' as false. + } else if (messagePermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + sendReplyIntentToReceiver(true); processed = true; } else if (messagePermission == CachedBluetoothDevice.ACCESS_REJECTED) { - sendIntentToReceiver(intentName, false, - null, false); // dummy value, no effect since previous param is null + sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad messagePermission: " + messagePermission); } } - if(DEBUG) Log.d(TAG,"checkUserChoice(): returning " + processed); + if (DEBUG) Log.d(TAG,"checkUserChoice(): returning " + processed); return processed; } - private void sendIntentToReceiver(final String intentName, final boolean allowed, - final String extraName, final boolean extraValue) { - Intent intent = new Intent(intentName); + private void sendReplyIntentToReceiver(final boolean allowed) { + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); 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); - } + allowed ? BluetoothDevice.CONNECTION_ACCESS_YES + : BluetoothDevice.CONNECTION_ACCESS_NO); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, mRequestType); mContext.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH_ADMIN); diff --git a/src/com/android/settings/bluetooth/BluetoothProgressCategory.java b/src/com/android/settings/bluetooth/BluetoothProgressCategory.java index 1c81360..9f64f9d 100644 --- a/src/com/android/settings/bluetooth/BluetoothProgressCategory.java +++ b/src/com/android/settings/bluetooth/BluetoothProgressCategory.java @@ -22,8 +22,26 @@ import com.android.settings.R; import android.content.Context; import android.util.AttributeSet; +/** + * A Bluetooth discovery progress category + */ public class BluetoothProgressCategory extends ProgressCategory { + public BluetoothProgressCategory(Context context) { + this(context, null); + } + public BluetoothProgressCategory(Context context, AttributeSet attrs) { - super(context, attrs, R.string.bluetooth_no_devices_found); + this(context, attrs, 0); + } + + public BluetoothProgressCategory(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BluetoothProgressCategory(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + setEmptyTextRes(R.string.bluetooth_no_devices_found); } } diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java index bbbeee5..dd2c9df 100755 --- a/src/com/android/settings/bluetooth/BluetoothSettings.java +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -18,58 +18,76 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; -import android.app.ActionBar; import android.app.Activity; +import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; 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.content.res.Resources; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; 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.widget.Switch; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Index; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.widget.SwitchBar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; /** * BluetoothSettings is the Settings screen for Bluetooth configuration and * connection management. */ -public final class BluetoothSettings extends DeviceListPreferenceFragment { +public final class BluetoothSettings extends DeviceListPreferenceFragment implements Indexable { private static final String TAG = "BluetoothSettings"; 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 static final int MENU_ID_SHOW_RECEIVED = Menu.FIRST + 2; /* 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"; - private BluetoothEnabler mBluetoothEnabler; + private static View mSettingsDialogView = null; - private BluetoothDiscoverableEnabler mDiscoverableEnabler; + private BluetoothEnabler mBluetoothEnabler; private PreferenceGroup mPairedDevicesCategory; - private PreferenceGroup mAvailableDevicesCategory; private boolean mAvailableDevicesCategoryIsPresent; - private boolean mActivityStarted; + + private boolean mInitialScanStarted; + private boolean mInitiateDiscoverable; private TextView mEmptyView; + private SwitchBar mSwitchBar; private final IntentFilter mIntentFilter; @@ -80,15 +98,23 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); + final String action = intent.getAction(); + final int state = + intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { - updateDeviceName(); + updateDeviceName(context); + } + + if (state == BluetoothAdapter.STATE_ON) { + mInitiateDiscoverable = true; } } - private void updateDeviceName() { + private void updateDeviceName(Context context) { if (mLocalAdapter.isEnabled() && mMyDevicePreference != null) { - mMyDevicePreference.setTitle(mLocalAdapter.getName()); + mMyDevicePreference.setSummary(context.getResources().getString( + R.string.bluetooth_is_visible_message, mLocalAdapter.getName())); } } }; @@ -101,36 +127,29 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mActivityStarted = (savedInstanceState == null); // don't auto start scan after rotation + mInitialScanStarted = (savedInstanceState != null); // don't auto start scan after rotation + mInitiateDiscoverable = true; mEmptyView = (TextView) getView().findViewById(android.R.id.empty); getListView().setEmptyView(mEmptyView); + + final SettingsActivity activity = (SettingsActivity) getActivity(); + mSwitchBar = activity.getSwitchBar(); + + mBluetoothEnabler = new BluetoothEnabler(activity, mSwitchBar); + mBluetoothEnabler.setupSwitchBar(); } @Override - void addPreferencesForActivity() { - addPreferencesFromResource(R.xml.bluetooth_settings); + public void onDestroyView() { + super.onDestroyView(); - Activity activity = getActivity(); - - 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.setPaddingRelative(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.END)); - } - } + mBluetoothEnabler.teardownSwitchBar(); + } - mBluetoothEnabler = new BluetoothEnabler(activity, actionBarSwitch); + @Override + void addPreferencesForActivity() { + addPreferencesFromResource(R.xml.bluetooth_settings); setHasOptionsMenu(true); } @@ -140,16 +159,22 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { // resume BluetoothEnabler before calling super.onResume() so we don't get // any onDeviceAdded() callbacks before setting up view in updateContent() if (mBluetoothEnabler != null) { - mBluetoothEnabler.resume(); + mBluetoothEnabler.resume(getActivity()); } super.onResume(); - if (mDiscoverableEnabler != null) { - mDiscoverableEnabler.resume(); + mInitiateDiscoverable = true; + + if (isUiRestricted()) { + setDeviceListGroup(getPreferenceScreen()); + removeAllDevices(); + mEmptyView.setText(R.string.bluetooth_empty_list_user_restricted); + return; } + getActivity().registerReceiver(mReceiver, mIntentFilter); if (mLocalAdapter != null) { - updateContent(mLocalAdapter.getBluetoothState(), mActivityStarted); + updateContent(mLocalAdapter.getBluetoothState()); } } @@ -159,17 +184,22 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { if (mBluetoothEnabler != null) { mBluetoothEnabler.pause(); } - getActivity().unregisterReceiver(mReceiver); - if (mDiscoverableEnabler != null) { - mDiscoverableEnabler.pause(); + + // Make the device only visible to connected devices. + mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + + if (isUiRestricted()) { + return; } + + getActivity().unregisterReceiver(mReceiver); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (mLocalAdapter == null) return; // If the user is not allowed to configure bluetooth, do not show the menu. - if (isRestrictedAndNotPinProtected()) return; + if (isUiRestricted()) return; boolean bluetoothIsEnabled = mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON; boolean isDiscovering = mLocalAdapter.isDiscovering(); @@ -177,11 +207,8 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { 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) + 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_SHOW_RECEIVED, 0, R.string.bluetooth_show_received_files) @@ -203,11 +230,6 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { getFragmentManager(), "rename device"); return true; - case MENU_ID_VISIBILITY_TIMEOUT: - new BluetoothVisibilityTimeoutFragment().show( - getFragmentManager(), "visibility timeout"); - return true; - case MENU_ID_SHOW_RECEIVED: Intent intent = new Intent(BTOPP_ACTION_OPEN_RECEIVED_FILES); getActivity().sendBroadcast(intent); @@ -217,10 +239,23 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { } private void startScanning() { - if (isRestrictedAndNotPinProtected()) return; + if (isUiRestricted()) { + return; + } + if (!mAvailableDevicesCategoryIsPresent) { getPreferenceScreen().addPreference(mAvailableDevicesCategory); + mAvailableDevicesCategoryIsPresent = true; } + + if (mAvailableDevicesCategory != null) { + setDeviceListGroup(mAvailableDevicesCategory); + removeAllDevices(); + } + + mLocalManager.getCachedDeviceManager().clearNonBondedDevices(); + mAvailableDevicesCategory.removeAll(); + mInitialScanStarted = true; mLocalAdapter.startScanning(true); } @@ -231,16 +266,18 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { } private void addDeviceCategory(PreferenceGroup preferenceGroup, int titleId, - BluetoothDeviceFilter.Filter filter) { + BluetoothDeviceFilter.Filter filter, boolean addCachedDevices) { preferenceGroup.setTitle(titleId); getPreferenceScreen().addPreference(preferenceGroup); setFilter(filter); setDeviceListGroup(preferenceGroup); - addCachedDevices(); + if (addCachedDevices) { + addCachedDevices(); + } preferenceGroup.setEnabled(true); } - private void updateContent(int bluetoothState, boolean scanState) { + private void updateContent(int bluetoothState) { final PreferenceScreen preferenceScreen = getPreferenceScreen(); int messageId = 0; @@ -250,28 +287,9 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { preferenceScreen.setOrderingAsAdded(true); mDevicePreferenceMap.clear(); - // 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 (!isRestrictedAndNotPinProtected()) { - if (mDiscoverableEnabler == null) { - mDiscoverableEnabler = new BluetoothDiscoverableEnabler(getActivity(), - mLocalAdapter, mMyDevicePreference); - mDiscoverableEnabler.resume(); - LocalBluetoothManager.getInstance(getActivity()).setDiscoverableEnabler( - mDiscoverableEnabler); - } + if (isUiRestricted()) { + messageId = R.string.bluetooth_empty_list_user_restricted; + break; } // Paired devices category @@ -282,44 +300,47 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { } addDeviceCategory(mPairedDevicesCategory, R.string.bluetooth_preference_paired_devices, - BluetoothDeviceFilter.BONDED_DEVICE_FILTER); + BluetoothDeviceFilter.BONDED_DEVICE_FILTER, true); int numberOfPairedDevices = mPairedDevicesCategory.getPreferenceCount(); - if (mDiscoverableEnabler != null) { - mDiscoverableEnabler.setNumberOfPairedDevices(numberOfPairedDevices); + if (isUiRestricted() || numberOfPairedDevices <= 0) { + preferenceScreen.removePreference(mPairedDevicesCategory); } // Available devices category if (mAvailableDevicesCategory == null) { - mAvailableDevicesCategory = new BluetoothProgressCategory(getActivity(), null); + mAvailableDevicesCategory = new BluetoothProgressCategory(getActivity()); + mAvailableDevicesCategory.setSelectable(false); } else { mAvailableDevicesCategory.removeAll(); } - if (!isRestrictedAndNotPinProtected()) { - addDeviceCategory(mAvailableDevicesCategory, - R.string.bluetooth_preference_found_devices, - BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER); - } + addDeviceCategory(mAvailableDevicesCategory, + R.string.bluetooth_preference_found_devices, + BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER, mInitialScanStarted); int numberOfAvailableDevices = mAvailableDevicesCategory.getPreferenceCount(); - mAvailableDevicesCategoryIsPresent = true; - if (numberOfAvailableDevices == 0) { - preferenceScreen.removePreference(mAvailableDevicesCategory); - mAvailableDevicesCategoryIsPresent = false; + if (!mInitialScanStarted) { + startScanning(); } - if (numberOfPairedDevices == 0) { - preferenceScreen.removePreference(mPairedDevicesCategory); - if (scanState == true) { - mActivityStarted = false; - startScanning(); - } else { - if (!mAvailableDevicesCategoryIsPresent) { - getPreferenceScreen().addPreference(mAvailableDevicesCategory); - } - } + if (mMyDevicePreference == null) { + mMyDevicePreference = new Preference(getActivity()); } + + mMyDevicePreference.setSummary(getResources().getString( + R.string.bluetooth_is_visible_message, mLocalAdapter.getName())); + mMyDevicePreference.setSelectable(false); + preferenceScreen.addPreference(mMyDevicePreference); + getActivity().invalidateOptionsMenu(); + + // mLocalAdapter.setScanMode is internally synchronized so it is okay for multiple + // threads to execute. + if (mInitiateDiscoverable) { + // Make the device visible to other devices. + mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + mInitiateDiscoverable = false; + } return; // not break case BluetoothAdapter.STATE_TURNING_OFF: @@ -328,55 +349,126 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { case BluetoothAdapter.STATE_OFF: messageId = R.string.bluetooth_empty_list_bluetooth_off; + if (isUiRestricted()) { + messageId = R.string.bluetooth_empty_list_user_restricted; + } break; case BluetoothAdapter.STATE_TURNING_ON: messageId = R.string.bluetooth_turning_on; + mInitialScanStarted = false; break; } setDeviceListGroup(preferenceScreen); removeAllDevices(); mEmptyView.setText(messageId); - getActivity().invalidateOptionsMenu(); + if (!isUiRestricted()) { + getActivity().invalidateOptionsMenu(); + } } @Override public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); - updateContent(bluetoothState, true); + updateContent(bluetoothState); } @Override public void onScanningStateChanged(boolean started) { super.onScanningStateChanged(started); // Update options' enabled state - getActivity().invalidateOptionsMenu(); + if (getActivity() != null) { + getActivity().invalidateOptionsMenu(); + } } public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { setDeviceListGroup(getPreferenceScreen()); removeAllDevices(); - updateContent(mLocalAdapter.getBluetoothState(), false); + updateContent(mLocalAdapter.getBluetoothState()); } - private final View.OnClickListener mDeviceProfilesListener = new View.OnClickListener() { + 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) { - if (isRestrictedAndNotPinProtected()) return; + if (!(v.getTag() instanceof CachedBluetoothDevice)) { + Log.w(TAG, "onClick() called for other View: " + v); + return; + } - CachedBluetoothDevice device = (CachedBluetoothDevice) v.getTag(); + final CachedBluetoothDevice device = (CachedBluetoothDevice) v.getTag(); + final Activity activity = getActivity(); + DeviceProfilesSettings profileFragment = (DeviceProfilesSettings)activity. + getFragmentManager().findFragmentById(R.id.bluetooth_fragment_settings); - Bundle args = new Bundle(1); - args.putParcelable(DeviceProfilesSettings.EXTRA_DEVICE, device.getDevice()); + if (mSettingsDialogView != null){ + ViewGroup parent = (ViewGroup) mSettingsDialogView.getParent(); + if (parent != null) { + parent.removeView(mSettingsDialogView); + } + } + + if (profileFragment == null) { + LayoutInflater inflater = getActivity().getLayoutInflater(); + mSettingsDialogView = inflater.inflate(R.layout.bluetooth_device_settings, null); + profileFragment = (DeviceProfilesSettings)activity.getFragmentManager() + .findFragmentById(R.id.bluetooth_fragment_settings); - ((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 + // To enable scrolling we store the name field in a seperate header and add to + // the ListView of the profileFragment. + View header = inflater.inflate(R.layout.bluetooth_device_settings_header, null); + profileFragment.getListView().addHeaderView(header); } + + final View dialogLayout = mSettingsDialogView; + AlertDialog.Builder settingsDialog = new AlertDialog.Builder(activity); + profileFragment.setDevice(device); + final EditText deviceName = (EditText)dialogLayout.findViewById(R.id.name); + deviceName.setText(device.getName(), TextView.BufferType.EDITABLE); + + final DeviceProfilesSettings dpsFragment = profileFragment; + final Context context = v.getContext(); + settingsDialog.setView(dialogLayout); + settingsDialog.setTitle(R.string.bluetooth_preference_paired_devices); + settingsDialog.setPositiveButton(R.string.okay, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + EditText deviceName = (EditText)dialogLayout.findViewById(R.id.name); + device.setName(deviceName.getText().toString()); + } + }); + + settingsDialog.setNegativeButton(R.string.forget, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + device.unpair(); + com.android.settings.bluetooth.Utils.updateSearchIndex(activity, + BluetoothSettings.class.getName(), device.getName(), + context.getResources().getString(R.string.bluetooth_settings), + R.drawable.ic_settings_bluetooth2, false); + } + }); + + // We must ensure that the fragment gets destroyed to avoid duplicate fragments. + settingsDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + public void onDismiss(final DialogInterface dialog) { + if (!activity.isDestroyed()) { + activity.getFragmentManager().beginTransaction().remove(dpsFragment) + .commitAllowingStateLoss(); + } + } + }); + + AlertDialog dialog = settingsDialog.create(); + dialog.create(); + dialog.show(); + + // We must ensure that clicking on the EditText will bring up the keyboard. + dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); } }; @@ -397,4 +489,39 @@ public final class BluetoothSettings extends DeviceListPreferenceFragment { protected int getHelpResource() { return R.string.help_url_bluetooth; } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.bluetooth_settings); + data.screenTitle = res.getString(R.string.bluetooth_settings); + result.add(data); + + // Add cached paired BT devices + LocalBluetoothManager lbtm = LocalBluetoothManager.getInstance(context); + // LocalBluetoothManager.getInstance can return null if the device does not + // support bluetooth (e.g. the emulator). + if (lbtm != null) { + Set<BluetoothDevice> bondedDevices = + lbtm.getBluetoothAdapter().getBondedDevices(); + + for (BluetoothDevice device : bondedDevices) { + data = new SearchIndexableRaw(context); + data.title = device.getName(); + data.screenTitle = res.getString(R.string.bluetooth_settings); + data.enabled = enabled; + result.add(data); + } + } + return result; + } + }; } diff --git a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java index 1263797..e61b3fd 100755 --- a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java +++ b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java @@ -68,27 +68,22 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { private int mMessagePermissionChoice; - private int mPhonebookRejectedTimes; - - private int mMessageRejectedTimes; + private int mMessageRejectionCount; private final Collection<Callback> mCallbacks = new ArrayList<Callback>(); // Following constants indicate the user's choices of Phone book/message access settings // User hasn't made any choice or settings app has wiped out the memory - final static int ACCESS_UNKNOWN = 0; + public final static int ACCESS_UNKNOWN = 0; // User has accepted the connection and let Settings app remember the decision - final static int ACCESS_ALLOWED = 1; + public final static int ACCESS_ALLOWED = 1; // User has rejected the connection and let Settings app remember the decision - final static int ACCESS_REJECTED = 2; + public final static int ACCESS_REJECTED = 2; - // how many times did User reject the connection to make the rejected persist. - final static int PERSIST_REJECTED_TIMES_LIMIT = 2; + // How many times user should reject the connection to make the choice persist. + private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2; - private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission"; - private final static String MESSAGE_PREFS_NAME = "bluetooth_message_permission"; - private final static String PHONEBOOK_REJECT_TIMES = "bluetooth_phonebook_reject"; - private final static String MESSAGE_REJECT_TIMES = "bluetooth_message_reject"; + private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject"; /** * When we connect to multiple profiles, we only want to display a single @@ -138,7 +133,9 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { } mProfileConnectionState.put(profile, newProfileState); if (newProfileState == BluetoothProfile.STATE_CONNECTED) { - if (!mProfiles.contains(profile)) { + if (profile instanceof MapProfile) { + profile.setPreferred(mDevice, true); + } else if (!mProfiles.contains(profile)) { mRemovedProfiles.remove(profile); mProfiles.add(profile); if (profile instanceof PanProfile && @@ -147,15 +144,8 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { mLocalNapRoleConnected = true; } } - if (profile instanceof MapProfile) { - profile.setPreferred(mDevice, true); - } } else if (profile instanceof MapProfile && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { - if (mProfiles.contains(profile)) { - mRemovedProfiles.add(profile); - mProfiles.remove(profile); - } profile.setPreferred(mDevice, false); } else if (mLocalNapRoleConnected && profile instanceof PanProfile && ((PanProfile) profile).isLocalRoleNap(mDevice) && @@ -370,10 +360,9 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { fetchName(); fetchBtClass(); updateProfiles(); - fetchPhonebookPermissionChoice(); - fetchMessagePermissionChoice(); - fetchPhonebookRejectTimes(); - fetchMessageRejectTimes(); + migratePhonebookPermissionChoice(); + migrateMessagePermissionChoice(); + fetchMessageRejectionCount(); mVisible = false; dispatchAttributesChanged(); @@ -387,19 +376,30 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { return mName; } - void setName(String name) { - if (!mName.equals(name)) { - if (TextUtils.isEmpty(name)) { - // TODO: use friendly name for unknown device (bug 1181856) + /** + * Populate name from BluetoothDevice.ACTION_FOUND intent + */ + void setNewName(String name) { + if (mName == null) { + mName = name; + if (mName == null || TextUtils.isEmpty(mName)) { mName = mDevice.getAddress(); - } else { - mName = name; - mDevice.setAlias(name); } dispatchAttributesChanged(); } } + /** + * user changes the device name + */ + void setName(String name) { + if (!mName.equals(name)) { + mName = name; + mDevice.setAlias(name); + dispatchAttributesChanged(); + } + } + void refreshName() { fetchName(); dispatchAttributesChanged(); @@ -541,10 +541,8 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { mConnectAfterPairing = false; // cancel auto-connect setPhonebookPermissionChoice(ACCESS_UNKNOWN); setMessagePermissionChoice(ACCESS_UNKNOWN); - mPhonebookRejectedTimes = 0; - savePhonebookRejectTimes(); - mMessageRejectedTimes = 0; - saveMessageRejectTimes(); + mMessageRejectionCount = 0; + saveMessageRejectionCount(); } refresh(); @@ -657,104 +655,116 @@ final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { } int getPhonebookPermissionChoice() { - return mPhonebookPermissionChoice; + int permission = mDevice.getPhonebookAccessPermission(); + if (permission == BluetoothDevice.ACCESS_ALLOWED) { + return ACCESS_ALLOWED; + } else if (permission == BluetoothDevice.ACCESS_REJECTED) { + return ACCESS_REJECTED; + } + return ACCESS_UNKNOWN; } void setPhonebookPermissionChoice(int permissionChoice) { - // if user reject it, only save it when reject exceed limit. - if (permissionChoice == ACCESS_REJECTED) { - mPhonebookRejectedTimes++; - savePhonebookRejectTimes(); - if (mPhonebookRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) { - return; - } + int permission = BluetoothDevice.ACCESS_UNKNOWN; + if (permissionChoice == ACCESS_ALLOWED) { + permission = BluetoothDevice.ACCESS_ALLOWED; + } else if (permissionChoice == ACCESS_REJECTED) { + permission = BluetoothDevice.ACCESS_REJECTED; } - - mPhonebookPermissionChoice = permissionChoice; - - SharedPreferences.Editor editor = - mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit(); - if (permissionChoice == ACCESS_UNKNOWN) { - editor.remove(mDevice.getAddress()); - } else { - editor.putInt(mDevice.getAddress(), permissionChoice); - } - editor.commit(); - } - - private void fetchPhonebookPermissionChoice() { - SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, - Context.MODE_PRIVATE); - mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(), - ACCESS_UNKNOWN); + mDevice.setPhonebookAccessPermission(permission); } - private void fetchPhonebookRejectTimes() { - SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES, - Context.MODE_PRIVATE); - mPhonebookRejectedTimes = preference.getInt(mDevice.getAddress(), 0); - } + // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth + // app's shared preferences). + private void migratePhonebookPermissionChoice() { + SharedPreferences preferences = mContext.getSharedPreferences( + "bluetooth_phonebook_permission", Context.MODE_PRIVATE); + if (!preferences.contains(mDevice.getAddress())) { + return; + } - private void savePhonebookRejectTimes() { - SharedPreferences.Editor editor = - mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES, - Context.MODE_PRIVATE).edit(); - if (mPhonebookRejectedTimes == 0) { - editor.remove(mDevice.getAddress()); - } else { - editor.putInt(mDevice.getAddress(), mPhonebookRejectedTimes); + if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { + int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); + if (oldPermission == ACCESS_ALLOWED) { + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); + } else if (oldPermission == ACCESS_REJECTED) { + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); + } } + + SharedPreferences.Editor editor = preferences.edit(); + editor.remove(mDevice.getAddress()); editor.commit(); } int getMessagePermissionChoice() { - return mMessagePermissionChoice; + int permission = mDevice.getMessageAccessPermission(); + if (permission == BluetoothDevice.ACCESS_ALLOWED) { + return ACCESS_ALLOWED; + } else if (permission == BluetoothDevice.ACCESS_REJECTED) { + return ACCESS_REJECTED; + } + return ACCESS_UNKNOWN; } void setMessagePermissionChoice(int permissionChoice) { - // if user reject it, only save it when reject exceed limit. - if (permissionChoice == ACCESS_REJECTED) { - mMessageRejectedTimes++; - saveMessageRejectTimes(); - if (mMessageRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) { - return; - } + int permission = BluetoothDevice.ACCESS_UNKNOWN; + if (permissionChoice == ACCESS_ALLOWED) { + permission = BluetoothDevice.ACCESS_ALLOWED; + } else if (permissionChoice == ACCESS_REJECTED) { + permission = BluetoothDevice.ACCESS_REJECTED; } + mDevice.setMessageAccessPermission(permission); + } - mMessagePermissionChoice = permissionChoice; + // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth + // app's shared preferences). + private void migrateMessagePermissionChoice() { + SharedPreferences preferences = mContext.getSharedPreferences( + "bluetooth_message_permission", Context.MODE_PRIVATE); + if (!preferences.contains(mDevice.getAddress())) { + return; + } - SharedPreferences.Editor editor = - mContext.getSharedPreferences(MESSAGE_PREFS_NAME, Context.MODE_PRIVATE).edit(); - if (permissionChoice == ACCESS_UNKNOWN) { - editor.remove(mDevice.getAddress()); - } else { - editor.putInt(mDevice.getAddress(), permissionChoice); + if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { + int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); + if (oldPermission == ACCESS_ALLOWED) { + mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); + } else if (oldPermission == ACCESS_REJECTED) { + mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); + } } + + SharedPreferences.Editor editor = preferences.edit(); + editor.remove(mDevice.getAddress()); editor.commit(); } - private void fetchMessagePermissionChoice() { - SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_PREFS_NAME, - Context.MODE_PRIVATE); - mMessagePermissionChoice = preference.getInt(mDevice.getAddress(), - ACCESS_UNKNOWN); + /** + * @return Whether this rejection should persist. + */ + boolean checkAndIncreaseMessageRejectionCount() { + if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) { + mMessageRejectionCount++; + saveMessageRejectionCount(); + } + return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST; } - private void fetchMessageRejectTimes() { - SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, - Context.MODE_PRIVATE); - mMessageRejectedTimes = preference.getInt(mDevice.getAddress(), 0); + private void fetchMessageRejectionCount() { + SharedPreferences preference = mContext.getSharedPreferences( + MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE); + mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0); } - private void saveMessageRejectTimes() { - SharedPreferences.Editor editor = - mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, Context.MODE_PRIVATE).edit(); - if (mMessageRejectedTimes == 0) { + private void saveMessageRejectionCount() { + SharedPreferences.Editor editor = mContext.getSharedPreferences( + MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit(); + if (mMessageRejectionCount == 0) { editor.remove(mDevice.getAddress()); } else { - editor.putInt(mDevice.getAddress(), mMessageRejectedTimes); + editor.putInt(mDevice.getAddress(), mMessageRejectionCount); } editor.commit(); } - } diff --git a/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java b/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java index ff282cc..2b0e7f1 100755 --- a/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java +++ b/src/com/android/settings/bluetooth/CachedBluetoothDeviceManager.java @@ -86,7 +86,9 @@ final class CachedBluetoothDeviceManager { BluetoothDevice device) { CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, adapter, profileManager, device); - mCachedDevices.add(newDevice); + synchronized (mCachedDevices) { + mCachedDevices.add(newDevice); + } return newDevice; } @@ -110,6 +112,15 @@ final class CachedBluetoothDeviceManager { return device.getAddress(); } + public synchronized void clearNonBondedDevices() { + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { + mCachedDevices.remove(i); + } + } + } + public synchronized void onScanningStateChanged(boolean started) { if (!started) return; @@ -142,8 +153,8 @@ final class CachedBluetoothDeviceManager { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { - cachedDevice.setVisible(false); - mCachedDevices.remove(i); + cachedDevice.setVisible(false); + mCachedDevices.remove(i); } else { // For bonded devices, we need to clear the connection status so that // when BT is enabled next time, device connection status shall be retrieved diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java index e2faf7f..e7208b5 100644 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java @@ -25,9 +25,7 @@ import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.util.Log; -import com.android.settings.ProgressCategory; import com.android.settings.RestrictedSettingsFragment; -import com.android.settings.SettingsPreferenceFragment; import java.util.Collection; import java.util.WeakHashMap; @@ -98,7 +96,7 @@ public abstract class DeviceListPreferenceFragment extends @Override public void onResume() { super.onResume(); - if (mLocalManager == null) return; + if (mLocalManager == null || isUiRestricted()) return; mLocalManager.setForegroundActivity(getActivity()); mLocalManager.getEventManager().registerCallback(this); @@ -109,7 +107,9 @@ public abstract class DeviceListPreferenceFragment extends @Override public void onPause() { super.onPause(); - if (mLocalManager == null) return; + if (mLocalManager == null || isUiRestricted()) { + return; + } removeAllDevices(); mLocalManager.setForegroundActivity(null); @@ -167,6 +167,12 @@ public abstract class DeviceListPreferenceFragment extends } void createDevicePreference(CachedBluetoothDevice cachedDevice) { + if (mDeviceListGroup == null) { + Log.w(TAG, "Trying to create a device preference before the list group/category " + + "exists!"); + return; + } + BluetoothDevicePreference preference = new BluetoothDevicePreference( getActivity(), cachedDevice); diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java index 4b6a6b0..354d03c 100644 --- a/src/com/android/settings/bluetooth/DevicePickerFragment.java +++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java @@ -17,7 +17,6 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; - import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevicePicker; @@ -25,6 +24,9 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import com.android.settings.R; @@ -33,6 +35,7 @@ import com.android.settings.R; * connection management. */ public final class DevicePickerFragment extends DeviceListPreferenceFragment { + private static final int MENU_ID_REFRESH = Menu.FIRST; public DevicePickerFragment() { super(null /* Not tied to any user restrictions. */); @@ -56,12 +59,36 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { } @Override + void initDevicePreference(BluetoothDevicePreference preference) { + preference.setWidgetLayoutResource(R.layout.preference_empty_list); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, MENU_ID_REFRESH, 0, R.string.bluetooth_search_for_devices) + .setEnabled(true) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_ID_REFRESH: + mLocalAdapter.startScanning(true); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(getString(R.string.device_picker)); UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); mStartScanOnResume = !um.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH) && (savedInstanceState == null); // don't start scan after rotation + setHasOptionsMenu(true); } @Override diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java index 335d888..757535a 100755 --- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java +++ b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java @@ -17,6 +17,7 @@ package com.android.settings.bluetooth; import android.app.AlertDialog; +import android.app.Fragment; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; @@ -36,8 +37,11 @@ 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; +import com.android.settings.search.Index; +import com.android.settings.search.SearchIndexableRaw; import java.util.HashMap; @@ -50,14 +54,12 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment implements CachedBluetoothDevice.Callback, Preference.OnPreferenceChangeListener { private static final String TAG = "DeviceProfilesSettings"; - 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_PBAP_SERVER = "PBAP Server"; - public static final String EXTRA_DEVICE = "device"; - private RenameEditTextPreference mRenameDeviceNamePref; - private LocalBluetoothManager mManager; private CachedBluetoothDevice mCachedDevice; + private LocalBluetoothManager mManager; private LocalBluetoothProfileManager mProfileManager; private PreferenceGroup mProfileContainer; @@ -69,66 +71,19 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment 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) { super.onCreate(savedInstanceState); - BluetoothDevice device; - if (savedInstanceState != null) { - device = savedInstanceState.getParcelable(EXTRA_DEVICE); - } else { - Bundle args = getArguments(); - device = args.getParcelable(EXTRA_DEVICE); - } - addPreferencesFromResource(R.xml.bluetooth_device_advanced); getPreferenceScreen().setOrderingAsAdded(false); mProfileContainer = (PreferenceGroup) findPreference(KEY_PROFILE_CONTAINER); - mDeviceNamePref = (EditTextPreference) findPreference(KEY_RENAME_DEVICE); + mProfileContainer.setLayoutResource(R.layout.bluetooth_preference_category); - if (device == null) { - Log.w(TAG, "Activity started without a remote Bluetooth device"); - finish(); - return; // TODO: test this failure path - } - mRenameDeviceNamePref = new RenameEditTextPreference(); mManager = LocalBluetoothManager.getInstance(getActivity()); CachedBluetoothDeviceManager deviceManager = mManager.getCachedDeviceManager(); mProfileManager = mManager.getProfileManager(); - mCachedDevice = deviceManager.findDevice(device); - if (mCachedDevice == null) { - Log.w(TAG, "Device not found, cannot connect to it"); - finish(); - return; // TODO: test this failure path - } - - String deviceName = mCachedDevice.getName(); - mDeviceNamePref.setSummary(deviceName); - mDeviceNamePref.setText(deviceName); - mDeviceNamePref.setOnPreferenceChangeListener(this); - - // Add a preference for each profile - addPreferencesForProfiles(); } @Override @@ -138,12 +93,14 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mDisconnectDialog.dismiss(); mDisconnectDialog = null; } + if (mCachedDevice != null) { + mCachedDevice.unregisterCallback(this); + } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putParcelable(EXTRA_DEVICE, mCachedDevice.getDevice()); } @Override @@ -151,18 +108,13 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment super.onResume(); 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); + if (mCachedDevice != null) { + mCachedDevice.registerCallback(this); + if (mCachedDevice.getBondState() == BluetoothDevice.BOND_NONE) { + finish(); + return; } + refresh(); } } @@ -170,15 +122,45 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment public void onPause() { super.onPause(); - mCachedDevice.unregisterCallback(this); + if (mCachedDevice != null) { + mCachedDevice.unregisterCallback(this); + } + mManager.setForegroundActivity(null); } + public void setDevice(CachedBluetoothDevice cachedDevice) { + mCachedDevice = cachedDevice; + + if (isResumed()) { + mCachedDevice.registerCallback(this); + addPreferencesForProfiles(); + refresh(); + } + } + private void addPreferencesForProfiles() { + mProfileContainer.removeAll(); for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { Preference pref = createProfilePreference(profile); mProfileContainer.addPreference(pref); } + + final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); + // Only provide PBAP cabability if the client device has requested PBAP. + if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); + CheckBoxPreference pbapPref = createProfilePreference(psp); + mProfileContainer.addPreference(pbapPref); + } + + final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); + final int mapPermission = mCachedDevice.getMessagePermissionChoice(); + if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + CheckBoxPreference mapPreference = createProfilePreference(mapProfile); + mProfileContainer.addPreference(mapPreference); + } + showOrHideProfileGroup(); } @@ -203,6 +185,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment */ private CheckBoxPreference createProfilePreference(LocalBluetoothProfile profile) { CheckBoxPreference pref = new CheckBoxPreference(getActivity()); + pref.setLayoutResource(R.layout.preference_start_widget); pref.setKey(profile.toString()); pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); pref.setPersistent(false); @@ -214,28 +197,11 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment pref.setIcon(getResources().getDrawable(iconResource)); } - /** - * Gray out profile while connecting and disconnecting - */ - pref.setEnabled(!mCachedDevice.isBusy()); - refreshProfilePreference(pref, profile); return pref; } - @Override - public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { - String key = preference.getKey(); - if (key.equals(KEY_UNPAIR)) { - unpairDevice(); - finish(); - return true; - } - - return super.onPreferenceTreeClick(screen, preference); - } - public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference == mDeviceNamePref) { mCachedDevice.setName((String) newValue); @@ -253,13 +219,26 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment private void onProfileClicked(LocalBluetoothProfile profile, CheckBoxPreference profilePref) { BluetoothDevice device = mCachedDevice.getDevice(); + if (profilePref.getKey().equals(KEY_PBAP_SERVER)) { + final int newPermission = mCachedDevice.getPhonebookPermissionChoice() + == CachedBluetoothDevice.ACCESS_ALLOWED ? CachedBluetoothDevice.ACCESS_REJECTED + : CachedBluetoothDevice.ACCESS_ALLOWED; + mCachedDevice.setPhonebookPermissionChoice(newPermission); + profilePref.setChecked(newPermission == CachedBluetoothDevice.ACCESS_ALLOWED); + return; + } + int status = profile.getConnectionStatus(device); boolean isConnected = status == BluetoothProfile.STATE_CONNECTED; - if (isConnected) { - askDisconnect(getActivity(), profile); + if (profilePref.isChecked()) { + askDisconnect(mManager.getForegroundActivity(), profile); } else { + if (profile instanceof MapProfile) { + mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); + refreshProfilePreference(profilePref, profile); + } if (profile.isPreferred(device)) { // profile is preferred but not connected: disable auto-connect profile.setPreferred(device, false); @@ -291,6 +270,11 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment public void onClick(DialogInterface dialog, int which) { device.disconnect(profile); profile.setPreferred(device.getDevice(), false); + if (profile instanceof MapProfile) { + device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); + refreshProfilePreference( + (CheckBoxPreference)findPreference(profile.toString()), profile); + } } }; @@ -298,14 +282,16 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); } + @Override public void onDeviceAttributesChanged() { refresh(); } private void refresh() { - String deviceName = mCachedDevice.getName(); - mDeviceNamePref.setSummary(deviceName); - mDeviceNamePref.setText(deviceName); + final EditText deviceNameField = (EditText) getView().findViewById(R.id.name); + if (deviceNameField != null) { + deviceNameField.setText(mCachedDevice.getName()); + } refreshProfiles(); } @@ -327,6 +313,7 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment mProfileContainer.removePreference(profilePref); } } + showOrHideProfileGroup(); } @@ -334,12 +321,19 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment LocalBluetoothProfile profile) { BluetoothDevice device = mCachedDevice.getDevice(); - /* - * Gray out checkbox while connecting and disconnecting - */ + // Gray out checkbox while connecting and disconnecting. profilePref.setEnabled(!mCachedDevice.isBusy()); - profilePref.setChecked(profile.isPreferred(device)); - profilePref.setSummary(profile.getSummaryResourceForDevice(device)); + + if (profile instanceof MapProfile) { + profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() + == CachedBluetoothDevice.ACCESS_ALLOWED); + } else if (profile instanceof PbapServerProfile) { + // Handle PBAP specially. + profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() + == CachedBluetoothDevice.ACCESS_ALLOWED); + } else { + profilePref.setChecked(profile.isPreferred(device)); + } } private LocalBluetoothProfile getProfileOf(Preference pref) { @@ -359,8 +353,4 @@ public final class DeviceProfilesSettings extends SettingsPreferenceFragment private int getProfilePreferenceIndex(int profIndex) { return mProfileContainer.getOrder() + profIndex * 10; } - - private void unpairDevice() { - mCachedDevice.unpair(); - } } diff --git a/src/com/android/settings/bluetooth/HeadsetProfile.java b/src/com/android/settings/bluetooth/HeadsetProfile.java index 1caeb65..45b81ab 100755 --- a/src/com/android/settings/bluetooth/HeadsetProfile.java +++ b/src/com/android/settings/bluetooth/HeadsetProfile.java @@ -115,7 +115,7 @@ final class HeadsetProfile implements LocalBluetoothProfile { List<BluetoothDevice> sinks = mService.getConnectedDevices(); if (sinks != null) { for (BluetoothDevice sink : sinks) { - mService.disconnect(sink); + Log.d(TAG,"Not disconnecting device = " + sink); } } return mService.connect(device); @@ -124,24 +124,33 @@ final class HeadsetProfile implements LocalBluetoothProfile { public boolean disconnect(BluetoothDevice device) { if (mService == null) return false; List<BluetoothDevice> deviceList = mService.getConnectedDevices(); - if (!deviceList.isEmpty() && deviceList.get(0).equals(device)) { - // Downgrade priority as user is disconnecting the headset. - if (mService.getPriority(device) > BluetoothProfile.PRIORITY_ON) { - mService.setPriority(device, BluetoothProfile.PRIORITY_ON); + if (!deviceList.isEmpty()) { + for (BluetoothDevice dev : deviceList) { + if (dev.equals(device)) { + if (V) Log.d(TAG,"Downgrade priority as user" + + "is disconnecting the headset"); + // Downgrade priority as user is disconnecting the headset. + if (mService.getPriority(device) > BluetoothProfile.PRIORITY_ON) { + mService.setPriority(device, BluetoothProfile.PRIORITY_ON); + } + return mService.disconnect(device); + } } - return mService.disconnect(device); - } else { - return false; } + return false; } public int getConnectionStatus(BluetoothDevice device) { if (mService == null) return BluetoothProfile.STATE_DISCONNECTED; List<BluetoothDevice> deviceList = mService.getConnectedDevices(); - - return !deviceList.isEmpty() && deviceList.get(0).equals(device) - ? mService.getConnectionState(device) - : BluetoothProfile.STATE_DISCONNECTED; + if (!deviceList.isEmpty()){ + for (BluetoothDevice dev : deviceList) { + if (dev.equals(device)) { + return mService.getConnectionState(device); + } + } + } + return BluetoothProfile.STATE_DISCONNECTED; } public boolean isPreferred(BluetoothDevice device) { diff --git a/src/com/android/settings/bluetooth/HidProfile.java b/src/com/android/settings/bluetooth/HidProfile.java index 8df2845..91e715d 100755 --- a/src/com/android/settings/bluetooth/HidProfile.java +++ b/src/com/android/settings/bluetooth/HidProfile.java @@ -169,7 +169,7 @@ final class HidProfile implements LocalBluetoothProfile { public int getDrawableResource(BluetoothClass btClass) { if (btClass == null) { - return R.drawable.ic_bt_keyboard_hid; + return R.drawable.ic_lockscreen_ime; } return getHidClassDrawable(btClass); } @@ -178,7 +178,7 @@ final class HidProfile implements LocalBluetoothProfile { switch (btClass.getDeviceClass()) { case BluetoothClass.Device.PERIPHERAL_KEYBOARD: case BluetoothClass.Device.PERIPHERAL_KEYBOARD_POINTING: - return R.drawable.ic_bt_keyboard_hid; + return R.drawable.ic_lockscreen_ime; case BluetoothClass.Device.PERIPHERAL_POINTING: return R.drawable.ic_bt_pointing_hid; default: diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java index 8fff9648..2a6a759 100644 --- a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java +++ b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java @@ -309,6 +309,10 @@ final class LocalBluetoothProfileManager { return mPbapProfile; } + MapProfile getMapProfile(){ + return mMapProfile; + } + /** * Fill in a list of LocalBluetoothProfile objects that are supported by * the local device and the remote device. diff --git a/src/com/android/settings/bluetooth/MapProfile.java b/src/com/android/settings/bluetooth/MapProfile.java index fb1b180..f47e24f 100644 --- a/src/com/android/settings/bluetooth/MapProfile.java +++ b/src/com/android/settings/bluetooth/MapProfile.java @@ -113,7 +113,7 @@ final class MapProfile implements LocalBluetoothProfile { public boolean connect(BluetoothDevice device) { if(V)Log.d(TAG,"connect() - should not get called"); - return true; // MAP never connects out + return false; // MAP never connects out } public boolean disconnect(BluetoothDevice device) { diff --git a/src/com/android/settings/bluetooth/PbapServerProfile.java b/src/com/android/settings/bluetooth/PbapServerProfile.java index 1f5ca32..87e51a5 100755 --- a/src/com/android/settings/bluetooth/PbapServerProfile.java +++ b/src/com/android/settings/bluetooth/PbapServerProfile.java @@ -118,16 +118,17 @@ final class PbapServerProfile implements LocalBluetoothProfile { } public int getNameResource(BluetoothDevice device) { - return 0; + return R.string.bluetooth_profile_pbap; } public int getSummaryResourceForDevice(BluetoothDevice device) { - return 0; + return R.string.bluetooth_profile_pbap_summary; } public int getDrawableResource(BluetoothClass btClass) { - return 0; + return R.drawable.ic_bt_cellphone; } + protected void finalize() { if (V) Log.d(TAG, "finalize()"); if (mService != null) { diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index fb44d5a..e9230de 100755 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -24,6 +24,8 @@ import android.content.DialogInterface; import android.widget.Toast; import com.android.settings.R; +import com.android.settings.search.Index; +import com.android.settings.search.SearchIndexableRaw; /** * Utils is a helper class that contains constants for various @@ -93,7 +95,6 @@ final class Utils { Context activity = manager.getForegroundActivity(); if(manager.isForegroundActivity()) { new AlertDialog.Builder(activity) - .setIconAttribute(android.R.attr.alertDialogIcon) .setTitle(R.string.bluetooth_error_title) .setMessage(message) .setPositiveButton(android.R.string.ok, null) @@ -102,4 +103,19 @@ final class Utils { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } } + + /** + * Update the search Index for a specific class name and resources. + */ + public static void updateSearchIndex(Context context, String className, String title, + String screenTitle, int iconResId, boolean enabled) { + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.className = className; + data.title = title; + data.screenTitle = screenTitle; + data.iconResId = iconResId; + data.enabled = enabled; + + Index.getInstance(context).updateFromSearchIndexableData(data); + } } diff --git a/src/com/android/settings/dashboard/DashboardCategory.java b/src/com/android/settings/dashboard/DashboardCategory.java new file mode 100644 index 0000000..164d988 --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardCategory.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.content.res.Resources; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Parcelable.Creator; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +public class DashboardCategory implements Parcelable { + + /** + * Default value for {@link com.android.settings.dashboard.DashboardCategory#id DashboardCategory.id} + * indicating that no identifier value is set. All other values (including those below -1) + * are valid. + */ + public static final long CAT_ID_UNDEFINED = -1; + + /** + * Identifier for this tile, to correlate with a new list when + * it is updated. The default value is + * {@link com.android.settings.dashboard.DashboardTile#TILE_ID_UNDEFINED}, meaning no id. + * @attr ref android.R.styleable#PreferenceHeader_id + */ + public long id = CAT_ID_UNDEFINED; + + /** + * Resource ID of title of the category that is shown to the user. + */ + public int titleRes; + + /** + * Title of the category that is shown to the user. + */ + public CharSequence title; + + /** + * List of the category's children + */ + public List<DashboardTile> tiles = new ArrayList<DashboardTile>(); + + + public DashboardCategory() { + // Empty + } + + public void addTile(DashboardTile tile) { + tiles.add(tile); + } + + public void addTile(int n, DashboardTile tile) { + tiles.add(n, tile); + } + + public void removeTile(DashboardTile tile) { + tiles.remove(tile); + } + + public void removeTile(int n) { + tiles.remove(n); + } + + public int getTilesCount() { + return tiles.size(); + } + + public DashboardTile getTile(int n) { + return tiles.get(n); + } + + /** + * Return the currently set title. If {@link #titleRes} is set, + * this resource is loaded from <var>res</var> and returned. Otherwise + * {@link #title} is returned. + */ + public CharSequence getTitle(Resources res) { + if (titleRes != 0) { + return res.getText(titleRes); + } + return title; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(titleRes); + TextUtils.writeToParcel(title, dest, flags); + + final int count = tiles.size(); + dest.writeInt(count); + + for (int n = 0; n < count; n++) { + DashboardTile tile = tiles.get(n); + tile.writeToParcel(dest, flags); + } + } + + public void readFromParcel(Parcel in) { + titleRes = in.readInt(); + title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + + final int count = in.readInt(); + + for (int n = 0; n < count; n++) { + DashboardTile tile = DashboardTile.CREATOR.createFromParcel(in); + tiles.add(tile); + } + } + + DashboardCategory(Parcel in) { + readFromParcel(in); + } + + public static final Creator<DashboardCategory> CREATOR = new Creator<DashboardCategory>() { + public DashboardCategory createFromParcel(Parcel source) { + return new DashboardCategory(source); + } + + public DashboardCategory[] newArray(int size) { + return new DashboardCategory[size]; + } + }; +} diff --git a/src/com/android/settings/dashboard/DashboardContainerView.java b/src/com/android/settings/dashboard/DashboardContainerView.java new file mode 100644 index 0000000..f009891 --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardContainerView.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import com.android.settings.R; + +public class DashboardContainerView extends ViewGroup { + + private float mCellGapX; + private float mCellGapY; + + private int mNumRows; + private int mNumColumns; + + public DashboardContainerView(Context context, AttributeSet attrs) { + super(context, attrs); + + final Resources res = context.getResources(); + mCellGapX = res.getDimension(R.dimen.dashboard_cell_gap_x); + mCellGapY = res.getDimension(R.dimen.dashboard_cell_gap_y); + mNumColumns = res.getInteger(R.integer.dashboard_num_columns); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int availableWidth = (int) (width - getPaddingLeft() - getPaddingRight() - + (mNumColumns - 1) * mCellGapX); + float cellWidth = (float) Math.ceil(((float) availableWidth) / mNumColumns); + final int N = getChildCount(); + + int cellHeight = 0; + int cursor = 0; + + for (int i = 0; i < N; ++i) { + DashboardTileView v = (DashboardTileView) getChildAt(i); + if (v.getVisibility() == View.GONE) { + continue; + } + + ViewGroup.LayoutParams lp = v.getLayoutParams(); + int colSpan = v.getColumnSpan(); + lp.width = (int) ((colSpan * cellWidth) + (colSpan - 1) * mCellGapX); + + // Measure the child + int newWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width); + int newHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height); + v.measure(newWidthSpec, newHeightSpec); + + // Save the cell height + if (cellHeight <= 0) { + cellHeight = v.getMeasuredHeight(); + } + + lp.height = cellHeight; + + cursor += colSpan; + } + + mNumRows = (int) Math.ceil((float) cursor / mNumColumns); + final int newHeight = (int) ((mNumRows * cellHeight) + ((mNumRows - 1) * mCellGapY)) + + getPaddingTop() + getPaddingBottom(); + + setMeasuredDimension(width, newHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int N = getChildCount(); + final boolean isLayoutRtl = isLayoutRtl(); + final int width = getWidth(); + + int x = getPaddingStart(); + int y = getPaddingTop(); + int cursor = 0; + + for (int i = 0; i < N; ++i) { + final DashboardTileView child = (DashboardTileView) getChildAt(i); + final ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (child.getVisibility() == GONE) { + continue; + } + + final int col = cursor % mNumColumns; + final int colSpan = child.getColumnSpan(); + + final int childWidth = lp.width; + final int childHeight = lp.height; + + int row = cursor / mNumColumns; + + if (row == mNumRows - 1) { + child.setDividerVisibility(false); + } + + // Push the item to the next row if it can't fit on this one + if ((col + colSpan) > mNumColumns) { + x = getPaddingStart(); + y += childHeight + mCellGapY; + row++; + } + + final int childLeft = (isLayoutRtl) ? width - x - childWidth : x; + final int childRight = childLeft + childWidth; + + final int childTop = y; + final int childBottom = childTop + childHeight; + + // Layout the container + child.layout(childLeft, childTop, childRight, childBottom); + + // Offset the position by the cell gap or reset the position and cursor when we + // reach the end of the row + cursor += child.getColumnSpan(); + if (cursor < (((row + 1) * mNumColumns))) { + x += childWidth + mCellGapX; + } else { + x = getPaddingStart(); + y += childHeight + mCellGapY; + } + } + } +} diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java new file mode 100644 index 0000000..cf398d7 --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardSummary.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.app.Fragment; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.settings.R; +import com.android.settings.SettingsActivity; + +import java.util.List; + +public class DashboardSummary extends Fragment { + private static final String LOG_TAG = "DashboardSummary"; + + private LayoutInflater mLayoutInflater; + private ViewGroup mDashboard; + + private static final int MSG_REBUILD_UI = 1; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REBUILD_UI: { + final Context context = getActivity(); + rebuildUI(context); + } break; + } + } + }; + + private class HomePackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + rebuildUI(context); + } + } + private HomePackageReceiver mHomePackageReceiver = new HomePackageReceiver(); + + @Override + public void onResume() { + super.onResume(); + + sendRebuildUI(); + + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addDataScheme("package"); + getActivity().registerReceiver(mHomePackageReceiver, filter); + } + + @Override + public void onPause() { + super.onPause(); + + getActivity().unregisterReceiver(mHomePackageReceiver); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + mLayoutInflater = inflater; + + final View rootView = inflater.inflate(R.layout.dashboard, container, false); + mDashboard = (ViewGroup) rootView.findViewById(R.id.dashboard_container); + + return rootView; + } + + private void rebuildUI(Context context) { + if (!isAdded()) { + Log.w(LOG_TAG, "Cannot build the DashboardSummary UI yet as the Fragment is not added"); + return; + } + + long start = System.currentTimeMillis(); + final Resources res = getResources(); + + mDashboard.removeAllViews(); + + List<DashboardCategory> categories = + ((SettingsActivity) context).getDashboardCategories(true); + + final int count = categories.size(); + + for (int n = 0; n < count; n++) { + DashboardCategory category = categories.get(n); + + View categoryView = mLayoutInflater.inflate(R.layout.dashboard_category, mDashboard, + false); + + TextView categoryLabel = (TextView) categoryView.findViewById(R.id.category_title); + categoryLabel.setText(category.getTitle(res)); + + ViewGroup categoryContent = + (ViewGroup) categoryView.findViewById(R.id.category_content); + + final int tilesCount = category.getTilesCount(); + for (int i = 0; i < tilesCount; i++) { + DashboardTile tile = category.getTile(i); + + DashboardTileView tileView = new DashboardTileView(context); + updateTileView(context, res, tile, tileView.getImageView(), + tileView.getTitleTextView(), tileView.getStatusTextView()); + + tileView.setTile(tile); + + categoryContent.addView(tileView); + } + + // Add the category + mDashboard.addView(categoryView); + } + long delta = System.currentTimeMillis() - start; + Log.d(LOG_TAG, "rebuildUI took: " + delta + " ms"); + } + + private void updateTileView(Context context, Resources res, DashboardTile tile, + ImageView tileIcon, TextView tileTextView, TextView statusTextView) { + + if (tile.iconRes > 0) { + tileIcon.setImageResource(tile.iconRes); + } else { + tileIcon.setImageDrawable(null); + tileIcon.setBackground(null); + } + + tileTextView.setText(tile.getTitle(res)); + + CharSequence summary = tile.getSummary(res); + if (!TextUtils.isEmpty(summary)) { + statusTextView.setVisibility(View.VISIBLE); + statusTextView.setText(summary); + } else { + statusTextView.setVisibility(View.GONE); + } + } + + private void sendRebuildUI() { + if (!mHandler.hasMessages(MSG_REBUILD_UI)) { + mHandler.sendEmptyMessage(MSG_REBUILD_UI); + } + } +} diff --git a/src/com/android/settings/dashboard/DashboardTile.java b/src/com/android/settings/dashboard/DashboardTile.java new file mode 100644 index 0000000..1f1d9c2 --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardTile.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Description of a single dashboard tile that the user can select. + */ +public class DashboardTile implements Parcelable { + /** + * Default value for {@link com.android.settings.dashboard.DashboardTile#id DashboardTile.id} + * indicating that no identifier value is set. All other values (including those below -1) + * are valid. + */ + public static final long TILE_ID_UNDEFINED = -1; + + /** + * Identifier for this tile, to correlate with a new list when + * it is updated. The default value is + * {@link com.android.settings.dashboard.DashboardTile#TILE_ID_UNDEFINED}, meaning no id. + * @attr ref android.R.styleable#PreferenceHeader_id + */ + public long id = TILE_ID_UNDEFINED; + + /** + * Resource ID of title of the tile that is shown to the user. + * @attr ref android.R.styleable#PreferenceHeader_title + */ + public int titleRes; + + /** + * Title of the tile that is shown to the user. + * @attr ref android.R.styleable#PreferenceHeader_title + */ + public CharSequence title; + + /** + * Resource ID of optional summary describing what this tile controls. + * @attr ref android.R.styleable#PreferenceHeader_summary + */ + public int summaryRes; + + /** + * Optional summary describing what this tile controls. + * @attr ref android.R.styleable#PreferenceHeader_summary + */ + public CharSequence summary; + + /** + * Optional icon resource to show for this tile. + * @attr ref android.R.styleable#PreferenceHeader_icon + */ + public int iconRes; + + /** + * Full class name of the fragment to display when this tile is + * selected. + * @attr ref android.R.styleable#PreferenceHeader_fragment + */ + public String fragment; + + /** + * Optional arguments to supply to the fragment when it is + * instantiated. + */ + public Bundle fragmentArguments; + + /** + * Intent to launch when the preference is selected. + */ + public Intent intent; + + /** + * Optional additional data for use by subclasses of the activity + */ + public Bundle extras; + + public DashboardTile() { + // Empty + } + + /** + * Return the currently set title. If {@link #titleRes} is set, + * this resource is loaded from <var>res</var> and returned. Otherwise + * {@link #title} is returned. + */ + public CharSequence getTitle(Resources res) { + if (titleRes != 0) { + return res.getText(titleRes); + } + return title; + } + + /** + * Return the currently set summary. If {@link #summaryRes} is set, + * this resource is loaded from <var>res</var> and returned. Otherwise + * {@link #summary} is returned. + */ + public CharSequence getSummary(Resources res) { + if (summaryRes != 0) { + return res.getText(summaryRes); + } + return summary; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + dest.writeInt(titleRes); + TextUtils.writeToParcel(title, dest, flags); + dest.writeInt(summaryRes); + TextUtils.writeToParcel(summary, dest, flags); + dest.writeInt(iconRes); + dest.writeString(fragment); + dest.writeBundle(fragmentArguments); + if (intent != null) { + dest.writeInt(1); + intent.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeBundle(extras); + } + + public void readFromParcel(Parcel in) { + id = in.readLong(); + titleRes = in.readInt(); + title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + summaryRes = in.readInt(); + summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + iconRes = in.readInt(); + fragment = in.readString(); + fragmentArguments = in.readBundle(); + if (in.readInt() != 0) { + intent = Intent.CREATOR.createFromParcel(in); + } + extras = in.readBundle(); + } + + DashboardTile(Parcel in) { + readFromParcel(in); + } + + public static final Creator<DashboardTile> CREATOR = new Creator<DashboardTile>() { + public DashboardTile createFromParcel(Parcel source) { + return new DashboardTile(source); + } + public DashboardTile[] newArray(int size) { + return new DashboardTile[size]; + } + }; +} diff --git a/src/com/android/settings/dashboard/DashboardTileView.java b/src/com/android/settings/dashboard/DashboardTileView.java new file mode 100644 index 0000000..a54217b --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardTileView.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; + +import android.widget.ImageView; +import android.widget.TextView; +import com.android.settings.R; +import com.android.settings.Utils; + +public class DashboardTileView extends FrameLayout implements View.OnClickListener { + + private static final int DEFAULT_COL_SPAN = 1; + + private ImageView mImageView; + private TextView mTitleTextView; + private TextView mStatusTextView; + private View mDivider; + + private int mColSpan = DEFAULT_COL_SPAN; + + private DashboardTile mTile; + + public DashboardTileView(Context context) { + this(context, null); + } + + public DashboardTileView(Context context, AttributeSet attrs) { + super(context, attrs); + + final View view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this); + + mImageView = (ImageView) view.findViewById(R.id.icon); + mTitleTextView = (TextView) view.findViewById(R.id.title); + mStatusTextView = (TextView) view.findViewById(R.id.status); + mDivider = view.findViewById(R.id.tile_divider); + + setOnClickListener(this); + setBackgroundResource(R.drawable.dashboard_tile_background); + setFocusable(true); + } + + public TextView getTitleTextView() { + return mTitleTextView; + } + + public TextView getStatusTextView() { + return mStatusTextView; + } + + public ImageView getImageView() { + return mImageView; + } + + public void setTile(DashboardTile tile) { + mTile = tile; + } + + public void setDividerVisibility(boolean visible) { + mDivider.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + void setColumnSpan(int span) { + mColSpan = span; + } + + int getColumnSpan() { + return mColSpan; + } + + @Override + public void onClick(View v) { + if (mTile.fragment != null) { + Utils.startWithFragment(getContext(), mTile.fragment, mTile.fragmentArguments, null, 0, + mTile.titleRes, mTile.getTitle(getResources())); + } else if (mTile.intent != null) { + getContext().startActivity(mTile.intent); + } + } +} diff --git a/src/com/android/settings/dashboard/NoHomeDialogFragment.java b/src/com/android/settings/dashboard/NoHomeDialogFragment.java new file mode 100644 index 0000000..a795cc9 --- /dev/null +++ b/src/com/android/settings/dashboard/NoHomeDialogFragment.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.os.Bundle; +import com.android.settings.R; + +public class NoHomeDialogFragment extends DialogFragment { + public static void show(Activity parent) { + final NoHomeDialogFragment dialog = new NoHomeDialogFragment(); + dialog.show(parent.getFragmentManager(), null); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setMessage(R.string.only_one_home_message) + .setPositiveButton(android.R.string.ok, null) + .create(); + } +} diff --git a/src/com/android/settings/dashboard/SearchResultsSummary.java b/src/com/android/settings/dashboard/SearchResultsSummary.java new file mode 100644 index 0000000..1ae0c25 --- /dev/null +++ b/src/com/android/settings/dashboard/SearchResultsSummary.java @@ -0,0 +1,629 @@ +/* + * Copyright (C) 2014 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.dashboard; + +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; +import com.android.settings.search.Index; + +import java.util.HashMap; + +public class SearchResultsSummary extends Fragment { + + private static final String LOG_TAG = "SearchResultsSummary"; + + private static final String EMPTY_QUERY = ""; + private static char ELLIPSIS = '\u2026'; + + private static final String SAVE_KEY_SHOW_RESULTS = ":settings:show_results"; + + private SearchView mSearchView; + + private ListView mResultsListView; + private SearchResultsAdapter mResultsAdapter; + private UpdateSearchResultsTask mUpdateSearchResultsTask; + + private ListView mSuggestionsListView; + private SuggestionsAdapter mSuggestionsAdapter; + private UpdateSuggestionsTask mUpdateSuggestionsTask; + + private ViewGroup mLayoutSuggestions; + private ViewGroup mLayoutResults; + + private String mQuery; + + private boolean mShowResults; + + /** + * A basic AsyncTask for updating the query results cursor + */ + private class UpdateSearchResultsTask extends AsyncTask<String, Void, Cursor> { + @Override + protected Cursor doInBackground(String... params) { + return Index.getInstance(getActivity()).search(params[0]); + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (!isCancelled()) { + setResultsCursor(cursor); + setResultsVisibility(cursor.getCount() > 0); + } else if (cursor != null) { + cursor.close(); + } + } + } + + /** + * A basic AsyncTask for updating the suggestions cursor + */ + private class UpdateSuggestionsTask extends AsyncTask<String, Void, Cursor> { + @Override + protected Cursor doInBackground(String... params) { + return Index.getInstance(getActivity()).getSuggestions(params[0]); + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (!isCancelled()) { + setSuggestionsCursor(cursor); + setSuggestionsVisibility(cursor.getCount() > 0); + } else if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mResultsAdapter = new SearchResultsAdapter(getActivity()); + mSuggestionsAdapter = new SuggestionsAdapter(getActivity()); + + if (savedInstanceState != null) { + mShowResults = savedInstanceState.getBoolean(SAVE_KEY_SHOW_RESULTS); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(SAVE_KEY_SHOW_RESULTS, mShowResults); + } + + @Override + public void onStop() { + super.onStop(); + + clearSuggestions(); + clearResults(); + } + + @Override + public void onDestroy() { + mResultsListView = null; + mResultsAdapter = null; + mUpdateSearchResultsTask = null; + + mSuggestionsListView = null; + mSuggestionsAdapter = null; + mUpdateSuggestionsTask = null; + + mSearchView = null; + + super.onDestroy(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final View view = inflater.inflate(R.layout.search_panel, container, false); + + mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions); + mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results); + + mResultsListView = (ListView) view.findViewById(R.id.list_results); + mResultsListView.setAdapter(mResultsAdapter); + mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + // We have a header, so we need to decrement the position by one + position--; + + // Some Monkeys could create a case where they were probably clicking on the + // List Header and thus the position passed was "0" and then by decrement was "-1" + if (position < 0) { + return; + } + + final Cursor cursor = mResultsAdapter.mCursor; + cursor.moveToPosition(position); + + final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); + final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE); + final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION); + final String key = cursor.getString(Index.COLUMN_INDEX_KEY); + + final SettingsActivity sa = (SettingsActivity) getActivity(); + sa.needToRevertToInitialFragment(); + + if (TextUtils.isEmpty(action)) { + Bundle args = new Bundle(); + args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); + + Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); + } else { + final Intent intent = new Intent(action); + + final String targetPackage = cursor.getString( + Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); + final String targetClass = cursor.getString( + Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS); + if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) { + final ComponentName component = + new ComponentName(targetPackage, targetClass); + intent.setComponent(component); + } + intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); + + sa.startActivity(intent); + } + + saveQueryToDatabase(); + } + }); + mResultsListView.addHeaderView( + LayoutInflater.from(getActivity()).inflate( + R.layout.search_panel_results_header, mResultsListView, false), + null, false); + + mSuggestionsListView = (ListView) view.findViewById(R.id.list_suggestions); + mSuggestionsListView.setAdapter(mSuggestionsAdapter); + mSuggestionsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + // We have a header, so we need to decrement the position by one + position--; + // Some Monkeys could create a case where they were probably clicking on the + // List Header and thus the position passed was "0" and then by decrement was "-1" + if (position < 0) { + return; + } + final Cursor cursor = mSuggestionsAdapter.mCursor; + cursor.moveToPosition(position); + + mShowResults = true; + mQuery = cursor.getString(0); + mSearchView.setQuery(mQuery, false); + } + }); + mSuggestionsListView.addHeaderView( + LayoutInflater.from(getActivity()).inflate( + R.layout.search_panel_suggestions_header, mSuggestionsListView, false), + null, false); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (!mShowResults) { + showSomeSuggestions(); + } + } + + public void setSearchView(SearchView searchView) { + mSearchView = searchView; + } + + private void setSuggestionsVisibility(boolean visible) { + if (mLayoutSuggestions != null) { + mLayoutSuggestions.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + private void setResultsVisibility(boolean visible) { + if (mLayoutResults != null) { + mLayoutResults.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + private void saveQueryToDatabase() { + Index.getInstance(getActivity()).addSavedQuery(mQuery); + } + + public boolean onQueryTextSubmit(String query) { + mQuery = getFilteredQueryString(query); + mShowResults = true; + setSuggestionsVisibility(false); + updateSearchResults(); + saveQueryToDatabase(); + return true; + } + + public boolean onQueryTextChange(String query) { + final String newQuery = getFilteredQueryString(query); + + mQuery = newQuery; + + if (TextUtils.isEmpty(mQuery)) { + mShowResults = false; + setResultsVisibility(false); + updateSuggestions(); + } else { + mShowResults = true; + setSuggestionsVisibility(false); + updateSearchResults(); + } + + return true; + } + + public void showSomeSuggestions() { + setResultsVisibility(false); + mQuery = EMPTY_QUERY; + updateSuggestions(); + } + + private void clearSuggestions() { + if (mUpdateSuggestionsTask != null) { + mUpdateSuggestionsTask.cancel(false); + mUpdateSuggestionsTask = null; + } + setSuggestionsCursor(null); + } + + private void setSuggestionsCursor(Cursor cursor) { + if (mSuggestionsAdapter == null) { + return; + } + Cursor oldCursor = mSuggestionsAdapter.swapCursor(cursor); + if (oldCursor != null) { + oldCursor.close(); + } + } + + private void clearResults() { + if (mUpdateSearchResultsTask != null) { + mUpdateSearchResultsTask.cancel(false); + mUpdateSearchResultsTask = null; + } + setResultsCursor(null); + } + + private void setResultsCursor(Cursor cursor) { + if (mResultsAdapter == null) { + return; + } + Cursor oldCursor = mResultsAdapter.swapCursor(cursor); + if (oldCursor != null) { + oldCursor.close(); + } + } + + private String getFilteredQueryString(CharSequence query) { + if (query == null) { + return null; + } + final StringBuilder filtered = new StringBuilder(); + for (int n = 0; n < query.length(); n++) { + char c = query.charAt(n); + if (!Character.isLetterOrDigit(c) && !Character.isSpaceChar(c)) { + continue; + } + filtered.append(c); + } + return filtered.toString(); + } + + private void clearAllTasks() { + if (mUpdateSearchResultsTask != null) { + mUpdateSearchResultsTask.cancel(false); + mUpdateSearchResultsTask = null; + } + if (mUpdateSuggestionsTask != null) { + mUpdateSuggestionsTask.cancel(false); + mUpdateSuggestionsTask = null; + } + } + + private void updateSuggestions() { + clearAllTasks(); + if (mQuery == null) { + setSuggestionsCursor(null); + } else { + mUpdateSuggestionsTask = new UpdateSuggestionsTask(); + mUpdateSuggestionsTask.execute(mQuery); + } + } + + private void updateSearchResults() { + clearAllTasks(); + if (TextUtils.isEmpty(mQuery)) { + setResultsVisibility(false); + setResultsCursor(null); + } else { + mUpdateSearchResultsTask = new UpdateSearchResultsTask(); + mUpdateSearchResultsTask.execute(mQuery); + } + } + + private static class SuggestionItem { + public String query; + + public SuggestionItem(String query) { + this.query = query; + } + } + + private static class SuggestionsAdapter extends BaseAdapter { + + private static final int COLUMN_SUGGESTION_QUERY = 0; + private static final int COLUMN_SUGGESTION_TIMESTAMP = 1; + + private Context mContext; + private Cursor mCursor; + private LayoutInflater mInflater; + private boolean mDataValid = false; + + public SuggestionsAdapter(Context context) { + mContext = context; + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDataValid = false; + } + + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + mCursor = newCursor; + if (newCursor != null) { + mDataValid = true; + notifyDataSetChanged(); + } else { + mDataValid = false; + notifyDataSetInvalidated(); + } + return oldCursor; + } + + @Override + public int getCount() { + if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; + return mCursor.getCount(); + } + + @Override + public Object getItem(int position) { + if (mDataValid && mCursor.moveToPosition(position)) { + final String query = mCursor.getString(COLUMN_SUGGESTION_QUERY); + + return new SuggestionItem(query); + } + return null; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (!mDataValid && convertView == null) { + throw new IllegalStateException( + "this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + View view; + + if (convertView == null) { + view = mInflater.inflate(R.layout.search_suggestion_item, parent, false); + } else { + view = convertView; + } + + TextView query = (TextView) view.findViewById(R.id.title); + + SuggestionItem item = (SuggestionItem) getItem(position); + query.setText(item.query); + + return view; + } + } + + private static class SearchResult { + public Context context; + public String title; + public String summaryOn; + public String summaryOff; + public String entries; + public int iconResId; + public String key; + + public SearchResult(Context context, String title, String summaryOn, String summaryOff, + String entries, int iconResId, String key) { + this.context = context; + this.title = title; + this.summaryOn = summaryOn; + this.summaryOff = summaryOff; + this.entries = entries; + this.iconResId = iconResId; + this.key = key; + } + } + + private static class SearchResultsAdapter extends BaseAdapter { + + private Context mContext; + private Cursor mCursor; + private LayoutInflater mInflater; + private boolean mDataValid; + private HashMap<String, Context> mContextMap = new HashMap<String, Context>(); + + private static final String PERCENT_RECLACE = "%s"; + private static final String DOLLAR_REPLACE = "$s"; + + public SearchResultsAdapter(Context context) { + mContext = context; + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDataValid = false; + } + + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + mCursor = newCursor; + if (newCursor != null) { + mDataValid = true; + notifyDataSetChanged(); + } else { + mDataValid = false; + notifyDataSetInvalidated(); + } + return oldCursor; + } + + @Override + public int getCount() { + if (!mDataValid || mCursor == null || mCursor.isClosed()) return 0; + return mCursor.getCount(); + } + + @Override + public Object getItem(int position) { + if (mDataValid && mCursor.moveToPosition(position)) { + final String title = mCursor.getString(Index.COLUMN_INDEX_TITLE); + final String summaryOn = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_ON); + final String summaryOff = mCursor.getString(Index.COLUMN_INDEX_SUMMARY_OFF); + final String entries = mCursor.getString(Index.COLUMN_INDEX_ENTRIES); + final String iconResStr = mCursor.getString(Index.COLUMN_INDEX_ICON); + final String className = mCursor.getString( + Index.COLUMN_INDEX_CLASS_NAME); + final String packageName = mCursor.getString( + Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); + final String key = mCursor.getString( + Index.COLUMN_INDEX_KEY); + + Context packageContext; + if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(packageName)) { + packageContext = mContextMap.get(packageName); + if (packageContext == null) { + try { + packageContext = mContext.createPackageContext(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Cannot create Context for package: " + packageName); + return null; + } + mContextMap.put(packageName, packageContext); + } + } else { + packageContext = mContext; + } + + final int iconResId = TextUtils.isEmpty(iconResStr) ? + R.drawable.empty_icon : Integer.parseInt(iconResStr); + + return new SearchResult(packageContext, title, summaryOn, summaryOff, + entries, iconResId, key); + } + return null; + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (!mDataValid && convertView == null) { + throw new IllegalStateException( + "this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + View view; + TextView textTitle; + ImageView imageView; + + if (convertView == null) { + view = mInflater.inflate(R.layout.search_result_item, parent, false); + } else { + view = convertView; + } + + textTitle = (TextView) view.findViewById(R.id.title); + imageView = (ImageView) view.findViewById(R.id.icon); + + final SearchResult result = (SearchResult) getItem(position); + textTitle.setText(result.title); + + if (result.iconResId != R.drawable.empty_icon) { + final Context packageContext = result.context; + final Drawable drawable; + try { + drawable = packageContext.getDrawable(result.iconResId); + imageView.setImageDrawable(drawable); + } catch (Resources.NotFoundException nfe) { + // Not much we can do except logging + Log.e(LOG_TAG, "Cannot load Drawable for " + result.title); + } + } else { + imageView.setImageDrawable(null); + imageView.setBackgroundResource(R.drawable.empty_icon); + } + + return view; + } + } +} diff --git a/src/com/android/settings/deviceinfo/Memory.java b/src/com/android/settings/deviceinfo/Memory.java index 999611d..1958f38 100644 --- a/src/com/android/settings/deviceinfo/Memory.java +++ b/src/com/android/settings/deviceinfo/Memory.java @@ -40,7 +40,6 @@ 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; @@ -49,8 +48,12 @@ import android.view.MenuItem; import android.widget.Toast; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; import com.google.android.collect.Lists; import java.util.ArrayList; @@ -60,7 +63,7 @@ import java.util.List; * Panel showing storage usage on disk for known {@link StorageVolume} returned * by {@link StorageManager}. Calculates and displays usage of data types. */ -public class Memory extends SettingsPreferenceFragment { +public class Memory extends SettingsPreferenceFragment implements Indexable { private static final String TAG = "MemorySettings"; private static final String TAG_CONFIRM_CLEAR_CACHE = "confirmClearCache"; @@ -186,14 +189,13 @@ public class Memory extends SettingsPreferenceFragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.storage_usb: - if (getActivity() instanceof PreferenceActivity) { - ((PreferenceActivity) getActivity()).startPreferencePanel( + if (getActivity() instanceof SettingsActivity) { + ((SettingsActivity) getActivity()).startPreferencePanel( UsbSettings.class.getCanonicalName(), - null, - R.string.storage_title_usb, null, - this, 0); + null, R.string.storage_title_usb, null, this, 0); } else { - startFragment(this, UsbSettings.class.getCanonicalName(), -1, null); + startFragment(this, UsbSettings.class.getCanonicalName(), + R.string.storage_title_usb, -1, null); } return true; } @@ -424,4 +426,78 @@ public class Memory extends SettingsPreferenceFragment { return builder.create(); } } + + /** + * Enable indexing of searchable data + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.storage_settings); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.internal_storage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + final StorageVolume[] storageVolumes = StorageManager.from(context).getVolumeList(); + for (StorageVolume volume : storageVolumes) { + if (!volume.isEmulated()) { + data.title = volume.getDescription(context); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + } + } + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_size); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_available); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_apps_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_dcim_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_music_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_downloads_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_media_cache_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.memory_media_misc_usage); + data.screenTitle = context.getString(R.string.storage_settings); + result.add(data); + + return result; + } + }; + } diff --git a/src/com/android/settings/deviceinfo/Status.java b/src/com/android/settings/deviceinfo/Status.java index b27b241..bb71a02 100644 --- a/src/com/android/settings/deviceinfo/Status.java +++ b/src/com/android/settings/deviceinfo/Status.java @@ -18,6 +18,7 @@ package com.android.settings.deviceinfo; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -35,13 +36,16 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.preference.Preference; import android.preference.PreferenceActivity; -import android.preference.PreferenceScreen; import android.telephony.CellBroadcastMessage; import android.telephony.PhoneNumberUtils; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.Toast; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; @@ -324,7 +328,7 @@ public class Status extends PreferenceActivity { } } - String rawNumber = mPhone.getLine1Number(); // may be null or empty + String rawNumber = mTelephonyManager.getLine1Number(); // may be null or empty String formattedNumber = null; if (!TextUtils.isEmpty(rawNumber)) { formattedNumber = PhoneNumberUtils.formatNumber(rawNumber); @@ -364,6 +368,27 @@ public class Status extends PreferenceActivity { } else { removePreferenceFromScreen(KEY_SERIAL_NUMBER); } + + // Make every pref on this screen copy its data to the clipboard on longpress. + // Super convenient for capturing the IMEI, MAC addr, serial, etc. + getListView().setOnItemLongClickListener( + new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, + int position, long id) { + ListAdapter listAdapter = (ListAdapter) parent.getAdapter(); + Preference pref = (Preference) listAdapter.getItem(position); + + ClipboardManager cm = (ClipboardManager) + getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(pref.getSummary()); + Toast.makeText( + Status.this, + com.android.internal.R.string.text_copied, + Toast.LENGTH_SHORT).show(); + return true; + } + }); } @Override @@ -521,6 +546,7 @@ public class Status extends PreferenceActivity { if ((ServiceState.STATE_OUT_OF_SERVICE == state) || (ServiceState.STATE_POWER_OFF == state)) { mSignalStrength.setSummary("0"); + return; } int signalDbm = mPhoneStateReceiver.getSignalStrengthDbm(); diff --git a/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java b/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java index 29b1e92..a98f8d9 100644 --- a/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java +++ b/src/com/android/settings/deviceinfo/StorageVolumePreferenceCategory.java @@ -38,6 +38,7 @@ import android.provider.MediaStore; import android.text.format.Formatter; import com.android.settings.R; +import com.android.settings.Settings; import com.android.settings.deviceinfo.StorageMeasurement.MeasurementDetails; import com.android.settings.deviceinfo.StorageMeasurement.MeasurementReceiver; import com.google.android.collect.Lists; @@ -252,6 +253,9 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory { mMountTogglePreference.setEnabled(true); mMountTogglePreference.setTitle(mResources.getString(R.string.sd_eject)); mMountTogglePreference.setSummary(mResources.getString(R.string.sd_eject_summary)); + addPreference(mUsageBarPreference); + addPreference(mItemTotal); + addPreference(mItemAvailable); } else { if (Environment.MEDIA_UNMOUNTED.equals(state) || Environment.MEDIA_NOFS.equals(state) || Environment.MEDIA_UNMOUNTABLE.equals(state)) { @@ -425,8 +429,7 @@ public class StorageVolumePreferenceCategory extends PreferenceCategory { intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, mVolume); } else if (pref == mItemApps) { intent = new Intent(Intent.ACTION_MANAGE_PACKAGE_STORAGE); - intent.setClass(getContext(), - com.android.settings.Settings.ManageApplicationsActivity.class); + intent.setClass(getContext(), Settings.ManageApplicationsActivity.class); } else if (pref == mItemDownloads) { intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).putExtra( DownloadManager.INTENT_EXTRAS_SORT_BY_SIZE, true); diff --git a/src/com/android/settings/deviceinfo/UsageBarPreference.java b/src/com/android/settings/deviceinfo/UsageBarPreference.java index 371c772..4763b79 100644 --- a/src/com/android/settings/deviceinfo/UsageBarPreference.java +++ b/src/com/android/settings/deviceinfo/UsageBarPreference.java @@ -35,18 +35,21 @@ public class UsageBarPreference extends Preference { private final List<PercentageBarChart.Entry> mEntries = Lists.newArrayList(); - public UsageBarPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - setLayoutResource(R.layout.preference_memoryusage); - } - public UsageBarPreference(Context context) { - super(context); - setLayoutResource(R.layout.preference_memoryusage); + this(context, null); } public UsageBarPreference(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public UsageBarPreference(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } + + public UsageBarPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setLayoutResource(R.layout.preference_memoryusage); } diff --git a/src/com/android/settings/users/CircleFramedDrawable.java b/src/com/android/settings/drawable/CircleFramedDrawable.java index 671cfbe..97c96a0 100644 --- a/src/com/android/settings/users/CircleFramedDrawable.java +++ b/src/com/android/settings/drawable/CircleFramedDrawable.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.users; +package com.android.settings.drawable; import android.content.Context; import android.content.res.Resources; @@ -37,7 +37,7 @@ import com.android.settings.R; * Converts the user avatar icon to a circularly clipped one. * TODO: Move this to an internal framework class and share with the one in Keyguard. */ -class CircleFramedDrawable extends Drawable { +public class CircleFramedDrawable extends Drawable { private final Bitmap mBitmap; private final int mSize; @@ -107,7 +107,7 @@ class CircleFramedDrawable extends Drawable { canvas.drawPath(fillPath, mPaint); // mask in the icon where the bitmap is opaque - mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); + mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(icon, cropRect, circleRect, mPaint); // prepare paint for frame drawing diff --git a/src/com/android/settings/fuelgauge/BatteryEntry.java b/src/com/android/settings/fuelgauge/BatteryEntry.java new file mode 100644 index 0000000..4ff4dfd --- /dev/null +++ b/src/com/android/settings/fuelgauge/BatteryEntry.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2014 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.fuelgauge; + +import android.app.AppGlobals; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; +import android.os.BatteryStats; +import android.os.Handler; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import com.android.internal.os.BatterySipper; +import com.android.settings.R; +import com.android.settings.Utils; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Wraps the power usage data of a BatterySipper with information about package name + * and icon image. + */ +public class BatteryEntry { + public static final int MSG_UPDATE_NAME_ICON = 1; + public static final int MSG_REPORT_FULLY_DRAWN = 2; + + static final HashMap<String,UidToDetail> sUidCache = new HashMap<String,UidToDetail>(); + + static final ArrayList<BatteryEntry> mRequestQueue = new ArrayList<BatteryEntry>(); + static Handler sHandler; + + static private class NameAndIconLoader extends Thread { + private boolean mAbort = false; + + public NameAndIconLoader() { + super("BatteryUsage Icon Loader"); + } + + public void abort() { + mAbort = true; + } + + @Override + public void run() { + while (true) { + BatteryEntry be; + synchronized (mRequestQueue) { + if (mRequestQueue.isEmpty() || mAbort) { + if (sHandler != null) { + sHandler.sendEmptyMessage(MSG_REPORT_FULLY_DRAWN); + } + mRequestQueue.clear(); + return; + } + be = mRequestQueue.remove(0); + } + be.loadNameAndIcon(); + } + } + } + + private static NameAndIconLoader mRequestThread; + + public static void startRequestQueue() { + if (sHandler != null) { + synchronized (mRequestQueue) { + if (!mRequestQueue.isEmpty()) { + if (mRequestThread != null) { + mRequestThread.abort(); + } + mRequestThread = new NameAndIconLoader(); + mRequestThread.setPriority(Thread.MIN_PRIORITY); + mRequestThread.start(); + mRequestQueue.notify(); + } + } + } + } + + public static void stopRequestQueue() { + synchronized (mRequestQueue) { + if (mRequestThread != null) { + mRequestThread.abort(); + mRequestThread = null; + sHandler = null; + } + } + } + + public static void clearUidCache() { + sUidCache.clear(); + } + + public final Context context; + public final BatterySipper sipper; + + public String name; + public Drawable icon; + public int iconId; // For passing to the detail screen. + public String defaultPackageName; + + static class UidToDetail { + String name; + String packageName; + Drawable icon; + } + + public BatteryEntry(Context context, Handler handler, UserManager um, BatterySipper sipper) { + sHandler = handler; + this.context = context; + this.sipper = sipper; + switch (sipper.drainType) { + case IDLE: + name = context.getResources().getString(R.string.power_idle); + iconId = R.drawable.ic_settings_phone_idle; + break; + case CELL: + name = context.getResources().getString(R.string.power_cell); + iconId = R.drawable.ic_settings_cell_standby; + break; + case PHONE: + name = context.getResources().getString(R.string.power_phone); + iconId = R.drawable.ic_settings_voice_calls; + break; + case WIFI: + name = context.getResources().getString(R.string.power_wifi); + iconId = R.drawable.ic_settings_wifi; + break; + case BLUETOOTH: + name = context.getResources().getString(R.string.power_bluetooth); + iconId = R.drawable.ic_settings_bluetooth; + break; + case SCREEN: + name = context.getResources().getString(R.string.power_screen); + iconId = R.drawable.ic_settings_display; + break; + case FLASHLIGHT: + name = context.getResources().getString(R.string.power_flashlight); + iconId = R.drawable.ic_settings_display; + break; + case APP: + name = sipper.packageWithHighestDrain; + break; + case USER: { + UserInfo info = um.getUserInfo(sipper.userId); + if (info != null) { + icon = Utils.getUserIcon(context, um, info); + name = info != null ? info.name : null; + if (name == null) { + name = Integer.toString(info.id); + } + name = context.getResources().getString( + R.string.running_process_item_user_label, name); + } else { + icon = null; + name = context.getResources().getString( + R.string.running_process_item_removed_user_label); + } + } break; + case UNACCOUNTED: + name = context.getResources().getString(R.string.power_unaccounted); + iconId = R.drawable.ic_power_system; + break; + case OVERCOUNTED: + name = context.getResources().getString(R.string.power_overcounted); + iconId = R.drawable.ic_power_system; + break; + } + if (iconId > 0) { + icon = context.getResources().getDrawable(iconId); + } + if ((name == null || iconId == 0) && this.sipper.uidObj != null) { + getQuickNameIconForUid(this.sipper.uidObj); + } + } + + public Drawable getIcon() { + return icon; + } + + /** + * Gets the application name + */ + public String getLabel() { + return name; + } + + void getQuickNameIconForUid(BatteryStats.Uid uidObj) { + final int uid = uidObj.getUid(); + final String uidString = Integer.toString(uid); + if (sUidCache.containsKey(uidString)) { + UidToDetail utd = sUidCache.get(uidString); + defaultPackageName = utd.packageName; + name = utd.name; + icon = utd.icon; + return; + } + PackageManager pm = context.getPackageManager(); + String[] packages = pm.getPackagesForUid(uid); + icon = pm.getDefaultActivityIcon(); + if (packages == null) { + //name = Integer.toString(uid); + if (uid == 0) { + name = context.getResources().getString(R.string.process_kernel_label); + } else if ("mediaserver".equals(name)) { + name = context.getResources().getString(R.string.process_mediaserver_label); + } + iconId = R.drawable.ic_power_system; + icon = context.getResources().getDrawable(iconId); + return; + } else { + //name = packages[0]; + } + if (sHandler != null) { + synchronized (mRequestQueue) { + mRequestQueue.add(this); + } + } + } + + /** + * Loads the app label and icon image and stores into the cache. + */ + public void loadNameAndIcon() { + // Bail out if the current sipper is not an App sipper. + if (sipper.uidObj == null) { + return; + } + PackageManager pm = context.getPackageManager(); + final int uid = sipper.uidObj.getUid(); + final Drawable defaultActivityIcon = pm.getDefaultActivityIcon(); + sipper.mPackages = pm.getPackagesForUid(uid); + if (sipper.mPackages == null) { + name = Integer.toString(uid); + return; + } + + String[] packageLabels = new String[sipper.mPackages.length]; + System.arraycopy(sipper.mPackages, 0, packageLabels, 0, sipper.mPackages.length); + + // Convert package names to user-facing labels where possible + IPackageManager ipm = AppGlobals.getPackageManager(); + final int userId = UserHandle.getUserId(uid); + for (int i = 0; i < packageLabels.length; i++) { + try { + final ApplicationInfo ai = ipm.getApplicationInfo(packageLabels[i], + 0 /* no flags */, userId); + if (ai == null) { + Log.d(PowerUsageSummary.TAG, "Retrieving null app info for package " + + packageLabels[i] + ", user " + userId); + continue; + } + CharSequence label = ai.loadLabel(pm); + if (label != null) { + packageLabels[i] = label.toString(); + } + if (ai.icon != 0) { + defaultPackageName = sipper.mPackages[i]; + icon = ai.loadIcon(pm); + break; + } + } catch (RemoteException e) { + Log.d(PowerUsageSummary.TAG, "Error while retrieving app info for package " + + packageLabels[i] + ", user " + userId, e); + } + } + if (icon == null) { + icon = defaultActivityIcon; + } + + if (packageLabels.length == 1) { + name = packageLabels[0]; + } else { + // Look for an official name for this UID. + for (String pkgName : sipper.mPackages) { + try { + final PackageInfo pi = ipm.getPackageInfo(pkgName, 0 /* no flags */, userId); + if (pi == null) { + Log.d(PowerUsageSummary.TAG, "Retrieving null package info for package " + + pkgName + ", user " + userId); + continue; + } + if (pi.sharedUserLabel != 0) { + final CharSequence nm = pm.getText(pkgName, + pi.sharedUserLabel, pi.applicationInfo); + if (nm != null) { + name = nm.toString(); + if (pi.applicationInfo.icon != 0) { + defaultPackageName = pkgName; + icon = pi.applicationInfo.loadIcon(pm); + } + break; + } + } + } catch (RemoteException e) { + Log.d(PowerUsageSummary.TAG, "Error while retrieving package info for package " + + pkgName + ", user " + userId, e); + } + } + } + final String uidString = Integer.toString(sipper.uidObj.getUid()); + UidToDetail utd = new UidToDetail(); + utd.name = name; + utd.icon = icon; + utd.packageName = defaultPackageName; + sUidCache.put(uidString, utd); + if (sHandler != null) { + sHandler.sendMessage(sHandler.obtainMessage(MSG_UPDATE_NAME_ICON, this)); + } + } +} diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java b/src/com/android/settings/fuelgauge/BatteryHistoryChart.java index 55a0457..765f101 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryChart.java @@ -16,7 +16,17 @@ package com.android.settings.fuelgauge; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.DashPathEffect; +import android.os.BatteryManager; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.text.format.Formatter; +import android.util.Log; +import android.util.TimeUtils; import com.android.settings.R; +import com.android.settings.Utils; import android.content.Context; import android.content.res.ColorStateList; @@ -33,8 +43,16 @@ import android.text.TextPaint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; +import libcore.icu.LocaleData; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; public class BatteryHistoryChart extends View { + static final boolean DEBUG = false; + static final String TAG = "BatteryHistoryChart"; + static final int CHART_DATA_X_MASK = 0x0000ffff; static final int CHART_DATA_BIN_MASK = 0xffff0000; static final int CHART_DATA_BIN_SHIFT = 16; @@ -69,7 +87,7 @@ public class BatteryHistoryChart extends View { void addTick(int x, int bin) { if (bin != mLastBin && mNumTicks < mTicks.length) { - mTicks[mNumTicks] = x | bin << CHART_DATA_BIN_SHIFT; + mTicks[mNumTicks] = (x&CHART_DATA_X_MASK) | (bin<<CHART_DATA_BIN_SHIFT); mNumTicks++; mLastBin = bin; } @@ -102,9 +120,6 @@ public class BatteryHistoryChart extends View { static final int SERIF = 2; static final int MONOSPACE = 3; - static final int BATTERY_WARN = 29; - static final int BATTERY_CRITICAL = 14; - // First value if for phone off; first value is "scanning"; following values // are battery stats signal strength buckets. static final int NUM_PHONE_SIGNALS = 7; @@ -113,137 +128,266 @@ public class BatteryHistoryChart extends View { final Paint mBatteryGoodPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mBatteryWarnPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mBatteryCriticalPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + final Paint mTimeRemainPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mChargingPaint = new Paint(); final Paint mScreenOnPaint = new Paint(); final Paint mGpsOnPaint = new Paint(); final Paint mWifiRunningPaint = new Paint(); - final Paint mWakeLockPaint = new Paint(); + final Paint mCpuRunningPaint = new Paint(); + final Paint mDateLinePaint = new Paint(); final ChartData mPhoneSignalChart = new ChartData(); final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - + final TextPaint mHeaderTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + final Paint mDebugRectPaint = new Paint(); + final Path mBatLevelPath = new Path(); final Path mBatGoodPath = new Path(); final Path mBatWarnPath = new Path(); final Path mBatCriticalPath = new Path(); + final Path mTimeRemainPath = new Path(); final Path mChargingPath = new Path(); final Path mScreenOnPath = new Path(); final Path mGpsOnPath = new Path(); final Path mWifiRunningPath = new Path(); - final Path mWakeLockPath = new Path(); - - int mFontSize; + final Path mCpuRunningPath = new Path(); + final Path mDateLinePath = new Path(); BatteryStats mStats; + Intent mBatteryBroadcast; long mStatsPeriod; + int mBatteryLevel; + String mMaxPercentLabelString; + String mMinPercentLabelString; String mDurationString; - String mTotalDurationString; + String mChargeLabelString; + String mChargeDurationString; + String mDrainString; String mChargingLabel; String mScreenOnLabel; String mGpsOnLabel; String mWifiRunningLabel; - String mWakeLockLabel; + String mCpuRunningLabel; String mPhoneSignalLabel; - + + int mChartMinHeight; + int mHeaderHeight; + + int mBatteryWarnLevel; + int mBatteryCriticalLevel; + int mTextAscent; int mTextDescent; + int mHeaderTextAscent; + int mHeaderTextDescent; + int mMaxPercentLabelStringWidth; + int mMinPercentLabelStringWidth; int mDurationStringWidth; - int mTotalDurationStringWidth; + int mChargeLabelStringWidth; + int mChargeDurationStringWidth; + int mDrainStringWidth; boolean mLargeMode; + int mLastWidth = -1; + int mLastHeight = -1; + int mLineWidth; int mThinLineWidth; int mChargingOffset; int mScreenOnOffset; int mGpsOnOffset; int mWifiRunningOffset; - int mWakeLockOffset; + int mCpuRunningOffset; int mPhoneSignalOffset; int mLevelOffset; int mLevelTop; int mLevelBottom; - static final int PHONE_SIGNAL_X_MASK = CHART_DATA_X_MASK; - static final int PHONE_SIGNAL_BIN_MASK = CHART_DATA_BIN_MASK; - static final int PHONE_SIGNAL_BIN_SHIFT = CHART_DATA_BIN_SHIFT; - + int mLevelLeft; + int mLevelRight; + int mNumHist; long mHistStart; + long mHistDataEnd; long mHistEnd; + long mStartWallTime; + long mEndDataWallTime; + long mEndWallTime; + boolean mDischarging; int mBatLow; int mBatHigh; boolean mHaveWifi; boolean mHaveGps; boolean mHavePhoneSignal; - + + final ArrayList<TimeLabel> mTimeLabels = new ArrayList<TimeLabel>(); + final ArrayList<DateLabel> mDateLabels = new ArrayList<DateLabel>(); + + Bitmap mBitmap; + Canvas mCanvas; + + static class TextAttrs { + ColorStateList textColor = null; + int textSize = 15; + int typefaceIndex = -1; + int styleIndex = -1; + + void retrieve(Context context, TypedArray from, int index) { + TypedArray appearance = null; + int ap = from.getResourceId(index, -1); + if (ap != -1) { + appearance = context.obtainStyledAttributes(ap, + com.android.internal.R.styleable.TextAppearance); + } + if (appearance != null) { + int n = appearance.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = appearance.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.TextAppearance_textColor: + textColor = appearance.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextAppearance_textSize: + textSize = appearance.getDimensionPixelSize(attr, textSize); + break; + + case com.android.internal.R.styleable.TextAppearance_typeface: + typefaceIndex = appearance.getInt(attr, -1); + break; + + case com.android.internal.R.styleable.TextAppearance_textStyle: + styleIndex = appearance.getInt(attr, -1); + break; + } + } + + appearance.recycle(); + } + } + + void apply(Context context, TextPaint paint) { + paint.density = context.getResources().getDisplayMetrics().density; + paint.setCompatibilityScaling( + context.getResources().getCompatibilityInfo().applicationScale); + + paint.setColor(textColor.getDefaultColor()); + paint.setTextSize(textSize); + + Typeface tf = null; + switch (typefaceIndex) { + case SANS: + tf = Typeface.SANS_SERIF; + break; + + case SERIF: + tf = Typeface.SERIF; + break; + + case MONOSPACE: + tf = Typeface.MONOSPACE; + break; + } + + setTypeface(paint, tf, styleIndex); + } + + public void setTypeface(TextPaint paint, Typeface tf, int style) { + if (style > 0) { + if (tf == null) { + tf = Typeface.defaultFromStyle(style); + } else { + tf = Typeface.create(tf, style); + } + + paint.setTypeface(tf); + // now compute what (if any) algorithmic styling is needed + int typefaceStyle = tf != null ? tf.getStyle() : 0; + int need = style & ~typefaceStyle; + paint.setFakeBoldText((need & Typeface.BOLD) != 0); + paint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); + } else { + paint.setFakeBoldText(false); + paint.setTextSkewX(0); + paint.setTypeface(tf); + } + } + } + + static class TimeLabel { + final int x; + final String label; + final int width; + + TimeLabel(TextPaint paint, int x, Calendar cal, boolean use24hr) { + this.x = x; + final String bestFormat = DateFormat.getBestDateTimePattern( + Locale.getDefault(), use24hr ? "km" : "ha"); + label = DateFormat.format(bestFormat, cal).toString(); + width = (int)paint.measureText(label); + } + } + + static class DateLabel { + final int x; + final String label; + final int width; + + DateLabel(TextPaint paint, int x, Calendar cal, boolean dayFirst) { + this.x = x; + final String bestFormat = DateFormat.getBestDateTimePattern( + Locale.getDefault(), dayFirst ? "dM" : "Md"); + label = DateFormat.format(bestFormat, cal).toString(); + width = (int)paint.measureText(label); + } + } + public BatteryHistoryChart(Context context, AttributeSet attrs) { super(context, attrs); - - mBatteryBackgroundPaint.setARGB(255, 128, 128, 128); + + if (DEBUG) Log.d(TAG, "New BatteryHistoryChart!"); + + mBatteryWarnLevel = mContext.getResources().getInteger( + com.android.internal.R.integer.config_lowBatteryWarningLevel); + mBatteryCriticalLevel = mContext.getResources().getInteger( + com.android.internal.R.integer.config_criticalBatteryWarningLevel); + + mThinLineWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 2, getResources().getDisplayMetrics()); + + mBatteryBackgroundPaint.setColor(0xFF009688); mBatteryBackgroundPaint.setStyle(Paint.Style.FILL); - mBatteryGoodPaint.setARGB(128, 0, 255, 0); + mBatteryGoodPaint.setARGB(128, 0, 128, 0); mBatteryGoodPaint.setStyle(Paint.Style.STROKE); - mBatteryWarnPaint.setARGB(128, 255, 255, 0); + mBatteryWarnPaint.setARGB(128, 128, 128, 0); mBatteryWarnPaint.setStyle(Paint.Style.STROKE); - mBatteryCriticalPaint.setARGB(192, 255, 0, 0); + mBatteryCriticalPaint.setARGB(192, 128, 0, 0); mBatteryCriticalPaint.setStyle(Paint.Style.STROKE); - mChargingPaint.setARGB(255, 0, 128, 0); + mTimeRemainPaint.setColor(0xFFCED7BB); + mTimeRemainPaint.setStyle(Paint.Style.FILL); mChargingPaint.setStyle(Paint.Style.STROKE); mScreenOnPaint.setStyle(Paint.Style.STROKE); mGpsOnPaint.setStyle(Paint.Style.STROKE); mWifiRunningPaint.setStyle(Paint.Style.STROKE); - mWakeLockPaint.setStyle(Paint.Style.STROKE); - mPhoneSignalChart.setColors(new int[] { - 0x00000000, 0xffa00000, 0xffa0a000, 0xff808020, - 0xff808040, 0xff808060, 0xff008000 - }); - - mTextPaint.density = getResources().getDisplayMetrics().density; - mTextPaint.setCompatibilityScaling( - getResources().getCompatibilityInfo().applicationScale); - + mCpuRunningPaint.setStyle(Paint.Style.STROKE); + mPhoneSignalChart.setColors(com.android.settings.Utils.BADNESS_COLORS); + mDebugRectPaint.setARGB(255, 255, 0, 0); + mDebugRectPaint.setStyle(Paint.Style.STROKE); + mScreenOnPaint.setColor(0xFF009688); + mGpsOnPaint.setColor(0xFF009688); + mWifiRunningPaint.setColor(0xFF009688); + mCpuRunningPaint.setColor(0xFF009688); + mChargingPaint.setColor(0xFF009688); + TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.BatteryHistoryChart, 0, 0); - - ColorStateList textColor = null; - int textSize = 15; - int typefaceIndex = -1; - int styleIndex = -1; - - TypedArray appearance = null; - int ap = a.getResourceId(R.styleable.BatteryHistoryChart_android_textAppearance, -1); - if (ap != -1) { - appearance = context.obtainStyledAttributes(ap, - com.android.internal.R.styleable. - TextAppearance); - } - if (appearance != null) { - int n = appearance.getIndexCount(); - for (int i = 0; i < n; i++) { - int attr = appearance.getIndex(i); - - switch (attr) { - case com.android.internal.R.styleable.TextAppearance_textColor: - textColor = appearance.getColorStateList(attr); - break; - case com.android.internal.R.styleable.TextAppearance_textSize: - textSize = appearance.getDimensionPixelSize(attr, textSize); - break; + final TextAttrs mainTextAttrs = new TextAttrs(); + final TextAttrs headTextAttrs = new TextAttrs(); + mainTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_android_textAppearance); + headTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_headerAppearance); - case com.android.internal.R.styleable.TextAppearance_typeface: - typefaceIndex = appearance.getInt(attr, -1); - break; - - case com.android.internal.R.styleable.TextAppearance_textStyle: - styleIndex = appearance.getInt(attr, -1); - break; - } - } - - appearance.recycle(); - } - int shadowcolor = 0; float dx=0, dy=0, r=0; @@ -269,134 +413,222 @@ public class BatteryHistoryChart extends View { break; case R.styleable.BatteryHistoryChart_android_textColor: - textColor = a.getColorStateList(attr); + mainTextAttrs.textColor = a.getColorStateList(attr); + headTextAttrs.textColor = a.getColorStateList(attr); break; case R.styleable.BatteryHistoryChart_android_textSize: - textSize = a.getDimensionPixelSize(attr, textSize); + mainTextAttrs.textSize = a.getDimensionPixelSize(attr, mainTextAttrs.textSize); + headTextAttrs.textSize = a.getDimensionPixelSize(attr, headTextAttrs.textSize); break; case R.styleable.BatteryHistoryChart_android_typeface: - typefaceIndex = a.getInt(attr, typefaceIndex); + mainTextAttrs.typefaceIndex = a.getInt(attr, mainTextAttrs.typefaceIndex); + headTextAttrs.typefaceIndex = a.getInt(attr, headTextAttrs.typefaceIndex); break; case R.styleable.BatteryHistoryChart_android_textStyle: - styleIndex = a.getInt(attr, styleIndex); + mainTextAttrs.styleIndex = a.getInt(attr, mainTextAttrs.styleIndex); + headTextAttrs.styleIndex = a.getInt(attr, headTextAttrs.styleIndex); + break; + + case R.styleable.BatteryHistoryChart_barPrimaryColor: + mBatteryBackgroundPaint.setColor(a.getInt(attr, 0)); + mScreenOnPaint.setColor(a.getInt(attr, 0)); + mGpsOnPaint.setColor(a.getInt(attr, 0)); + mWifiRunningPaint.setColor(a.getInt(attr, 0)); + mCpuRunningPaint.setColor(a.getInt(attr, 0)); + mChargingPaint.setColor(a.getInt(attr, 0)); + break; + + case R.styleable.BatteryHistoryChart_barPredictionColor: + mTimeRemainPaint.setColor(a.getInt(attr, 0)); + break; + + case R.styleable.BatteryHistoryChart_chartMinHeight: + mChartMinHeight = a.getDimensionPixelSize(attr, 0); break; } } a.recycle(); - mTextPaint.setColor(textColor.getDefaultColor()); - mTextPaint.setTextSize(textSize); - - Typeface tf = null; - switch (typefaceIndex) { - case SANS: - tf = Typeface.SANS_SERIF; - break; - - case SERIF: - tf = Typeface.SERIF; - break; + mainTextAttrs.apply(context, mTextPaint); + headTextAttrs.apply(context, mHeaderTextPaint); - case MONOSPACE: - tf = Typeface.MONOSPACE; - break; + mDateLinePaint.set(mTextPaint); + mDateLinePaint.setStyle(Paint.Style.STROKE); + int hairlineWidth = mThinLineWidth/2; + if (hairlineWidth < 1) { + hairlineWidth = 1; } - - setTypeface(tf, styleIndex); - + mDateLinePaint.setStrokeWidth(hairlineWidth); + mDateLinePaint.setPathEffect(new DashPathEffect(new float[] { + mThinLineWidth * 2, mThinLineWidth * 2 }, 0)); + if (shadowcolor != 0) { mTextPaint.setShadowLayer(r, dx, dy, shadowcolor); + mHeaderTextPaint.setShadowLayer(r, dx, dy, shadowcolor); } } - - public void setTypeface(Typeface tf, int style) { - if (style > 0) { - if (tf == null) { - tf = Typeface.defaultFromStyle(style); - } else { - tf = Typeface.create(tf, style); - } - mTextPaint.setTypeface(tf); - // now compute what (if any) algorithmic styling is needed - int typefaceStyle = tf != null ? tf.getStyle() : 0; - int need = style & ~typefaceStyle; - mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); - mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); - } else { - mTextPaint.setFakeBoldText(false); - mTextPaint.setTextSkewX(0); - mTextPaint.setTypeface(tf); - } - } - - void setStats(BatteryStats stats) { + void setStats(BatteryStats stats, Intent broadcast) { mStats = stats; - - long uSecTime = mStats.computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, + mBatteryBroadcast = broadcast; + + if (DEBUG) Log.d(TAG, "Setting stats..."); + + final long elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + + long uSecTime = mStats.computeBatteryRealtime(elapsedRealtimeUs, BatteryStats.STATS_SINCE_CHARGED); mStatsPeriod = uSecTime; - String durationString = Utils.formatElapsedTime(getContext(), mStatsPeriod / 1000, true); - mDurationString = getContext().getString(R.string.battery_stats_on_battery, - durationString); mChargingLabel = getContext().getString(R.string.battery_stats_charging_label); mScreenOnLabel = getContext().getString(R.string.battery_stats_screen_on_label); mGpsOnLabel = getContext().getString(R.string.battery_stats_gps_on_label); mWifiRunningLabel = getContext().getString(R.string.battery_stats_wifi_running_label); - mWakeLockLabel = getContext().getString(R.string.battery_stats_wake_lock_label); + mCpuRunningLabel = getContext().getString(R.string.battery_stats_wake_lock_label); mPhoneSignalLabel = getContext().getString(R.string.battery_stats_phone_signal_label); - + + mMaxPercentLabelString = Utils.formatPercentage(100); + mMinPercentLabelString = Utils.formatPercentage(0); + + mBatteryLevel = com.android.settings.Utils.getBatteryLevel(mBatteryBroadcast); + long remainingTimeUs = 0; + mDischarging = true; + if (mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) == 0) { + final long drainTime = mStats.computeBatteryTimeRemaining(elapsedRealtimeUs); + if (drainTime > 0) { + remainingTimeUs = drainTime; + String timeString = Formatter.formatShortElapsedTime(getContext(), + drainTime / 1000); + mChargeLabelString = getContext().getResources().getString( + R.string.power_discharging_duration, mBatteryLevel, timeString); + } else { + mChargeLabelString = Utils.formatPercentage(mBatteryLevel); + } + } else { + final long chargeTime = mStats.computeChargeTimeRemaining(elapsedRealtimeUs); + final String statusLabel = com.android.settings.Utils.getBatteryStatus(getResources(), + mBatteryBroadcast); + final int status = mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { + mDischarging = false; + remainingTimeUs = chargeTime; + String timeString = Formatter.formatShortElapsedTime(getContext(), + chargeTime / 1000); + int plugType = mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + int resId; + if (plugType == BatteryManager.BATTERY_PLUGGED_AC) { + resId = R.string.power_charging_duration_ac; + } else if (plugType == BatteryManager.BATTERY_PLUGGED_USB) { + resId = R.string.power_charging_duration_usb; + } else if (plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) { + resId = R.string.power_charging_duration_wireless; + } else { + resId = R.string.power_charging_duration; + } + mChargeLabelString = getContext().getResources().getString( + resId, mBatteryLevel, timeString); + } else { + mChargeLabelString = getContext().getResources().getString( + R.string.power_charging, mBatteryLevel, statusLabel); + } + } + mDrainString = ""; + mChargeDurationString = ""; + setContentDescription(mChargeLabelString); + int pos = 0; int lastInteresting = 0; byte lastLevel = -1; mBatLow = 0; mBatHigh = 100; + mStartWallTime = 0; + mEndDataWallTime = 0; + mEndWallTime = 0; + mHistStart = 0; + mHistEnd = 0; + long lastWallTime = 0; + long lastRealtime = 0; int aggrStates = 0; + int aggrStates2 = 0; boolean first = true; if (stats.startIteratingHistoryLocked()) { final HistoryItem rec = new HistoryItem(); while (stats.getNextHistoryLocked(rec)) { pos++; - if (rec.cmd == HistoryItem.CMD_UPDATE) { - if (first) { - first = false; - mHistStart = rec.time; + if (first) { + first = false; + mHistStart = rec.time; + } + if (rec.cmd == HistoryItem.CMD_CURRENT_TIME + || rec.cmd == HistoryItem.CMD_RESET) { + // If there is a ridiculously large jump in time, then we won't be + // able to create a good chart with that data, so just ignore the + // times we got before and pretend like our data extends back from + // the time we have now. + // Also, if we are getting a time change and we are less than 5 minutes + // since the start of the history real time, then also use this new + // time to compute the base time, since whatever time we had before is + // pretty much just noise. + if (rec.currentTime > (lastWallTime+(180*24*60*60*1000L)) + || rec.time < (mHistStart+(5*60*1000L))) { + mStartWallTime = 0; + } + lastWallTime = rec.currentTime; + lastRealtime = rec.time; + if (mStartWallTime == 0) { + mStartWallTime = lastWallTime - (lastRealtime-mHistStart); } + } + if (rec.isDeltaData()) { if (rec.batteryLevel != lastLevel || pos == 1) { lastLevel = rec.batteryLevel; } lastInteresting = pos; - mHistEnd = rec.time; + mHistDataEnd = rec.time; aggrStates |= rec.states; + aggrStates2 |= rec.states2; } } } + mHistEnd = mHistDataEnd + (remainingTimeUs/1000); + mEndDataWallTime = lastWallTime + mHistDataEnd - lastRealtime; + mEndWallTime = mEndDataWallTime + (remainingTimeUs/1000); mNumHist = lastInteresting; mHaveGps = (aggrStates&HistoryItem.STATE_GPS_ON_FLAG) != 0; - mHaveWifi = (aggrStates&HistoryItem.STATE_WIFI_RUNNING_FLAG) != 0; + mHaveWifi = (aggrStates2&HistoryItem.STATE2_WIFI_RUNNING_FLAG) != 0 + || (aggrStates&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG + |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG + |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0; if (!com.android.settings.Utils.isWifiOnly(getContext())) { mHavePhoneSignal = true; } if (mHistEnd <= mHistStart) mHistEnd = mHistStart+1; - mTotalDurationString = Utils.formatElapsedTime(getContext(), mHistEnd - mHistStart, true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mDurationStringWidth = (int)mTextPaint.measureText(mDurationString); - mTotalDurationStringWidth = (int)mTextPaint.measureText(mTotalDurationString); + mMaxPercentLabelStringWidth = (int)mTextPaint.measureText(mMaxPercentLabelString); + mMinPercentLabelStringWidth = (int)mTextPaint.measureText(mMinPercentLabelString); + mDrainStringWidth = (int)mHeaderTextPaint.measureText(mDrainString); + mChargeLabelStringWidth = (int)mHeaderTextPaint.measureText(mChargeLabelString); + mChargeDurationStringWidth = (int)mHeaderTextPaint.measureText(mChargeDurationString); mTextAscent = (int)mTextPaint.ascent(); mTextDescent = (int)mTextPaint.descent(); + mHeaderTextAscent = (int)mHeaderTextPaint.ascent(); + mHeaderTextDescent = (int)mHeaderTextPaint.descent(); + int headerTextHeight = mHeaderTextDescent - mHeaderTextAscent; + mHeaderHeight = headerTextHeight*2 - mTextAscent; + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(mChartMinHeight+mHeaderHeight, heightMeasureSpec)); } void finishPaths(int w, int h, int levelh, int startX, int y, Path curLevelPath, int lastX, boolean lastCharging, boolean lastScreenOn, boolean lastGpsOn, - boolean lastWifiRunning, boolean lastWakeLock, Path lastPath) { + boolean lastWifiRunning, boolean lastCpuRunning, Path lastPath) { if (curLevelPath != null) { if (lastX >= 0 && lastX < w) { if (lastPath != null) { @@ -421,22 +653,69 @@ public class BatteryHistoryChart extends View { if (lastWifiRunning) { mWifiRunningPath.lineTo(w, h-mWifiRunningOffset); } - if (lastWakeLock) { - mWakeLockPath.lineTo(w, h-mWakeLockOffset); + if (lastCpuRunning) { + mCpuRunningPath.lineTo(w, h - mCpuRunningOffset); } if (mHavePhoneSignal) { mPhoneSignalChart.finish(w); } } - + + private boolean is24Hour() { + return DateFormat.is24HourFormat(getContext()); + } + + private boolean isDayFirst() { + String value = Settings.System.getString(mContext.getContentResolver(), + Settings.System.DATE_FORMAT); + if (value == null) { + LocaleData d = LocaleData.get(mContext.getResources().getConfiguration().locale); + value = d.shortDateFormat4; + } + return value.indexOf('M') > value.indexOf('d'); + } + + /* + private void buildTime() { + java.text.DateFormat shortDateFormat = DateFormat.getDateFormat(context); + final Calendar now = Calendar.getInstance(); + mDummyDate.setTimeZone(now.getTimeZone()); + // We use December 31st because it's unambiguous when demonstrating the date format. + // We use 13:00 so we can demonstrate the 12/24 hour options. + 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(), true)); + mDatePref.setSummary(shortDateFormat.format(now.getTime())); + mDateFormat.setSummary(shortDateFormat.format(dummyDate)); + mTime24Pref.setSummary(DateFormat.getTimeFormat(getActivity()).format(dummyDate)); + + } + */ + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - + + if (DEBUG) Log.d(TAG, "onSizeChanged: " + oldw + "x" + oldh + " to " + w + "x" + h); + + if (mLastWidth == w && mLastHeight == h) { + return; + } + + if (mLastWidth == 0 || mLastHeight == 0) { + return; + } + + if (DEBUG) Log.d(TAG, "Rebuilding chart for: " + w + "x" + h); + + mLastWidth = w; + mLastHeight = h; + mBitmap = null; + mCanvas = null; + int textHeight = mTextDescent - mTextAscent; - mThinLineWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - 2, getResources().getDisplayMetrics()); - if (h > (textHeight*6)) { + if (h > ((textHeight*10)+mChartMinHeight)) { mLargeMode = true; if (h > (textHeight*15)) { // Plenty of room for the chart. @@ -445,21 +724,17 @@ public class BatteryHistoryChart extends View { // 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); - mWifiRunningPaint.setARGB(255, 32, 64, 255); - mWakeLockPaint.setARGB(255, 32, 64, 255); } else { mLargeMode = false; mLineWidth = mThinLineWidth; - mLevelTop = 0; - mScreenOnPaint.setARGB(255, 0, 0, 255); - mGpsOnPaint.setARGB(255, 0, 0, 255); - mWifiRunningPaint.setARGB(255, 0, 0, 255); - mWakeLockPaint.setARGB(255, 0, 0, 255); } if (mLineWidth <= 0) mLineWidth = 1; + + mLevelTop = mHeaderHeight; + mLevelLeft = mMaxPercentLabelStringWidth + mThinLineWidth*3; + mLevelRight = w; + int levelWidth = mLevelRight-mLevelLeft; + mTextPaint.setStrokeWidth(mThinLineWidth); mBatteryGoodPaint.setStrokeWidth(mThinLineWidth); mBatteryWarnPaint.setStrokeWidth(mThinLineWidth); @@ -468,27 +743,27 @@ public class BatteryHistoryChart extends View { mScreenOnPaint.setStrokeWidth(mLineWidth); mGpsOnPaint.setStrokeWidth(mLineWidth); mWifiRunningPaint.setStrokeWidth(mLineWidth); - mWakeLockPaint.setStrokeWidth(mLineWidth); + mCpuRunningPaint.setStrokeWidth(mLineWidth); + mDebugRectPaint.setStrokeWidth(1); + + int fullBarOffset = textHeight + mLineWidth; if (mLargeMode) { - int barOffset = textHeight + mLineWidth; mChargingOffset = mLineWidth; - mScreenOnOffset = mChargingOffset + barOffset; - mWakeLockOffset = mScreenOnOffset + barOffset; - mWifiRunningOffset = mWakeLockOffset + barOffset; - mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? barOffset : 0); - mPhoneSignalOffset = mGpsOnOffset + (mHaveGps ? barOffset : 0); - mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? barOffset : 0) - + ((mLineWidth*3)/2); + mScreenOnOffset = mChargingOffset + fullBarOffset; + mCpuRunningOffset = mScreenOnOffset + fullBarOffset; + mWifiRunningOffset = mCpuRunningOffset + fullBarOffset; + mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? fullBarOffset : 0); + mPhoneSignalOffset = mGpsOnOffset + (mHaveGps ? fullBarOffset : 0); + mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? fullBarOffset : 0) + + mLineWidth*2 + mLineWidth/2; if (mHavePhoneSignal) { mPhoneSignalChart.init(w); } } else { mScreenOnOffset = mGpsOnOffset = mWifiRunningOffset - = mWakeLockOffset = mLineWidth; - mChargingOffset = mLineWidth*2; - mPhoneSignalOffset = 0; - mLevelOffset = mLineWidth*3; + = mCpuRunningOffset = mChargingOffset = mPhoneSignalOffset = 0; + mLevelOffset = fullBarOffset + mThinLineWidth*4; if (mHavePhoneSignal) { mPhoneSignalChart.init(0); } @@ -497,34 +772,57 @@ public class BatteryHistoryChart extends View { mBatLevelPath.reset(); mBatGoodPath.reset(); mBatWarnPath.reset(); + mTimeRemainPath.reset(); mBatCriticalPath.reset(); mScreenOnPath.reset(); mGpsOnPath.reset(); mWifiRunningPath.reset(); - mWakeLockPath.reset(); + mCpuRunningPath.reset(); mChargingPath.reset(); - - final long timeStart = mHistStart; - final long timeChange = mHistEnd-mHistStart; - + + mTimeLabels.clear(); + mDateLabels.clear(); + + final long walltimeStart = mStartWallTime; + final long walltimeChange = mEndWallTime > walltimeStart + ? (mEndWallTime-walltimeStart) : 1; + long curWalltime = mStartWallTime; + long lastRealtime = 0; + final int batLow = mBatLow; final int batChange = mBatHigh-mBatLow; - + final int levelh = h - mLevelOffset - mLevelTop; mLevelBottom = mLevelTop + levelh; - - int x = 0, y = 0, startX = 0, lastX = -1, lastY = -1; + + int x = mLevelLeft, y = 0, startX = mLevelLeft, lastX = -1, lastY = -1; int i = 0; Path curLevelPath = null; Path lastLinePath = null; boolean lastCharging = false, lastScreenOn = false, lastGpsOn = false; - boolean lastWifiRunning = false, lastWakeLock = false; + boolean lastWifiRunning = false, lastWifiSupplRunning = false, lastCpuRunning = false; + int lastWifiSupplState = BatteryStats.WIFI_SUPPL_STATE_INVALID; final int N = mNumHist; - if (mStats.startIteratingHistoryLocked()) { + if (mEndDataWallTime > mStartWallTime && mStats.startIteratingHistoryLocked()) { final HistoryItem rec = new HistoryItem(); while (mStats.getNextHistoryLocked(rec) && i < N) { - if (rec.cmd == BatteryStats.HistoryItem.CMD_UPDATE) { - x = (int)(((rec.time-timeStart)*w)/timeChange); + if (rec.isDeltaData()) { + curWalltime += rec.time-lastRealtime; + lastRealtime = rec.time; + x = mLevelLeft + (int)(((curWalltime-walltimeStart)*levelWidth)/walltimeChange); + if (x < 0) { + x = 0; + } + if (false) { + StringBuilder sb = new StringBuilder(128); + sb.append("walloff="); + TimeUtils.formatDuration(curWalltime - walltimeStart, sb); + sb.append(" wallchange="); + TimeUtils.formatDuration(walltimeChange, sb); + sb.append(" x="); + sb.append(x); + Log.d("foo", sb.toString()); + } y = mLevelTop + levelh - ((rec.batteryLevel-batLow)*(levelh-1))/batChange; if (lastX != x) { @@ -533,17 +831,19 @@ public class BatteryHistoryChart extends View { // Don't plot changes within a pixel. Path path; byte value = rec.batteryLevel; - if (value <= BATTERY_CRITICAL) path = mBatCriticalPath; - else if (value <= BATTERY_WARN) path = mBatWarnPath; - else path = mBatGoodPath; + if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; + else if (value <= mBatteryWarnLevel) path = mBatWarnPath; + else path = null; //mBatGoodPath; if (path != lastLinePath) { if (lastLinePath != null) { lastLinePath.lineTo(x, y); } - path.moveTo(x, y); + if (path != null) { + path.moveTo(x, y); + } lastLinePath = path; - } else { + } else if (path != null) { path.lineTo(x, y); } @@ -559,156 +859,421 @@ public class BatteryHistoryChart extends View { } } - final boolean charging = - (rec.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0; - if (charging != lastCharging) { - if (charging) { - mChargingPath.moveTo(x, h-mChargingOffset); - } else { - mChargingPath.lineTo(x, h-mChargingOffset); + if (mLargeMode) { + final boolean charging = + (rec.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0; + if (charging != lastCharging) { + if (charging) { + mChargingPath.moveTo(x, h-mChargingOffset); + } else { + mChargingPath.lineTo(x, h-mChargingOffset); + } + lastCharging = charging; } - lastCharging = charging; - } - final boolean screenOn = - (rec.states&HistoryItem.STATE_SCREEN_ON_FLAG) != 0; - if (screenOn != lastScreenOn) { - if (screenOn) { - mScreenOnPath.moveTo(x, h-mScreenOnOffset); - } else { - mScreenOnPath.lineTo(x, h-mScreenOnOffset); + final boolean screenOn = + (rec.states&HistoryItem.STATE_SCREEN_ON_FLAG) != 0; + if (screenOn != lastScreenOn) { + if (screenOn) { + mScreenOnPath.moveTo(x, h-mScreenOnOffset); + } else { + mScreenOnPath.lineTo(x, h-mScreenOnOffset); + } + lastScreenOn = screenOn; } - lastScreenOn = screenOn; - } - final boolean gpsOn = - (rec.states&HistoryItem.STATE_GPS_ON_FLAG) != 0; - if (gpsOn != lastGpsOn) { - if (gpsOn) { - mGpsOnPath.moveTo(x, h-mGpsOnOffset); - } else { - mGpsOnPath.lineTo(x, h-mGpsOnOffset); + final boolean gpsOn = + (rec.states&HistoryItem.STATE_GPS_ON_FLAG) != 0; + if (gpsOn != lastGpsOn) { + if (gpsOn) { + mGpsOnPath.moveTo(x, h-mGpsOnOffset); + } else { + mGpsOnPath.lineTo(x, h-mGpsOnOffset); + } + lastGpsOn = gpsOn; } - lastGpsOn = gpsOn; - } - final boolean wifiRunning = - (rec.states&HistoryItem.STATE_WIFI_RUNNING_FLAG) != 0; - if (wifiRunning != lastWifiRunning) { - if (wifiRunning) { - mWifiRunningPath.moveTo(x, h-mWifiRunningOffset); + final int wifiSupplState = + ((rec.states2&HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK) + >> HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT); + boolean wifiRunning; + if (lastWifiSupplState != wifiSupplState) { + lastWifiSupplState = wifiSupplState; + switch (wifiSupplState) { + case BatteryStats.WIFI_SUPPL_STATE_DISCONNECTED: + case BatteryStats.WIFI_SUPPL_STATE_DORMANT: + case BatteryStats.WIFI_SUPPL_STATE_INACTIVE: + case BatteryStats.WIFI_SUPPL_STATE_INTERFACE_DISABLED: + case BatteryStats.WIFI_SUPPL_STATE_INVALID: + case BatteryStats.WIFI_SUPPL_STATE_UNINITIALIZED: + wifiRunning = lastWifiSupplRunning = false; + break; + default: + wifiRunning = lastWifiSupplRunning = true; + break; + } } else { - mWifiRunningPath.lineTo(x, h-mWifiRunningOffset); + wifiRunning = lastWifiSupplRunning; + } + if ((rec.states&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG + |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG + |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0) { + wifiRunning = true; + } + if (wifiRunning != lastWifiRunning) { + if (wifiRunning) { + mWifiRunningPath.moveTo(x, h-mWifiRunningOffset); + } else { + mWifiRunningPath.lineTo(x, h-mWifiRunningOffset); + } + lastWifiRunning = wifiRunning; } - lastWifiRunning = wifiRunning; - } - final boolean wakeLock = - (rec.states&HistoryItem.STATE_WAKE_LOCK_FLAG) != 0; - if (wakeLock != lastWakeLock) { - if (wakeLock) { - mWakeLockPath.moveTo(x, h-mWakeLockOffset); - } else { - mWakeLockPath.lineTo(x, h-mWakeLockOffset); + final boolean cpuRunning = + (rec.states&HistoryItem.STATE_CPU_RUNNING_FLAG) != 0; + if (cpuRunning != lastCpuRunning) { + if (cpuRunning) { + mCpuRunningPath.moveTo(x, h - mCpuRunningOffset); + } else { + mCpuRunningPath.lineTo(x, h - mCpuRunningOffset); + } + lastCpuRunning = cpuRunning; + } + + if (mLargeMode && mHavePhoneSignal) { + int bin; + if (((rec.states&HistoryItem.STATE_PHONE_STATE_MASK) + >> HistoryItem.STATE_PHONE_STATE_SHIFT) + == ServiceState.STATE_POWER_OFF) { + bin = 0; + } else if ((rec.states&HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) { + bin = 1; + } else { + bin = (rec.states&HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK) + >> HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT; + bin += 2; + } + mPhoneSignalChart.addTick(x, bin); } - lastWakeLock = wakeLock; } - if (mLargeMode && mHavePhoneSignal) { - int bin; - if (((rec.states&HistoryItem.STATE_PHONE_STATE_MASK) - >> HistoryItem.STATE_PHONE_STATE_SHIFT) - == ServiceState.STATE_POWER_OFF) { - bin = 0; - } else if ((rec.states&HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) { - bin = 1; + } else { + long lastWalltime = curWalltime; + if (rec.cmd == HistoryItem.CMD_CURRENT_TIME + || rec.cmd == HistoryItem.CMD_RESET) { + if (rec.currentTime >= mStartWallTime) { + curWalltime = rec.currentTime; } else { - bin = (rec.states&HistoryItem.STATE_SIGNAL_STRENGTH_MASK) - >> HistoryItem.STATE_SIGNAL_STRENGTH_SHIFT; - bin += 2; + curWalltime = mStartWallTime + (rec.time-mHistStart); } - mPhoneSignalChart.addTick(x, bin); + lastRealtime = rec.time; } - } else if (rec.cmd != BatteryStats.HistoryItem.CMD_OVERFLOW) { - if (curLevelPath != null) { - finishPaths(x+1, h, levelh, startX, lastY, curLevelPath, lastX, - lastCharging, lastScreenOn, lastGpsOn, lastWifiRunning, - lastWakeLock, lastLinePath); - lastX = lastY = -1; - curLevelPath = null; - lastLinePath = null; - lastCharging = lastScreenOn = lastGpsOn = lastWakeLock = false; + if (rec.cmd != HistoryItem.CMD_OVERFLOW + && (rec.cmd != HistoryItem.CMD_CURRENT_TIME + || Math.abs(lastWalltime-curWalltime) > (60*60*1000))) { + if (curLevelPath != null) { + finishPaths(x+1, h, levelh, startX, lastY, curLevelPath, lastX, + lastCharging, lastScreenOn, lastGpsOn, lastWifiRunning, + lastCpuRunning, lastLinePath); + lastX = lastY = -1; + curLevelPath = null; + lastLinePath = null; + lastCharging = lastScreenOn = lastGpsOn = lastCpuRunning = false; + } } } i++; } + mStats.finishIteratingHistoryLocked(); } - - finishPaths(w, h, levelh, startX, lastY, curLevelPath, lastX, + + if (lastY < 0 || lastX < 0) { + // Didn't get any data... + x = lastX = mLevelLeft; + y = lastY = mLevelTop + levelh - ((mBatteryLevel-batLow)*(levelh-1))/batChange; + Path path; + byte value = (byte)mBatteryLevel; + if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; + else if (value <= mBatteryWarnLevel) path = mBatWarnPath; + else path = null; //mBatGoodPath; + if (path != null) { + path.moveTo(x, y); + lastLinePath = path; + } + mBatLevelPath.moveTo(x, y); + curLevelPath = mBatLevelPath; + x = w; + } else { + // Figure out where the actual data ends on the screen. + x = mLevelLeft + (int)(((mEndDataWallTime-walltimeStart)*levelWidth)/walltimeChange); + if (x < 0) { + x = 0; + } + } + + finishPaths(x, h, levelh, startX, lastY, curLevelPath, lastX, lastCharging, lastScreenOn, lastGpsOn, lastWifiRunning, - lastWakeLock, lastLinePath); + lastCpuRunning, lastLinePath); + + if (x < w) { + // If we reserved room for the remaining time, create a final path to draw + // that part of the UI. + mTimeRemainPath.moveTo(x, lastY); + int fullY = mLevelTop + levelh - ((100-batLow)*(levelh-1))/batChange; + int emptyY = mLevelTop + levelh - ((0-batLow)*(levelh-1))/batChange; + if (mDischarging) { + mTimeRemainPath.lineTo(mLevelRight, emptyY); + } else { + mTimeRemainPath.lineTo(mLevelRight, fullY); + mTimeRemainPath.lineTo(mLevelRight, emptyY); + } + mTimeRemainPath.lineTo(x, emptyY); + mTimeRemainPath.close(); + } + + if (mStartWallTime > 0 && mEndWallTime > mStartWallTime) { + // Create the time labels at the bottom. + boolean is24hr = is24Hour(); + Calendar calStart = Calendar.getInstance(); + calStart.setTimeInMillis(mStartWallTime); + calStart.set(Calendar.MILLISECOND, 0); + calStart.set(Calendar.SECOND, 0); + calStart.set(Calendar.MINUTE, 0); + long startRoundTime = calStart.getTimeInMillis(); + if (startRoundTime < mStartWallTime) { + calStart.set(Calendar.HOUR_OF_DAY, calStart.get(Calendar.HOUR_OF_DAY)+1); + startRoundTime = calStart.getTimeInMillis(); + } + Calendar calEnd = Calendar.getInstance(); + calEnd.setTimeInMillis(mEndWallTime); + calEnd.set(Calendar.MILLISECOND, 0); + calEnd.set(Calendar.SECOND, 0); + calEnd.set(Calendar.MINUTE, 0); + long endRoundTime = calEnd.getTimeInMillis(); + if (startRoundTime < endRoundTime) { + addTimeLabel(calStart, mLevelLeft, mLevelRight, is24hr); + Calendar calMid = Calendar.getInstance(); + calMid.setTimeInMillis(mStartWallTime+((mEndWallTime-mStartWallTime)/2)); + calMid.set(Calendar.MILLISECOND, 0); + calMid.set(Calendar.SECOND, 0); + calMid.set(Calendar.MINUTE, 0); + long calMidMillis = calMid.getTimeInMillis(); + if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { + addTimeLabel(calMid, mLevelLeft, mLevelRight, is24hr); + } + addTimeLabel(calEnd, mLevelLeft, mLevelRight, is24hr); + } + + // Create the date labels if the chart includes multiple days + if (calStart.get(Calendar.DAY_OF_YEAR) != calEnd.get(Calendar.DAY_OF_YEAR) || + calStart.get(Calendar.YEAR) != calEnd.get(Calendar.YEAR)) { + boolean isDayFirst = isDayFirst(); + calStart.set(Calendar.HOUR_OF_DAY, 0); + startRoundTime = calStart.getTimeInMillis(); + if (startRoundTime < mStartWallTime) { + calStart.set(Calendar.DAY_OF_YEAR, calStart.get(Calendar.DAY_OF_YEAR) + 1); + startRoundTime = calStart.getTimeInMillis(); + } + calEnd.set(Calendar.HOUR_OF_DAY, 0); + endRoundTime = calEnd.getTimeInMillis(); + if (startRoundTime < endRoundTime) { + addDateLabel(calStart, mLevelLeft, mLevelRight, isDayFirst); + Calendar calMid = Calendar.getInstance(); + calMid.setTimeInMillis(startRoundTime + ((endRoundTime - startRoundTime) / 2)); + calMid.set(Calendar.HOUR_OF_DAY, 0); + long calMidMillis = calMid.getTimeInMillis(); + if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { + addDateLabel(calMid, mLevelLeft, mLevelRight, isDayFirst); + } + } + addDateLabel(calEnd, mLevelLeft, mLevelRight, isDayFirst); + } + } + + if (mTimeLabels.size() < 2) { + // If there are fewer than 2 time labels, then they are useless. Just + // show an axis label giving the entire duration. + mDurationString = Formatter.formatShortElapsedTime(getContext(), + mEndWallTime - mStartWallTime); + mDurationStringWidth = (int)mTextPaint.measureText(mDurationString); + } else { + mDurationString = null; + mDurationStringWidth = 0; + } } - + + void addTimeLabel(Calendar cal, int levelLeft, int levelRight, boolean is24hr) { + final long walltimeStart = mStartWallTime; + final long walltimeChange = mEndWallTime-walltimeStart; + mTimeLabels.add(new TimeLabel(mTextPaint, + levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) + / walltimeChange), + cal, is24hr)); + } + + void addDateLabel(Calendar cal, int levelLeft, int levelRight, boolean isDayFirst) { + final long walltimeStart = mStartWallTime; + final long walltimeChange = mEndWallTime-walltimeStart; + mDateLabels.add(new DateLabel(mTextPaint, + levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) + / walltimeChange), + cal, isDayFirst)); + } + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int width = getWidth(); final int height = getHeight(); + + //buildBitmap(width, height); + + if (DEBUG) Log.d(TAG, "onDraw: " + width + "x" + height); + //canvas.drawBitmap(mBitmap, 0, 0, null); + drawChart(canvas, width, height); + } + + void buildBitmap(int width, int height) { + if (mBitmap != null && width == mBitmap.getWidth() && height == mBitmap.getHeight()) { + return; + } + + if (DEBUG) Log.d(TAG, "buildBitmap: " + width + "x" + height); + + mBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, + Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + drawChart(mCanvas, width, height); + } + + void drawChart(Canvas canvas, int width, int height) { final boolean layoutRtl = isLayoutRtl(); final int textStartX = layoutRtl ? width : 0; - mTextPaint.setTextAlign(layoutRtl ? Paint.Align.RIGHT : Paint.Align.LEFT); + final int textEndX = layoutRtl ? 0 : width; + final Paint.Align textAlignLeft = layoutRtl ? Paint.Align.RIGHT : Paint.Align.LEFT; + final Paint.Align textAlignRight = layoutRtl ? Paint.Align.LEFT : Paint.Align.RIGHT; + if (DEBUG) { + canvas.drawRect(1, 1, width, height, mDebugRectPaint); + } + + if (DEBUG) Log.d(TAG, "Drawing level path."); canvas.drawPath(mBatLevelPath, mBatteryBackgroundPaint); - if (mLargeMode) { - int durationHalfWidth = mTotalDurationStringWidth / 2; - if (layoutRtl) durationHalfWidth = -durationHalfWidth; - canvas.drawText(mDurationString, textStartX, -mTextAscent + (mLineWidth / 2), - mTextPaint); - canvas.drawText(mTotalDurationString, (width / 2) - durationHalfWidth, - mLevelBottom - mTextAscent + mThinLineWidth, mTextPaint); - } else { - int durationHalfWidth = mDurationStringWidth / 2; - if (layoutRtl) durationHalfWidth = -durationHalfWidth; - canvas.drawText(mDurationString, (width / 2) - durationHalfWidth, - (height / 2) - ((mTextDescent - mTextAscent) / 2) - mTextAscent, mTextPaint); + if (!mTimeRemainPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing time remain path."); + canvas.drawPath(mTimeRemainPath, mTimeRemainPaint); + } + if (mTimeLabels.size() > 1) { + int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); + int ytick = mLevelBottom+mThinLineWidth+(mThinLineWidth/2); + mTextPaint.setTextAlign(Paint.Align.LEFT); + int lastX = 0; + for (int i=0; i<mTimeLabels.size(); i++) { + TimeLabel label = mTimeLabels.get(i); + if (i == 0) { + int x = label.x - label.width/2; + if (x < 0) { + x = 0; + } + if (DEBUG) Log.d(TAG, "Drawing left label: " + label.label + " @ " + x); + canvas.drawText(label.label, x, y, mTextPaint); + canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); + lastX = x + label.width; + } else if (i < (mTimeLabels.size()-1)) { + int x = label.x - label.width/2; + if (x < (lastX+mTextAscent)) { + continue; + } + TimeLabel nextLabel = mTimeLabels.get(i+1); + if (x > (width-nextLabel.width-mTextAscent)) { + continue; + } + if (DEBUG) Log.d(TAG, "Drawing middle label: " + label.label + " @ " + x); + canvas.drawText(label.label, x, y, mTextPaint); + canvas.drawLine(label.x, ytick, label.x, ytick + mThinLineWidth, mTextPaint); + lastX = x + label.width; + } else { + int x = label.x - label.width/2; + if ((x+label.width) >= width) { + x = width-1-label.width; + } + if (DEBUG) Log.d(TAG, "Drawing right label: " + label.label + " @ " + x); + canvas.drawText(label.label, x, y, mTextPaint); + canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); + } + } + } else if (mDurationString != null) { + int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); + mTextPaint.setTextAlign(Paint.Align.LEFT); + canvas.drawText(mDurationString, + mLevelLeft + (mLevelRight-mLevelLeft)/2 - mDurationStringWidth/2, + y, mTextPaint); } + + int headerTop = -mHeaderTextAscent + (mHeaderTextDescent-mHeaderTextAscent)/3; + mHeaderTextPaint.setTextAlign(textAlignLeft); + if (DEBUG) Log.d(TAG, "Drawing charge label string: " + mChargeLabelString); + canvas.drawText(mChargeLabelString, textStartX, headerTop, mHeaderTextPaint); + int stringHalfWidth = mChargeDurationStringWidth / 2; + if (layoutRtl) stringHalfWidth = -stringHalfWidth; + int headerCenter = ((width-mChargeDurationStringWidth-mDrainStringWidth)/2) + + (layoutRtl ? mDrainStringWidth : mChargeLabelStringWidth); + if (DEBUG) Log.d(TAG, "Drawing charge duration string: " + mChargeDurationString); + canvas.drawText(mChargeDurationString, headerCenter - stringHalfWidth, headerTop, + mHeaderTextPaint); + mHeaderTextPaint.setTextAlign(textAlignRight); + if (DEBUG) Log.d(TAG, "Drawing drain string: " + mDrainString); + canvas.drawText(mDrainString, textEndX, headerTop, mHeaderTextPaint); + if (!mBatGoodPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing good battery path"); canvas.drawPath(mBatGoodPath, mBatteryGoodPaint); } if (!mBatWarnPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing warn battery path"); canvas.drawPath(mBatWarnPath, mBatteryWarnPaint); } if (!mBatCriticalPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing critical battery path"); canvas.drawPath(mBatCriticalPath, mBatteryCriticalPaint); } if (mHavePhoneSignal) { + if (DEBUG) Log.d(TAG, "Drawing phone signal path"); int top = height-mPhoneSignalOffset - (mLineWidth/2); mPhoneSignalChart.draw(canvas, top, mLineWidth); } if (!mScreenOnPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing screen on path"); canvas.drawPath(mScreenOnPath, mScreenOnPaint); } if (!mChargingPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing charging path"); canvas.drawPath(mChargingPath, mChargingPaint); } if (mHaveGps) { if (!mGpsOnPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing gps path"); canvas.drawPath(mGpsOnPath, mGpsOnPaint); } } if (mHaveWifi) { if (!mWifiRunningPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing wifi path"); canvas.drawPath(mWifiRunningPath, mWifiRunningPaint); } } - if (!mWakeLockPath.isEmpty()) { - canvas.drawPath(mWakeLockPath, mWakeLockPaint); + if (!mCpuRunningPath.isEmpty()) { + if (DEBUG) Log.d(TAG, "Drawing running path"); + canvas.drawPath(mCpuRunningPath, mCpuRunningPaint); } if (mLargeMode) { + if (DEBUG) Log.d(TAG, "Drawing large mode labels"); + Paint.Align align = mTextPaint.getTextAlign(); + mTextPaint.setTextAlign(textAlignLeft); // large-mode labels always aligned to start if (mHavePhoneSignal) { canvas.drawText(mPhoneSignalLabel, textStartX, height - mPhoneSignalOffset - mTextDescent, mTextPaint); @@ -721,19 +1286,59 @@ public class BatteryHistoryChart extends View { canvas.drawText(mWifiRunningLabel, textStartX, height - mWifiRunningOffset - mTextDescent, mTextPaint); } - canvas.drawText(mWakeLockLabel, textStartX, - height - mWakeLockOffset - mTextDescent, mTextPaint); + canvas.drawText(mCpuRunningLabel, textStartX, + height - mCpuRunningOffset - mTextDescent, mTextPaint); canvas.drawText(mChargingLabel, textStartX, height - mChargingOffset - mTextDescent, mTextPaint); canvas.drawText(mScreenOnLabel, textStartX, height - mScreenOnOffset - mTextDescent, mTextPaint); - canvas.drawLine(0, mLevelBottom+(mThinLineWidth/2), width, - mLevelBottom+(mThinLineWidth/2), mTextPaint); - canvas.drawLine(0, mLevelTop, 0, - mLevelBottom+(mThinLineWidth/2), mTextPaint); + mTextPaint.setTextAlign(align); + } + + canvas.drawLine(mLevelLeft-mThinLineWidth, mLevelTop, mLevelLeft-mThinLineWidth, + mLevelBottom+(mThinLineWidth/2), mTextPaint); + if (mLargeMode) { for (int i=0; i<10; i++) { - int y = mLevelTop + ((mLevelBottom-mLevelTop)*i)/10; - canvas.drawLine(0, y, mThinLineWidth*2, y, mTextPaint); + int y = mLevelTop + mThinLineWidth/2 + ((mLevelBottom-mLevelTop)*i)/10; + canvas.drawLine(mLevelLeft-mThinLineWidth*2-mThinLineWidth/2, y, + mLevelLeft-mThinLineWidth-mThinLineWidth/2, y, mTextPaint); + } + } + if (DEBUG) Log.d(TAG, "Drawing max percent, origw=" + mMaxPercentLabelStringWidth + + ", noww=" + (int)mTextPaint.measureText(mMaxPercentLabelString)); + canvas.drawText(mMaxPercentLabelString, 0, mLevelTop, mTextPaint); + canvas.drawText(mMinPercentLabelString, + mMaxPercentLabelStringWidth-mMinPercentLabelStringWidth, + mLevelBottom - mThinLineWidth, mTextPaint); + canvas.drawLine(mLevelLeft/2, mLevelBottom+mThinLineWidth, width, + mLevelBottom+mThinLineWidth, mTextPaint); + + if (mDateLabels.size() > 0) { + int ytop = mLevelTop + mTextAscent; + int ybottom = mLevelBottom; + int lastLeft = mLevelRight; + mTextPaint.setTextAlign(Paint.Align.LEFT); + for (int i=mDateLabels.size()-1; i>=0; i--) { + DateLabel label = mDateLabels.get(i); + int left = label.x - mThinLineWidth; + int x = label.x + mThinLineWidth*2; + if ((x+label.width) >= lastLeft) { + x = label.x - mThinLineWidth*2 - label.width; + left = x - mThinLineWidth; + if (left >= lastLeft) { + // okay we give up. + continue; + } + } + if (left < mLevelLeft) { + // Won't fit on left, give up. + continue; + } + mDateLinePath.reset(); + mDateLinePath.moveTo(label.x, ytop); + mDateLinePath.lineTo(label.x, ybottom); + canvas.drawPath(mDateLinePath, mDateLinePaint); + canvas.drawText(label.label, x, ytop - mTextAscent, mTextPaint); } } } diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java b/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java index ad6fb30..63e4e13 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java @@ -17,37 +17,37 @@ package com.android.settings.fuelgauge; import android.app.Fragment; +import android.content.Intent; +import android.os.BatteryStats; import android.os.Bundle; -import android.os.Parcel; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.android.internal.os.BatteryStatsImpl; +import com.android.internal.os.BatteryStatsHelper; import com.android.settings.R; public class BatteryHistoryDetail extends Fragment { public static final String EXTRA_STATS = "stats"; + public static final String EXTRA_BROADCAST = "broadcast"; - private BatteryStatsImpl mStats; + private BatteryStats mStats; + private Intent mBatteryBroadcast; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - byte[] data = getArguments().getByteArray(EXTRA_STATS); - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(data, 0, data.length); - parcel.setDataPosition(0); - mStats = com.android.internal.os.BatteryStatsImpl.CREATOR - .createFromParcel(parcel); + String histFile = getArguments().getString(EXTRA_STATS); + mStats = BatteryStatsHelper.statsFromFile(getActivity(), histFile); + mBatteryBroadcast = getArguments().getParcelable(EXTRA_BROADCAST); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.preference_batteryhistory, null); + View view = inflater.inflate(R.layout.battery_history_chart, null); BatteryHistoryChart chart = (BatteryHistoryChart)view.findViewById( R.id.battery_history_chart); - chart.setStats(mStats); + chart.setStats(mStats, mBatteryBroadcast); return view; } } diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java index 4579db7..e7326b1 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java @@ -17,10 +17,12 @@ package com.android.settings.fuelgauge; import android.content.Context; +import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.BatteryStats; import android.preference.Preference; import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -33,24 +35,55 @@ import com.android.settings.R; */ public class BatteryHistoryPreference extends Preference { - private BatteryStats mStats; + final private BatteryStats mStats; + final private Intent mBatteryBroadcast; - public BatteryHistoryPreference(Context context, BatteryStats stats) { + private boolean mHideLabels; + private View mLabelHeader; + private BatteryHistoryChart mChart; + + public BatteryHistoryPreference(Context context, BatteryStats stats, Intent batteryBroadcast) { super(context); setLayoutResource(R.layout.preference_batteryhistory); mStats = stats; + mBatteryBroadcast = batteryBroadcast; } BatteryStats getStats() { return mStats; } + public void setHideLabels(boolean hide) { + if (mHideLabels != hide) { + mHideLabels = hide; + if (mLabelHeader != null) { + mLabelHeader.setVisibility(hide ? View.GONE : View.VISIBLE); + } + } + } + @Override protected void onBindView(View view) { super.onBindView(view); BatteryHistoryChart chart = (BatteryHistoryChart)view.findViewById( R.id.battery_history_chart); - chart.setStats(mStats); + if (mChart == null) { + // First time: use and initialize this chart. + chart.setStats(mStats, mBatteryBroadcast); + mChart = chart; + } else { + // All future times: forget the newly inflated chart, re-use the + // already initialized chart from last time. + ViewGroup parent = (ViewGroup)chart.getParent(); + int index = parent.indexOfChild(chart); + parent.removeViewAt(index); + if (mChart.getParent() != null) { + ((ViewGroup)mChart.getParent()).removeView(mChart); + } + parent.addView(mChart, index); + } + mLabelHeader = view.findViewById(R.id.labelsHeader); + mLabelHeader.setVisibility(mHideLabels ? View.GONE : View.VISIBLE); } } diff --git a/src/com/android/settings/fuelgauge/BatterySaverSettings.java b/src/com/android/settings/fuelgauge/BatterySaverSettings.java new file mode 100644 index 0000000..bd989d0 --- /dev/null +++ b/src/com/android/settings/fuelgauge/BatterySaverSettings.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2014 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.fuelgauge; + +import static android.os.PowerManager.ACTION_POWER_SAVE_MODE_CHANGING; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.PowerManager; +import android.provider.Settings.Global; +import android.util.Log; +import android.widget.Switch; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.notification.SettingPref; +import com.android.settings.widget.SwitchBar; + +public class BatterySaverSettings extends SettingsPreferenceFragment + implements SwitchBar.OnSwitchChangeListener { + private static final String TAG = "BatterySaverSettings"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String KEY_TURN_ON_AUTOMATICALLY = "turn_on_automatically"; + private static final long WAIT_FOR_SWITCH_ANIM = 500; + + private final Handler mHandler = new Handler(); + private final SettingsObserver mSettingsObserver = new SettingsObserver(mHandler); + private final Receiver mReceiver = new Receiver(); + + private Context mContext; + private boolean mCreated; + private SettingPref mTriggerPref; + private SwitchBar mSwitchBar; + private Switch mSwitch; + private boolean mValidListener; + private PowerManager mPowerManager; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (mCreated) return; + mCreated = true; + addPreferencesFromResource(R.xml.battery_saver_settings); + + mContext = getActivity(); + mSwitchBar = ((SettingsActivity) mContext).getSwitchBar(); + mSwitch = mSwitchBar.getSwitch(); + mSwitchBar.show(); + + mTriggerPref = new SettingPref(SettingPref.TYPE_GLOBAL, KEY_TURN_ON_AUTOMATICALLY, + Global.LOW_POWER_MODE_TRIGGER_LEVEL, + 0, /*default*/ + getResources().getIntArray(R.array.battery_saver_trigger_values)) { + @Override + protected String getCaption(Resources res, int value) { + if (value > 0 && value < 100) { + return res.getString(R.string.battery_saver_turn_on_automatically_pct, value); + } + return res.getString(R.string.battery_saver_turn_on_automatically_never); + } + }; + mTriggerPref.init(this); + + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mSwitchBar.hide(); + } + + @Override + public void onResume() { + super.onResume(); + mSettingsObserver.setListening(true); + mReceiver.setListening(true); + if (!mValidListener) { + mSwitchBar.addOnSwitchChangeListener(this); + mValidListener = true; + } + updateSwitch(); + } + + @Override + public void onPause() { + super.onPause(); + mSettingsObserver.setListening(false); + mReceiver.setListening(false); + if (mValidListener) { + mSwitchBar.removeOnSwitchChangeListener(this); + mValidListener = false; + } + } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + mHandler.removeCallbacks(mStartMode); + if (isChecked) { + mHandler.postDelayed(mStartMode, WAIT_FOR_SWITCH_ANIM); + } else { + if (DEBUG) Log.d(TAG, "Stopping low power mode from settings"); + trySetPowerSaveMode(false); + } + } + + private void trySetPowerSaveMode(boolean mode) { + if (!mPowerManager.setPowerSaveMode(mode)) { + if (DEBUG) Log.d(TAG, "Setting mode failed, fallback to current value"); + mHandler.post(mUpdateSwitch); + } + } + + private void updateSwitch() { + final boolean mode = mPowerManager.isPowerSaveMode(); + if (DEBUG) Log.d(TAG, "updateSwitch: isChecked=" + mSwitch.isChecked() + " mode=" + mode); + if (mode == mSwitch.isChecked()) return; + + // set listener to null so that that code below doesn't trigger onCheckedChanged() + if (mValidListener) { + mSwitchBar.removeOnSwitchChangeListener(this); + } + mSwitch.setChecked(mode); + if (mValidListener) { + mSwitchBar.addOnSwitchChangeListener(this); + } + } + + private final Runnable mUpdateSwitch = new Runnable() { + @Override + public void run() { + updateSwitch(); + } + }; + + private final Runnable mStartMode = new Runnable() { + @Override + public void run() { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Starting low power mode from settings"); + trySetPowerSaveMode(true); + } + }); + } + }; + + private final class Receiver extends BroadcastReceiver { + private boolean mRegistered; + + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "Received " + intent.getAction()); + mHandler.post(mUpdateSwitch); + } + + public void setListening(boolean listening) { + if (listening && !mRegistered) { + mContext.registerReceiver(this, new IntentFilter(ACTION_POWER_SAVE_MODE_CHANGING)); + mRegistered = true; + } else if (!listening && mRegistered) { + mContext.unregisterReceiver(this); + mRegistered = false; + } + } + } + + private final class SettingsObserver extends ContentObserver { + private final Uri LOW_POWER_MODE_TRIGGER_LEVEL_URI + = Global.getUriFor(Global.LOW_POWER_MODE_TRIGGER_LEVEL); + + public SettingsObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (LOW_POWER_MODE_TRIGGER_LEVEL_URI.equals(uri)) { + mTriggerPref.update(mContext); + } + } + + public void setListening(boolean listening) { + final ContentResolver cr = getContentResolver(); + if (listening) { + cr.registerContentObserver(LOW_POWER_MODE_TRIGGER_LEVEL_URI, false, this); + } else { + cr.unregisterContentObserver(this); + } + } + } +} diff --git a/src/com/android/settings/fuelgauge/BatterySipper.java b/src/com/android/settings/fuelgauge/BatterySipper.java deleted file mode 100644 index fcc8f69..0000000 --- a/src/com/android/settings/fuelgauge/BatterySipper.java +++ /dev/null @@ -1,242 +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.fuelgauge; - -import com.android.settings.R; -import com.android.settings.fuelgauge.PowerUsageDetail.DrainType; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.BatteryStats.Uid; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * Contains information about package name, icon image, power usage about an - * application or a system service. - */ -public class BatterySipper implements Comparable<BatterySipper> { - final Context mContext; - /* Cache cleared when PowerUsageSummary is destroyed */ - static final HashMap<String,UidToDetail> sUidCache = new HashMap<String,UidToDetail>(); - final ArrayList<BatterySipper> mRequestQueue; - final Handler mHandler; - String name; - Drawable icon; - int iconId; // For passing to the detail screen. - Uid uidObj; - double value; - double[] values; - DrainType drainType; - long usageTime; - long cpuTime; - long gpsTime; - long wifiRunningTime; - long cpuFgTime; - long wakeLockTime; - long mobileRxBytes; - long mobileTxBytes; - long wifiRxBytes; - long wifiTxBytes; - double percent; - double noCoveragePercent; - String defaultPackageName; - String[] mPackages; - - static class UidToDetail { - String name; - String packageName; - Drawable icon; - } - - BatterySipper(Context context, ArrayList<BatterySipper> requestQueue, - Handler handler, String label, DrainType drainType, - int iconId, Uid uid, double[] values) { - mContext = context; - mRequestQueue = requestQueue; - mHandler = handler; - this.values = values; - name = label; - this.drainType = drainType; - if (iconId > 0) { - icon = mContext.getResources().getDrawable(iconId); - } - if (values != null) value = values[0]; - if ((label == null || iconId == 0) && uid != null) { - getQuickNameIconForUid(uid); - } - uidObj = uid; - } - - double getSortValue() { - return value; - } - - double[] getValues() { - return values; - } - - public Drawable getIcon() { - return icon; - } - - /** - * Gets the application name - */ - public String getLabel() { - return name; - } - - @Override - public int compareTo(BatterySipper other) { - // Return the flipped value because we want the items in descending order - return Double.compare(other.getSortValue(), getSortValue()); - } - - /** - * Gets a list of packages associated with the current user - */ - public String[] getPackages() { - return mPackages; - } - - public int getUid() { - // Bail out if the current sipper is not an App sipper. - if (uidObj == null) { - return 0; - } - return uidObj.getUid(); - } - - void getQuickNameIconForUid(Uid uidObj) { - final int uid = uidObj.getUid(); - final String uidString = Integer.toString(uid); - if (sUidCache.containsKey(uidString)) { - UidToDetail utd = sUidCache.get(uidString); - defaultPackageName = utd.packageName; - name = utd.name; - icon = utd.icon; - return; - } - PackageManager pm = mContext.getPackageManager(); - String[] packages = pm.getPackagesForUid(uid); - icon = pm.getDefaultActivityIcon(); - if (packages == null) { - //name = Integer.toString(uid); - if (uid == 0) { - name = mContext.getResources().getString(R.string.process_kernel_label); - } else if ("mediaserver".equals(name)) { - name = mContext.getResources().getString(R.string.process_mediaserver_label); - } - iconId = R.drawable.ic_power_system; - icon = mContext.getResources().getDrawable(iconId); - return; - } else { - //name = packages[0]; - } - if (mHandler != null) { - synchronized (mRequestQueue) { - mRequestQueue.add(this); - } - } - } - - public static void clearUidCache() { - sUidCache.clear(); - } - - /** - * Loads the app label and icon image and stores into the cache. - */ - public void loadNameAndIcon() { - // Bail out if the current sipper is not an App sipper. - if (uidObj == null) { - return; - } - PackageManager pm = mContext.getPackageManager(); - final int uid = uidObj.getUid(); - final Drawable defaultActivityIcon = pm.getDefaultActivityIcon(); - mPackages = pm.getPackagesForUid(uid); - if (mPackages == null) { - name = Integer.toString(uid); - return; - } - - String[] packageLabels = new String[mPackages.length]; - System.arraycopy(mPackages, 0, packageLabels, 0, mPackages.length); - - int preferredIndex = -1; - // Convert package names to user-facing labels where possible - for (int i = 0; i < packageLabels.length; i++) { - // Check if package matches preferred package - if (packageLabels[i].equals(name)) preferredIndex = i; - try { - ApplicationInfo ai = pm.getApplicationInfo(packageLabels[i], 0); - CharSequence label = ai.loadLabel(pm); - if (label != null) { - packageLabels[i] = label.toString(); - } - if (ai.icon != 0) { - defaultPackageName = mPackages[i]; - icon = ai.loadIcon(pm); - break; - } - } catch (NameNotFoundException e) { - } - } - if (icon == null) icon = defaultActivityIcon; - - if (packageLabels.length == 1) { - name = packageLabels[0]; - } else { - // Look for an official name for this UID. - for (String pkgName : mPackages) { - try { - final PackageInfo pi = pm.getPackageInfo(pkgName, 0); - if (pi.sharedUserLabel != 0) { - final CharSequence nm = pm.getText(pkgName, - pi.sharedUserLabel, pi.applicationInfo); - if (nm != null) { - name = nm.toString(); - if (pi.applicationInfo.icon != 0) { - defaultPackageName = pkgName; - icon = pi.applicationInfo.loadIcon(pm); - } - break; - } - } - } catch (PackageManager.NameNotFoundException e) { - } - } - } - final String uidString = Integer.toString(uidObj.getUid()); - UidToDetail utd = new UidToDetail(); - utd.name = name; - utd.icon = icon; - utd.packageName = defaultPackageName; - sUidCache.put(uidString, utd); - if (mHandler != null) { - mHandler.sendMessage( - mHandler.obtainMessage(BatteryStatsHelper.MSG_UPDATE_NAME_ICON, this)); - } - } -} diff --git a/src/com/android/settings/fuelgauge/BatteryStatsHelper.java b/src/com/android/settings/fuelgauge/BatteryStatsHelper.java deleted file mode 100644 index 0191692..0000000 --- a/src/com/android/settings/fuelgauge/BatteryStatsHelper.java +++ /dev/null @@ -1,836 +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.fuelgauge; - -import static android.os.BatteryStats.NETWORK_MOBILE_RX_BYTES; -import static android.os.BatteryStats.NETWORK_MOBILE_TX_BYTES; -import static android.os.BatteryStats.NETWORK_WIFI_RX_BYTES; -import static android.os.BatteryStats.NETWORK_WIFI_TX_BYTES; - -import android.app.Activity; -import android.content.Context; -import android.content.pm.UserInfo; -import android.graphics.drawable.Drawable; -import android.hardware.Sensor; -import android.hardware.SensorManager; -import android.os.BatteryStats; -import android.os.BatteryStats.Uid; -import android.os.Bundle; -import android.os.Handler; -import android.os.Parcel; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.UserManager; -import android.preference.PreferenceActivity; -import android.telephony.SignalStrength; -import android.util.Log; -import android.util.SparseArray; - -import com.android.internal.app.IBatteryStats; -import com.android.internal.os.BatteryStatsImpl; -import com.android.internal.os.PowerProfile; -import com.android.internal.util.FastPrintWriter; -import com.android.settings.R; -import com.android.settings.fuelgauge.PowerUsageDetail.DrainType; -import com.android.settings.users.UserUtils; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * A helper class for retrieving the power usage information for all applications and services. - * - * The caller must initialize this class as soon as activity object is ready to use (for example, in - * onAttach() for Fragment), call create() in onCreate() and call destroy() in onDestroy(). - */ -public class BatteryStatsHelper { - - private static final boolean DEBUG = false; - - private static final String TAG = BatteryStatsHelper.class.getSimpleName(); - - private static BatteryStatsImpl sStatsXfer; - private IBatteryStats mBatteryInfo; - private UserManager mUm; - private BatteryStatsImpl mStats; - private PowerProfile mPowerProfile; - - private final List<BatterySipper> mUsageList = new ArrayList<BatterySipper>(); - private final List<BatterySipper> mWifiSippers = new ArrayList<BatterySipper>(); - private final List<BatterySipper> mBluetoothSippers = new ArrayList<BatterySipper>(); - private final SparseArray<List<BatterySipper>> mUserSippers - = new SparseArray<List<BatterySipper>>(); - private final SparseArray<Double> mUserPower = new SparseArray<Double>(); - - private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; - - private long mStatsPeriod = 0; - private double mMaxPower = 1; - private double mTotalPower; - private double mWifiPower; - private double mBluetoothPower; - - // How much the apps together have left WIFI running. - private long mAppWifiRunning; - - /** Queue for fetching name and icon for an application */ - private ArrayList<BatterySipper> mRequestQueue = new ArrayList<BatterySipper>(); - - private Activity mActivity; - private Handler mHandler; - - private class NameAndIconLoader extends Thread { - private boolean mAbort = false; - - public NameAndIconLoader() { - super("BatteryUsage Icon Loader"); - } - - public void abort() { - mAbort = true; - } - - @Override - public void run() { - while (true) { - BatterySipper bs; - synchronized (mRequestQueue) { - if (mRequestQueue.isEmpty() || mAbort) { - mHandler.sendEmptyMessage(MSG_REPORT_FULLY_DRAWN); - return; - } - bs = mRequestQueue.remove(0); - } - bs.loadNameAndIcon(); - } - } - } - - private NameAndIconLoader mRequestThread; - - public BatteryStatsHelper(Activity activity, Handler handler) { - mActivity = activity; - mHandler = handler; - } - - /** Clears the current stats and forces recreating for future use. */ - public void clearStats() { - mStats = null; - } - - public BatteryStatsImpl getStats() { - if (mStats == null) { - load(); - } - return mStats; - } - - public PowerProfile getPowerProfile() { - return mPowerProfile; - } - - public void create(Bundle icicle) { - if (icicle != null) { - mStats = sStatsXfer; - } - mBatteryInfo = IBatteryStats.Stub.asInterface( - ServiceManager.getService(BatteryStats.SERVICE_NAME)); - mUm = (UserManager) mActivity.getSystemService(Context.USER_SERVICE); - mPowerProfile = new PowerProfile(mActivity); - } - - public void pause() { - if (mRequestThread != null) { - mRequestThread.abort(); - } - } - - public void destroy() { - if (mActivity.isChangingConfigurations()) { - sStatsXfer = mStats; - } else { - BatterySipper.sUidCache.clear(); - } - } - - public void startBatteryDetailPage( - PreferenceActivity caller, BatterySipper sipper, boolean showLocationButton) { - // Initialize mStats if necessary. - getStats(); - - Bundle args = new Bundle(); - args.putString(PowerUsageDetail.EXTRA_TITLE, sipper.name); - args.putInt(PowerUsageDetail.EXTRA_PERCENT, (int) - Math.ceil(sipper.getSortValue() * 100 / mTotalPower)); - args.putInt(PowerUsageDetail.EXTRA_GAUGE, (int) - Math.ceil(sipper.getSortValue() * 100 / mMaxPower)); - args.putLong(PowerUsageDetail.EXTRA_USAGE_DURATION, mStatsPeriod); - args.putString(PowerUsageDetail.EXTRA_ICON_PACKAGE, sipper.defaultPackageName); - args.putInt(PowerUsageDetail.EXTRA_ICON_ID, sipper.iconId); - args.putDouble(PowerUsageDetail.EXTRA_NO_COVERAGE, sipper.noCoveragePercent); - if (sipper.uidObj != null) { - args.putInt(PowerUsageDetail.EXTRA_UID, sipper.uidObj.getUid()); - } - args.putSerializable(PowerUsageDetail.EXTRA_DRAIN_TYPE, sipper.drainType); - args.putBoolean(PowerUsageDetail.EXTRA_SHOW_LOCATION_BUTTON, showLocationButton); - - int[] types; - double[] values; - switch (sipper.drainType) { - case APP: - case USER: - { - Uid uid = sipper.uidObj; - types = new int[] { - R.string.usage_type_cpu, - R.string.usage_type_cpu_foreground, - R.string.usage_type_wake_lock, - R.string.usage_type_gps, - R.string.usage_type_wifi_running, - R.string.usage_type_data_recv, - R.string.usage_type_data_send, - R.string.usage_type_data_wifi_recv, - R.string.usage_type_data_wifi_send, - R.string.usage_type_audio, - R.string.usage_type_video, - }; - values = new double[] { - sipper.cpuTime, - sipper.cpuFgTime, - sipper.wakeLockTime, - sipper.gpsTime, - sipper.wifiRunningTime, - sipper.mobileRxBytes, - sipper.mobileTxBytes, - sipper.wifiRxBytes, - sipper.wifiTxBytes, - 0, - 0 - }; - - if (sipper.drainType == DrainType.APP) { - Writer result = new StringWriter(); - PrintWriter printWriter = new FastPrintWriter(result, false, 1024); - mStats.dumpLocked(printWriter, "", mStatsType, uid.getUid()); - printWriter.flush(); - args.putString(PowerUsageDetail.EXTRA_REPORT_DETAILS, result.toString()); - - result = new StringWriter(); - printWriter = new FastPrintWriter(result, false, 1024); - mStats.dumpCheckinLocked(printWriter, mStatsType, uid.getUid()); - printWriter.flush(); - args.putString(PowerUsageDetail.EXTRA_REPORT_CHECKIN_DETAILS, - result.toString()); - } - } - break; - case CELL: - { - types = new int[] { - R.string.usage_type_on_time, - R.string.usage_type_no_coverage - }; - values = new double[] { - sipper.usageTime, - sipper.noCoveragePercent - }; - } - break; - case WIFI: - { - types = new int[] { - R.string.usage_type_wifi_running, - R.string.usage_type_cpu, - R.string.usage_type_cpu_foreground, - R.string.usage_type_wake_lock, - R.string.usage_type_data_recv, - R.string.usage_type_data_send, - R.string.usage_type_data_wifi_recv, - R.string.usage_type_data_wifi_send, - }; - values = new double[] { - sipper.usageTime, - sipper.cpuTime, - sipper.cpuFgTime, - sipper.wakeLockTime, - sipper.mobileRxBytes, - sipper.mobileTxBytes, - sipper.wifiRxBytes, - sipper.wifiTxBytes, - }; - } break; - case BLUETOOTH: - { - types = new int[] { - R.string.usage_type_on_time, - R.string.usage_type_cpu, - R.string.usage_type_cpu_foreground, - R.string.usage_type_wake_lock, - R.string.usage_type_data_recv, - R.string.usage_type_data_send, - R.string.usage_type_data_wifi_recv, - R.string.usage_type_data_wifi_send, - }; - values = new double[] { - sipper.usageTime, - sipper.cpuTime, - sipper.cpuFgTime, - sipper.wakeLockTime, - sipper.mobileRxBytes, - sipper.mobileTxBytes, - sipper.wifiRxBytes, - sipper.wifiTxBytes, - }; - } break; - default: - { - types = new int[] { - R.string.usage_type_on_time - }; - values = new double[] { - sipper.usageTime - }; - } - } - args.putIntArray(PowerUsageDetail.EXTRA_DETAIL_TYPES, types); - args.putDoubleArray(PowerUsageDetail.EXTRA_DETAIL_VALUES, values); - caller.startPreferencePanel(PowerUsageDetail.class.getName(), args, - R.string.details_title, null, null, 0); - } - - /** - * Refreshes the power usage list. - * @param includeZeroConsumption whether includes those applications which have consumed very - * little power up till now. - */ - public void refreshStats(boolean includeZeroConsumption) { - // Initialize mStats if necessary. - getStats(); - - mMaxPower = 0; - mTotalPower = 0; - mWifiPower = 0; - mBluetoothPower = 0; - mAppWifiRunning = 0; - - mUsageList.clear(); - mWifiSippers.clear(); - mBluetoothSippers.clear(); - mUserSippers.clear(); - mUserPower.clear(); - - processAppUsage(includeZeroConsumption); - processMiscUsage(); - - Collections.sort(mUsageList); - - if (mHandler != null) { - synchronized (mRequestQueue) { - if (!mRequestQueue.isEmpty()) { - if (mRequestThread != null) { - mRequestThread.abort(); - } - mRequestThread = new NameAndIconLoader(); - mRequestThread.setPriority(Thread.MIN_PRIORITY); - mRequestThread.start(); - mRequestQueue.notify(); - } - } - } - } - - private void processAppUsage(boolean includeZeroConsumption) { - SensorManager sensorManager = (SensorManager) mActivity.getSystemService( - Context.SENSOR_SERVICE); - final int which = mStatsType; - final int speedSteps = mPowerProfile.getNumSpeedSteps(); - final double[] powerCpuNormal = new double[speedSteps]; - final long[] cpuSpeedStepTimes = new long[speedSteps]; - for (int p = 0; p < speedSteps; p++) { - powerCpuNormal[p] = mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_ACTIVE, p); - } - final double mobilePowerPerByte = getMobilePowerPerByte(); - final double wifiPowerPerByte = getWifiPowerPerByte(); - long uSecTime = mStats.computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, which); - long appWakelockTime = 0; - BatterySipper osApp = null; - mStatsPeriod = uSecTime; - SparseArray<? extends Uid> uidStats = mStats.getUidStats(); - final int NU = uidStats.size(); - for (int iu = 0; iu < NU; iu++) { - Uid u = uidStats.valueAt(iu); - double p; // in mAs - double power = 0; // in mAs - double highestDrain = 0; - String packageWithHighestDrain = null; - //mUsageList.add(new AppUsage(u.getUid(), new double[] {power})); - Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); - long cpuTime = 0; - long cpuFgTime = 0; - long wakelockTime = 0; - long gpsTime = 0; - if (DEBUG) Log.i(TAG, "UID " + u.getUid()); - if (processStats.size() > 0) { - // Process CPU time - for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent - : processStats.entrySet()) { - Uid.Proc ps = ent.getValue(); - final long userTime = ps.getUserTime(which); - final long systemTime = ps.getSystemTime(which); - final long foregroundTime = ps.getForegroundTime(which); - cpuFgTime += foregroundTime * 10; // convert to millis - final long tmpCpuTime = (userTime + systemTime) * 10; // convert to millis - int totalTimeAtSpeeds = 0; - // Get the total first - for (int step = 0; step < speedSteps; step++) { - cpuSpeedStepTimes[step] = ps.getTimeAtCpuSpeedStep(step, which); - totalTimeAtSpeeds += cpuSpeedStepTimes[step]; - } - if (totalTimeAtSpeeds == 0) totalTimeAtSpeeds = 1; - // Then compute the ratio of time spent at each speed - double processPower = 0; - for (int step = 0; step < speedSteps; step++) { - double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; - processPower += ratio * tmpCpuTime * powerCpuNormal[step]; - } - cpuTime += tmpCpuTime; - if (DEBUG && processPower != 0) { - Log.i(TAG, String.format("process %s, cpu power=%.2f", - ent.getKey(), processPower / 1000)); - } - power += processPower; - if (packageWithHighestDrain == null - || packageWithHighestDrain.startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); - } else if (highestDrain < processPower - && !ent.getKey().startsWith("*")) { - highestDrain = processPower; - packageWithHighestDrain = ent.getKey(); - } - } - } - if (cpuFgTime > cpuTime) { - if (DEBUG && cpuFgTime > cpuTime + 10000) { - Log.i(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time"); - } - cpuTime = cpuFgTime; // Statistics may not have been gathered yet. - } - power /= 1000; - if (DEBUG && power != 0) Log.i(TAG, String.format("total cpu power=%.2f", power)); - - // Process wake lock usage - Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = u.getWakelockStats(); - for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> wakelockEntry - : wakelockStats.entrySet()) { - Uid.Wakelock wakelock = wakelockEntry.getValue(); - // Only care about partial wake locks since full wake locks - // are canceled when the user turns the screen off. - BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); - if (timer != null) { - wakelockTime += timer.getTotalTimeLocked(uSecTime, which); - } - } - wakelockTime /= 1000; // convert to millis - appWakelockTime += wakelockTime; - - // Add cost of holding a wake lock - p = (wakelockTime - * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / 1000; - power += p; - if (DEBUG && p != 0) Log.i(TAG, String.format("wakelock power=%.2f", p)); - - // Add cost of mobile traffic - final long mobileRx = u.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, mStatsType); - final long mobileTx = u.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, mStatsType); - p = (mobileRx + mobileTx) * mobilePowerPerByte; - power += p; - if (DEBUG && p != 0) Log.i(TAG, String.format("mobile power=%.2f", p)); - - // Add cost of wifi traffic - final long wifiRx = u.getNetworkActivityCount(NETWORK_WIFI_RX_BYTES, mStatsType); - final long wifiTx = u.getNetworkActivityCount(NETWORK_WIFI_TX_BYTES, mStatsType); - p = (wifiRx + wifiTx) * wifiPowerPerByte; - power += p; - if (DEBUG && p != 0) Log.i(TAG, String.format("wifi power=%.2f", p)); - - // Add cost of keeping WIFI running. - long wifiRunningTimeMs = u.getWifiRunningTime(uSecTime, which) / 1000; - mAppWifiRunning += wifiRunningTimeMs; - p = (wifiRunningTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / 1000; - power += p; - if (DEBUG && p != 0) Log.i(TAG, String.format("wifi running power=%.2f", p)); - - // Add cost of WIFI scans - long wifiScanTimeMs = u.getWifiScanTime(uSecTime, which) / 1000; - p = (wifiScanTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) / 1000; - power += p; - if (DEBUG && p != 0) Log.i(TAG, String.format("wifi scanning power=%.2f", p)); - for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) { - long batchScanTimeMs = u.getWifiBatchedScanTime(bin, uSecTime, which) / 1000; - p = (batchScanTimeMs - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, bin)); - power += p; - if (DEBUG && p != 0) { - Log.i(TAG, String.format("wifi batched scanning lvl %d = %.2f", bin, p)); - } - } - - // Process Sensor usage - Map<Integer, ? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); - for (Map.Entry<Integer, ? extends BatteryStats.Uid.Sensor> sensorEntry - : sensorStats.entrySet()) { - Uid.Sensor sensor = sensorEntry.getValue(); - int sensorHandle = sensor.getHandle(); - BatteryStats.Timer timer = sensor.getSensorTime(); - long sensorTime = timer.getTotalTimeLocked(uSecTime, which) / 1000; - double multiplier = 0; - switch (sensorHandle) { - case Uid.Sensor.GPS: - multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); - gpsTime = sensorTime; - break; - default: - List<Sensor> sensorList = sensorManager.getSensorList( - android.hardware.Sensor.TYPE_ALL); - for (android.hardware.Sensor s : sensorList) { - if (s.getHandle() == sensorHandle) { - multiplier = s.getPower(); - break; - } - } - } - p = (multiplier * sensorTime) / 1000; - power += p; - if (DEBUG && p != 0) { - Log.i(TAG, String.format("sensor %s power=%.2f", sensor.toString(), p)); - } - } - - if (DEBUG) Log.i(TAG, String.format("UID %d total power=%.2f", u.getUid(), power)); - - // Add the app to the list if it is consuming power - boolean isOtherUser = false; - final int userId = UserHandle.getUserId(u.getUid()); - if (power != 0 || includeZeroConsumption || u.getUid() == 0) { - BatterySipper app = new BatterySipper(mActivity, mRequestQueue, mHandler, - packageWithHighestDrain, DrainType.APP, 0, u, - new double[] {power}); - app.cpuTime = cpuTime; - app.gpsTime = gpsTime; - app.wifiRunningTime = wifiRunningTimeMs; - app.cpuFgTime = cpuFgTime; - app.wakeLockTime = wakelockTime; - app.mobileRxBytes = mobileRx; - app.mobileTxBytes = mobileTx; - app.wifiRxBytes = wifiRx; - app.wifiTxBytes = wifiTx; - if (u.getUid() == Process.WIFI_UID) { - mWifiSippers.add(app); - } else if (u.getUid() == Process.BLUETOOTH_UID) { - mBluetoothSippers.add(app); - } else if (userId != UserHandle.myUserId() - && UserHandle.getAppId(u.getUid()) >= Process.FIRST_APPLICATION_UID) { - isOtherUser = true; - List<BatterySipper> list = mUserSippers.get(userId); - if (list == null) { - list = new ArrayList<BatterySipper>(); - mUserSippers.put(userId, list); - } - list.add(app); - } else { - mUsageList.add(app); - } - if (u.getUid() == 0) { - osApp = app; - } - } - if (power != 0 || includeZeroConsumption) { - if (u.getUid() == Process.WIFI_UID) { - mWifiPower += power; - } else if (u.getUid() == Process.BLUETOOTH_UID) { - mBluetoothPower += power; - } else if (isOtherUser) { - Double userPower = mUserPower.get(userId); - if (userPower == null) { - userPower = power; - } else { - userPower += power; - } - mUserPower.put(userId, userPower); - } else { - if (power > mMaxPower) mMaxPower = power; - mTotalPower += power; - } - } - } - - // The device has probably been awake for longer than the screen on - // time and application wake lock time would account for. Assign - // this remainder to the OS, if possible. - if (osApp != null) { - long wakeTimeMillis = mStats.computeBatteryUptime( - SystemClock.uptimeMillis() * 1000, which) / 1000; - wakeTimeMillis -= appWakelockTime + (mStats.getScreenOnTime( - SystemClock.elapsedRealtime(), which) / 1000); - if (wakeTimeMillis > 0) { - double power = (wakeTimeMillis - * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / 1000; - if (DEBUG) Log.i(TAG, "OS wakeLockTime " + wakeTimeMillis + " power " + power); - osApp.wakeLockTime += wakeTimeMillis; - osApp.value += power; - osApp.values[0] += power; - if (osApp.value > mMaxPower) mMaxPower = osApp.value; - mTotalPower += power; - } - } - } - - private void addPhoneUsage(long uSecNow) { - long phoneOnTimeMs = mStats.getPhoneOnTime(uSecNow, mStatsType) / 1000; - double phoneOnPower = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) - * phoneOnTimeMs / 1000; - addEntry(mActivity.getString(R.string.power_phone), DrainType.PHONE, phoneOnTimeMs, - R.drawable.ic_settings_voice_calls, phoneOnPower); - } - - private void addScreenUsage(long uSecNow) { - double power = 0; - long screenOnTimeMs = mStats.getScreenOnTime(uSecNow, mStatsType) / 1000; - power += screenOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON); - final double screenFullPower = - mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); - for (int i = 0; i < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; i++) { - double screenBinPower = screenFullPower * (i + 0.5f) - / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; - long brightnessTime = mStats.getScreenBrightnessTime(i, uSecNow, mStatsType) / 1000; - power += screenBinPower * brightnessTime; - if (DEBUG) { - Log.i(TAG, "Screen bin power = " + (int) screenBinPower + ", time = " - + brightnessTime); - } - } - power /= 1000; // To seconds - addEntry(mActivity.getString(R.string.power_screen), DrainType.SCREEN, screenOnTimeMs, - R.drawable.ic_settings_display, power); - } - - private void addRadioUsage(long uSecNow) { - double power = 0; - final int BINS = SignalStrength.NUM_SIGNAL_STRENGTH_BINS; - long signalTimeMs = 0; - for (int i = 0; i < BINS; i++) { - long strengthTimeMs = mStats.getPhoneSignalStrengthTime(i, uSecNow, mStatsType) / 1000; - power += strengthTimeMs / 1000 - * mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ON, i); - signalTimeMs += strengthTimeMs; - } - long scanningTimeMs = mStats.getPhoneSignalScanningTime(uSecNow, mStatsType) / 1000; - power += scanningTimeMs / 1000 * mPowerProfile.getAveragePower( - PowerProfile.POWER_RADIO_SCANNING); - BatterySipper bs = - addEntry(mActivity.getString(R.string.power_cell), DrainType.CELL, - signalTimeMs, R.drawable.ic_settings_cell_standby, power); - if (signalTimeMs != 0) { - bs.noCoveragePercent = mStats.getPhoneSignalStrengthTime(0, uSecNow, mStatsType) - / 1000 * 100.0 / signalTimeMs; - } - } - - private void aggregateSippers(BatterySipper bs, List<BatterySipper> from, String tag) { - for (int i=0; i<from.size(); i++) { - BatterySipper wbs = from.get(i); - if (DEBUG) Log.i(TAG, tag + " adding sipper " + wbs + ": cpu=" + wbs.cpuTime); - bs.cpuTime += wbs.cpuTime; - bs.gpsTime += wbs.gpsTime; - bs.wifiRunningTime += wbs.wifiRunningTime; - bs.cpuFgTime += wbs.cpuFgTime; - bs.wakeLockTime += wbs.wakeLockTime; - bs.mobileRxBytes += wbs.mobileRxBytes; - bs.mobileTxBytes += wbs.mobileTxBytes; - bs.wifiRxBytes += wbs.wifiRxBytes; - bs.wifiTxBytes += wbs.wifiTxBytes; - } - } - - private void addWiFiUsage(long uSecNow) { - long onTimeMs = mStats.getWifiOnTime(uSecNow, mStatsType) / 1000; - long runningTimeMs = mStats.getGlobalWifiRunningTime(uSecNow, mStatsType) / 1000; - if (DEBUG) Log.i(TAG, "WIFI runningTime=" + runningTimeMs - + " app runningTime=" + mAppWifiRunning); - runningTimeMs -= mAppWifiRunning; - if (runningTimeMs < 0) runningTimeMs = 0; - double wifiPower = (onTimeMs * 0 /* TODO */ - * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON) - + runningTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / 1000; - if (DEBUG) Log.i(TAG, "WIFI power=" + wifiPower + " from procs=" + mWifiPower); - BatterySipper bs = addEntry(mActivity.getString(R.string.power_wifi), DrainType.WIFI, - runningTimeMs, R.drawable.ic_settings_wifi, wifiPower + mWifiPower); - aggregateSippers(bs, mWifiSippers, "WIFI"); - } - - private void addIdleUsage(long uSecNow) { - long idleTimeMs = (uSecNow - mStats.getScreenOnTime(uSecNow, mStatsType)) / 1000; - double idlePower = (idleTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE)) - / 1000; - addEntry(mActivity.getString(R.string.power_idle), DrainType.IDLE, idleTimeMs, - R.drawable.ic_settings_phone_idle, idlePower); - } - - private void addBluetoothUsage(long uSecNow) { - long btOnTimeMs = mStats.getBluetoothOnTime(uSecNow, mStatsType) / 1000; - double btPower = btOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_ON) - / 1000; - int btPingCount = mStats.getBluetoothPingCount(); - btPower += (btPingCount - * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_AT_CMD)) / 1000; - BatterySipper bs = addEntry(mActivity.getString(R.string.power_bluetooth), - DrainType.BLUETOOTH, btOnTimeMs, R.drawable.ic_settings_bluetooth, - btPower + mBluetoothPower); - aggregateSippers(bs, mBluetoothSippers, "Bluetooth"); - } - - private void addUserUsage() { - for (int i=0; i<mUserSippers.size(); i++) { - final int userId = mUserSippers.keyAt(i); - final List<BatterySipper> sippers = mUserSippers.valueAt(i); - UserInfo info = mUm.getUserInfo(userId); - Drawable icon; - String name; - if (info != null) { - icon = UserUtils.getUserIcon(mActivity, mUm, info, mActivity.getResources()); - name = info != null ? info.name : null; - if (name == null) { - name = Integer.toString(info.id); - } - name = mActivity.getResources().getString( - R.string.running_process_item_user_label, name); - } else { - icon = null; - name = mActivity.getResources().getString( - R.string.running_process_item_removed_user_label); - } - Double userPower = mUserPower.get(userId); - double power = (userPower != null) ? userPower : 0.0; - BatterySipper bs = addEntry(name, DrainType.USER, 0, 0, power); - bs.icon = icon; - aggregateSippers(bs, sippers, "User"); - } - } - - /** - * Return estimated power (in mAs) of sending a byte with the mobile radio. - */ - private double getMobilePowerPerByte() { - final long MOBILE_BPS = 200000; // TODO: Extract average bit rates from system - final double MOBILE_POWER = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) - / 3600; - - final long mobileRx = mStats.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, mStatsType); - final long mobileTx = mStats.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, mStatsType); - final long mobileData = mobileRx + mobileTx; - - final long radioDataUptimeMs = mStats.getRadioDataUptime() / 1000; - final long mobileBps = radioDataUptimeMs != 0 - ? mobileData * 8 * 1000 / radioDataUptimeMs - : MOBILE_BPS; - - return MOBILE_POWER / (mobileBps / 8); - } - - /** - * Return estimated power (in mAs) of sending a byte with the Wi-Fi radio. - */ - private double getWifiPowerPerByte() { - final long WIFI_BPS = 1000000; // TODO: Extract average bit rates from system - final double WIFI_POWER = mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ACTIVE) - / 3600; - return WIFI_POWER / (WIFI_BPS / 8); - } - - private void processMiscUsage() { - final int which = mStatsType; - long uSecTime = SystemClock.elapsedRealtime() * 1000; - final long uSecNow = mStats.computeBatteryRealtime(uSecTime, which); - final long timeSinceUnplugged = uSecNow; - if (DEBUG) { - Log.i(TAG, "Uptime since last unplugged = " + (timeSinceUnplugged / 1000)); - } - - addUserUsage(); - addPhoneUsage(uSecNow); - addScreenUsage(uSecNow); - addWiFiUsage(uSecNow); - addBluetoothUsage(uSecNow); - addIdleUsage(uSecNow); // Not including cellular idle power - // Don't compute radio usage if it's a wifi-only device - if (!com.android.settings.Utils.isWifiOnly(mActivity)) { - addRadioUsage(uSecNow); - } - } - - private BatterySipper addEntry(String label, DrainType drainType, long time, int iconId, - double power) { - if (power > mMaxPower) mMaxPower = power; - mTotalPower += power; - BatterySipper bs = new BatterySipper(mActivity, mRequestQueue, mHandler, - label, drainType, iconId, null, new double[] {power}); - bs.usageTime = time; - bs.iconId = iconId; - mUsageList.add(bs); - return bs; - } - - public List<BatterySipper> getUsageList() { - return mUsageList; - } - - static final int MSG_UPDATE_NAME_ICON = 1; - static final int MSG_REPORT_FULLY_DRAWN = 2; - - public double getMaxPower() { - return mMaxPower; - } - - public double getTotalPower() { - return mTotalPower; - } - - private void load() { - try { - byte[] data = mBatteryInfo.getStatistics(); - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(data, 0, data.length); - parcel.setDataPosition(0); - mStats = com.android.internal.os.BatteryStatsImpl.CREATOR - .createFromParcel(parcel); - mStats.distributeWorkLocked(BatteryStats.STATS_SINCE_CHARGED); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException:", e); - } - } -} diff --git a/src/com/android/settings/fuelgauge/PowerGaugePreference.java b/src/com/android/settings/fuelgauge/PowerGaugePreference.java index c8bfa21..97012e4 100644 --- a/src/com/android/settings/fuelgauge/PowerGaugePreference.java +++ b/src/com/android/settings/fuelgauge/PowerGaugePreference.java @@ -25,31 +25,34 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.Utils; /** * Custom preference for displaying power consumption as a bar and an icon on * the left for the subsystem/app type. */ public class PowerGaugePreference extends Preference { - private BatterySipper mInfo; + private BatteryEntry mInfo; private int mProgress; private CharSequence mProgressText; + private final CharSequence mContentDescription; - public PowerGaugePreference(Context context, Drawable icon, BatterySipper info) { + public PowerGaugePreference(Context context, Drawable icon, CharSequence contentDescription, + BatteryEntry info) { super(context); - setLayoutResource(R.layout.app_percentage_item); + setLayoutResource(R.layout.preference_app_percentage); setIcon(icon != null ? icon : new ColorDrawable(0)); mInfo = info; + mContentDescription = contentDescription; } public void setPercent(double percentOfMax, double percentOfTotal) { mProgress = (int) Math.ceil(percentOfMax); - mProgressText = getContext().getResources().getString( - R.string.percentage, (int) Math.ceil(percentOfTotal)); + mProgressText = Utils.formatPercentage((int) (percentOfTotal + 0.5)); notifyChanged(); } - BatterySipper getInfo() { + BatteryEntry getInfo() { return mInfo; } @@ -62,5 +65,10 @@ public class PowerGaugePreference extends Preference { final TextView text1 = (TextView) view.findViewById(android.R.id.text1); text1.setText(mProgressText); + + if (mContentDescription != null) { + final TextView titleView = (TextView) view.findViewById(android.R.id.title); + titleView.setContentDescription(mContentDescription); + } } } diff --git a/src/com/android/settings/fuelgauge/PowerUsageDetail.java b/src/com/android/settings/fuelgauge/PowerUsageDetail.java index 45e4516..9dca029 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/PowerUsageDetail.java @@ -34,13 +34,11 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.BatteryStats; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; -import android.preference.PreferenceActivity; -import android.provider.Settings; import android.text.TextUtils; -import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -49,26 +47,24 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.internal.util.FastPrintWriter; import com.android.settings.DisplaySettings; import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import com.android.settings.WirelessSettings; import com.android.settings.applications.InstalledAppDetails; import com.android.settings.bluetooth.BluetoothSettings; import com.android.settings.location.LocationSettings; import com.android.settings.wifi.WifiSettings; -public class PowerUsageDetail extends Fragment implements Button.OnClickListener { +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; - enum DrainType { - IDLE, - CELL, - PHONE, - WIFI, - BLUETOOTH, - SCREEN, - APP, - USER - } +public class PowerUsageDetail extends Fragment implements Button.OnClickListener { // Note: Must match the sequence of the DrainType private static int[] sDrainTypeDesciptions = new int[] { @@ -77,11 +73,202 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener R.string.battery_desc_voice, R.string.battery_desc_wifi, R.string.battery_desc_bluetooth, + R.string.battery_desc_flashlight, R.string.battery_desc_display, R.string.battery_desc_apps, R.string.battery_desc_users, + R.string.battery_desc_unaccounted, + R.string.battery_desc_overcounted, }; + public static void startBatteryDetailPage( + SettingsActivity caller, BatteryStatsHelper helper, int statsType, BatteryEntry entry, + boolean showLocationButton) { + // Initialize mStats if necessary. + helper.getStats(); + + final int dischargeAmount = helper.getStats().getDischargeAmount(statsType); + Bundle args = new Bundle(); + args.putString(PowerUsageDetail.EXTRA_TITLE, entry.name); + args.putInt(PowerUsageDetail.EXTRA_PERCENT, (int) + ((entry.sipper.value * dischargeAmount / helper.getTotalPower()) + .5)); + args.putInt(PowerUsageDetail.EXTRA_GAUGE, (int) + Math.ceil(entry.sipper.value * 100 / helper.getMaxPower())); + args.putLong(PowerUsageDetail.EXTRA_USAGE_DURATION, helper.getStatsPeriod()); + args.putString(PowerUsageDetail.EXTRA_ICON_PACKAGE, entry.defaultPackageName); + args.putInt(PowerUsageDetail.EXTRA_ICON_ID, entry.iconId); + args.putDouble(PowerUsageDetail.EXTRA_NO_COVERAGE, entry.sipper.noCoveragePercent); + if (entry.sipper.uidObj != null) { + args.putInt(PowerUsageDetail.EXTRA_UID, entry.sipper.uidObj.getUid()); + } + args.putSerializable(PowerUsageDetail.EXTRA_DRAIN_TYPE, entry.sipper.drainType); + args.putBoolean(PowerUsageDetail.EXTRA_SHOW_LOCATION_BUTTON, showLocationButton); + + int userId = UserHandle.myUserId(); + int[] types; + double[] values; + switch (entry.sipper.drainType) { + case APP: + case USER: + { + BatteryStats.Uid uid = entry.sipper.uidObj; + types = new int[] { + R.string.usage_type_cpu, + R.string.usage_type_cpu_foreground, + R.string.usage_type_wake_lock, + R.string.usage_type_gps, + R.string.usage_type_wifi_running, + R.string.usage_type_data_recv, + R.string.usage_type_data_send, + R.string.usage_type_radio_active, + R.string.usage_type_data_wifi_recv, + R.string.usage_type_data_wifi_send, + R.string.usage_type_audio, + R.string.usage_type_video, + }; + values = new double[] { + entry.sipper.cpuTime, + entry.sipper.cpuFgTime, + entry.sipper.wakeLockTime, + entry.sipper.gpsTime, + entry.sipper.wifiRunningTime, + entry.sipper.mobileRxPackets, + entry.sipper.mobileTxPackets, + entry.sipper.mobileActive, + entry.sipper.wifiRxPackets, + entry.sipper.wifiTxPackets, + 0, + 0 + }; + + if (entry.sipper.drainType == BatterySipper.DrainType.APP) { + Writer result = new StringWriter(); + PrintWriter printWriter = new FastPrintWriter(result, false, 1024); + helper.getStats().dumpLocked(caller, printWriter, "", helper.getStatsType(), + uid.getUid()); + printWriter.flush(); + args.putString(PowerUsageDetail.EXTRA_REPORT_DETAILS, result.toString()); + + result = new StringWriter(); + printWriter = new FastPrintWriter(result, false, 1024); + helper.getStats().dumpCheckinLocked(caller, printWriter, helper.getStatsType(), + uid.getUid()); + printWriter.flush(); + args.putString(PowerUsageDetail.EXTRA_REPORT_CHECKIN_DETAILS, + result.toString()); + userId = UserHandle.getUserId(uid.getUid()); + } + } + break; + case CELL: + { + types = new int[] { + R.string.usage_type_on_time, + R.string.usage_type_no_coverage, + R.string.usage_type_radio_active, + }; + values = new double[] { + entry.sipper.usageTime, + entry.sipper.noCoveragePercent, + entry.sipper.mobileActive + }; + } + break; + case WIFI: + { + types = new int[] { + R.string.usage_type_wifi_running, + R.string.usage_type_cpu, + R.string.usage_type_cpu_foreground, + R.string.usage_type_wake_lock, + R.string.usage_type_data_recv, + R.string.usage_type_data_send, + R.string.usage_type_data_wifi_recv, + R.string.usage_type_data_wifi_send, + }; + values = new double[] { + entry.sipper.usageTime, + entry.sipper.cpuTime, + entry.sipper.cpuFgTime, + entry.sipper.wakeLockTime, + entry.sipper.mobileRxPackets, + entry.sipper.mobileTxPackets, + entry.sipper.wifiRxPackets, + entry.sipper.wifiTxPackets, + }; + } break; + case BLUETOOTH: + { + types = new int[] { + R.string.usage_type_on_time, + R.string.usage_type_cpu, + R.string.usage_type_cpu_foreground, + R.string.usage_type_wake_lock, + R.string.usage_type_data_recv, + R.string.usage_type_data_send, + R.string.usage_type_data_wifi_recv, + R.string.usage_type_data_wifi_send, + }; + values = new double[] { + entry.sipper.usageTime, + entry.sipper.cpuTime, + entry.sipper.cpuFgTime, + entry.sipper.wakeLockTime, + entry.sipper.mobileRxPackets, + entry.sipper.mobileTxPackets, + entry.sipper.wifiRxPackets, + entry.sipper.wifiTxPackets, + }; + } break; + case UNACCOUNTED: + { + types = new int[] { + R.string.usage_type_total_battery_capacity, + R.string.usage_type_computed_power, + R.string.usage_type_actual_power, + }; + values = new double[] { + helper.getPowerProfile().getBatteryCapacity(), + helper.getComputedPower(), + helper.getMinDrainedPower(), + }; + } break; + case OVERCOUNTED: + { + types = new int[] { + R.string.usage_type_total_battery_capacity, + R.string.usage_type_computed_power, + R.string.usage_type_actual_power, + }; + values = new double[] { + helper.getPowerProfile().getBatteryCapacity(), + helper.getComputedPower(), + helper.getMaxDrainedPower(), + }; + } break; + default: + { + types = new int[] { + R.string.usage_type_on_time + }; + values = new double[] { + entry.sipper.usageTime + }; + } + } + args.putIntArray(PowerUsageDetail.EXTRA_DETAIL_TYPES, types); + args.putDoubleArray(PowerUsageDetail.EXTRA_DETAIL_VALUES, values); + + // This is a workaround, see b/17523189 + if (userId == UserHandle.myUserId()) { + caller.startPreferencePanel(PowerUsageDetail.class.getName(), args, + R.string.details_title, null, null, 0); + } else { + caller.startPreferencePanelAsUser(PowerUsageDetail.class.getName(), args, + R.string.details_title, null, new UserHandle(userId)); + } + } + public static final int ACTION_DISPLAY_SETTINGS = 1; public static final int ACTION_WIFI_SETTINGS = 2; public static final int ACTION_BLUETOOTH_SETTINGS = 3; @@ -124,8 +311,9 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener private Button mReportButton; private ViewGroup mDetailsParent; private ViewGroup mControlsParent; + private ViewGroup mMessagesParent; private long mStartTime; - private DrainType mDrainType; + private BatterySipper.DrainType mDrainType; private Drawable mAppIcon; private double mNoCoverage; // Percentage of time that there was no coverage @@ -175,7 +363,7 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener final int gaugeValue = args.getInt(EXTRA_GAUGE, 1); mUsageSince = args.getInt(EXTRA_USAGE_SINCE, USAGE_SINCE_UNPLUGGED); mUid = args.getInt(EXTRA_UID, 0); - mDrainType = (DrainType) args.getSerializable(EXTRA_DRAIN_TYPE); + mDrainType = (BatterySipper.DrainType) args.getSerializable(EXTRA_DRAIN_TYPE); mNoCoverage = args.getDouble(EXTRA_NO_COVERAGE, 0); String iconPackage = args.getString(EXTRA_ICON_PACKAGE); int iconId = args.getInt(EXTRA_ICON_ID, 0); @@ -209,7 +397,7 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener mTitleView.setText(mTitle); final TextView text1 = (TextView)mRootView.findViewById(android.R.id.text1); - text1.setText(getString(R.string.percentage, percentage)); + text1.setText(Utils.formatPercentage(percentage)); mTwoButtonsPanel = (ViewGroup)mRootView.findViewById(R.id.two_buttons_panel); mForceStopButton = (Button)mRootView.findViewById(R.id.left_button); @@ -224,10 +412,12 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener mDetailsParent = (ViewGroup)mRootView.findViewById(R.id.details); mControlsParent = (ViewGroup)mRootView.findViewById(R.id.controls); + mMessagesParent = (ViewGroup)mRootView.findViewById(R.id.messages); fillDetailsSection(); fillPackagesSection(mUid); fillControlsSection(mUid); + fillMessagesSection(mUid); if (mUid >= Process.FIRST_APPLICATION_UID) { mForceStopButton.setText(R.string.force_stop); @@ -238,8 +428,8 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener mReportButton.setOnClickListener(this); // check if error reporting is enabled in secure settings - int enabled = Settings.Global.getInt(getActivity().getContentResolver(), - Settings.Global.SEND_ACTION_APP_ERROR, 0); + int enabled = android.provider.Settings.Global.getInt(getActivity().getContentResolver(), + android.provider.Settings.Global.SEND_ACTION_APP_ERROR, 0); if (enabled != 0) { if (mPackages != null && mPackages.length > 0) { try { @@ -269,35 +459,35 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener Bundle args = new Bundle(); args.putString(InstalledAppDetails.ARG_PACKAGE_NAME, mPackages[0]); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.startPreferencePanel(InstalledAppDetails.class.getName(), args, + SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(InstalledAppDetails.class.getName(), args, R.string.application_info_label, null, null, 0); } private void doAction(int action) { - PreferenceActivity pa = (PreferenceActivity)getActivity(); + SettingsActivity sa = (SettingsActivity)getActivity(); switch (action) { case ACTION_DISPLAY_SETTINGS: - pa.startPreferencePanel(DisplaySettings.class.getName(), null, + sa.startPreferencePanel(DisplaySettings.class.getName(), null, R.string.display_settings_title, null, null, 0); break; case ACTION_WIFI_SETTINGS: - pa.startPreferencePanel(WifiSettings.class.getName(), null, + sa.startPreferencePanel(WifiSettings.class.getName(), null, R.string.wifi_settings, null, null, 0); break; case ACTION_BLUETOOTH_SETTINGS: - pa.startPreferencePanel(BluetoothSettings.class.getName(), null, + sa.startPreferencePanel(BluetoothSettings.class.getName(), null, R.string.bluetooth_settings, null, null, 0); break; case ACTION_WIRELESS_SETTINGS: - pa.startPreferencePanel(WirelessSettings.class.getName(), null, + sa.startPreferencePanel(WirelessSettings.class.getName(), null, R.string.radio_controls_title, null, null, 0); break; case ACTION_APP_DETAILS: startApplicationDetailsActivity(); break; case ACTION_LOCATION_SETTINGS: - pa.startPreferencePanel(LocationSettings.class.getName(), null, + sa.startPreferencePanel(LocationSettings.class.getName(), null, R.string.location_settings_title, null, null, 0); break; case ACTION_FORCE_STOP: @@ -322,12 +512,17 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener case R.string.usage_type_data_send: case R.string.usage_type_data_wifi_recv: case R.string.usage_type_data_wifi_send: - final long bytes = (long) (mValues[i]); - value = Formatter.formatFileSize(getActivity(), bytes); + final long packets = (long) (mValues[i]); + value = Long.toString(packets); break; case R.string.usage_type_no_coverage: final int percentage = (int) Math.floor(mValues[i]); - value = getActivity().getString(R.string.percentage, percentage); + value = Utils.formatPercentage(percentage); + break; + case R.string.usage_type_total_battery_capacity: + case R.string.usage_type_computed_power: + case R.string.usage_type_actual_power: + value = getActivity().getString(R.string.mah, (long)(mValues[i])); break; case R.string.usage_type_gps: mUsesGps = true; @@ -419,6 +614,28 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener actionButton.setTag(new Integer(action)); } + private void fillMessagesSection(int uid) { + boolean removeHeader = true; + switch (mDrainType) { + case UNACCOUNTED: + addMessage(R.string.battery_msg_unaccounted); + removeHeader = false; + break; + } + if (removeHeader) { + mMessagesParent.setVisibility(View.GONE); + } + } + + private void addMessage(int message) { + final Resources res = getResources(); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View item = inflater.inflate(R.layout.power_usage_message_item, null); + mMessagesParent.addView(item); + TextView messageView = (TextView) item.findViewById(R.id.message); + messageView.setText(res.getText(message)); + } + private void removePackagesSection() { View view; if ((view = mRootView.findViewById(R.id.packages_section_title)) != null) { @@ -433,8 +650,9 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener if (mPackages == null) return; ActivityManager am = (ActivityManager)getActivity().getSystemService( Context.ACTIVITY_SERVICE); + final int userId = UserHandle.getUserId(mUid); for (int i = 0; i < mPackages.length; i++) { - am.forceStopPackage(mPackages[i]); + am.forceStopPackageAsUser(mPackages[i], userId); } checkForceStop(); } @@ -531,8 +749,7 @@ public class PowerUsageDetail extends Fragment implements Button.OnClickListener //if (ai.icon != 0) { // icon = ai.loadIcon(pm); //} - ViewGroup item = (ViewGroup) inflater.inflate(R.layout.power_usage_package_item, - null); + View item = inflater.inflate(R.layout.power_usage_package_item, null); packagesParent.addView(item); TextView labelView = (TextView) item.findViewById(R.id.label); labelView.setText(mPackages[i]); diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index dc86b8d..5ee4fa0 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -21,13 +21,15 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.drawable.Drawable; import android.os.BatteryStats; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.os.Parcel; +import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; @@ -36,9 +38,12 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.PowerProfile; import com.android.settings.HelpUtils; import com.android.settings.R; +import com.android.settings.SettingsActivity; import java.util.List; @@ -50,22 +55,30 @@ public class PowerUsageSummary extends PreferenceFragment { private static final boolean DEBUG = false; - private static final String TAG = "PowerUsageSummary"; + 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 String BATTERY_HISTORY_FILE = "tmp_bat_history.bin"; private static final int MENU_STATS_TYPE = Menu.FIRST; private static final int MENU_STATS_REFRESH = Menu.FIRST + 1; - private static final int MENU_HELP = Menu.FIRST + 2; + private static final int MENU_BATTERY_SAVER = Menu.FIRST + 2; + private static final int MENU_HELP = Menu.FIRST + 3; + + private UserManager mUm; + private BatteryHistoryPreference mHistPref; private PreferenceGroup mAppListGroup; - private Preference mBatteryStatusPref; + private String mBatteryLevel; + private String mBatteryStatus; private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; - private static final int MIN_POWER_THRESHOLD = 5; + private static final int MIN_POWER_THRESHOLD_MILLI_AMP = 5; private static final int MAX_ITEMS_TO_LIST = 10; + private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; + private static final int SECONDS_IN_HOUR = 60 * 60; private BatteryStatsHelper mStatsHelper; @@ -74,15 +87,11 @@ public class PowerUsageSummary extends PreferenceFragment { @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); - mStatsHelper.clearStats(); - refreshStats(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action) + && updateBatteryStatus(intent)) { + if (!mHandler.hasMessages(MSG_REFRESH_STATS)) { + mHandler.sendEmptyMessageDelayed(MSG_REFRESH_STATS, 500); + } } } }; @@ -90,7 +99,8 @@ public class PowerUsageSummary extends PreferenceFragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); - mStatsHelper = new BatteryStatsHelper(activity, mHandler); + mUm = (UserManager) activity.getSystemService(Context.USER_SERVICE); + mStatsHelper = new BatteryStatsHelper(activity, true); } @Override @@ -100,42 +110,61 @@ public class PowerUsageSummary extends PreferenceFragment { addPreferencesFromResource(R.xml.power_usage_summary); mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST); - mBatteryStatusPref = mAppListGroup.findPreference(KEY_BATTERY_STATUS); setHasOptionsMenu(true); } @Override + public void onStart() { + super.onStart(); + mStatsHelper.clearStats(); + } + + @Override public void onResume() { super.onResume(); - getActivity().registerReceiver(mBatteryInfoReceiver, - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + BatteryStatsHelper.dropFile(getActivity(), BATTERY_HISTORY_FILE); + updateBatteryStatus(getActivity().registerReceiver(mBatteryInfoReceiver, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED))); + if (mHandler.hasMessages(MSG_REFRESH_STATS)) { + mHandler.removeMessages(MSG_REFRESH_STATS); + mStatsHelper.clearStats(); + } refreshStats(); } @Override public void onPause() { - mStatsHelper.pause(); - mHandler.removeMessages(BatteryStatsHelper.MSG_UPDATE_NAME_ICON); + BatteryEntry.stopRequestQueue(); + mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); getActivity().unregisterReceiver(mBatteryInfoReceiver); super.onPause(); } @Override + public void onStop() { + super.onStop(); + mHandler.removeMessages(MSG_REFRESH_STATS); + } + + @Override public void onDestroy() { super.onDestroy(); - mStatsHelper.destroy(); + if (getActivity().isChangingConfigurations()) { + mStatsHelper.storeState(); + BatteryEntry.clearUidCache(); + } } @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { if (preference instanceof BatteryHistoryPreference) { - Parcel hist = Parcel.obtain(); - mStatsHelper.getStats().writeToParcelWithoutUids(hist, 0); - byte[] histData = hist.marshall(); + mStatsHelper.storeStatsHistoryInFile(BATTERY_HISTORY_FILE); Bundle args = new Bundle(); - args.putByteArray(BatteryHistoryDetail.EXTRA_STATS, histData); - PreferenceActivity pa = (PreferenceActivity)getActivity(); - pa.startPreferencePanel(BatteryHistoryDetail.class.getName(), args, + args.putString(BatteryHistoryDetail.EXTRA_STATS, BATTERY_HISTORY_FILE); + args.putParcelable(BatteryHistoryDetail.EXTRA_BROADCAST, + mStatsHelper.getBatteryBroadcast()); + SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(BatteryHistoryDetail.class.getName(), args, R.string.history_details_title, null, null, 0); return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -143,8 +172,9 @@ public class PowerUsageSummary extends PreferenceFragment { return false; } PowerGaugePreference pgp = (PowerGaugePreference) preference; - BatterySipper sipper = pgp.getInfo(); - mStatsHelper.startBatteryDetailPage((PreferenceActivity) getActivity(), sipper, true); + BatteryEntry entry = pgp.getInfo(); + PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), mStatsHelper, + mStatsType, entry, true); return super.onPreferenceTreeClick(preferenceScreen, preference); } @@ -156,11 +186,14 @@ public class PowerUsageSummary extends PreferenceFragment { .setAlphabeticShortcut('t'); } MenuItem refresh = menu.add(0, MENU_STATS_REFRESH, 0, R.string.menu_stats_refresh) - .setIcon(R.drawable.ic_menu_refresh_holo_dark) + .setIcon(com.android.internal.R.drawable.ic_menu_refresh) .setAlphabeticShortcut('r'); refresh.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + MenuItem batterySaver = menu.add(0, MENU_BATTERY_SAVER, 0, R.string.battery_saver); + batterySaver.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + String helpUrl; if (!TextUtils.isEmpty(helpUrl = getResources().getString(R.string.help_url_battery))) { final MenuItem help = menu.add(0, MENU_HELP, 0, R.string.help_label); @@ -182,6 +215,12 @@ public class PowerUsageSummary extends PreferenceFragment { case MENU_STATS_REFRESH: mStatsHelper.clearStats(); refreshStats(); + mHandler.removeMessages(MSG_REFRESH_STATS); + return true; + case MENU_BATTERY_SAVER: + final SettingsActivity sa = (SettingsActivity) getActivity(); + sa.startPreferencePanel(BatterySaverSettings.class.getName(), null, + R.string.battery_saver, null, null, 0); return true; default: return false; @@ -191,69 +230,140 @@ public class PowerUsageSummary extends PreferenceFragment { private void addNotAvailableMessage() { Preference notAvailable = new Preference(getActivity()); notAvailable.setTitle(R.string.power_usage_not_available); + mHistPref.setHideLabels(true); mAppListGroup.addPreference(notAvailable); } + private boolean updateBatteryStatus(Intent intent) { + if (intent != null) { + String batteryLevel = com.android.settings.Utils.getBatteryPercentage(intent); + String batteryStatus = com.android.settings.Utils.getBatteryStatus(getResources(), + intent); + if (!batteryLevel.equals(mBatteryLevel) || !batteryStatus.equals(mBatteryStatus)) { + mBatteryLevel = batteryLevel; + mBatteryStatus = batteryStatus; + return true; + } + } + return false; + } + private void refreshStats() { mAppListGroup.removeAll(); mAppListGroup.setOrderingAsAdded(false); + mHistPref = new BatteryHistoryPreference(getActivity(), mStatsHelper.getStats(), + mStatsHelper.getBatteryBroadcast()); + mHistPref.setOrder(-1); + mAppListGroup.addPreference(mHistPref); + boolean addedSome = false; - mBatteryStatusPref.setOrder(-2); - mAppListGroup.addPreference(mBatteryStatusPref); - BatteryHistoryPreference hist = new BatteryHistoryPreference( - getActivity(), mStatsHelper.getStats()); - hist.setOrder(-1); - mAppListGroup.addPreference(hist); + final PowerProfile powerProfile = mStatsHelper.getPowerProfile(); + final BatteryStats stats = mStatsHelper.getStats(); + final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); + if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP) { + final List<UserHandle> profiles = mUm.getUserProfiles(); - if (mStatsHelper.getPowerProfile().getAveragePower( - PowerProfile.POWER_SCREEN_FULL) < 10) { - addNotAvailableMessage(); - return; - } - mStatsHelper.refreshStats(false); - List<BatterySipper> usageList = mStatsHelper.getUsageList(); - for (BatterySipper sipper : usageList) { - if (sipper.getSortValue() < MIN_POWER_THRESHOLD) continue; - final double percentOfTotal = - ((sipper.getSortValue() / mStatsHelper.getTotalPower()) * 100); - if (percentOfTotal < 1) continue; - PowerGaugePreference pref = - new PowerGaugePreference(getActivity(), sipper.getIcon(), sipper); - final double percentOfMax = - (sipper.getSortValue() * 100) / mStatsHelper.getMaxPower(); - sipper.percent = percentOfTotal; - pref.setTitle(sipper.name); - pref.setOrder(Integer.MAX_VALUE - (int) sipper.getSortValue()); // Invert the order - pref.setPercent(percentOfMax, percentOfTotal); - if (sipper.uidObj != null) { - pref.setKey(Integer.toString(sipper.uidObj.getUid())); + mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, profiles); + + final List<BatterySipper> usageList = mStatsHelper.getUsageList(); + + final int dischargeAmount = stats != null ? stats.getDischargeAmount(mStatsType) : 0; + final int numSippers = usageList.size(); + for (int i = 0; i < numSippers; i++) { + final BatterySipper sipper = usageList.get(i); + if ((sipper.value * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) { + continue; + } + final double percentOfTotal = + ((sipper.value / mStatsHelper.getTotalPower()) * dischargeAmount); + if (((int) (percentOfTotal + .5)) < 1) { + continue; + } + if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) { + // Don't show over-counted unless it is at least 2/3 the size of + // the largest real entry, and its percent of total is more significant + if (sipper.value < ((mStatsHelper.getMaxRealPower()*2)/3)) { + continue; + } + if (percentOfTotal < 10) { + continue; + } + if ("user".equals(Build.TYPE)) { + continue; + } + } + if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) { + // Don't show over-counted unless it is at least 1/2 the size of + // the largest real entry, and its percent of total is more significant + if (sipper.value < (mStatsHelper.getMaxRealPower()/2)) { + continue; + } + if (percentOfTotal < 5) { + continue; + } + if ("user".equals(Build.TYPE)) { + continue; + } + } + final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid())); + final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper); + final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(), + userHandle); + final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(), + userHandle); + final PowerGaugePreference pref = new PowerGaugePreference(getActivity(), + badgedIcon, contentDescription, entry); + + final double percentOfMax = (sipper.value * 100) / mStatsHelper.getMaxPower(); + sipper.percent = percentOfTotal; + pref.setTitle(entry.getLabel()); + pref.setOrder(i + 1); + pref.setPercent(percentOfMax, percentOfTotal); + if (sipper.uidObj != null) { + pref.setKey(Integer.toString(sipper.uidObj.getUid())); + } + addedSome = true; + mAppListGroup.addPreference(pref); + if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) { + break; + } } - mAppListGroup.addPreference(pref); - if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST+1)) break; } + if (!addedSome) { + addNotAvailableMessage(); + } + + BatteryEntry.startRequestQueue(); } + static final int MSG_REFRESH_STATS = 100; + Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { - case BatteryStatsHelper.MSG_UPDATE_NAME_ICON: - BatterySipper bs = (BatterySipper) msg.obj; + case BatteryEntry.MSG_UPDATE_NAME_ICON: + BatteryEntry entry = (BatteryEntry) msg.obj; PowerGaugePreference pgp = (PowerGaugePreference) findPreference( - Integer.toString(bs.uidObj.getUid())); + Integer.toString(entry.sipper.uidObj.getUid())); if (pgp != null) { - pgp.setIcon(bs.icon); - pgp.setTitle(bs.name); + final int userId = UserHandle.getUserId(entry.sipper.getUid()); + final UserHandle userHandle = new UserHandle(userId); + pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle)); + pgp.setTitle(entry.name); } break; - case BatteryStatsHelper.MSG_REPORT_FULLY_DRAWN: + case BatteryEntry.MSG_REPORT_FULLY_DRAWN: Activity activity = getActivity(); if (activity != null) { activity.reportFullyDrawn(); } break; + case MSG_REFRESH_STATS: + mStatsHelper.clearStats(); + refreshStats(); } super.handleMessage(msg); } diff --git a/src/com/android/settings/fuelgauge/Utils.java b/src/com/android/settings/fuelgauge/Utils.java deleted file mode 100644 index 9a06c9f..0000000 --- a/src/com/android/settings/fuelgauge/Utils.java +++ /dev/null @@ -1,84 +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.fuelgauge; - -import android.content.Context; - -import com.android.settings.R; - -/** - * Contains utility functions for formatting elapsed time and consumed bytes - */ -public class Utils { - private static final int SECONDS_PER_MINUTE = 60; - private static final int SECONDS_PER_HOUR = 60 * 60; - private static final int SECONDS_PER_DAY = 24 * 60 * 60; - - /** - * Returns elapsed time for the given millis, in the following format: - * 2d 5h 40m 29s - * @param context the application context - * @param millis the elapsed time in milli seconds - * @return the formatted elapsed time - */ - public static String formatElapsedTime(Context context, double millis, boolean inclSeconds) { - StringBuilder sb = new StringBuilder(); - int seconds = (int) Math.floor(millis / 1000); - if (!inclSeconds) { - // Round up. - seconds += 30; - } - - int days = 0, hours = 0, minutes = 0; - if (seconds >= SECONDS_PER_DAY) { - days = seconds / SECONDS_PER_DAY; - seconds -= days * SECONDS_PER_DAY; - } - if (seconds >= SECONDS_PER_HOUR) { - hours = seconds / SECONDS_PER_HOUR; - seconds -= hours * SECONDS_PER_HOUR; - } - if (seconds >= SECONDS_PER_MINUTE) { - minutes = seconds / SECONDS_PER_MINUTE; - seconds -= minutes * SECONDS_PER_MINUTE; - } - if (inclSeconds) { - if (days > 0) { - sb.append(context.getString(R.string.battery_history_days, - days, hours, minutes, seconds)); - } else if (hours > 0) { - sb.append(context.getString(R.string.battery_history_hours, - hours, minutes, seconds)); - } else if (minutes > 0) { - sb.append(context.getString(R.string.battery_history_minutes, minutes, seconds)); - } else { - sb.append(context.getString(R.string.battery_history_seconds, seconds)); - } - } else { - if (days > 0) { - sb.append(context.getString(R.string.battery_history_days_no_seconds, - days, hours, minutes)); - } else if (hours > 0) { - sb.append(context.getString(R.string.battery_history_hours_no_seconds, - hours, minutes)); - } else { - sb.append(context.getString(R.string.battery_history_minutes_no_seconds, minutes)); - } - } - return sb.toString(); - } -} diff --git a/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java b/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java deleted file mode 100644 index f440bc8..0000000 --- a/src/com/android/settings/inputmethod/CheckBoxAndSettingsPreference.java +++ /dev/null @@ -1,117 +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.inputmethod; - -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; - -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 SettingsPreferenceFragment mFragment; - private TextView mTitleText; - private TextView mSummaryText; - private ImageView mSettingsButton; - 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); - View textLayout = view.findViewById(R.id.inputmethod_pref); - textLayout.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View arg0) { - onCheckBoxClicked(); - } - }); - - mSettingsButton = (ImageView) view.findViewById(R.id.inputmethod_settings); - mTitleText = (TextView)view.findViewById(android.R.id.title); - mSummaryText = (TextView)view.findViewById(android.R.id.summary); - mSettingsButton.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View clickedView) { - onSettingsButtonClicked(); - } - }); - 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() { - if (isChecked()) { - setChecked(false); - } else { - setChecked(true); - } - } - - protected void onSettingsButtonClicked() { - if (mFragment != null && mSettingsIntent != null) { - mFragment.startActivity(mSettingsIntent); - } - } - - private void enableSettingsButton() { - if (mSettingsButton != null) { - if (mSettingsIntent == null) { - mSettingsButton.setVisibility(View.GONE); - } else { - final boolean checked = isChecked(); - mSettingsButton.setEnabled(checked); - mSettingsButton.setClickable(checked); - mSettingsButton.setFocusable(checked); - if (!checked) { - mSettingsButton.setAlpha(Utils.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 dbfa1bc..bae9dbc 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java +++ b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java @@ -16,20 +16,17 @@ package com.android.settings.inputmethod; -import com.android.settings.R; -import com.android.settings.Settings.KeyboardLayoutPickerActivity; -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.app.Fragment; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; @@ -38,74 +35,78 @@ import android.hardware.input.InputManager; import android.hardware.input.KeyboardLayout; import android.os.Bundle; import android.os.Handler; +import android.os.UserHandle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceCategory; +import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.provider.Settings; import android.provider.Settings.System; +import android.speech.RecognitionService; +import android.speech.tts.TtsEngines; import android.text.TextUtils; import android.view.InputDevice; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.BaseAdapter; +import android.view.inputmethod.InputMethodSubtype; +import android.view.textservice.SpellCheckerInfo; +import android.view.textservice.TextServicesManager; + +import com.android.internal.app.LocalePicker; +import com.android.settings.R; +import com.android.settings.Settings.KeyboardLayoutPickerActivity; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.SubSettings; +import com.android.settings.UserDictionarySettings; +import com.android.settings.Utils; +import com.android.settings.VoiceInputOutputSettings; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import java.text.Collator; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.TreeSet; public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener, InputManager.InputDeviceListener, - KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener { - + KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener, Indexable, + InputMethodPreference.OnSavePreferenceListener { + private static final String KEY_SPELL_CHECKERS = "spellcheckers_settings"; 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"; + private static final String KEY_PREVIOUSLY_ENABLED_SUBTYPES = "previously_enabled_subtypes"; // 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 PreferenceCategory mKeyboardSettingsCategory; private PreferenceCategory mHardKeyboardCategory; private PreferenceCategory mGameControllerCategory; private Preference mLanguagePref; - private final ArrayList<InputMethodPreference> mInputMethodPreferenceList = - new ArrayList<InputMethodPreference>(); - private final ArrayList<PreferenceScreen> mHardKeyboardPreferenceList = - new ArrayList<PreferenceScreen>(); + private final ArrayList<InputMethodPreference> mInputMethodPreferenceList = new ArrayList<>(); + private final ArrayList<PreferenceScreen> mHardKeyboardPreferenceList = new ArrayList<>(); private InputManager mIm; private InputMethodManager mImm; - private boolean mIsOnlyImeSettings; + private boolean mShowsOnlyFullImeAndKeyboardList; private Handler mHandler; private SettingsObserver mSettingsObserver; private Intent mIntentWaitingForResult; private InputMethodSettingValuesWrapper mInputMethodSettingValues; - - private final OnPreferenceChangeListener mOnImePreferenceChangedListener = - new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference arg0, Object arg1) { - InputMethodSettingValuesWrapper.getInstance( - arg0.getContext()).refreshAllInputMethodAndSubtypes(); - ((BaseAdapter)getPreferenceScreen().getRootAdapter()).notifyDataSetChanged(); - updateInputMethodPreferenceViews(); - return true; - } - }; + private DevicePolicyManager mDpm; @Override public void onCreate(Bundle icicle) { @@ -113,13 +114,17 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment addPreferencesFromResource(R.xml.language_settings); + final Activity activity = getActivity(); + mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(activity); + try { mDefaultInputMethodSelectorVisibility = Integer.valueOf( getString(R.string.input_method_selector_visibility_default_value)); } catch (NumberFormatException e) { } - if (getActivity().getAssets().getLocales().length == 1) { + if (activity.getAssets().getLocales().length == 1) { // No "Select language" pref if there's only one system locale available. getPreferenceScreen().removePreference(findPreference(KEY_PHONE_LANGUAGE)); } else { @@ -142,46 +147,50 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment mGameControllerCategory = (PreferenceCategory)findPreference( "game_controller_settings_category"); + final Intent startingIntent = activity.getIntent(); // Filter out irrelevant features if invoked from IME settings button. - mIsOnlyImeSettings = Settings.ACTION_INPUT_METHOD_SETTINGS.equals( - getActivity().getIntent().getAction()); - getActivity().getIntent().setAction(null); - if (mIsOnlyImeSettings) { + mShowsOnlyFullImeAndKeyboardList = Settings.ACTION_INPUT_METHOD_SETTINGS.equals( + startingIntent.getAction()); + if (mShowsOnlyFullImeAndKeyboardList) { getPreferenceScreen().removeAll(); getPreferenceScreen().addPreference(mHardKeyboardCategory); if (SHOW_INPUT_METHOD_SWITCHER_SETTINGS) { getPreferenceScreen().addPreference(mShowInputMethodSelectorPref); } + mKeyboardSettingsCategory.removeAll(); getPreferenceScreen().addPreference(mKeyboardSettingsCategory); } - // Build IME preference category. - mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(getActivity()); - - mKeyboardSettingsCategory.removeAll(); - if (!mIsOnlyImeSettings) { - final PreferenceScreen currentIme = new PreferenceScreen(getActivity(), null); - currentIme.setKey(KEY_CURRENT_INPUT_METHOD); - currentIme.setTitle(getResources().getString(R.string.current_input_method)); - mKeyboardSettingsCategory.addPreference(currentIme); - } - // Build hard keyboard and game controller preference categories. - mIm = (InputManager)getActivity().getSystemService(Context.INPUT_SERVICE); + mIm = (InputManager)activity.getSystemService(Context.INPUT_SERVICE); updateInputDevices(); // Spell Checker - 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); + final Preference spellChecker = findPreference(KEY_SPELL_CHECKERS); + if (spellChecker != null) { + // Note: KEY_SPELL_CHECKERS preference is marked as persistent="false" in XML. + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(spellChecker); + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(activity, SubSettings.class); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, + SpellCheckersSettings.class.getName()); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, + R.string.spellcheckers_settings_title); + spellChecker.setIntent(intent); } mHandler = new Handler(); - mSettingsObserver = new SettingsObserver(mHandler, getActivity()); + mSettingsObserver = new SettingsObserver(mHandler, activity); + mDpm = (DevicePolicyManager) (getActivity(). + getSystemService(Context.DEVICE_POLICY_SERVICE)); + + // If we've launched from the keyboard layout notification, go ahead and just show the + // keyboard layout dialog. + final InputDeviceIdentifier identifier = + startingIntent.getParcelableExtra(Settings.EXTRA_INPUT_DEVICE_IDENTIFIER); + if (mShowsOnlyFullImeAndKeyboardList && identifier != null) { + showKeyboardLayoutDialog(identifier); + } } private void updateInputMethodSelectorSummary(int value) { @@ -226,7 +235,7 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment targetFragment = UserDictionaryList.class; } startFragment(InputMethodAndLanguageSettings.this, - targetFragment.getCanonicalName(), -1, extras); + targetFragment.getCanonicalName(), -1, -1, extras); return true; } }); @@ -240,36 +249,22 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment mSettingsObserver.resume(); mIm.registerInputDeviceListener(this, null); - if (!mIsOnlyImeSettings) { + final Preference spellChecker = findPreference(KEY_SPELL_CHECKERS); + if (spellChecker != null) { + final TextServicesManager tsm = (TextServicesManager) getSystemService( + Context.TEXT_SERVICES_MANAGER_SERVICE); + if (tsm.isSpellCheckerEnabled()) { + final SpellCheckerInfo sci = tsm.getCurrentSpellChecker(); + spellChecker.setSummary(sci.loadLabel(getPackageManager())); + } else { + spellChecker.setSummary(R.string.switch_off_text); + } + } + + if (!mShowsOnlyFullImeAndKeyboardList) { if (mLanguagePref != null) { - Configuration conf = getResources().getConfiguration(); - String language = conf.locale.getLanguage(); - String localeString; - // TODO: This is not an accurate way to display the locale, as it is - // just working around the fact that we support limited dialects - // and want to pretend that the language is valid for all locales. - // We need a way to support languages that aren't tied to a particular - // locale instead of hiding the locale qualifier. - if (language.equals("zz")) { - String country = conf.locale.getCountry(); - if (country.equals("ZZ")) { - localeString = "[Developer] Accented English (zz_ZZ)"; - } else if (country.equals("ZY")) { - localeString = "[Developer] Fake Bi-Directional (zz_ZY)"; - } else { - localeString = ""; - } - } else if (hasOnlyOneLanguageInstance(language, - Resources.getSystem().getAssets().getLocales())) { - localeString = conf.locale.getDisplayLanguage(conf.locale); - } else { - localeString = conf.locale.getDisplayName(conf.locale); - } - if (localeString.length() > 1) { - localeString = Character.toUpperCase(localeString.charAt(0)) - + localeString.substring(1); - mLanguagePref.setSummary(localeString); - } + String localeName = getLocaleName(getActivity()); + mLanguagePref.setSummary(localeName); } updateUserDictionaryPreference(findPreference(KEY_USER_DICTIONARY_SETTINGS)); @@ -278,16 +273,6 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } } - // Hard keyboard - if (!mHardKeyboardPreferenceList.isEmpty()) { - for (int i = 0; i < sHardKeyboardKeys.length; ++i) { - CheckBoxPreference chkPref = (CheckBoxPreference) - mHardKeyboardCategory.findPreference(sHardKeyboardKeys[i]); - chkPref.setChecked( - System.getInt(getContentResolver(), sSystemSettingNames[i], 1) > 0); - } - } - updateInputDevices(); // Refresh internal states in mInputMethodSettingValues to keep the latest @@ -343,15 +328,6 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } } else if (preference instanceof CheckBoxPreference) { final CheckBoxPreference chkPref = (CheckBoxPreference) preference; - if (!mHardKeyboardPreferenceList.isEmpty()) { - 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; - } - } - } if (chkPref == mGameControllerCategory.findPreference("vibrate_input_devices")) { System.putInt(getContentResolver(), Settings.System.VIBRATE_INPUT_DEVICES, chkPref.isChecked() ? 1 : 0); @@ -361,18 +337,19 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment return super.onPreferenceTreeClick(preferenceScreen, preference); } - private boolean hasOnlyOneLanguageInstance(String languageCode, String[] locales) { - int count = 0; - for (String localeCode : locales) { - if (localeCode.length() > 2 - && localeCode.startsWith(languageCode)) { - count++; - if (count > 1) { - return false; - } + private static String getLocaleName(Context context) { + // We want to show the same string that the LocalePicker used. + // TODO: should this method be in LocalePicker instead? + Locale currentLocale = context.getResources().getConfiguration().locale; + List<LocalePicker.LocaleInfo> locales = LocalePicker.getAllAssetLocales(context, true); + for (LocalePicker.LocaleInfo locale : locales) { + if (locale.getLocale().equals(currentLocale)) { + return locale.getLabel(); } } - return count == 1; + // This can't happen as long as the locale was one set by Settings. + // Fall back in case a developer is testing an unsupported locale. + return currentLocale.getDisplayName(currentLocale); } private void saveInputMethodSelectorVisibility(String value) { @@ -406,31 +383,37 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment private void updateInputMethodPreferenceViews() { synchronized (mInputMethodPreferenceList) { // Clear existing "InputMethodPreference"s - for (final InputMethodPreference imp : mInputMethodPreferenceList) { - mKeyboardSettingsCategory.removePreference(imp); + for (final InputMethodPreference pref : mInputMethodPreferenceList) { + mKeyboardSettingsCategory.removePreference(pref); } mInputMethodPreferenceList.clear(); - final List<InputMethodInfo> imis = mInputMethodSettingValues.getInputMethodList(); + List<String> permittedList = mDpm.getPermittedInputMethodsForCurrentUser(); + final Context context = getActivity(); + final List<InputMethodInfo> imis = mShowsOnlyFullImeAndKeyboardList + ? mInputMethodSettingValues.getInputMethodList() + : mImm.getEnabledInputMethodList(); final int N = (imis == null ? 0 : imis.size()); for (int i = 0; i < N; ++i) { final InputMethodInfo imi = imis.get(i); - final InputMethodPreference pref = getInputMethodPreference(imi); - pref.setOnImePreferenceChangeListener(mOnImePreferenceChangedListener); + final boolean isAllowedByOrganization = permittedList == null + || permittedList.contains(imi.getPackageName()); + final InputMethodPreference pref = new InputMethodPreference( + context, imi, mShowsOnlyFullImeAndKeyboardList /* hasSwitch */, + isAllowedByOrganization, this); mInputMethodPreferenceList.add(pref); } - - if (!mInputMethodPreferenceList.isEmpty()) { - Collections.sort(mInputMethodPreferenceList); - for (int i = 0; i < N; ++i) { - mKeyboardSettingsCategory.addPreference(mInputMethodPreferenceList.get(i)); - } - } - - // update views status - for (Preference pref : mInputMethodPreferenceList) { - if (pref instanceof InputMethodPreference) { - ((InputMethodPreference) pref).updatePreferenceViews(); + final Collator collator = Collator.getInstance(); + Collections.sort(mInputMethodPreferenceList, new Comparator<InputMethodPreference>() { + @Override + public int compare(InputMethodPreference lhs, InputMethodPreference rhs) { + return lhs.compareTo(rhs, collator); } + }); + for (int i = 0; i < N; ++i) { + final InputMethodPreference pref = mInputMethodPreferenceList.get(i); + mKeyboardSettingsCategory.addPreference(pref); + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + pref.updatePreferenceViews(); } } updateCurrentImeName(); @@ -443,6 +426,74 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment mInputMethodSettingValues.getInputMethodList(), null); } + @Override + public void onSaveInputMethodPreference(final InputMethodPreference pref) { + final InputMethodInfo imi = pref.getInputMethodInfo(); + if (!pref.isChecked()) { + // An IME is being disabled. Save enabled subtypes of the IME to shared preference to be + // able to re-enable these subtypes when the IME gets re-enabled. + saveEnabledSubtypesOf(imi); + } + final boolean hasHardwareKeyboard = getResources().getConfiguration().keyboard + == Configuration.KEYBOARD_QWERTY; + InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(this, getContentResolver(), + mImm.getInputMethodList(), hasHardwareKeyboard); + // Update input method settings and preference list. + mInputMethodSettingValues.refreshAllInputMethodAndSubtypes(); + if (pref.isChecked()) { + // An IME is being enabled. Load the previously enabled subtypes from shared preference + // and enable these subtypes. + restorePreviouslyEnabledSubtypesOf(imi); + } + for (final InputMethodPreference p : mInputMethodPreferenceList) { + p.updatePreferenceViews(); + } + } + + private void saveEnabledSubtypesOf(final InputMethodInfo imi) { + final HashSet<String> enabledSubtypeIdSet = new HashSet<>(); + final List<InputMethodSubtype> enabledSubtypes = mImm.getEnabledInputMethodSubtypeList( + imi, true /* allowsImplicitlySelectedSubtypes */); + for (final InputMethodSubtype subtype : enabledSubtypes) { + final String subtypeId = Integer.toString(subtype.hashCode()); + enabledSubtypeIdSet.add(subtypeId); + } + final HashMap<String, HashSet<String>> imeToEnabledSubtypeIdsMap = + loadPreviouslyEnabledSubtypeIdsMap(); + final String imiId = imi.getId(); + imeToEnabledSubtypeIdsMap.put(imiId, enabledSubtypeIdSet); + savePreviouslyEnabledSubtypeIdsMap(imeToEnabledSubtypeIdsMap); + } + + private void restorePreviouslyEnabledSubtypesOf(final InputMethodInfo imi) { + final HashMap<String, HashSet<String>> imeToEnabledSubtypeIdsMap = + loadPreviouslyEnabledSubtypeIdsMap(); + final String imiId = imi.getId(); + final HashSet<String> enabledSubtypeIdSet = imeToEnabledSubtypeIdsMap.remove(imiId); + if (enabledSubtypeIdSet == null) { + return; + } + savePreviouslyEnabledSubtypeIdsMap(imeToEnabledSubtypeIdsMap); + InputMethodAndSubtypeUtil.enableInputMethodSubtypesOf( + getContentResolver(), imiId, enabledSubtypeIdSet); + } + + private HashMap<String, HashSet<String>> loadPreviouslyEnabledSubtypeIdsMap() { + final Context context = getActivity(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String imesAndSubtypesString = prefs.getString(KEY_PREVIOUSLY_ENABLED_SUBTYPES, null); + return InputMethodAndSubtypeUtil.parseInputMethodsAndSubtypesString(imesAndSubtypesString); + } + + private void savePreviouslyEnabledSubtypeIdsMap( + final HashMap<String, HashSet<String>> subtypesMap) { + final Context context = getActivity(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String imesAndSubtypesString = InputMethodAndSubtypeUtil + .buildInputMethodsAndSubtypesString(subtypesMap); + prefs.edit().putString(KEY_PREVIOUSLY_ENABLED_SUBTYPES, imesAndSubtypesString).apply(); + } + private void updateCurrentImeName() { final Context context = getActivity(); if (context == null || mImm == null) return; @@ -451,34 +502,13 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment final CharSequence curIme = mInputMethodSettingValues.getCurrentInputMethodName(context); if (!TextUtils.isEmpty(curIme)) { - synchronized(this) { + synchronized (this) { curPref.setSummary(curIme); } } } } - private InputMethodPreference getInputMethodPreference(InputMethodInfo imi) { - 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 - final InputMethodPreference pref = - new InputMethodPreference(this, intent, mImm, imi); - pref.setKey(imi.getId()); - pref.setTitle(label); - return pref; - } - private void updateInputDevices() { updateHardKeyboards(); updateGameControllers(); @@ -486,35 +516,33 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment private void updateHardKeyboards() { mHardKeyboardPreferenceList.clear(); - if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY) { - final int[] devices = InputDevice.getDeviceIds(); - for (int i = 0; i < devices.length; i++) { - InputDevice device = InputDevice.getDevice(devices[i]); - if (device != null - && !device.isVirtual() - && device.isFullKeyboard()) { - final InputDeviceIdentifier identifier = device.getIdentifier(); - final String keyboardLayoutDescriptor = - mIm.getCurrentKeyboardLayoutForInputDevice(identifier); - final KeyboardLayout keyboardLayout = keyboardLayoutDescriptor != null ? - mIm.getKeyboardLayout(keyboardLayoutDescriptor) : null; - - final PreferenceScreen pref = new PreferenceScreen(getActivity(), null); - pref.setTitle(device.getName()); - if (keyboardLayout != null) { - pref.setSummary(keyboardLayout.toString()); - } else { - pref.setSummary(R.string.keyboard_layout_default_label); - } - pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - showKeyboardLayoutDialog(identifier); - return true; - } - }); - mHardKeyboardPreferenceList.add(pref); + final int[] devices = InputDevice.getDeviceIds(); + for (int i = 0; i < devices.length; i++) { + InputDevice device = InputDevice.getDevice(devices[i]); + if (device != null + && !device.isVirtual() + && device.isFullKeyboard()) { + final InputDeviceIdentifier identifier = device.getIdentifier(); + final String keyboardLayoutDescriptor = + mIm.getCurrentKeyboardLayoutForInputDevice(identifier); + final KeyboardLayout keyboardLayout = keyboardLayoutDescriptor != null ? + mIm.getKeyboardLayout(keyboardLayoutDescriptor) : null; + + final PreferenceScreen pref = new PreferenceScreen(getActivity(), null); + pref.setTitle(device.getName()); + if (keyboardLayout != null) { + pref.setSummary(keyboardLayout.toString()); + } else { + pref.setSummary(R.string.keyboard_layout_default_label); } + pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showKeyboardLayoutDialog(identifier); + return true; + } + }); + mHardKeyboardPreferenceList.add(pref); } } @@ -541,8 +569,8 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) { - KeyboardLayoutDialogFragment fragment = - new KeyboardLayoutDialogFragment(inputDeviceIdentifier); + KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment( + inputDeviceIdentifier); fragment.setTargetFragment(this, 0); fragment.show(getActivity().getFragmentManager(), "keyboardLayout"); } @@ -582,7 +610,7 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment } } - private boolean haveInputDeviceWithVibrator() { + private static boolean haveInputDeviceWithVibrator() { final int[] devices = InputDevice.getDeviceIds(); for (int i = 0; i < devices.length; i++) { InputDevice device = InputDevice.getDevice(devices[i]); @@ -617,4 +645,190 @@ public class InputMethodAndLanguageSettings extends SettingsPreferenceFragment mContext.getContentResolver().unregisterContentObserver(this); } } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + List<SearchIndexableRaw> indexables = new ArrayList<>(); + + final String screenTitle = context.getString(R.string.language_keyboard_settings_title); + + // Locale picker. + if (context.getAssets().getLocales().length > 1) { + String localeName = getLocaleName(context); + SearchIndexableRaw indexable = new SearchIndexableRaw(context); + indexable.key = KEY_PHONE_LANGUAGE; + indexable.title = context.getString(R.string.phone_language); + indexable.summaryOn = localeName; + indexable.summaryOff = localeName; + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + // Spell checker. + SearchIndexableRaw indexable = new SearchIndexableRaw(context); + indexable.key = KEY_SPELL_CHECKERS; + indexable.title = context.getString(R.string.spellcheckers_settings_title); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + // User dictionary. + if (UserDictionaryList.getUserDictionaryLocalesSet(context) != null) { + indexable = new SearchIndexableRaw(context); + indexable.key = "user_dict_settings"; + indexable.title = context.getString(R.string.user_dict_settings_title); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + // Keyboard settings. + indexable = new SearchIndexableRaw(context); + indexable.key = "keyboard_settings"; + indexable.title = context.getString(R.string.keyboard_settings_category); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + InputMethodSettingValuesWrapper immValues = InputMethodSettingValuesWrapper + .getInstance(context); + immValues.refreshAllInputMethodAndSubtypes(); + + // Current IME. + String currImeName = immValues.getCurrentInputMethodName(context).toString(); + indexable = new SearchIndexableRaw(context); + indexable.key = KEY_CURRENT_INPUT_METHOD; + indexable.title = context.getString(R.string.current_input_method); + indexable.summaryOn = currImeName; + indexable.summaryOff = currImeName; + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService( + Context.INPUT_METHOD_SERVICE); + + // All other IMEs. + List<InputMethodInfo> inputMethods = immValues.getInputMethodList(); + final int inputMethodCount = (inputMethods == null ? 0 : inputMethods.size()); + for (int i = 0; i < inputMethodCount; ++i) { + InputMethodInfo inputMethod = inputMethods.get(i); + + StringBuilder builder = new StringBuilder(); + List<InputMethodSubtype> subtypes = inputMethodManager + .getEnabledInputMethodSubtypeList(inputMethod, true); + final int subtypeCount = subtypes.size(); + for (int j = 0; j < subtypeCount; j++) { + InputMethodSubtype subtype = subtypes.get(j); + if (builder.length() > 0) { + builder.append(','); + } + CharSequence subtypeLabel = subtype.getDisplayName(context, + inputMethod.getPackageName(), inputMethod.getServiceInfo() + .applicationInfo); + builder.append(subtypeLabel); + } + String summary = builder.toString(); + + ServiceInfo serviceInfo = inputMethod.getServiceInfo(); + ComponentName componentName = new ComponentName(serviceInfo.packageName, + serviceInfo.name); + + indexable = new SearchIndexableRaw(context); + indexable.key = componentName.flattenToString(); + indexable.title = inputMethod.loadLabel(context.getPackageManager()).toString(); + indexable.summaryOn = summary; + indexable.summaryOff = summary; + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + // Hard keyboards + InputManager inputManager = (InputManager) context.getSystemService( + Context.INPUT_SERVICE); + boolean hasHardKeyboards = false; + + final int[] devices = InputDevice.getDeviceIds(); + for (int i = 0; i < devices.length; i++) { + InputDevice device = InputDevice.getDevice(devices[i]); + if (device == null || device.isVirtual() || !device.isFullKeyboard()) { + continue; + } + + hasHardKeyboards = true; + + InputDeviceIdentifier identifier = device.getIdentifier(); + String keyboardLayoutDescriptor = + inputManager.getCurrentKeyboardLayoutForInputDevice(identifier); + KeyboardLayout keyboardLayout = keyboardLayoutDescriptor != null ? + inputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null; + + String summary; + if (keyboardLayout != null) { + summary = keyboardLayout.toString(); + } else { + summary = context.getString(R.string.keyboard_layout_default_label); + } + + indexable = new SearchIndexableRaw(context); + indexable.key = device.getName(); + indexable.title = device.getName(); + indexable.summaryOn = summary; + indexable.summaryOff = summary; + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + if (hasHardKeyboards) { + // Hard keyboard category. + indexable = new SearchIndexableRaw(context); + indexable.key = "builtin_keyboard_settings"; + indexable.title = context.getString( + R.string.builtin_keyboard_settings_title); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + // Voice input + indexable = new SearchIndexableRaw(context); + indexable.key = "voice_input_settings"; + indexable.title = context.getString(R.string.voice_input_settings); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + // Text-to-speech. + TtsEngines ttsEngines = new TtsEngines(context); + if (!ttsEngines.getEngines().isEmpty()) { + indexable = new SearchIndexableRaw(context); + indexable.key = "tts_settings"; + indexable.title = context.getString(R.string.tts_settings_title); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + // Pointer settings. + indexable = new SearchIndexableRaw(context); + indexable.key = "pointer_settings_category"; + indexable.title = context.getString(R.string.pointer_settings_category); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + indexable = new SearchIndexableRaw(context); + indexable.key = "pointer_speed"; + indexable.title = context.getString(R.string.pointer_speed); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + // Game controllers. + if (haveInputDeviceWithVibrator()) { + indexable = new SearchIndexableRaw(context); + indexable.key = "vibrate_input_devices"; + indexable.title = context.getString(R.string.vibrate_input_devices); + indexable.summaryOn = context.getString(R.string.vibrate_input_devices_summary); + indexable.summaryOff = context.getString(R.string.vibrate_input_devices_summary); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + } + + return indexables; + } + }; } diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java index 419a877..146f512 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java @@ -16,91 +16,87 @@ package com.android.settings.inputmethod; -import com.android.internal.inputmethod.InputMethodUtils; -import com.android.settings.R; -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; -import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.preference.TwoStatePreference; import android.text.TextUtils; -import android.util.Log; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; -import java.util.Locale; -public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { - private static final String TAG =InputMethodAndSubtypeEnabler.class.getSimpleName(); - private AlertDialog mDialog = null; +public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment + implements OnPreferenceChangeListener { private boolean mHaveHardKeyboard; - final private HashMap<String, List<Preference>> mInputMethodAndSubtypePrefsMap = - new HashMap<String, List<Preference>>(); - final private HashMap<String, CheckBoxPreference> mSubtypeAutoSelectionCBMap = - new HashMap<String, CheckBoxPreference>(); + private final HashMap<String, List<Preference>> mInputMethodAndSubtypePrefsMap = + new HashMap<>(); + private final HashMap<String, TwoStatePreference> mAutoSelectionPrefsMap = new HashMap<>(); private InputMethodManager mImm; - private List<InputMethodInfo> mInputMethodProperties; - private String mInputMethodId; - private String mTitle; - private String mSystemLocale = ""; - private Collator mCollator = Collator.getInstance(); + // TODO: Change mInputMethodInfoList to Map + private List<InputMethodInfo> mInputMethodInfoList; + private Collator mCollator; @Override - public void onCreate(Bundle icicle) { + public void onCreate(final Bundle icicle) { super.onCreate(icicle); mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); final 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( + final String targetImi = getStringExtraFromIntentOrArguments( android.provider.Settings.EXTRA_INPUT_METHOD_ID); - if (mInputMethodId == null && (arguments != null)) { - final String inputMethodId = - 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; + + mInputMethodInfoList = mImm.getInputMethodList(); + mCollator = Collator.getInstance(); + + final PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity()); + final int imiCount = mInputMethodInfoList.size(); + for (int index = 0; index < imiCount; ++index) { + final InputMethodInfo imi = mInputMethodInfoList.get(index); + // Add subtype preferences of this IME when it is specified or no IME is specified. + if (imi.getId().equals(targetImi) || TextUtils.isEmpty(targetImi)) { + addInputMethodSubtypePreferences(imi, root); } } + setPreferenceScreen(root); + } - final Locale locale = config.locale; - mSystemLocale = locale.toString(); - mCollator = Collator.getInstance(locale); - onCreateIMM(); - setPreferenceScreen(createPreferenceHierarchy()); + private String getStringExtraFromIntentOrArguments(final String name) { + final Intent intent = getActivity().getIntent(); + final String fromIntent = intent.getStringExtra(name); + if (fromIntent != null) { + return fromIntent; + } + final Bundle arguments = getArguments(); + return (arguments == null) ? null : arguments.getString(name); } @Override - public void onActivityCreated(Bundle icicle) { + public void onActivityCreated(final Bundle icicle) { super.onActivityCreated(icicle); - if (!TextUtils.isEmpty(mTitle)) { - getActivity().setTitle(mTitle); + final String title = getStringExtraFromIntentOrArguments(Intent.EXTRA_TITLE); + if (!TextUtils.isEmpty(title)) { + getActivity().setTitle(title); } } @@ -112,323 +108,196 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { InputMethodSettingValuesWrapper .getInstance(getActivity()).refreshAllInputMethodAndSubtypes(); InputMethodAndSubtypeUtil.loadInputMethodSubtypeList( - this, getContentResolver(), mInputMethodProperties, mInputMethodAndSubtypePrefsMap); - updateAutoSelectionCB(); + this, getContentResolver(), mInputMethodInfoList, mInputMethodAndSubtypePrefsMap); + updateAutoSelectionPreferences(); } @Override public void onPause() { super.onPause(); // Clear all subtypes of all IMEs to make sure - clearImplicitlyEnabledSubtypes(null); + updateImplicitlyEnabledSubtypes(null /* targetImiId */, false /* check */); InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(this, getContentResolver(), - mInputMethodProperties, mHaveHardKeyboard); + mInputMethodInfoList, mHaveHardKeyboard); } @Override - public boolean onPreferenceTreeClick( - PreferenceScreen preferenceScreen, Preference preference) { - - if (preference instanceof CheckBoxPreference) { - final CheckBoxPreference chkPref = (CheckBoxPreference) preference; - - for (String imiId: mSubtypeAutoSelectionCBMap.keySet()) { - if (mSubtypeAutoSelectionCBMap.get(imiId) == chkPref) { - // We look for the first preference item in subtype enabler. - // The first item is used for turning on/off subtype auto selection. - // We are in the subtype enabler and trying selecting subtypes automatically. - setSubtypeAutoSelectionEnabled(imiId, chkPref.isChecked()); - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - } - - final String id = chkPref.getKey(); - if (chkPref.isChecked()) { - InputMethodInfo selImi = null; - final int N = mInputMethodProperties.size(); - for (int i = 0; i < N; i++) { - InputMethodInfo imi = mInputMethodProperties.get(i); - if (id.equals(imi.getId())) { - selImi = imi; - if (InputMethodUtils.isSystemIme(imi)) { - InputMethodAndSubtypeUtil.setSubtypesPreferenceEnabled( - this, mInputMethodProperties, id, true); - // This is a built-in IME, so no need to warn. - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - break; - } - } - if (selImi == null) { - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - chkPref.setChecked(false); - if (mDialog == null) { - mDialog = (new AlertDialog.Builder(getActivity())) - .setTitle(android.R.string.dialog_alert_title) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - chkPref.setChecked(true); - InputMethodAndSubtypeUtil.setSubtypesPreferenceEnabled( - InputMethodAndSubtypeEnabler.this, - mInputMethodProperties, id, true); - } - - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - - }) - .create(); - } else { - if (mDialog.isShowing()) { - mDialog.dismiss(); - } - } - mDialog.setMessage(getResources().getString( - R.string.ime_security_warning, - selImi.getServiceInfo().applicationInfo.loadLabel(getPackageManager()))); - mDialog.show(); - } else { - InputMethodAndSubtypeUtil.setSubtypesPreferenceEnabled( - this, mInputMethodProperties, id, false); - updateAutoSelectionCB(); + public boolean onPreferenceChange(final Preference pref, final Object newValue) { + if (!(newValue instanceof Boolean)) { + return true; // Invoke default behavior. + } + final boolean isChecking = (Boolean) newValue; + for (final String imiId : mAutoSelectionPrefsMap.keySet()) { + // An auto select subtype preference is changing. + if (mAutoSelectionPrefsMap.get(imiId) == pref) { + final TwoStatePreference autoSelectionPref = (TwoStatePreference) pref; + autoSelectionPref.setChecked(isChecking); + // Enable or disable subtypes depending on the auto selection preference. + setAutoSelectionSubtypesEnabled(imiId, autoSelectionPref.isChecked()); + return false; } } - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mDialog != null) { - mDialog.dismiss(); - mDialog = null; + // A subtype preference is changing. + if (pref instanceof InputMethodSubtypePreference) { + final InputMethodSubtypePreference subtypePref = (InputMethodSubtypePreference) pref; + subtypePref.setChecked(isChecking); + if (!subtypePref.isChecked()) { + // It takes care of the case where no subtypes are explicitly enabled then the auto + // selection preference is going to be checked. + updateAutoSelectionPreferences(); + } + return false; } + return true; // Invoke default behavior. } - private void onCreateIMM() { - InputMethodManager imm = (InputMethodManager) getSystemService( - Context.INPUT_METHOD_SERVICE); - - // TODO: Change mInputMethodProperties to Map - mInputMethodProperties = imm.getInputMethodList(); - } - - private PreferenceScreen createPreferenceHierarchy() { - // Root - final PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity()); + private void addInputMethodSubtypePreferences(final InputMethodInfo imi, + final PreferenceScreen root) { final Context context = getActivity(); - - int N = (mInputMethodProperties == null ? 0 : mInputMethodProperties.size()); - - for (int i = 0; i < N; ++i) { - final InputMethodInfo imi = mInputMethodProperties.get(i); - final int subtypeCount = imi.getSubtypeCount(); - if (subtypeCount <= 1) continue; - final String imiId = imi.getId(); - // Add this subtype to the list when no IME is specified or when the IME of this - // subtype is the specified IME. - if (!TextUtils.isEmpty(mInputMethodId) && !mInputMethodId.equals(imiId)) { - continue; - } - final PreferenceCategory keyboardSettingsCategory = new PreferenceCategory(context); - root.addPreference(keyboardSettingsCategory); - final PackageManager pm = getPackageManager(); - final CharSequence label = imi.loadLabel(pm); - - keyboardSettingsCategory.setTitle(label); - keyboardSettingsCategory.setKey(imiId); - // TODO: Use toggle Preference if images are ready. - final CheckBoxPreference autoCB = new CheckBoxPreference(context); - mSubtypeAutoSelectionCBMap.put(imiId, autoCB); - keyboardSettingsCategory.addPreference(autoCB); - - final PreferenceCategory activeInputMethodsCategory = new PreferenceCategory(context); - activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes); - root.addPreference(activeInputMethodsCategory); - - boolean isAutoSubtype = false; - CharSequence autoSubtypeLabel = null; - final ArrayList<Preference> subtypePreferences = new ArrayList<Preference>(); - if (subtypeCount > 0) { - for (int j = 0; j < subtypeCount; ++j) { - final InputMethodSubtype subtype = imi.getSubtypeAt(j); - final CharSequence subtypeLabel = subtype.getDisplayName(context, - imi.getPackageName(), imi.getServiceInfo().applicationInfo); - if (subtype.overridesImplicitlyEnabledSubtype()) { - if (!isAutoSubtype) { - isAutoSubtype = true; - autoSubtypeLabel = subtypeLabel; - } - } else { - final CheckBoxPreference chkbxPref = new SubtypeCheckBoxPreference( - context, subtype.getLocale(), mSystemLocale, mCollator); - chkbxPref.setKey(imiId + subtype.hashCode()); - chkbxPref.setTitle(subtypeLabel); - subtypePreferences.add(chkbxPref); - } - } - Collections.sort(subtypePreferences); - for (int j = 0; j < subtypePreferences.size(); ++j) { - activeInputMethodsCategory.addPreference(subtypePreferences.get(j)); + final int subtypeCount = imi.getSubtypeCount(); + if (subtypeCount <= 1) { + return; + } + final String imiId = imi.getId(); + final PreferenceCategory keyboardSettingsCategory = new PreferenceCategory(context); + root.addPreference(keyboardSettingsCategory); + final PackageManager pm = getPackageManager(); + final CharSequence label = imi.loadLabel(pm); + + keyboardSettingsCategory.setTitle(label); + keyboardSettingsCategory.setKey(imiId); + // TODO: Use toggle Preference if images are ready. + final TwoStatePreference autoSelectionPref = new SwitchWithNoTextPreference(context); + mAutoSelectionPrefsMap.put(imiId, autoSelectionPref); + keyboardSettingsCategory.addPreference(autoSelectionPref); + autoSelectionPref.setOnPreferenceChangeListener(this); + + final PreferenceCategory activeInputMethodsCategory = new PreferenceCategory(context); + activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes); + root.addPreference(activeInputMethodsCategory); + + CharSequence autoSubtypeLabel = null; + final ArrayList<Preference> subtypePreferences = new ArrayList<>(); + for (int index = 0; index < subtypeCount; ++index) { + final InputMethodSubtype subtype = imi.getSubtypeAt(index); + if (subtype.overridesImplicitlyEnabledSubtype()) { + if (autoSubtypeLabel == null) { + autoSubtypeLabel = subtype.getDisplayName( + context, imi.getPackageName(), imi.getServiceInfo().applicationInfo); } - mInputMethodAndSubtypePrefsMap.put(imiId, subtypePreferences); + } else { + final Preference subtypePref = new InputMethodSubtypePreference( + context, subtype, imi); + subtypePreferences.add(subtypePref); } - if (isAutoSubtype) { - if (TextUtils.isEmpty(autoSubtypeLabel)) { - Log.w(TAG, "Title for auto subtype is empty."); - autoCB.setTitle("---"); - } else { - autoCB.setTitle(autoSubtypeLabel); + } + Collections.sort(subtypePreferences, new Comparator<Preference>() { + @Override + public int compare(final Preference lhs, final Preference rhs) { + if (lhs instanceof InputMethodSubtypePreference) { + return ((InputMethodSubtypePreference) lhs).compareTo(rhs, mCollator); } - } else { - autoCB.setTitle(R.string.use_system_language_to_select_input_method_subtypes); + return lhs.compareTo(rhs); } + }); + final int prefCount = subtypePreferences.size(); + for (int index = 0; index < prefCount; ++index) { + final Preference pref = subtypePreferences.get(index); + activeInputMethodsCategory.addPreference(pref); + pref.setOnPreferenceChangeListener(this); + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + } + mInputMethodAndSubtypePrefsMap.put(imiId, subtypePreferences); + if (TextUtils.isEmpty(autoSubtypeLabel)) { + autoSelectionPref.setTitle( + R.string.use_system_language_to_select_input_method_subtypes); + } else { + autoSelectionPref.setTitle(autoSubtypeLabel); } - return root; } - private boolean isNoSubtypesExplicitlySelected(String imiId) { - boolean allSubtypesOff = true; + private boolean isNoSubtypesExplicitlySelected(final String imiId) { final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); - for (Preference subtypePref: subtypePrefs) { - if (subtypePref instanceof CheckBoxPreference - && ((CheckBoxPreference)subtypePref).isChecked()) { - allSubtypesOff = false; - break; + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference && ((TwoStatePreference)pref).isChecked()) { + return false; } } - return allSubtypesOff; + return true; } - private void setSubtypeAutoSelectionEnabled(String imiId, boolean autoSelectionEnabled) { - CheckBoxPreference autoSelectionCB = mSubtypeAutoSelectionCBMap.get(imiId); - if (autoSelectionCB == null) return; - autoSelectionCB.setChecked(autoSelectionEnabled); + private void setAutoSelectionSubtypesEnabled(final String imiId, + final boolean autoSelectionEnabled) { + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); + if (autoSelectionPref == null) { + return; + } + autoSelectionPref.setChecked(autoSelectionEnabled); final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); - for (Preference subtypePref: subtypePrefs) { - if (subtypePref instanceof CheckBoxPreference) { + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference) { // When autoSelectionEnabled is true, all subtype prefs need to be disabled with // implicitly checked subtypes. In case of false, all subtype prefs need to be // enabled. - subtypePref.setEnabled(!autoSelectionEnabled); + pref.setEnabled(!autoSelectionEnabled); if (autoSelectionEnabled) { - ((CheckBoxPreference)subtypePref).setChecked(false); + ((TwoStatePreference)pref).setChecked(false); } } } if (autoSelectionEnabled) { - InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(this, getContentResolver(), - mInputMethodProperties, mHaveHardKeyboard); - setCheckedImplicitlyEnabledSubtypes(imiId); + InputMethodAndSubtypeUtil.saveInputMethodSubtypeList( + this, getContentResolver(), mInputMethodInfoList, mHaveHardKeyboard); + updateImplicitlyEnabledSubtypes(imiId, true /* check */); } } - private void setCheckedImplicitlyEnabledSubtypes(String targetImiId) { - updateImplicitlyEnabledSubtypes(targetImiId, true); - } - - private void clearImplicitlyEnabledSubtypes(String targetImiId) { - updateImplicitlyEnabledSubtypes(targetImiId, false); - } - - private void updateImplicitlyEnabledSubtypes(String targetImiId, boolean check) { + private void updateImplicitlyEnabledSubtypes(final String targetImiId, final boolean check) { // When targetImiId is null, apply to all subtypes of all IMEs - for (InputMethodInfo imi: mInputMethodProperties) { - String imiId = imi.getId(); - if (targetImiId != null && !targetImiId.equals(imiId)) continue; - final CheckBoxPreference autoCB = mSubtypeAutoSelectionCBMap.get(imiId); + for (final InputMethodInfo imi : mInputMethodInfoList) { + final String imiId = imi.getId(); + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); // No need to update implicitly enabled subtypes when the user has unchecked the // "subtype auto selection". - if (autoCB == null || !autoCB.isChecked()) continue; - final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); - final List<InputMethodSubtype> implicitlyEnabledSubtypes = - mImm.getEnabledInputMethodSubtypeList(imi, true); - if (subtypePrefs == null || implicitlyEnabledSubtypes == null) continue; - for (Preference subtypePref: subtypePrefs) { - if (subtypePref instanceof CheckBoxPreference) { - CheckBoxPreference cb = (CheckBoxPreference)subtypePref; - cb.setChecked(false); - if (check) { - for (InputMethodSubtype subtype: implicitlyEnabledSubtypes) { - String implicitlyEnabledSubtypePrefKey = imiId + subtype.hashCode(); - if (cb.getKey().equals(implicitlyEnabledSubtypePrefKey)) { - cb.setChecked(true); - break; - } - } - } - } + if (autoSelectionPref == null || !autoSelectionPref.isChecked()) { + continue; + } + if (imiId.equals(targetImiId) || targetImiId == null) { + updateImplicitlyEnabledSubtypesOf(imi, check); } } } - private void updateAutoSelectionCB() { - for (String imiId: mInputMethodAndSubtypePrefsMap.keySet()) { - setSubtypeAutoSelectionEnabled(imiId, isNoSubtypesExplicitlySelected(imiId)); + private void updateImplicitlyEnabledSubtypesOf(final InputMethodInfo imi, final boolean check) { + final String imiId = imi.getId(); + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + final List<InputMethodSubtype> implicitlyEnabledSubtypes = + mImm.getEnabledInputMethodSubtypeList(imi, true); + if (subtypePrefs == null || implicitlyEnabledSubtypes == null) { + return; } - setCheckedImplicitlyEnabledSubtypes(null); - } - - private static class SubtypeCheckBoxPreference extends CheckBoxPreference { - private final boolean mIsSystemLocale; - private final boolean mIsSystemLanguage; - private final Collator mCollator; - - public SubtypeCheckBoxPreference( - Context context, String subtypeLocale, String systemLocale, Collator collator) { - super(context); - if (TextUtils.isEmpty(subtypeLocale)) { - mIsSystemLocale = false; - mIsSystemLanguage = false; - } else { - mIsSystemLocale = subtypeLocale.equals(systemLocale); - mIsSystemLanguage = mIsSystemLocale - || subtypeLocale.startsWith(systemLocale.substring(0, 2)); + for (final Preference pref : subtypePrefs) { + if (!(pref instanceof TwoStatePreference)) { + continue; } - mCollator = collator; - } - - @Override - public int compareTo(Preference p) { - if (p instanceof SubtypeCheckBoxPreference) { - final SubtypeCheckBoxPreference pref = ((SubtypeCheckBoxPreference)p); - final CharSequence t0 = getTitle(); - final CharSequence t1 = pref.getTitle(); - if (TextUtils.equals(t0, t1)) { - return 0; - } - if (mIsSystemLocale) { - return -1; - } - if (pref.mIsSystemLocale) { - return 1; - } - if (mIsSystemLanguage) { - return -1; - } - if (pref.mIsSystemLanguage) { - return 1; - } - if (TextUtils.isEmpty(t0)) { - return 1; - } - if (TextUtils.isEmpty(t1)) { - return -1; + final TwoStatePreference subtypePref = (TwoStatePreference)pref; + subtypePref.setChecked(false); + if (check) { + for (final InputMethodSubtype subtype : implicitlyEnabledSubtypes) { + final String implicitlyEnabledSubtypePrefKey = imiId + subtype.hashCode(); + if (subtypePref.getKey().equals(implicitlyEnabledSubtypePrefKey)) { + subtypePref.setChecked(true); + break; + } } - return mCollator.compare(t0.toString(), t1.toString()); - } else { - Log.w(TAG, "Illegal preference type."); - return super.compareTo(p); } } } + + private void updateAutoSelectionPreferences() { + for (final String imiId : mInputMethodAndSubtypePrefsMap.keySet()) { + setAutoSelectionSubtypesEnabled(imiId, isNoSubtypesExplicitlySelected(imiId)); + } + updateImplicitlyEnabledSubtypes(null /* targetImiId */, true /* check */); + } } diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnablerActivity.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnablerActivity.java index 5693e20..bafae2b 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnablerActivity.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnablerActivity.java @@ -15,27 +15,42 @@ */ package com.android.settings.inputmethod; -import android.app.Fragment; +import android.app.ActionBar; import android.content.Intent; -import android.preference.PreferenceActivity; +import android.os.Bundle; -import com.android.settings.ChooseLockPassword.ChooseLockPasswordFragment; +import com.android.settings.SettingsActivity; + +public class InputMethodAndSubtypeEnablerActivity extends SettingsActivity { + private static final String FRAGMENT_NAME = InputMethodAndSubtypeEnabler.class.getName(); + + @Override + protected void onCreate(final Bundle savedState) { + super.onCreate(savedState); + final ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + } + } + + @Override + public boolean onNavigateUp() { + finish(); + return true; + } -public class InputMethodAndSubtypeEnablerActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent modIntent = new Intent(super.getIntent()); if (!modIntent.hasExtra(EXTRA_SHOW_FRAGMENT)) { - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, InputMethodAndSubtypeEnabler.class.getName()); - modIntent.putExtra(EXTRA_NO_HEADERS, true); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, FRAGMENT_NAME); } return modIntent; } @Override protected boolean isValidFragment(String fragmentName) { - if (InputMethodAndSubtypeEnabler.class.getName().equals(fragmentName)) return true; - return false; + return FRAGMENT_NAME.equals(fragmentName); } - } diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java index 561302a..b184066 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeUtil.java @@ -16,13 +16,11 @@ package com.android.settings.inputmethod; -import com.android.internal.inputmethod.InputMethodUtils; -import com.android.settings.SettingsPreferenceFragment; - import android.content.ContentResolver; -import android.preference.CheckBoxPreference; +import android.content.SharedPreferences; import android.preference.Preference; import android.preference.PreferenceScreen; +import android.preference.TwoStatePreference; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.text.TextUtils; @@ -30,12 +28,16 @@ import android.util.Log; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; +import com.android.internal.inputmethod.InputMethodUtils; +import com.android.settings.SettingsPreferenceFragment; + import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -public class InputMethodAndSubtypeUtil { +// TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}. +class InputMethodAndSubtypeUtil { private static final boolean DEBUG = false; static final String TAG = "InputMethdAndSubtypeUtil"; @@ -50,40 +52,33 @@ public class InputMethodAndSubtypeUtil { private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); - private static void buildEnabledInputMethodsString( - StringBuilder builder, String imi, HashSet<String> subtypes) { - builder.append(imi); - // Inputmethod and subtypes are saved in the settings as follows: - // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 - for (String subtypeId: subtypes) { - builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); - } - } - - public static void buildInputMethodsAndSubtypesString( - StringBuilder builder, HashMap<String, HashSet<String>> imsList) { - boolean needsAppendSeparator = false; - for (String imi: imsList.keySet()) { - if (needsAppendSeparator) { + // InputMethods and subtypes are saved in the settings as follows: + // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 + static String buildInputMethodsAndSubtypesString( + final HashMap<String, HashSet<String>> imeToSubtypesMap) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imeToSubtypesMap.keySet()) { + if (builder.length() > 0) { builder.append(INPUT_METHOD_SEPARATER); - } else { - needsAppendSeparator = true; } - buildEnabledInputMethodsString(builder, imi, imsList.get(imi)); + final HashSet<String> subtypeIdSet = imeToSubtypesMap.get(imi); + builder.append(imi); + for (final String subtypeId : subtypeIdSet) { + builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); + } } + return builder.toString(); } - public static void buildDisabledSystemInputMethods( - StringBuilder builder, HashSet<String> imes) { - boolean needsAppendSeparator = false; - for (String ime: imes) { - if (needsAppendSeparator) { + private static String buildInputMethodsString(final HashSet<String> imiList) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imiList) { + if (builder.length() > 0) { builder.append(INPUT_METHOD_SEPARATER); - } else { - needsAppendSeparator = true; } - builder.append(ime); + builder.append(imi); } + return builder.toString(); } private static int getInputMethodSubtypeSelected(ContentResolver resolver) { @@ -108,34 +103,48 @@ public class InputMethodAndSubtypeUtil { ContentResolver resolver) { final String enabledInputMethodsStr = Settings.Secure.getString( resolver, Settings.Secure.ENABLED_INPUT_METHODS); - HashMap<String, HashSet<String>> imsList - = new HashMap<String, HashSet<String>>(); if (DEBUG) { Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr); } + return parseInputMethodsAndSubtypesString(enabledInputMethodsStr); + } - if (TextUtils.isEmpty(enabledInputMethodsStr)) { - return imsList; + static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( + final String inputMethodsAndSubtypesString) { + final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>(); + if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) { + return subtypesMap; } - sStringInputMethodSplitter.setString(enabledInputMethodsStr); + sStringInputMethodSplitter.setString(inputMethodsAndSubtypesString); while (sStringInputMethodSplitter.hasNext()) { - String nextImsStr = sStringInputMethodSplitter.next(); + final String nextImsStr = sStringInputMethodSplitter.next(); sStringInputMethodSubtypeSplitter.setString(nextImsStr); if (sStringInputMethodSubtypeSplitter.hasNext()) { - HashSet<String> subtypeHashes = new HashSet<String>(); - // The first element is ime id. - String imeId = sStringInputMethodSubtypeSplitter.next(); + final HashSet<String> subtypeIdSet = new HashSet<>(); + // The first element is {@link InputMethodInfoId}. + final String imiId = sStringInputMethodSubtypeSplitter.next(); while (sStringInputMethodSubtypeSplitter.hasNext()) { - subtypeHashes.add(sStringInputMethodSubtypeSplitter.next()); + subtypeIdSet.add(sStringInputMethodSubtypeSplitter.next()); } - imsList.put(imeId, subtypeHashes); + subtypesMap.put(imiId, subtypeIdSet); } } - return imsList; + return subtypesMap; + } + + static void enableInputMethodSubtypesOf(final ContentResolver resolver, final String imiId, + final HashSet<String> enabledSubtypeIdSet) { + final HashMap<String, HashSet<String>> enabledImeAndSubtypeIdsMap = + getEnabledInputMethodsAndSubtypeList(resolver); + enabledImeAndSubtypeIdsMap.put(imiId, enabledSubtypeIdSet); + final String enabledImesAndSubtypesString = buildInputMethodsAndSubtypesString( + enabledImeAndSubtypeIdsMap); + Settings.Secure.putString(resolver, + Settings.Secure.ENABLED_INPUT_METHODS, enabledImesAndSubtypesString); } private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) { - HashSet<String> set = new HashSet<String>(); + HashSet<String> set = new HashSet<>(); String disabledIMEsStr = Settings.Secure.getString( resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS); if (TextUtils.isEmpty(disabledIMEsStr)) { @@ -148,49 +157,52 @@ public class InputMethodAndSubtypeUtil { return set; } - public static void saveInputMethodSubtypeList(SettingsPreferenceFragment context, + static void saveInputMethodSubtypeList(SettingsPreferenceFragment context, ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, boolean hasHardKeyboard) { String currentInputMethodId = Settings.Secure.getString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD); final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver); - HashMap<String, HashSet<String>> enabledIMEAndSubtypesMap = + final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap = getEnabledInputMethodsAndSubtypeList(resolver); - HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); + final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); - final int imiCount = inputMethodInfos.size(); boolean needsToResetSelectedSubtype = false; - for (InputMethodInfo imi : inputMethodInfos) { + for (final InputMethodInfo imi : inputMethodInfos) { final String imiId = imi.getId(); - Preference pref = context.findPreference(imiId); - if (pref == null) continue; - // In the Configure input method screen or in the subtype enabler screen. - // pref is instance of CheckBoxPreference in the Configure input method screen. - final boolean isImeChecked = (pref instanceof CheckBoxPreference) ? - ((CheckBoxPreference) pref).isChecked() - : enabledIMEAndSubtypesMap.containsKey(imiId); + final Preference pref = context.findPreference(imiId); + if (pref == null) { + continue; + } + // In the choose input method screen or in the subtype enabler screen, + // <code>pref</code> is an instance of TwoStatePreference. + final boolean isImeChecked = (pref instanceof TwoStatePreference) ? + ((TwoStatePreference) pref).isChecked() + : enabledIMEsAndSubtypesMap.containsKey(imiId); final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); final boolean systemIme = InputMethodUtils.isSystemIme(imi); if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity())) || isImeChecked) { - if (!enabledIMEAndSubtypesMap.containsKey(imiId)) { + if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { // imiId has just been enabled - enabledIMEAndSubtypesMap.put(imiId, new HashSet<String>()); + enabledIMEsAndSubtypesMap.put(imiId, new HashSet<String>()); } - HashSet<String> subtypesSet = enabledIMEAndSubtypesMap.get(imiId); + final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId); boolean subtypePrefFound = false; final int subtypeCount = imi.getSubtypeCount(); for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype subtype = imi.getSubtypeAt(i); + final InputMethodSubtype subtype = imi.getSubtypeAt(i); final String subtypeHashCodeStr = String.valueOf(subtype.hashCode()); - CheckBoxPreference subtypePref = (CheckBoxPreference) context.findPreference( - imiId + subtypeHashCodeStr); + final TwoStatePreference subtypePref = (TwoStatePreference) context + .findPreference(imiId + subtypeHashCodeStr); // In the Configure input method screen which does not have subtype preferences. - if (subtypePref == null) continue; + if (subtypePref == null) { + continue; + } if (!subtypePrefFound) { - // Once subtype checkbox is found, subtypeSet needs to be cleared. + // Once subtype preference is found, subtypeSet needs to be cleared. // Because of system change, hashCode value could have been changed. subtypesSet.clear(); // If selected subtype preference is disabled, needs to reset. @@ -211,7 +223,7 @@ public class InputMethodAndSubtypeUtil { } } } else { - enabledIMEAndSubtypesMap.remove(imiId); + enabledIMEsAndSubtypesMap.remove(imiId); if (isCurrentInputMethod) { // We are processing the current input method, but found that it's not enabled. // This means that the current input method has been uninstalled. @@ -238,14 +250,13 @@ public class InputMethodAndSubtypeUtil { } } - StringBuilder builder = new StringBuilder(); - buildInputMethodsAndSubtypesString(builder, enabledIMEAndSubtypesMap); - StringBuilder disabledSysImesBuilder = new StringBuilder(); - buildDisabledSystemInputMethods(disabledSysImesBuilder, disabledSystemIMEs); + final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString( + enabledIMEsAndSubtypesMap); + final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs); if (DEBUG) { - Log.d(TAG, "--- Save enabled inputmethod settings. :" + builder.toString()); - Log.d(TAG, "--- Save disable system inputmethod settings. :" - + disabledSysImesBuilder.toString()); + Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString); + Log.d(TAG, "--- Save disabled system inputmethod settings. :" + + disabledSystemIMEsString); Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId); Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype); Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver)); @@ -262,10 +273,10 @@ public class InputMethodAndSubtypeUtil { } Settings.Secure.putString(resolver, - Settings.Secure.ENABLED_INPUT_METHODS, builder.toString()); - if (disabledSysImesBuilder.length() > 0) { + Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString); + if (disabledSystemIMEsString.length() > 0) { Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, - disabledSysImesBuilder.toString()); + disabledSystemIMEsString); } // If the current input method is unset, InputMethodManagerService will find the applicable // IME from the history and the system locale. @@ -273,22 +284,21 @@ public class InputMethodAndSubtypeUtil { currentInputMethodId != null ? currentInputMethodId : ""); } - public static void loadInputMethodSubtypeList( - SettingsPreferenceFragment context, ContentResolver resolver, - List<InputMethodInfo> inputMethodInfos, + static void loadInputMethodSubtypeList(final SettingsPreferenceFragment context, + final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, final Map<String, List<Preference>> inputMethodPrefsMap) { - HashMap<String, HashSet<String>> enabledSubtypes = + final HashMap<String, HashSet<String>> enabledSubtypes = getEnabledInputMethodsAndSubtypeList(resolver); - for (InputMethodInfo imi : inputMethodInfos) { + for (final InputMethodInfo imi : inputMethodInfos) { final String imiId = imi.getId(); - Preference pref = context.findPreference(imiId); - if (pref != null && pref instanceof CheckBoxPreference) { - CheckBoxPreference checkBoxPreference = (CheckBoxPreference) pref; - boolean isEnabled = enabledSubtypes.containsKey(imiId); - checkBoxPreference.setChecked(isEnabled); + final Preference pref = context.findPreference(imiId); + if (pref instanceof TwoStatePreference) { + final TwoStatePreference subtypePref = (TwoStatePreference) pref; + final boolean isEnabled = enabledSubtypes.containsKey(imiId); + subtypePref.setChecked(isEnabled); if (inputMethodPrefsMap != null) { - for (Preference childPref: inputMethodPrefsMap.get(imiId)) { + for (final Preference childPref: inputMethodPrefsMap.get(imiId)) { childPref.setEnabled(isEnabled); } } @@ -298,16 +308,17 @@ public class InputMethodAndSubtypeUtil { updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes); } - public static void setSubtypesPreferenceEnabled(SettingsPreferenceFragment context, - List<InputMethodInfo> inputMethodProperties, String id, boolean enabled) { - PreferenceScreen preferenceScreen = context.getPreferenceScreen(); - for (InputMethodInfo imi : inputMethodProperties) { + static void setSubtypesPreferenceEnabled(final SettingsPreferenceFragment context, + final List<InputMethodInfo> inputMethodProperties, final String id, + final boolean enabled) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { if (id.equals(imi.getId())) { final int subtypeCount = imi.getSubtypeCount(); for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype subtype = imi.getSubtypeAt(i); - CheckBoxPreference pref = (CheckBoxPreference) preferenceScreen.findPreference( - id + subtype.hashCode()); + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + subtype.hashCode()); if (pref != null) { pref.setEnabled(enabled); } @@ -316,28 +327,42 @@ public class InputMethodAndSubtypeUtil { } } - public static void updateSubtypesPreferenceChecked(SettingsPreferenceFragment context, - List<InputMethodInfo> inputMethodProperties, - HashMap<String, HashSet<String>> enabledSubtypes) { - PreferenceScreen preferenceScreen = context.getPreferenceScreen(); - for (InputMethodInfo imi : inputMethodProperties) { - String id = imi.getId(); - if (!enabledSubtypes.containsKey(id)) break; + private static void updateSubtypesPreferenceChecked(final SettingsPreferenceFragment context, + final List<InputMethodInfo> inputMethodProperties, + final HashMap<String, HashSet<String>> enabledSubtypes) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { + final String id = imi.getId(); + if (!enabledSubtypes.containsKey(id)) { + // There is no need to enable/disable subtypes of disabled IMEs. + continue; + } final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id); final int subtypeCount = imi.getSubtypeCount(); for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype subtype = imi.getSubtypeAt(i); - String hashCode = String.valueOf(subtype.hashCode()); + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final String hashCode = String.valueOf(subtype.hashCode()); if (DEBUG) { Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", " + enabledSubtypesSet.contains(hashCode)); } - CheckBoxPreference pref = (CheckBoxPreference) preferenceScreen.findPreference( - id + hashCode); + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + hashCode); if (pref != null) { pref.setChecked(enabledSubtypesSet.contains(hashCode)); } } } } + + static void removeUnnecessaryNonPersistentPreference(final Preference pref) { + final String key = pref.getKey(); + if (pref.isPersistent() || key == null) { + return; + } + final SharedPreferences prefs = pref.getSharedPreferences(); + if (prefs != null && prefs.contains(key)) { + prefs.edit().remove(key).apply(); + } + } } diff --git a/src/com/android/settings/inputmethod/InputMethodPreference.java b/src/com/android/settings/inputmethod/InputMethodPreference.java index aa6430f..5cf5d6a 100644..100755 --- a/src/com/android/settings/inputmethod/InputMethodPreference.java +++ b/src/com/android/settings/inputmethod/InputMethodPreference.java @@ -16,306 +16,248 @@ package com.android.settings.inputmethod; -import com.android.internal.inputmethod.InputMethodUtils; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; - import android.app.AlertDialog; -import android.app.Fragment; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.preference.CheckBoxPreference; import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.provider.Settings; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.SwitchPreference; 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 android.widget.Toast; +import com.android.internal.inputmethod.InputMethodUtils; +import com.android.settings.R; + import java.text.Collator; +import java.util.ArrayList; import java.util.List; -public class InputMethodPreference extends CheckBoxPreference { +/** + * Input method preference. + * + * This preference represents an IME. It is used for two purposes. 1) An instance with a switch + * is used to enable or disable the IME. 2) An instance without a switch is used to invoke the + * setting activity of the IME. + */ +class InputMethodPreference extends SwitchPreference implements OnPreferenceClickListener, + OnPreferenceChangeListener { private static final String TAG = InputMethodPreference.class.getSimpleName(); - private final SettingsPreferenceFragment mFragment; + private static final String EMPTY_TEXT = ""; + + interface OnSavePreferenceListener { + /** + * Called when this preference needs to be saved its state. + * + * Note that this preference is non-persistent and needs explicitly to be saved its state. + * Because changing one IME state may change other IMEs' state, this is a place to update + * other IMEs' state as well. + * + * @param pref This preference. + */ + public void onSaveInputMethodPreference(InputMethodPreference pref); + } + private final InputMethodInfo mImi; - private final InputMethodManager mImm; - private final boolean mIsValidSystemNonAuxAsciiCapableIme; - private final Intent mSettingsIntent; - private final boolean mIsSystemIme; - private final Collator mCollator; + private final boolean mHasPriorityInSorting; + private final OnSavePreferenceListener mOnSaveListener; + private final InputMethodSettingValuesWrapper mInputMethodSettingValues; + private final boolean mIsAllowedByOrganization; private AlertDialog mDialog = null; - private ImageView mInputMethodSettingsButton; - private TextView mTitleText; - private TextView mSummaryText; - private View mInputMethodPref; - private OnPreferenceChangeListener mOnImePreferenceChangeListener; - - private final OnClickListener mPrefOnclickListener = new OnClickListener() { - @Override - public void onClick(View arg0) { - if (!isEnabled()) { - return; - } - if (isChecked()) { - setChecked(false, true /* save */); - } else { - if (mIsSystemIme) { - setChecked(true, true /* save */); - } else { - showSecurityWarnDialog(mImi, InputMethodPreference.this); - } - } - } - }; - public InputMethodPreference(SettingsPreferenceFragment fragment, Intent settingsIntent, - InputMethodManager imm, InputMethodInfo imi) { - super(fragment.getActivity(), null, R.style.InputMethodPreferenceStyle); - setLayoutResource(R.layout.preference_inputmethod); - setWidgetLayoutResource(R.layout.preference_inputmethod_widget); - mFragment = fragment; - mSettingsIntent = settingsIntent; - mImm = imm; + /** + * A preference entry of an input method. + * + * @param context The Context this is associated with. + * @param imi The {@link InputMethodInfo} of this preference. + * @param isImeEnabler true if this preference is the IME enabler that has enable/disable + * switches for all available IMEs, not the list of enabled IMEs. + * @param isAllowedByOrganization false if the IME has been disabled by a device or profile + owner. + * @param onSaveListener The listener called when this preference has been changed and needs + * to save the state to shared preference. + */ + InputMethodPreference(final Context context, final InputMethodInfo imi, + final boolean isImeEnabler, final boolean isAllowedByOrganization, + final OnSavePreferenceListener onSaveListener) { + super(context); + setPersistent(false); mImi = imi; - mIsSystemIme = InputMethodUtils.isSystemIme(imi); - mCollator = Collator.getInstance(fragment.getResources().getConfiguration().locale); - final Context context = fragment.getActivity(); - mIsValidSystemNonAuxAsciiCapableIme = InputMethodSettingValuesWrapper - .getInstance(context).isValidSystemNonAuxAsciiCapableIme(imi, context); - updatePreferenceViews(); - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - mInputMethodPref = view.findViewById(R.id.inputmethod_pref); - mInputMethodPref.setOnClickListener(mPrefOnclickListener); - mInputMethodSettingsButton = (ImageView)view.findViewById(R.id.inputmethod_settings); - mTitleText = (TextView)view.findViewById(android.R.id.title); - mSummaryText = (TextView)view.findViewById(android.R.id.summary); - final boolean hasSubtypes = mImi.getSubtypeCount() > 1; - final String imiId = mImi.getId(); - if (hasSubtypes) { - mInputMethodPref.setOnLongClickListener(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; - } - }); - } - - 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); - final String msg = mFragment.getString( - R.string.failed_to_open_app_settings_toast, - mImi.loadLabel( - mFragment.getActivity().getPackageManager())); - Toast.makeText( - mFragment.getActivity(), msg, Toast.LENGTH_LONG).show(); - } - } - }); - } - 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); + mIsAllowedByOrganization = isAllowedByOrganization; + mOnSaveListener = onSaveListener; + if (!isImeEnabler) { + // Hide switch widget. + setWidgetLayoutResource(0 /* widgetLayoutResId */); } - if (mSettingsIntent == null) { - mInputMethodSettingsButton.setVisibility(View.GONE); + // Disable on/off switch texts. + setSwitchTextOn(EMPTY_TEXT); + setSwitchTextOff(EMPTY_TEXT); + setKey(imi.getId()); + setTitle(imi.loadLabel(context.getPackageManager())); + final String settingsActivity = imi.getSettingsActivity(); + if (TextUtils.isEmpty(settingsActivity)) { + setIntent(null); + } else { + // Set an intent to invoke settings activity of an input method. + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(imi.getPackageName(), settingsActivity); + setIntent(intent); } - updatePreferenceViews(); + mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context); + mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi) + && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); + setOnPreferenceClickListener(this); + setOnPreferenceChangeListener(this); } - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - updatePreferenceViews(); + public InputMethodInfo getInputMethodInfo() { + return mImi; } - public void updatePreferenceViews() { - final boolean isAlwaysChecked = - InputMethodSettingValuesWrapper.getInstance(getContext()).isAlwaysCheckedIme( - mImi, getContext()); - if (isAlwaysChecked) { - super.setChecked(true); - super.setEnabled(false); - } else { - super.setEnabled(true); - } - final boolean checked = isChecked(); - if (mInputMethodSettingsButton != null) { - mInputMethodSettingsButton.setEnabled(checked); - mInputMethodSettingsButton.setClickable(checked); - mInputMethodSettingsButton.setFocusable(checked); - if (!checked) { - mInputMethodSettingsButton.setAlpha(Utils.DISABLED_ALPHA); - } - } - if (mTitleText != null) { - mTitleText.setEnabled(true); + private boolean isImeEnabler() { + // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the + // switch widget at constructor. + return getWidgetLayoutResource() != 0; + } + + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + // Always returns false to prevent default behavior. + // See {@link TwoStatePreference#onClick()}. + if (!isImeEnabler()) { + // Prevent disabling an IME because this preference is for invoking a settings activity. + return false; } - if (mSummaryText != null) { - mSummaryText.setEnabled(checked); + if (isChecked()) { + // Disable this IME. + setChecked(false); + mOnSaveListener.onSaveInputMethodPreference(this); + return false; } - if (mInputMethodPref != null) { - mInputMethodPref.setEnabled(true); - mInputMethodPref.setLongClickable(checked); - final boolean enabled = isEnabled(); - mInputMethodPref.setOnClickListener(enabled ? mPrefOnclickListener : null); - if (!enabled) { - mInputMethodPref.setBackgroundColor(0); - } + if (InputMethodUtils.isSystemIme(mImi)) { + // Enable a system IME. No need to show a security warning dialog. + setChecked(true); + mOnSaveListener.onSaveInputMethodPreference(this); + return false; } - updateSummary(); + // Enable a 3rd party IME. + showSecurityWarnDialog(mImi); + return false; } - 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); + @Override + public boolean onPreferenceClick(final Preference preference) { + // Always returns true to prevent invoking an intent without catching exceptions. + // See {@link Preference#performClick(PreferenceScreen)}/ + if (isImeEnabler()) { + // Prevent invoking a settings activity because this preference is for enabling and + // disabling an input method. 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; } - } - - private 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 Context context = getContext(); + try { + final Intent intent = getIntent(); + if (intent != null) { + // Invoke a settings activity of an input method. + context.startActivity(intent); } - final CharSequence subtypeLabel = subtype.getDisplayName(mFragment.getActivity(), - mImi.getPackageName(), mImi.getServiceInfo().applicationInfo); - builder.append(subtypeLabel); + } catch (final ActivityNotFoundException e) { + Log.d(TAG, "IME's Settings Activity Not Found", e); + final String message = context.getString( + R.string.failed_to_open_app_settings_toast, + mImi.loadLabel(context.getPackageManager())); + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); } - return builder.toString(); + return true; } - private void updateSummary() { - final String summary = getSummaryString(); - if (TextUtils.isEmpty(summary)) { - return; - } - setSummary(summary); + void updatePreferenceViews() { + final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( + mImi, getContext()); + // Only when this preference has a switch and an input method should be always enabled, + // this preference should be disabled to prevent accidentally disabling an input method. + setEnabled(!((isAlwaysChecked && isImeEnabler()) || (!mIsAllowedByOrganization))); + setChecked(mInputMethodSettingValues.isEnabledImi(mImi)); + setSummary(getSummaryString()); } - /** - * Sets the checkbox state and optionally saves the settings. - * @param checked whether to check the box - * @param save whether to save IME settings - */ - private void setChecked(boolean checked, boolean save) { - final boolean wasChecked = isChecked(); - super.setChecked(checked); - if (save) { - saveImeSettings(); - if (wasChecked != checked && mOnImePreferenceChangeListener != null) { - mOnImePreferenceChangeListener.onPreferenceChange(this, checked); - } - } + private InputMethodManager getInputMethodManager() { + return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); } - public void setOnImePreferenceChangeListener(OnPreferenceChangeListener listener) { - mOnImePreferenceChangeListener = listener; + private String getSummaryString() { + final Context context = getContext(); + if (!mIsAllowedByOrganization) { + return context.getString(R.string.accessibility_feature_or_input_method_not_allowed); + } + final InputMethodManager imm = getInputMethodManager(); + final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true); + final ArrayList<CharSequence> subtypeLabels = new ArrayList<>(); + for (final InputMethodSubtype subtype : subtypes) { + final CharSequence label = subtype.getDisplayName( + context, mImi.getPackageName(), mImi.getServiceInfo().applicationInfo); + subtypeLabels.add(label); + } + // TODO: A delimiter of subtype labels should be localized. + return TextUtils.join(", ", subtypeLabels); } - private void showSecurityWarnDialog(InputMethodInfo imi, final InputMethodPreference chkPref) { + private void showSecurityWarnDialog(final InputMethodInfo imi) { if (mDialog != null && mDialog.isShowing()) { mDialog.dismiss(); } - mDialog = (new AlertDialog.Builder(mFragment.getActivity())) - .setTitle(android.R.string.dialog_alert_title) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - chkPref.setChecked(true, 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()))); + final Context context = getContext(); + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setCancelable(true /* cancelable */); + builder.setTitle(android.R.string.dialog_alert_title); + final CharSequence label = imi.getServiceInfo().applicationInfo.loadLabel( + context.getPackageManager()); + builder.setMessage(context.getString(R.string.ime_security_warning, label)); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + // The user confirmed to enable a 3rd party IME. + setChecked(true); + mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); + notifyChanged(); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + // The user canceled to enable a 3rd party IME. + setChecked(false); + mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); + notifyChanged(); + } + }); + mDialog = builder.create(); mDialog.show(); } - @Override - public int compareTo(Preference p) { - if (!(p instanceof InputMethodPreference)) { - return super.compareTo(p); + int compareTo(final InputMethodPreference rhs, final Collator collator) { + if (this == rhs) { + return 0; } - final InputMethodPreference imp = (InputMethodPreference) p; - final boolean priority0 = mIsSystemIme && mIsValidSystemNonAuxAsciiCapableIme; - final boolean priority1 = imp.mIsSystemIme && imp.mIsValidSystemNonAuxAsciiCapableIme; - if (priority0 == priority1) { + if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) { final CharSequence t0 = getTitle(); - final CharSequence t1 = imp.getTitle(); + final CharSequence t1 = rhs.getTitle(); if (TextUtils.isEmpty(t0)) { return 1; } if (TextUtils.isEmpty(t1)) { return -1; } - return mCollator.compare(t0.toString(), t1.toString()); + return collator.compare(t0.toString(), t1.toString()); } // Prefer always checked system IMEs - return priority0 ? -1 : 1; - } - - private void saveImeSettings() { - InputMethodAndSubtypeUtil.saveInputMethodSubtypeList( - mFragment, mFragment.getActivity().getContentResolver(), mImm.getInputMethodList(), - mFragment.getResources().getConfiguration().keyboard - == Configuration.KEYBOARD_QWERTY); + return mHasPriorityInSorting ? -1 : 1; } } diff --git a/src/com/android/settings/inputmethod/InputMethodSettingValuesWrapper.java b/src/com/android/settings/inputmethod/InputMethodSettingValuesWrapper.java index ccba143..ada476e 100644 --- a/src/com/android/settings/inputmethod/InputMethodSettingValuesWrapper.java +++ b/src/com/android/settings/inputmethod/InputMethodSettingValuesWrapper.java @@ -39,22 +39,20 @@ import java.util.Locale; * manually on some events when "InputMethodInfo"s and "InputMethodSubtype"s can be * changed. */ -public class InputMethodSettingValuesWrapper { +// TODO: Consolidate this with {@link InputMethodAndSubtypeUtil}. +class InputMethodSettingValuesWrapper { private static final String TAG = InputMethodSettingValuesWrapper.class.getSimpleName(); - private static final Locale ENGLISH_LOCALE = new Locale("en"); private static volatile InputMethodSettingValuesWrapper sInstance; - private final ArrayList<InputMethodInfo> mMethodList = new ArrayList<InputMethodInfo>(); - private final HashMap<String, InputMethodInfo> mMethodMap = - new HashMap<String, InputMethodInfo>(); + private final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); + private final HashMap<String, InputMethodInfo> mMethodMap = new HashMap<>(); private final InputMethodSettings mSettings; private final InputMethodManager mImm; - private final HashSet<InputMethodInfo> mAsciiCapableEnabledImis = - new HashSet<InputMethodInfo>(); + private final HashSet<InputMethodInfo> mAsciiCapableEnabledImis = new HashSet<>(); - public static InputMethodSettingValuesWrapper getInstance(Context context) { + static InputMethodSettingValuesWrapper getInstance(Context context) { if (sInstance == null) { - synchronized(TAG) { + synchronized (TAG) { if (sInstance == null) { sInstance = new InputMethodSettingValuesWrapper(context); } @@ -74,14 +72,13 @@ public class InputMethodSettingValuesWrapper { // Ensure singleton private InputMethodSettingValuesWrapper(Context context) { - mSettings = - new InputMethodSettings(context.getResources(), context.getContentResolver(), - mMethodMap, mMethodList, getDefaultCurrentUserId()); + mSettings = new InputMethodSettings(context.getResources(), context.getContentResolver(), + mMethodMap, mMethodList, getDefaultCurrentUserId()); mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); refreshAllInputMethodAndSubtypes(); } - public void refreshAllInputMethodAndSubtypes() { + void refreshAllInputMethodAndSubtypes() { synchronized (mMethodMap) { mMethodList.clear(); mMethodMap.clear(); @@ -113,13 +110,13 @@ public class InputMethodSettingValuesWrapper { } } - public List<InputMethodInfo> getInputMethodList() { + List<InputMethodInfo> getInputMethodList() { synchronized (mMethodMap) { return mMethodList; } } - public CharSequence getCurrentInputMethodName(Context context) { + CharSequence getCurrentInputMethodName(Context context) { synchronized (mMethodMap) { final InputMethodInfo imi = mMethodMap.get(mSettings.getSelectedInputMethod()); if (imi == null) { @@ -131,7 +128,7 @@ public class InputMethodSettingValuesWrapper { } } - public boolean isAlwaysCheckedIme(InputMethodInfo imi, Context context) { + boolean isAlwaysCheckedIme(InputMethodInfo imi, Context context) { final boolean isEnabled = isEnabledImi(imi); synchronized (mMethodMap) { if (mSettings.getEnabledInputMethodListLocked().size() <= 1 && isEnabled) { @@ -158,7 +155,7 @@ public class InputMethodSettingValuesWrapper { private int getEnabledValidSystemNonAuxAsciiCapableImeCount(Context context) { int count = 0; final List<InputMethodInfo> enabledImis; - synchronized(mMethodMap) { + synchronized (mMethodMap) { enabledImis = mSettings.getEnabledInputMethodListLocked(); } for (final InputMethodInfo imi : enabledImis) { @@ -172,9 +169,9 @@ public class InputMethodSettingValuesWrapper { return count; } - private boolean isEnabledImi(InputMethodInfo imi) { + boolean isEnabledImi(InputMethodInfo imi) { final List<InputMethodInfo> enabledImis; - synchronized(mMethodMap) { + synchronized (mMethodMap) { enabledImis = mSettings.getEnabledInputMethodListLocked(); } for (final InputMethodInfo tempImi : enabledImis) { @@ -185,8 +182,7 @@ public class InputMethodSettingValuesWrapper { return false; } - public boolean isValidSystemNonAuxAsciiCapableIme(InputMethodInfo imi, - Context context) { + boolean isValidSystemNonAuxAsciiCapableIme(InputMethodInfo imi, Context context) { if (imi.isAuxiliaryIme()) { return false; } @@ -196,7 +192,7 @@ public class InputMethodSettingValuesWrapper { if (mAsciiCapableEnabledImis.isEmpty()) { Log.w(TAG, "ascii capable subtype enabled imi not found. Fall back to English" + " Keyboard subtype."); - return InputMethodUtils.containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), + return InputMethodUtils.containsSubtypeOf(imi, Locale.ENGLISH.getLanguage(), InputMethodUtils.SUBTYPE_MODE_KEYBOARD); } return mAsciiCapableEnabledImis.contains(imi); diff --git a/src/com/android/settings/inputmethod/InputMethodSubtypePreference.java b/src/com/android/settings/inputmethod/InputMethodSubtypePreference.java new file mode 100644 index 0000000..6ded6ad --- /dev/null +++ b/src/com/android/settings/inputmethod/InputMethodSubtypePreference.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 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.preference.Preference; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.inputmethod.InputMethodUtils; + +import java.text.Collator; +import java.util.Locale; + +/** + * Input method subtype preference. + * + * This preference represents a subtype of an IME. It is used to enable or disable the subtype. + */ +class InputMethodSubtypePreference extends SwitchWithNoTextPreference { + private final boolean mIsSystemLocale; + private final boolean mIsSystemLanguage; + + InputMethodSubtypePreference(final Context context, final InputMethodSubtype subtype, + final InputMethodInfo imi) { + super(context); + setPersistent(false); + setKey(imi.getId() + subtype.hashCode()); + final CharSequence subtypeLabel = subtype.getDisplayName(context, + imi.getPackageName(), imi.getServiceInfo().applicationInfo); + setTitle(subtypeLabel); + final String subtypeLocaleString = subtype.getLocale(); + if (TextUtils.isEmpty(subtypeLocaleString)) { + mIsSystemLocale = false; + mIsSystemLanguage = false; + } else { + final Locale systemLocale = context.getResources().getConfiguration().locale; + mIsSystemLocale = subtypeLocaleString.equals(systemLocale.toString()); + mIsSystemLanguage = mIsSystemLocale + || InputMethodUtils.getLanguageFromLocaleString(subtypeLocaleString) + .equals(systemLocale.getLanguage()); + } + } + + int compareTo(final Preference rhs, final Collator collator) { + if (this == rhs) { + return 0; + } + if (rhs instanceof InputMethodSubtypePreference) { + final InputMethodSubtypePreference pref = (InputMethodSubtypePreference) rhs; + final CharSequence t0 = getTitle(); + final CharSequence t1 = rhs.getTitle(); + if (TextUtils.equals(t0, t1)) { + return 0; + } + if (mIsSystemLocale) { + return -1; + } + if (pref.mIsSystemLocale) { + return 1; + } + if (mIsSystemLanguage) { + return -1; + } + if (pref.mIsSystemLanguage) { + return 1; + } + if (TextUtils.isEmpty(t0)) { + return 1; + } + if (TextUtils.isEmpty(t1)) { + return -1; + } + return collator.compare(t0.toString(), t1.toString()); + } + return super.compareTo(rhs); + } +} diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java index 451b36e..c77b2c9 100644 --- a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java @@ -17,7 +17,6 @@ package com.android.settings.inputmethod; import com.android.settings.R; -import com.android.settings.Settings.KeyboardLayoutPickerActivity; import android.app.AlertDialog; import android.app.Activity; @@ -29,7 +28,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.Loader; -import android.content.res.Resources; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; import android.hardware.input.KeyboardLayout; diff --git a/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java b/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java deleted file mode 100644 index 5ea8bd7..0000000 --- a/src/com/android/settings/inputmethod/SingleSpellCheckerPreference.java +++ /dev/null @@ -1,218 +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.inputmethod; - -import com.android.settings.R; -import com.android.settings.Utils; - -import android.app.AlertDialog; -import android.content.ActivityNotFoundException; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.res.Resources; -import android.preference.Preference; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.textservice.SpellCheckerInfo; -import android.view.textservice.SpellCheckerSubtype; -import android.view.textservice.TextServicesManager; -import android.widget.ImageView; -import android.widget.RadioButton; -import android.widget.TextView; -import android.widget.Toast; - -public class SingleSpellCheckerPreference extends Preference { - private static final String TAG = SingleSpellCheckerPreference.class.getSimpleName(); - private static final boolean DBG = false; - - private final SpellCheckerInfo mSpellCheckerInfo; - - private final SpellCheckersSettings mFragment; - private final Resources mRes; - private final TextServicesManager mTsm; - private AlertDialog mDialog = null; - private TextView mTitleText; - private TextView mSummaryText; - private View mPrefAll; - private RadioButton mRadioButton; - private View mPrefLeftButton; - private View mSettingsButton; - private ImageView mSubtypeButton; - private Intent mSettingsIntent; - private boolean mSelected; - - public SingleSpellCheckerPreference(SpellCheckersSettings fragment, Intent settingsIntent, - SpellCheckerInfo sci, TextServicesManager tsm) { - super(fragment.getActivity(), null, 0); - mFragment = fragment; - mRes = fragment.getActivity().getResources(); - mTsm = tsm; - setLayoutResource(R.layout.preference_spellchecker); - mSpellCheckerInfo = sci; - mSelected = false; - final String settingsActivity = mSpellCheckerInfo.getSettingsActivity(); - if (!TextUtils.isEmpty(settingsActivity)) { - mSettingsIntent = new Intent(Intent.ACTION_MAIN); - mSettingsIntent.setClassName(mSpellCheckerInfo.getPackageName(), settingsActivity); - } else { - mSettingsIntent = null; - } - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - mPrefAll = view.findViewById(R.id.pref_all); - mRadioButton = (RadioButton)view.findViewById(R.id.pref_radio); - mPrefLeftButton = view.findViewById(R.id.pref_left_button); - mPrefLeftButton.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View arg0) { - onLeftButtonClicked(arg0); - } - }); - mTitleText = (TextView)view.findViewById(android.R.id.title); - mSummaryText = (TextView)view.findViewById(android.R.id.summary); - mSubtypeButton = (ImageView)view.findViewById(R.id.pref_right_button2); - mSubtypeButton.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View arg0) { - onSubtypeButtonClicked(arg0); - } - }); - mSettingsButton = view.findViewById(R.id.pref_right_button1); - mSettingsButton.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View arg0) { - onSettingsButtonClicked(arg0); - } - }); - updateSelectedState(mSelected); - } - - private void onLeftButtonClicked(View arg0) { - mFragment.onPreferenceClick(this); - } - - public SpellCheckerInfo getSpellCheckerInfo() { - return mSpellCheckerInfo; - } - - private void updateSelectedState(boolean selected) { - if (mPrefAll != null) { - mRadioButton.setChecked(selected); - enableButtons(selected); - } - } - - public void setSelected(boolean selected) { - mSelected = selected; - updateSelectedState(selected); - } - - private void onSubtypeButtonClicked(View arg0) { - if (mDialog != null && mDialog.isShowing()) { - mDialog.dismiss(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(mFragment.getActivity()); - builder.setTitle(R.string.phone_language); - final int size = mSpellCheckerInfo.getSubtypeCount(); - final CharSequence[] items = new CharSequence[size + 1]; - items[0] = mRes.getString(R.string.use_system_language_to_select_input_method_subtypes); - for (int i = 0; i < size; ++i) { - final SpellCheckerSubtype subtype = mSpellCheckerInfo.getSubtypeAt(i); - final CharSequence label = subtype.getDisplayName( - mFragment.getActivity(), mSpellCheckerInfo.getPackageName(), - mSpellCheckerInfo.getServiceInfo().applicationInfo); - items[i + 1] = label; - } - // default: "Use system language" - int checkedItem = 0; - // Allow no implicitly selected subtypes - final SpellCheckerSubtype currentScs = mTsm.getCurrentSpellCheckerSubtype(false); - if (currentScs != null) { - for (int i = 0; i < size; ++i) { - if (mSpellCheckerInfo.getSubtypeAt(i).equals(currentScs)) { - checkedItem = i + 1; - break; - } - } - } - builder.setSingleChoiceItems(items, checkedItem, new AlertDialog.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - mTsm.setSpellCheckerSubtype(null); - } else { - mTsm.setSpellCheckerSubtype(mSpellCheckerInfo.getSubtypeAt(which - 1)); - } - if (DBG) { - final SpellCheckerSubtype subtype = mTsm.getCurrentSpellCheckerSubtype(true); - Log.d(TAG, "Current spell check locale is " - + subtype == null ? "null" : subtype.getLocale()); - } - dialog.dismiss(); - } - }); - mDialog = builder.create(); - mDialog.show(); - } - - private void onSettingsButtonClicked(View arg0) { - if (mFragment != null && mSettingsIntent != null) { - try { - mFragment.startActivity(mSettingsIntent); - } catch (ActivityNotFoundException e) { - final String msg = mFragment.getString(R.string.failed_to_open_app_settings_toast, - mSpellCheckerInfo.loadLabel(mFragment.getActivity().getPackageManager())); - Toast.makeText(mFragment.getActivity(), msg, Toast.LENGTH_LONG).show(); - } - } - } - - private void enableButtons(boolean enabled) { - if (mSettingsButton != null) { - if (mSettingsIntent == null) { - mSettingsButton.setVisibility(View.GONE); - } else { - mSettingsButton.setEnabled(enabled); - mSettingsButton.setClickable(enabled); - mSettingsButton.setFocusable(enabled); - if (!enabled) { - mSettingsButton.setAlpha(Utils.DISABLED_ALPHA); - } - } - } - if (mSubtypeButton != null) { - if (mSpellCheckerInfo.getSubtypeCount() <= 0) { - mSubtypeButton.setVisibility(View.GONE); - } else { - mSubtypeButton.setEnabled(enabled); - mSubtypeButton.setClickable(enabled); - mSubtypeButton.setFocusable(enabled); - if (!enabled) { - mSubtypeButton.setAlpha(Utils.DISABLED_ALPHA); - } - } - } - } -} diff --git a/src/com/android/settings/inputmethod/SpellCheckerPreference.java b/src/com/android/settings/inputmethod/SpellCheckerPreference.java new file mode 100644 index 0000000..3787803 --- /dev/null +++ b/src/com/android/settings/inputmethod/SpellCheckerPreference.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 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.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.preference.Preference; +import android.text.TextUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.textservice.SpellCheckerInfo; +import android.widget.RadioButton; +import android.widget.Toast; + +import com.android.settings.R; +import com.android.settings.Utils; + +/** + * Spell checker service preference. + * + * This preference represents a spell checker service. It is used for two purposes. 1) A radio + * button on the left side is used to choose the current spell checker service. 2) A settings + * icon on the right side is used to invoke the setting activity of the spell checker service. + */ +class SpellCheckerPreference extends Preference implements OnClickListener { + interface OnRadioButtonPreferenceListener { + /** + * Called when this preference needs to be saved its state. + * + * Note that this preference is non-persistent and needs explicitly to be saved its state. + * Because changing one IME state may change other IMEs' state, this is a place to update + * other IMEs' state as well. + * + * @param pref This preference. + */ + public void onRadioButtonClicked(SpellCheckerPreference pref); + } + + private final SpellCheckerInfo mSci; + private final OnRadioButtonPreferenceListener mOnRadioButtonListener; + + private RadioButton mRadioButton; + private View mPrefLeftButton; + private View mSettingsButton; + private boolean mSelected; + + public SpellCheckerPreference(final Context context, final SpellCheckerInfo sci, + final OnRadioButtonPreferenceListener onRadioButtonListener) { + super(context, null, 0); + setPersistent(false); + setLayoutResource(R.layout.preference_spellchecker); + setWidgetLayoutResource(R.layout.preference_spellchecker_widget); + mSci = sci; + mOnRadioButtonListener = onRadioButtonListener; + setKey(sci.getId()); + setTitle(sci.loadLabel(context.getPackageManager())); + final String settingsActivity = mSci.getSettingsActivity(); + if (TextUtils.isEmpty(settingsActivity)) { + setIntent(null); + } else { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(mSci.getPackageName(), settingsActivity); + setIntent(intent); + } + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mRadioButton = (RadioButton)view.findViewById(R.id.pref_radio); + mPrefLeftButton = view.findViewById(R.id.pref_left_button); + mPrefLeftButton.setOnClickListener(this); + mSettingsButton = view.findViewById(R.id.pref_right_button); + mSettingsButton.setOnClickListener(this); + updateSelectedState(mSelected); + } + + @Override + public void onClick(final View v) { + if (v == mPrefLeftButton) { + mOnRadioButtonListener.onRadioButtonClicked(this); + return; + } + if (v == mSettingsButton) { + onSettingsButtonClicked(); + return; + } + } + + private void onSettingsButtonClicked() { + final Context context = getContext(); + try { + final Intent intent = getIntent(); + if (intent != null) { + // Invoke a settings activity of an spell checker. + context.startActivity(intent); + } + } catch (final ActivityNotFoundException e) { + final String message = context.getString(R.string.failed_to_open_app_settings_toast, + mSci.loadLabel(context.getPackageManager())); + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + } + + public SpellCheckerInfo getSpellCheckerInfo() { + return mSci; + } + + public void setSelected(final boolean selected) { + mSelected = selected; + updateSelectedState(selected); + } + + private void updateSelectedState(final boolean selected) { + if (mRadioButton != null) { + mRadioButton.setChecked(selected); + enableSettingsButton(isEnabled() && selected); + } + } + + private void enableSettingsButton(final boolean enabled) { + if (mSettingsButton == null) { + return; + } + if (getIntent() == null) { + mSettingsButton.setVisibility(View.GONE); + } else { + mSettingsButton.setEnabled(enabled); + mSettingsButton.setClickable(enabled); + mSettingsButton.setFocusable(enabled); + if (!enabled) { + mSettingsButton.setAlpha(Utils.DISABLED_ALPHA); + } + } + } +} diff --git a/src/com/android/settings/inputmethod/SpellCheckerUtils.java b/src/com/android/settings/inputmethod/SpellCheckerUtils.java deleted file mode 100644 index fe761a6..0000000 --- a/src/com/android/settings/inputmethod/SpellCheckerUtils.java +++ /dev/null @@ -1,47 +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.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 deleted file mode 100644 index 5e4ebba..0000000 --- a/src/com/android/settings/inputmethod/SpellCheckersPreference.java +++ /dev/null @@ -1,39 +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.inputmethod; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.textservice.TextServicesManager; - -public class SpellCheckersPreference extends CheckBoxAndSettingsPreference { - private final TextServicesManager mTsm; - - public SpellCheckersPreference(Context context, AttributeSet attrs) { - super(context, attrs); - mTsm = (TextServicesManager) context.getSystemService( - Context.TEXT_SERVICES_MANAGER_SERVICE); - setChecked(mTsm.isSpellCheckerEnabled()); - } - - @Override - protected void onCheckBoxClicked() { - super.onCheckBoxClicked(); - final boolean checked = isChecked(); - mTsm.setSpellCheckerEnabled(checked); - } -} diff --git a/src/com/android/settings/inputmethod/SpellCheckersSettings.java b/src/com/android/settings/inputmethod/SpellCheckersSettings.java index 8b1b867..5a8ccea 100644 --- a/src/com/android/settings/inputmethod/SpellCheckersSettings.java +++ b/src/com/android/settings/inputmethod/SpellCheckersSettings.java @@ -16,144 +16,221 @@ package com.android.settings.inputmethod; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; import android.util.Log; import android.view.textservice.SpellCheckerInfo; +import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; +import android.widget.Switch; -import java.util.ArrayList; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.inputmethod.SpellCheckerPreference.OnRadioButtonPreferenceListener; +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.SwitchBar.OnSwitchChangeListener; public class SpellCheckersSettings extends SettingsPreferenceFragment - implements Preference.OnPreferenceClickListener { + implements OnSwitchChangeListener, OnPreferenceClickListener, + OnRadioButtonPreferenceListener { private static final String TAG = SpellCheckersSettings.class.getSimpleName(); private static final boolean DBG = false; + private static final String KEY_SPELL_CHECKER_LANGUAGE = "spellchecker_language"; + private static final int ITEM_ID_USE_SYSTEM_LANGUAGE = 0; + + private SwitchBar mSwitchBar; + private Preference mSpellCheckerLanaguagePref; private AlertDialog mDialog = null; private SpellCheckerInfo mCurrentSci; private SpellCheckerInfo[] mEnabledScis; private TextServicesManager mTsm; - private final ArrayList<SingleSpellCheckerPreference> mSpellCheckers = - new ArrayList<SingleSpellCheckerPreference>(); @Override - public void onCreate(Bundle icicle) { + public void onCreate(final Bundle icicle) { super.onCreate(icicle); - mTsm = (TextServicesManager) getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); + addPreferencesFromResource(R.xml.spellchecker_prefs); - updateScreen(); + mSpellCheckerLanaguagePref = findPreference(KEY_SPELL_CHECKER_LANGUAGE); + mSpellCheckerLanaguagePref.setOnPreferenceClickListener(this); + + mTsm = (TextServicesManager) getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); + mCurrentSci = mTsm.getCurrentSpellChecker(); + mEnabledScis = mTsm.getEnabledSpellCheckers(); + populatePreferenceScreen(); } - @Override - public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { - return false; + private void populatePreferenceScreen() { + final PreferenceScreen screen = getPreferenceScreen(); + final Context context = getActivity(); + final int count = (mEnabledScis == null) ? 0 : mEnabledScis.length; + for (int index = 0; index < count; ++index) { + final SpellCheckerInfo sci = mEnabledScis[index]; + final SpellCheckerPreference pref = new SpellCheckerPreference(context, sci, this); + screen.addPreference(pref); + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + } } @Override public void onResume() { super.onResume(); - updateScreen(); + mSwitchBar = ((SettingsActivity)getActivity()).getSwitchBar(); + mSwitchBar.show(); + mSwitchBar.addOnSwitchChangeListener(this); + updatePreferenceScreen(); } @Override public void onPause() { super.onPause(); - saveState(); + mSwitchBar.removeOnSwitchChangeListener(this); } - private void saveState() { - SpellCheckerUtils.setCurrentSpellChecker(mTsm, mCurrentSci); + @Override + public void onSwitchChanged(final Switch switchView, final boolean isChecked) { + mTsm.setSpellCheckerEnabled(isChecked); + updatePreferenceScreen(); } - private void updateScreen() { - getPreferenceScreen().removeAll(); - updateEnabledSpellCheckers(); + private void updatePreferenceScreen() { + mCurrentSci = mTsm.getCurrentSpellChecker(); + final boolean isSpellCheckerEnabled = mTsm.isSpellCheckerEnabled(); + mSwitchBar.setChecked(isSpellCheckerEnabled); + + final SpellCheckerSubtype currentScs = mTsm.getCurrentSpellCheckerSubtype( + false /* allowImplicitlySelectedSubtype */); + mSpellCheckerLanaguagePref.setSummary(getSpellCheckerSubtypeLabel(mCurrentSci, currentScs)); + + final PreferenceScreen screen = getPreferenceScreen(); + final int count = screen.getPreferenceCount(); + for (int index = 0; index < count; index++) { + final Preference preference = screen.getPreference(index); + preference.setEnabled(isSpellCheckerEnabled); + if (preference instanceof SpellCheckerPreference) { + final SpellCheckerPreference pref = (SpellCheckerPreference)preference; + final SpellCheckerInfo sci = pref.getSpellCheckerInfo(); + pref.setSelected(mCurrentSci != null && mCurrentSci.getId().equals(sci.getId())); + } + } } - private void updateEnabledSpellCheckers() { - final PackageManager pm = getPackageManager(); - mCurrentSci = SpellCheckerUtils.getCurrentSpellChecker(mTsm); - mEnabledScis = SpellCheckerUtils.getEnabledSpellCheckers(mTsm); - if (mCurrentSci == null || mEnabledScis == null) { - return; + private CharSequence getSpellCheckerSubtypeLabel(final SpellCheckerInfo sci, + final SpellCheckerSubtype subtype) { + if (sci == null) { + return null; } - mSpellCheckers.clear(); - for (int i = 0; i < mEnabledScis.length; ++i) { - final SpellCheckerInfo sci = mEnabledScis[i]; - final SingleSpellCheckerPreference scPref = new SingleSpellCheckerPreference( - this, null, sci, mTsm); - mSpellCheckers.add(scPref); - scPref.setTitle(sci.loadLabel(pm)); - scPref.setSelected(mCurrentSci != null && mCurrentSci.getId().equals(sci.getId())); - getPreferenceScreen().addPreference(scPref); + if (subtype == null) { + return getString(R.string.use_system_language_to_select_input_method_subtypes); } + return subtype.getDisplayName( + getActivity(), sci.getPackageName(), sci.getServiceInfo().applicationInfo); } @Override - public boolean onPreferenceClick(Preference pref) { - SingleSpellCheckerPreference targetScp = null; - for (SingleSpellCheckerPreference scp : mSpellCheckers) { - if (pref.equals(scp)) { - targetScp = scp; - } + public boolean onPreferenceClick(final Preference pref) { + if (pref == mSpellCheckerLanaguagePref) { + showChooseLanguageDialog(); + return true; } - if (targetScp != null) { - if (!isSystemApp(targetScp.getSpellCheckerInfo())) { - showSecurityWarnDialog(targetScp); - } else { - changeCurrentSpellChecker(targetScp); - } + return false; + } + + @Override + public void onRadioButtonClicked(final SpellCheckerPreference pref) { + final SpellCheckerInfo sci = pref.getSpellCheckerInfo(); + final boolean isSystemApp = + (sci.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + if (isSystemApp) { + changeCurrentSpellChecker(sci); + } else { + showSecurityWarnDialog(pref); } - return true; } - private void showSecurityWarnDialog(final SingleSpellCheckerPreference scp) { + private static int convertSubtypeIndexToDialogItemId(final int index) { return index + 1; } + private static int convertDialogItemIdToSubtypeIndex(final int item) { return item - 1; } + + private void showChooseLanguageDialog() { if (mDialog != null && mDialog.isShowing()) { mDialog.dismiss(); } - mDialog = (new AlertDialog.Builder(getActivity())) - .setTitle(android.R.string.dialog_alert_title) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - changeCurrentSpellChecker(scp); - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - } - }) - .create(); - mDialog.setMessage(getResources().getString(R.string.spellchecker_security_warning, - scp.getSpellCheckerInfo().getServiceInfo().applicationInfo.loadLabel( - getActivity().getPackageManager()))); + final SpellCheckerInfo currentSci = mTsm.getCurrentSpellChecker(); + final SpellCheckerSubtype currentScs = mTsm.getCurrentSpellCheckerSubtype( + false /* allowImplicitlySelectedSubtype */); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.phone_language); + final int subtypeCount = currentSci.getSubtypeCount(); + final CharSequence[] items = new CharSequence[subtypeCount + 1 /* default */ ]; + items[ITEM_ID_USE_SYSTEM_LANGUAGE] = getSpellCheckerSubtypeLabel(currentSci, null); + int checkedItemId = ITEM_ID_USE_SYSTEM_LANGUAGE; + for (int index = 0; index < subtypeCount; ++index) { + final SpellCheckerSubtype subtype = currentSci.getSubtypeAt(index); + final int itemId = convertSubtypeIndexToDialogItemId(index); + items[itemId] = getSpellCheckerSubtypeLabel(currentSci, subtype); + if (subtype.equals(currentScs)) { + checkedItemId = itemId; + } + } + builder.setSingleChoiceItems(items, checkedItemId, new AlertDialog.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int item) { + if (item == ITEM_ID_USE_SYSTEM_LANGUAGE) { + mTsm.setSpellCheckerSubtype(null); + } else { + final int index = convertDialogItemIdToSubtypeIndex(item); + mTsm.setSpellCheckerSubtype(currentSci.getSubtypeAt(index)); + } + if (DBG) { + final SpellCheckerSubtype subtype = mTsm.getCurrentSpellCheckerSubtype( + true /* allowImplicitlySelectedSubtype */); + Log.d(TAG, "Current spell check locale is " + + subtype == null ? "null" : subtype.getLocale()); + } + dialog.dismiss(); + updatePreferenceScreen(); + } + }); + mDialog = builder.create(); mDialog.show(); } - private void changeCurrentSpellChecker(SingleSpellCheckerPreference scp) { - mTsm.setCurrentSpellChecker(scp.getSpellCheckerInfo()); - if (DBG) { - Log.d(TAG, "Current spell check is " - + SpellCheckerUtils.getCurrentSpellChecker(mTsm).getId()); + private void showSecurityWarnDialog(final SpellCheckerPreference pref) { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); } - updateScreen(); + final SpellCheckerInfo sci = pref.getSpellCheckerInfo(); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(android.R.string.dialog_alert_title); + builder.setMessage(getString(R.string.spellchecker_security_warning, pref.getTitle())); + builder.setCancelable(true); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + changeCurrentSpellChecker(sci); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + } + }); + mDialog = builder.create(); + mDialog.show(); } - private static boolean isSystemApp(SpellCheckerInfo sci) { - return (sci.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + private void changeCurrentSpellChecker(final SpellCheckerInfo sci) { + mTsm.setCurrentSpellChecker(sci); + if (DBG) { + Log.d(TAG, "Current spell check is " + mTsm.getCurrentSpellChecker().getId()); + } + updatePreferenceScreen(); } } diff --git a/src/com/android/settings/wifi/AccessPointCategoryForSetupWizardXL.java b/src/com/android/settings/inputmethod/SwitchWithNoTextPreference.java index 7a1623b..677c031 100644 --- a/src/com/android/settings/wifi/AccessPointCategoryForSetupWizardXL.java +++ b/src/com/android/settings/inputmethod/SwitchWithNoTextPreference.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2014 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. @@ -14,22 +14,17 @@ * limitations under the License. */ -package com.android.settings.wifi; - -import com.android.settings.ProgressCategoryBase; -import com.android.settings.R; +package com.android.settings.inputmethod; import android.content.Context; -import android.util.AttributeSet; +import android.preference.SwitchPreference; -public class AccessPointCategoryForSetupWizardXL extends ProgressCategoryBase { - public AccessPointCategoryForSetupWizardXL(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutResource(R.layout.access_point_category_for_setup_wizard_xl); - } +class SwitchWithNoTextPreference extends SwitchPreference { + private static final String EMPTY_TEXT = ""; - @Override - public void setProgress(boolean progressOn) { - notifyChanged(); + SwitchWithNoTextPreference(final Context context) { + super(context); + setSwitchTextOn(EMPTY_TEXT); + setSwitchTextOff(EMPTY_TEXT); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java index d81703e..638818a 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java +++ b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java @@ -146,7 +146,9 @@ public class UserDictionaryAddWordContents { // should not insert, because either A. the word exists with no shortcut, in which // case the exact same thing we want to insert is already there, or B. the word // exists with at least one shortcut, in which case it has priority on our word. - if (hasWord(newWord, context)) return UserDictionaryAddWordActivity.CODE_ALREADY_PRESENT; + if (TextUtils.isEmpty(newShortcut) && hasWord(newWord, context)) { + return UserDictionaryAddWordActivity.CODE_ALREADY_PRESENT; + } // Disallow duplicates. If the same word with no shortcut is defined, remove it; if // the same word with the same shortcut is defined, remove it; but we don't mind if diff --git a/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java b/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java index 86c3e79..4f231cb 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java +++ b/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java @@ -18,7 +18,6 @@ package com.android.settings.inputmethod; import android.app.Fragment; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -29,6 +28,7 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.inputmethod.UserDictionaryAddWordContents.LocaleRenderer; import java.util.ArrayList; @@ -137,8 +137,8 @@ public class UserDictionaryAddWordFragment extends Fragment final long id) { final LocaleRenderer locale = (LocaleRenderer)parent.getItemAtPosition(pos); if (locale.isMoreLanguages()) { - PreferenceActivity preferenceActivity = (PreferenceActivity)getActivity(); - preferenceActivity.startPreferenceFragment(new UserDictionaryLocalePicker(this), true); + SettingsActivity sa = (SettingsActivity)getActivity(); + sa.startPreferenceFragment(new UserDictionaryLocalePicker(this), true); } else { mContents.updateLocale(locale.getLocaleString()); } diff --git a/src/com/android/settings/inputmethod/UserDictionaryList.java b/src/com/android/settings/inputmethod/UserDictionaryList.java index 24acb4a..7439001 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryList.java +++ b/src/com/android/settings/inputmethod/UserDictionaryList.java @@ -72,22 +72,27 @@ public class UserDictionaryList extends SettingsPreferenceFragment { mLocale = locale; } - public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) { - @SuppressWarnings("deprecation") - final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI, - new String[] { UserDictionary.Words.LOCALE }, + public static TreeSet<String> getUserDictionaryLocalesSet(Context context) { + final Cursor cursor = context.getContentResolver().query( + UserDictionary.Words.CONTENT_URI, new String[] { UserDictionary.Words.LOCALE }, null, null, null); final TreeSet<String> localeSet = 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 { - final String locale = cursor.getString(columnIndex); - localeSet.add(null != locale ? locale : ""); - } while (cursor.moveToNext()); } + try { + if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + final String locale = cursor.getString(columnIndex); + localeSet.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); + } + // CAVEAT: Keep this for consistency of the implementation between Keyboard and Settings // if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { // // For ICS, we need to show "For all languages" in case that the keyboard locale @@ -96,7 +101,7 @@ public class UserDictionaryList extends SettingsPreferenceFragment { // } final InputMethodManager imm = - (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); + (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); final List<InputMethodInfo> imis = imm.getEnabledInputMethodList(); for (final InputMethodInfo imi : imis) { final List<InputMethodSubtype> subtypes = diff --git a/src/com/android/settings/location/DimmableIconPreference.java b/src/com/android/settings/location/DimmableIconPreference.java new file mode 100644 index 0000000..acde1c1 --- /dev/null +++ b/src/com/android/settings/location/DimmableIconPreference.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 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.location; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.preference.Preference; +import android.util.AttributeSet; + +/** + * A preference item that can dim the icon when it's disabled, either directly or because its parent + * is disabled. + */ +public class DimmableIconPreference extends Preference { + private static final int ICON_ALPHA_ENABLED = 255; + private static final int ICON_ALPHA_DISABLED = 102; + + public DimmableIconPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public DimmableIconPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DimmableIconPreference(Context context) { + super(context); + } + + private void dimIcon(boolean dimmed) { + Drawable icon = getIcon(); + if (icon != null) { + icon.mutate().setAlpha(dimmed ? ICON_ALPHA_DISABLED : ICON_ALPHA_ENABLED); + setIcon(icon); + } + } + + @Override + public void onParentChanged(Preference parent, boolean disableChild) { + dimIcon(disableChild); + super.onParentChanged(parent, disableChild); + } + + @Override + public void setEnabled(boolean enabled) { + dimIcon(!enabled); + super.setEnabled(enabled); + } +} diff --git a/src/com/android/settings/location/InjectedSetting.java b/src/com/android/settings/location/InjectedSetting.java index d8a3f49..5f4440a 100644 --- a/src/com/android/settings/location/InjectedSetting.java +++ b/src/com/android/settings/location/InjectedSetting.java @@ -23,7 +23,7 @@ import com.android.internal.annotations.Immutable; import com.android.internal.util.Preconditions; /** - * Specifies a setting that is being injected into Settings > Location > Location services. + * Specifies a setting that is being injected into Settings > Location > Location services. * * @see android.location.SettingInjectorService */ diff --git a/src/com/android/settings/location/LocationSettings.java b/src/com/android/settings/location/LocationSettings.java index 06a6650..c0a13c1 100644 --- a/src/com/android/settings/location/LocationSettings.java +++ b/src/com/android/settings/location/LocationSettings.java @@ -16,27 +16,22 @@ package com.android.settings.location; -import android.app.ActionBar; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.location.LocationManager; import android.location.SettingInjectorService; 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.provider.Settings; import android.util.Log; -import android.view.Gravity; -import android.widget.CompoundButton; import android.widget.Switch; import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.widget.SwitchBar; import java.util.Collections; import java.util.Comparator; @@ -46,7 +41,7 @@ import java.util.List; * Location access settings. */ public class LocationSettings extends LocationSettingsBase - implements CompoundButton.OnCheckedChangeListener { + implements SwitchBar.OnSwitchChangeListener { private static final String TAG = "LocationSettings"; @@ -57,24 +52,40 @@ public class LocationSettings extends LocationSettingsBase /** Key for preference category "Location services" */ private static final String KEY_LOCATION_SERVICES = "location_services"; + private SwitchBar mSwitchBar; private Switch mSwitch; - private boolean mValidListener; + private boolean mValidListener = false; private Preference mLocationMode; private PreferenceCategory mCategoryRecentLocationRequests; /** Receives UPDATE_INTENT */ private BroadcastReceiver mReceiver; + private SettingsInjector injector; - public LocationSettings() { - mValidListener = false; + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final SettingsActivity activity = (SettingsActivity) getActivity(); + + mSwitchBar = activity.getSwitchBar(); + mSwitch = mSwitchBar.getSwitch(); + mSwitchBar.show(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mSwitchBar.hide(); } @Override public void onResume() { super.onResume(); - mSwitch = new Switch(getActivity()); - mSwitch.setOnCheckedChangeListener(this); - mValidListener = true; createPreferenceHierarchy(); + if (!mValidListener) { + mSwitchBar.addOnSwitchChangeListener(this); + mValidListener = true; + } } @Override @@ -83,10 +94,15 @@ public class LocationSettings extends LocationSettingsBase getActivity().unregisterReceiver(mReceiver); } catch (RuntimeException e) { // Ignore exceptions caused by race condition + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Swallowing " + e); + } + } + if (mValidListener) { + mSwitchBar.removeOnSwitchChangeListener(this); + mValidListener = false; } super.onPause(); - mValidListener = false; - mSwitch.setOnCheckedChangeListener(null); } private void addPreferencesSorted(List<Preference> prefs, PreferenceGroup container) { @@ -103,7 +119,7 @@ public class LocationSettings extends LocationSettingsBase } private PreferenceScreen createPreferenceHierarchy() { - final PreferenceActivity activity = (PreferenceActivity) getActivity(); + final SettingsActivity activity = (SettingsActivity) getActivity(); PreferenceScreen root = getPreferenceScreen(); if (root != null) { root.removeAll(); @@ -141,22 +157,6 @@ public class LocationSettings extends LocationSettingsBase addLocationServices(activity, root); - // Only show the master switch when we're not in multi-pane mode, and not being used as - // Setup Wizard. - if (activity.onIsHidingHeaders() || !activity.onIsMultiPane()) { - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - mSwitch.setPaddingRelative(0, 0, padding, 0); - activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, - ActionBar.DISPLAY_SHOW_CUSTOM); - activity.getActionBar().setCustomView(mSwitch, new ActionBar.LayoutParams( - ActionBar.LayoutParams.WRAP_CONTENT, - ActionBar.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_VERTICAL | Gravity.END)); - } - - setHasOptionsMenu(true); - refreshLocationMode(); return root; } @@ -166,15 +166,12 @@ public class LocationSettings extends LocationSettingsBase * category if there are no injected settings. * * Reloads the settings whenever receives - * {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}. As a safety measure, - * also reloads on {@link LocationManager#MODE_CHANGED_ACTION} to ensure the settings are - * up-to-date after mode changes even if an affected app doesn't send the setting changed - * broadcast. + * {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}. */ private void addLocationServices(Context context, PreferenceScreen root) { PreferenceCategory categoryLocationServices = (PreferenceCategory) root.findPreference(KEY_LOCATION_SERVICES); - final SettingsInjector injector = new SettingsInjector(context); + injector = new SettingsInjector(context); List<Preference> locationServices = injector.getInjectedSettings(); mReceiver = new BroadcastReceiver() { @@ -189,7 +186,6 @@ public class LocationSettings extends LocationSettingsBase IntentFilter filter = new IntentFilter(); filter.addAction(SettingInjectorService.ACTION_INJECTED_SETTING_CHANGED); - filter.addAction(LocationManager.MODE_CHANGED_ACTION); context.registerReceiver(mReceiver, filter); if (locationServices.size() > 0) { @@ -208,16 +204,16 @@ public class LocationSettings extends LocationSettingsBase @Override public void onModeChanged(int mode, boolean restricted) { switch (mode) { - case Settings.Secure.LOCATION_MODE_OFF: + case android.provider.Settings.Secure.LOCATION_MODE_OFF: mLocationMode.setSummary(R.string.location_mode_location_off_title); break; - case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: + case android.provider.Settings.Secure.LOCATION_MODE_SENSORS_ONLY: mLocationMode.setSummary(R.string.location_mode_sensors_only_title); break; - case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: + case android.provider.Settings.Secure.LOCATION_MODE_BATTERY_SAVING: mLocationMode.setSummary(R.string.location_mode_battery_saving_title); break; - case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: + case android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: mLocationMode.setSummary(R.string.location_mode_high_accuracy_title); break; default: @@ -227,32 +223,37 @@ public class LocationSettings extends LocationSettingsBase // Restricted user can't change the location mode, so disable the master switch. But in some // corner cases, the location might still be enabled. In such case the master switch should // be disabled but checked. - boolean enabled = (mode != Settings.Secure.LOCATION_MODE_OFF); - mSwitch.setEnabled(!restricted); + boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF); + // Disable the whole switch bar instead of the switch itself. If we disabled the switch + // only, it would be re-enabled again if the switch bar is not disabled. + mSwitchBar.setEnabled(!restricted); mLocationMode.setEnabled(enabled && !restricted); mCategoryRecentLocationRequests.setEnabled(enabled); if (enabled != mSwitch.isChecked()) { // set listener to null so that that code below doesn't trigger onCheckedChanged() if (mValidListener) { - mSwitch.setOnCheckedChangeListener(null); + mSwitchBar.removeOnSwitchChangeListener(this); } mSwitch.setChecked(enabled); if (mValidListener) { - mSwitch.setOnCheckedChangeListener(this); + mSwitchBar.addOnSwitchChangeListener(this); } } + // As a safety measure, also reloads on location mode change to ensure the settings are + // up-to-date even if an affected app doesn't send the setting changed broadcast. + injector.reloadStatusMessages(); } /** * Listens to the state change of the location master switch. */ @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + public void onSwitchChanged(Switch switchView, boolean isChecked) { if (isChecked) { - setLocationMode(Settings.Secure.LOCATION_MODE_HIGH_ACCURACY); + setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY); } else { - setLocationMode(Settings.Secure.LOCATION_MODE_OFF); + setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_OFF); } } } diff --git a/src/com/android/settings/location/LocationSettingsBase.java b/src/com/android/settings/location/LocationSettingsBase.java index 86c2ee5..69fbd5c 100644 --- a/src/com/android/settings/location/LocationSettingsBase.java +++ b/src/com/android/settings/location/LocationSettingsBase.java @@ -16,12 +16,11 @@ package com.android.settings.location; -import android.app.LoaderManager.LoaderCallbacks; +import android.content.BroadcastReceiver; import android.content.Context; -import android.content.CursorLoader; import android.content.Intent; -import android.content.Loader; -import android.database.Cursor; +import android.content.IntentFilter; +import android.location.LocationManager; import android.os.Bundle; import android.os.UserManager; import android.provider.Settings; @@ -33,8 +32,7 @@ import com.android.settings.SettingsPreferenceFragment; * A base class that listens to location settings change and modifies location * settings. */ -public abstract class LocationSettingsBase extends SettingsPreferenceFragment - implements LoaderCallbacks<Cursor> { +public abstract class LocationSettingsBase extends SettingsPreferenceFragment { private static final String TAG = "LocationSettingsBase"; /** Broadcast intent action when the location mode is about to change. */ private static final String MODE_CHANGING_ACTION = @@ -42,8 +40,8 @@ public abstract class LocationSettingsBase extends SettingsPreferenceFragment private static final String CURRENT_MODE_KEY = "CURRENT_MODE"; private static final String NEW_MODE_KEY = "NEW_MODE"; - private static final int LOADER_ID_LOCATION_MODE = 1; private int mCurrentMode; + private BroadcastReceiver mReceiver; /** * Whether the fragment is actively running. @@ -53,17 +51,33 @@ public abstract class LocationSettingsBase extends SettingsPreferenceFragment @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - getLoaderManager().initLoader(LOADER_ID_LOCATION_MODE, null, this); + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Received location mode change intent: " + intent); + } + refreshLocationMode(); + } + }; } @Override public void onResume() { super.onResume(); mActive = true; + IntentFilter filter = new IntentFilter(); + filter.addAction(LocationManager.MODE_CHANGED_ACTION); + getActivity().registerReceiver(mReceiver, filter); } @Override public void onPause() { + try { + getActivity().unregisterReceiver(mReceiver); + } catch (RuntimeException e) { + // Ignore exceptions caused by race condition + } super.onPause(); mActive = false; } @@ -103,29 +117,10 @@ public abstract class LocationSettingsBase extends SettingsPreferenceFragment int mode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF); mCurrentMode = mode; + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, "Location mode has been changed"); + } onModeChanged(mode, isRestricted()); } } - - @Override - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - switch (id) { - case LOADER_ID_LOCATION_MODE: - return new CursorLoader(getActivity(), Settings.Secure.CONTENT_URI, null, - "(" + Settings.System.NAME + "=?)", - new String[] { Settings.Secure.LOCATION_MODE }, null); - default: - return null; - } - } - - @Override - public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - refreshLocationMode(); - } - - @Override - public void onLoaderReset(Loader<Cursor> loader) { - // Nothing to do here. - } } diff --git a/src/com/android/settings/location/RecentLocationApps.java b/src/com/android/settings/location/RecentLocationApps.java index 5708434..7e99725 100644 --- a/src/com/android/settings/location/RecentLocationApps.java +++ b/src/com/android/settings/location/RecentLocationApps.java @@ -16,20 +16,26 @@ package com.android.settings.location; -import android.app.ActivityManager; +import android.app.AppGlobals; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.IPackageManager; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Process; +import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.util.Log; +import android.view.View; +import android.widget.TextView; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.applications.InstalledAppDetails; import java.util.ArrayList; @@ -44,10 +50,10 @@ public class RecentLocationApps { private static final int RECENT_TIME_INTERVAL_MILLIS = 15 * 60 * 1000; - private final PreferenceActivity mActivity; + private final SettingsActivity mActivity; private final PackageManager mPackageManager; - public RecentLocationApps(PreferenceActivity activity) { + public RecentLocationApps(SettingsActivity activity) { mActivity = activity; mPackageManager = activity.getPackageManager(); } @@ -71,12 +77,35 @@ public class RecentLocationApps { } } - private Preference createRecentLocationEntry( + /** + * Subclass of {@link Preference} to intercept views and allow content description to be set on + * them for accessibility purposes. + */ + private static class AccessiblePreference extends DimmableIconPreference { + public CharSequence mContentDescription; + + public AccessiblePreference(Context context, CharSequence contentDescription) { + super(context); + mContentDescription = contentDescription; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + if (mContentDescription != null) { + final TextView titleView = (TextView) view.findViewById(android.R.id.title); + titleView.setContentDescription(mContentDescription); + } + } + } + + private AccessiblePreference createRecentLocationEntry( Drawable icon, CharSequence label, boolean isHighBattery, + CharSequence contentDescription, Preference.OnPreferenceClickListener listener) { - Preference pref = new Preference(mActivity); + AccessiblePreference pref = new AccessiblePreference(mActivity, contentDescription); pref.setIcon(icon); pref.setTitle(label); if (isHighBattery) { @@ -89,33 +118,37 @@ public class RecentLocationApps { } /** - * Fills a list of applications which queried location recently within - * specified time. + * Fills a list of applications which queried location recently within specified time. */ public List<Preference> getAppList() { // Retrieve a location usage list from AppOps AppOpsManager aoManager = (AppOpsManager) mActivity.getSystemService(Context.APP_OPS_SERVICE); - List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps( - new int[] { - AppOpsManager.OP_MONITOR_LOCATION, - AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, - }); + List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(new int[] { + AppOpsManager.OP_MONITOR_LOCATION, AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, }); // Process the AppOps list and generate a preference list. ArrayList<Preference> prefs = new ArrayList<Preference>(); - long now = System.currentTimeMillis(); - for (AppOpsManager.PackageOps ops : appOps) { + final long now = System.currentTimeMillis(); + final UserManager um = (UserManager) mActivity.getSystemService(Context.USER_SERVICE); + final List<UserHandle> profiles = um.getUserProfiles(); + + final int appOpsN = appOps.size(); + for (int i = 0; i < appOpsN; ++i) { + AppOpsManager.PackageOps ops = appOps.get(i); // Don't show the Android System in the list - it's not actionable for the user. - // Also don't show apps belonging to background users. + // Also don't show apps belonging to background users except managed users. + String packageName = ops.getPackageName(); int uid = ops.getUid(); - boolean isAndroidOs = (uid == Process.SYSTEM_UID) - && ANDROID_SYSTEM_PACKAGE_NAME.equals(ops.getPackageName()); - if (!isAndroidOs && ActivityManager.getCurrentUser() == UserHandle.getUserId(uid)) { - Preference pref = getPreferenceFromOps(now, ops); - if (pref != null) { - prefs.add(pref); - } + int userId = UserHandle.getUserId(uid); + boolean isAndroidOs = + (uid == Process.SYSTEM_UID) && ANDROID_SYSTEM_PACKAGE_NAME.equals(packageName); + if (isAndroidOs || !profiles.contains(new UserHandle(userId))) { + continue; + } + Preference preference = getPreferenceFromOps(um, now, ops); + if (preference != null) { + prefs.add(preference); } } @@ -130,7 +163,8 @@ public class RecentLocationApps { * When the PackageOps is fresh enough, this method returns a Preference pointing to the App * Info page for that package. */ - private Preference getPreferenceFromOps(long now, AppOpsManager.PackageOps ops) { + private Preference getPreferenceFromOps(final UserManager um, long now, + AppOpsManager.PackageOps ops) { String packageName = ops.getPackageName(); List<AppOpsManager.OpEntry> entries = ops.getOps(); boolean highBattery = false; @@ -161,30 +195,34 @@ public class RecentLocationApps { // The package is fresh enough, continue. - Preference pref = null; + int uid = ops.getUid(); + int userId = UserHandle.getUserId(uid); + + AccessiblePreference preference = null; try { - ApplicationInfo appInfo = mPackageManager.getApplicationInfo( - packageName, PackageManager.GET_META_DATA); - // Multiple users can install the same package. Each user gets a different Uid for - // the same package. - // - // Here we retrieve the Uid with package name, that will be the Uid for that package - // associated with the current active user. If the Uid differs from the Uid in ops, - // that means this entry belongs to another inactive user and we should ignore that. - if (appInfo.uid == ops.getUid()) { - pref = createRecentLocationEntry( - mPackageManager.getApplicationIcon(appInfo), - mPackageManager.getApplicationLabel(appInfo), - highBattery, - new PackageEntryClickedListener(packageName)); - } else if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "package " + packageName + " with Uid " + ops.getUid() + - " belongs to another inactive account, ignored."); - } - } catch (PackageManager.NameNotFoundException e) { - Log.wtf(TAG, "Package not found: " + packageName, e); + IPackageManager ipm = AppGlobals.getPackageManager(); + ApplicationInfo appInfo = + ipm.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId); + if (appInfo == null) { + Log.w(TAG, "Null application info retrieved for package " + packageName + + ", userId " + userId); + return null; + } + Resources res = mActivity.getResources(); + + final UserHandle userHandle = new UserHandle(userId); + Drawable appIcon = mPackageManager.getApplicationIcon(appInfo); + Drawable icon = mPackageManager.getUserBadgedIcon(appIcon, userHandle); + CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo); + CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle); + preference = createRecentLocationEntry(icon, + appLabel, highBattery, badgedAppLabel, + new PackageEntryClickedListener(packageName)); + } catch (RemoteException e) { + Log.w(TAG, "Error while retrieving application info for package " + packageName + + ", userId " + userId, e); } - return pref; + return preference; } } diff --git a/src/com/android/settings/location/SettingsInjector.java b/src/com/android/settings/location/SettingsInjector.java index b919080..edf67b8 100644 --- a/src/com/android/settings/location/SettingsInjector.java +++ b/src/com/android/settings/location/SettingsInjector.java @@ -244,18 +244,21 @@ class SettingsInjector { } /** - * Adds an injected setting to the root with status "Loading...". + * Adds an injected setting to the root. */ private Preference addServiceSetting(List<Preference> prefs, InjectedSetting info) { - Preference pref = new Preference(mContext); + Preference pref = new DimmableIconPreference(mContext); pref.setTitle(info.title); - pref.setSummary(R.string.location_loading_injected_setting); + pref.setSummary(null); PackageManager pm = mContext.getPackageManager(); Drawable icon = pm.getDrawable(info.packageName, info.iconId, null); pref.setIcon(icon); + // Activity to start if they click on the preference. Must start in new task to ensure + // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to Settings > Location. Intent settingIntent = new Intent(); settingIntent.setClassName(info.packageName, info.settingsActivity); + settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); pref.setIntent(settingIntent); prefs.add(pref); @@ -425,12 +428,11 @@ class SettingsInjector { @Override public void handleMessage(Message msg) { Bundle bundle = msg.getData(); - String summary = bundle.getString(SettingInjectorService.SUMMARY_KEY); boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle); } - preference.setSummary(summary); + preference.setSummary(null); preference.setEnabled(enabled); mHandler.sendMessage( mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this)); diff --git a/src/com/android/settings/net/DataUsageMeteredSettings.java b/src/com/android/settings/net/DataUsageMeteredSettings.java index fb000db..d567c7e 100644 --- a/src/com/android/settings/net/DataUsageMeteredSettings.java +++ b/src/com/android/settings/net/DataUsageMeteredSettings.java @@ -22,24 +22,31 @@ import static com.android.settings.DataUsageSummary.hasReadyMobileRadio; import static com.android.settings.DataUsageSummary.hasWifiRadio; import android.content.Context; +import android.content.res.Resources; import android.net.NetworkPolicy; import android.net.NetworkPolicyManager; import android.net.NetworkTemplate; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Bundle; -import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceCategory; +import android.preference.SwitchPreference; import android.telephony.TelephonyManager; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; + +import java.util.ArrayList; +import java.util.List; /** * Panel to configure {@link NetworkPolicy#metered} for networks. */ -public class DataUsageMeteredSettings extends SettingsPreferenceFragment { +public class DataUsageMeteredSettings extends SettingsPreferenceFragment implements Indexable { private static final boolean SHOW_MOBILE_CATEGORY = false; @@ -108,7 +115,7 @@ public class DataUsageMeteredSettings extends SettingsPreferenceFragment { return pref; } - private class MeteredPreference extends CheckBoxPreference { + private class MeteredPreference extends SwitchPreference { private final NetworkTemplate mTemplate; private boolean mBinding; @@ -141,4 +148,82 @@ public class DataUsageMeteredSettings extends SettingsPreferenceFragment { } } } + + /** + * For search + */ + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_menu_metered); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + + // Body + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_metered_body); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + + if (SHOW_MOBILE_CATEGORY && hasReadyMobileRadio(context)) { + // Mobile networks category + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_metered_mobile); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + + final TelephonyManager tele = TelephonyManager.from(context); + + data = new SearchIndexableRaw(context); + data.title = tele.getNetworkOperatorName(); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + } + + // Wi-Fi networks category + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_metered_wifi); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + + final WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + if (hasWifiRadio(context) && wifiManager.isWifiEnabled()) { + for (WifiConfiguration config : wifiManager.getConfiguredNetworks()) { + if (config.SSID != null) { + final String networkId = config.SSID; + + data = new SearchIndexableRaw(context); + data.title = removeDoubleQuotes(networkId); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + } + } + } else { + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.data_usage_metered_wifi_disabled); + data.screenTitle = res.getString(R.string.data_usage_menu_metered); + result.add(data); + } + + return result; + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final ArrayList<String> result = new ArrayList<String>(); + if (!SHOW_MOBILE_CATEGORY || !hasReadyMobileRadio(context)) { + result.add("mobile"); + } + + return result; + } + }; + } diff --git a/src/com/android/settings/net/UidDetail.java b/src/com/android/settings/net/UidDetail.java index fd44d47..0b14254 100644 --- a/src/com/android/settings/net/UidDetail.java +++ b/src/com/android/settings/net/UidDetail.java @@ -20,6 +20,8 @@ import android.graphics.drawable.Drawable; public class UidDetail { public CharSequence label; + public CharSequence contentDescription; public CharSequence[] detailLabels; + public CharSequence[] detailContentDescriptions; public Drawable icon; } diff --git a/src/com/android/settings/net/UidDetailProvider.java b/src/com/android/settings/net/UidDetailProvider.java index aca3e5d..4b54137 100644 --- a/src/com/android/settings/net/UidDetailProvider.java +++ b/src/com/android/settings/net/UidDetailProvider.java @@ -16,8 +16,10 @@ package com.android.settings.net; +import android.app.AppGlobals; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -27,23 +29,36 @@ import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.TrafficStats; import android.os.UserManager; +import android.os.UserHandle; +import android.os.RemoteException; import android.text.TextUtils; +import android.util.Log; import android.util.SparseArray; import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.users.UserUtils; /** * Return details about a specific UID, handling special cases like * {@link TrafficStats#UID_TETHERING} and {@link UserInfo}. */ public class UidDetailProvider { + private static final String TAG = "DataUsage"; private final Context mContext; private final SparseArray<UidDetail> mUidDetailCache; + public static final int OTHER_USER_RANGE_START = -2000; + public static int buildKeyForUser(int userHandle) { - return -(2000 + userHandle); + return OTHER_USER_RANGE_START - userHandle; + } + + public static boolean isKeyForUser(int key) { + return key <= OTHER_USER_RANGE_START; + } + + public static int getUserIdForKey(int key) { + return OTHER_USER_RANGE_START - key; } public UidDetailProvider(Context context) { @@ -114,14 +129,22 @@ public class UidDetailProvider { return detail; } + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + // Handle keys that are actually user handles - if (uid <= -2000) { - final int userHandle = (-uid) - 2000; - final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + if (isKeyForUser(uid)) { + final int userHandle = getUserIdForKey(uid); final UserInfo info = um.getUserInfo(userHandle); if (info != null) { - detail.label = res.getString(R.string.running_process_item_user_label, info.name); - detail.icon = UserUtils.getUserIcon(mContext, um, info, res); + if (info.isManagedProfile()) { + detail.label = res.getString(R.string.managed_user_title); + detail.icon = Resources.getSystem().getDrawable( + com.android.internal.R.drawable.ic_corp_icon); + } else { + detail.label = res.getString(R.string.running_process_item_user_label, + info.name); + detail.icon = Utils.getUserIcon(mContext, um, info); + } return detail; } } @@ -130,26 +153,43 @@ public class UidDetailProvider { final String[] packageNames = pm.getPackagesForUid(uid); final int length = packageNames != null ? packageNames.length : 0; try { + final int userId = UserHandle.getUserId(uid); + UserHandle userHandle = new UserHandle(userId); + IPackageManager ipm = AppGlobals.getPackageManager(); if (length == 1) { - final ApplicationInfo info = pm.getApplicationInfo(packageNames[0], 0); - detail.label = info.loadLabel(pm).toString(); - detail.icon = info.loadIcon(pm); + final ApplicationInfo info = ipm.getApplicationInfo(packageNames[0], + 0 /* no flags */, userId); + if (info != null) { + detail.label = info.loadLabel(pm).toString(); + detail.icon = um.getBadgedIconForUser(info.loadIcon(pm), + new UserHandle(userId)); + } } else if (length > 1) { detail.detailLabels = new CharSequence[length]; + detail.detailContentDescriptions = 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); + final ApplicationInfo appInfo = ipm.getApplicationInfo(packageName, + 0 /* no flags */, userId); + + if (appInfo != null) { + detail.detailLabels[i] = appInfo.loadLabel(pm).toString(); + detail.detailContentDescriptions[i] = um.getBadgedLabelForUser( + detail.detailLabels[i], userHandle); + if (packageInfo.sharedUserLabel != 0) { + detail.label = pm.getText(packageName, packageInfo.sharedUserLabel, + packageInfo.applicationInfo).toString(); + detail.icon = um.getBadgedIconForUser(appInfo.loadIcon(pm), userHandle); + } } } } + detail.contentDescription = um.getBadgedLabelForUser(detail.label, userHandle); } catch (NameNotFoundException e) { + Log.w(TAG, "Error while building UI detail for uid "+uid, e); + } catch (RemoteException e) { + Log.w(TAG, "Error while building UI detail for uid "+uid, e); } if (TextUtils.isEmpty(detail.label)) { diff --git a/src/com/android/settings/nfc/AndroidBeam.java b/src/com/android/settings/nfc/AndroidBeam.java index 158ca78..20201f4 100644 --- a/src/com/android/settings/nfc/AndroidBeam.java +++ b/src/com/android/settings/nfc/AndroidBeam.java @@ -17,89 +17,85 @@ package com.android.settings.nfc; import android.app.ActionBar; -import android.app.Activity; import android.app.Fragment; +import android.content.Context; import android.nfc.NfcAdapter; import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceActivity; -import android.view.Gravity; +import android.os.UserManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.ImageView; import android.widget.Switch; + import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.widget.SwitchBar; public class AndroidBeam extends Fragment - implements CompoundButton.OnCheckedChangeListener { + implements SwitchBar.OnSwitchChangeListener { private View mView; private NfcAdapter mNfcAdapter; - private Switch mActionBarSwitch; + private SwitchBar mSwitchBar; private CharSequence mOldActivityTitle; + private boolean mBeamDisallowed; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Activity activity = getActivity(); - - mActionBarSwitch = new Switch(activity); - if (activity instanceof PreferenceActivity) { - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - mActionBarSwitch.setPaddingRelative(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.END)); - mOldActivityTitle = activity.getActionBar().getTitle(); - activity.getActionBar().setTitle(R.string.android_beam_settings_title); - } + final ActionBar actionBar = getActivity().getActionBar(); - mActionBarSwitch.setOnCheckedChangeListener(this); + mOldActivityTitle = actionBar.getTitle(); + actionBar.setTitle(R.string.android_beam_settings_title); mNfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); - mActionBarSwitch.setChecked(mNfcAdapter.isNdefPushEnabled()); + mBeamDisallowed = ((UserManager) getActivity().getSystemService(Context.USER_SERVICE)) + .hasUserRestriction(UserManager.DISALLOW_OUTGOING_BEAM); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mView = inflater.inflate(R.layout.android_beam, container, false); - initView(mView); + return mView; } @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + SettingsActivity activity = (SettingsActivity) getActivity(); + + mSwitchBar = activity.getSwitchBar(); + mSwitchBar.setChecked(!mBeamDisallowed && mNfcAdapter.isNdefPushEnabled()); + mSwitchBar.addOnSwitchChangeListener(this); + mSwitchBar.setEnabled(!mBeamDisallowed); + mSwitchBar.show(); + } + + @Override public void onDestroyView() { super.onDestroyView(); - getActivity().getActionBar().setCustomView(null); if (mOldActivityTitle != null) { getActivity().getActionBar().setTitle(mOldActivityTitle); } - } - - private void initView(View view) { - mActionBarSwitch.setOnCheckedChangeListener(this); - mActionBarSwitch.setChecked(mNfcAdapter.isNdefPushEnabled()); + mSwitchBar.removeOnSwitchChangeListener(this); + mSwitchBar.hide(); } @Override - public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) { + public void onSwitchChanged(Switch switchView, boolean desiredState) { boolean success = false; - mActionBarSwitch.setEnabled(false); + mSwitchBar.setEnabled(false); if (desiredState) { success = mNfcAdapter.enableNdefPush(); } else { success = mNfcAdapter.disableNdefPush(); } if (success) { - mActionBarSwitch.setChecked(desiredState); + mSwitchBar.setChecked(desiredState); } - mActionBarSwitch.setEnabled(true); + mSwitchBar.setEnabled(true); } } diff --git a/src/com/android/settings/nfc/NfcEnabler.java b/src/com/android/settings/nfc/NfcEnabler.java index 018b8ae..ae61b13 100644 --- a/src/com/android/settings/nfc/NfcEnabler.java +++ b/src/com/android/settings/nfc/NfcEnabler.java @@ -21,9 +21,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.nfc.NfcAdapter; -import android.preference.CheckBoxPreference; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; import com.android.settings.R; @@ -34,10 +35,11 @@ import com.android.settings.R; */ public class NfcEnabler implements Preference.OnPreferenceChangeListener { private final Context mContext; - private final CheckBoxPreference mCheckbox; + private final SwitchPreference mSwitch; private final PreferenceScreen mAndroidBeam; private final NfcAdapter mNfcAdapter; private final IntentFilter mIntentFilter; + private boolean mBeamDisallowed; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -50,20 +52,25 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { } }; - public NfcEnabler(Context context, CheckBoxPreference checkBoxPreference, + public NfcEnabler(Context context, SwitchPreference switchPreference, PreferenceScreen androidBeam) { mContext = context; - mCheckbox = checkBoxPreference; + mSwitch = switchPreference; mAndroidBeam = androidBeam; mNfcAdapter = NfcAdapter.getDefaultAdapter(context); + mBeamDisallowed = ((UserManager) mContext.getSystemService(Context.USER_SERVICE)) + .hasUserRestriction(UserManager.DISALLOW_OUTGOING_BEAM); if (mNfcAdapter == null) { // NFC is not supported - mCheckbox.setEnabled(false); + mSwitch.setEnabled(false); mAndroidBeam.setEnabled(false); mIntentFilter = null; return; } + if (mBeamDisallowed) { + mAndroidBeam.setEnabled(false); + } mIntentFilter = new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); } @@ -73,7 +80,7 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { } handleNfcStateChanged(mNfcAdapter.getAdapterState()); mContext.registerReceiver(mReceiver, mIntentFilter); - mCheckbox.setOnPreferenceChangeListener(this); + mSwitch.setOnPreferenceChangeListener(this); } public void pause() { @@ -81,14 +88,14 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { return; } mContext.unregisterReceiver(mReceiver); - mCheckbox.setOnPreferenceChangeListener(null); + mSwitch.setOnPreferenceChangeListener(null); } public boolean onPreferenceChange(Preference preference, Object value) { // Turn NFC on/off final boolean desiredState = (Boolean) value; - mCheckbox.setEnabled(false); + mSwitch.setEnabled(false); if (desiredState) { mNfcAdapter.enable(); @@ -102,29 +109,29 @@ public class NfcEnabler implements Preference.OnPreferenceChangeListener { private void handleNfcStateChanged(int newState) { switch (newState) { case NfcAdapter.STATE_OFF: - mCheckbox.setChecked(false); - mCheckbox.setEnabled(true); + mSwitch.setChecked(false); + mSwitch.setEnabled(true); mAndroidBeam.setEnabled(false); mAndroidBeam.setSummary(R.string.android_beam_disabled_summary); break; case NfcAdapter.STATE_ON: - mCheckbox.setChecked(true); - mCheckbox.setEnabled(true); - mAndroidBeam.setEnabled(true); - if (mNfcAdapter.isNdefPushEnabled()) { + mSwitch.setChecked(true); + mSwitch.setEnabled(true); + mAndroidBeam.setEnabled(!mBeamDisallowed); + if (mNfcAdapter.isNdefPushEnabled() && !mBeamDisallowed) { mAndroidBeam.setSummary(R.string.android_beam_on_summary); } else { mAndroidBeam.setSummary(R.string.android_beam_off_summary); } break; case NfcAdapter.STATE_TURNING_ON: - mCheckbox.setChecked(true); - mCheckbox.setEnabled(false); + mSwitch.setChecked(true); + mSwitch.setEnabled(false); mAndroidBeam.setEnabled(false); break; case NfcAdapter.STATE_TURNING_OFF: - mCheckbox.setChecked(false); - mCheckbox.setEnabled(false); + mSwitch.setChecked(false); + mSwitch.setEnabled(false); mAndroidBeam.setEnabled(false); break; } diff --git a/src/com/android/settings/nfc/PaymentBackend.java b/src/com/android/settings/nfc/PaymentBackend.java index f84bc74..25572a7 100644 --- a/src/com/android/settings/nfc/PaymentBackend.java +++ b/src/com/android/settings/nfc/PaymentBackend.java @@ -24,6 +24,7 @@ import android.nfc.NfcAdapter; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.cardemulation.CardEmulation; import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; import java.util.ArrayList; import java.util.List; @@ -74,6 +75,20 @@ public class PaymentBackend { return appInfos; } + boolean isForegroundMode() { + try { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.NFC_PAYMENT_FOREGROUND) != 0; + } catch (SettingNotFoundException e) { + return false; + } + } + + void setForegroundMode(boolean foreground) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.NFC_PAYMENT_FOREGROUND, foreground ? 1 : 0) ; + } + ComponentName getDefaultPaymentApp() { String componentString = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT); diff --git a/src/com/android/settings/nfc/PaymentDefaultDialog.java b/src/com/android/settings/nfc/PaymentDefaultDialog.java index 61c6fdb..33ac947 100644 --- a/src/com/android/settings/nfc/PaymentDefaultDialog.java +++ b/src/com/android/settings/nfc/PaymentDefaultDialog.java @@ -19,14 +19,7 @@ package com.android.settings.nfc; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.cardemulation.CardEmulation; -import android.nfc.cardemulation.HostApduService; -import android.nfc.cardemulation.OffHostApduService; import android.os.Bundle; import android.util.Log; @@ -35,15 +28,13 @@ import com.android.internal.app.AlertController; import com.android.settings.R; import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; -import java.io.IOException; import java.util.List; -import org.xmlpull.v1.XmlPullParserException; - public final class PaymentDefaultDialog extends AlertActivity implements DialogInterface.OnClickListener { public static final String TAG = "PaymentDefaultDialog"; + private static final int PAYMENT_APP_MAX_CAPTION_LENGTH = 40; private PaymentBackend mBackend; private ComponentName mNewDefault; @@ -119,12 +110,14 @@ public final class PaymentDefaultDialog extends AlertActivity implements p.mTitle = getString(R.string.nfc_payment_set_default_label); if (defaultPaymentApp == null) { String formatString = getString(R.string.nfc_payment_set_default); - String msg = String.format(formatString, requestedPaymentApp.caption); + String msg = String.format(formatString, + sanitizePaymentAppCaption(requestedPaymentApp.caption.toString())); p.mMessage = msg; } else { String formatString = getString(R.string.nfc_payment_set_default_instead_of); - String msg = String.format(formatString, requestedPaymentApp.caption, - defaultPaymentApp.caption); + String msg = String.format(formatString, + sanitizePaymentAppCaption(requestedPaymentApp.caption.toString()), + sanitizePaymentAppCaption(defaultPaymentApp.caption.toString())); p.mMessage = msg; } p.mPositiveButtonText = getString(R.string.yes); @@ -136,4 +129,15 @@ public final class PaymentDefaultDialog extends AlertActivity implements return true; } + private String sanitizePaymentAppCaption(String input) { + String sanitizedString = input.replace('\n', ' ').replace('\r', ' ').trim(); + + + if (sanitizedString.length() > PAYMENT_APP_MAX_CAPTION_LENGTH) { + return sanitizedString.substring(0, PAYMENT_APP_MAX_CAPTION_LENGTH); + } + + return sanitizedString; + } + } diff --git a/src/com/android/settings/nfc/PaymentSettings.java b/src/com/android/settings/nfc/PaymentSettings.java index 7548c50..df4e396 100644 --- a/src/com/android/settings/nfc/PaymentSettings.java +++ b/src/com/android/settings/nfc/PaymentSettings.java @@ -22,7 +22,9 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.provider.Settings; @@ -48,7 +50,7 @@ import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; import java.util.List; public class PaymentSettings extends SettingsPreferenceFragment implements - OnClickListener { + OnClickListener, OnPreferenceChangeListener { public static final String TAG = "PaymentSettings"; private LayoutInflater mInflater; private PaymentBackend mPaymentBackend; @@ -67,6 +69,7 @@ public class PaymentSettings extends SettingsPreferenceFragment implements public void refresh() { PreferenceManager manager = getPreferenceManager(); PreferenceScreen screen = manager.createPreferenceScreen(getActivity()); + // Get all payment services List<PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); if (appInfos != null && appInfos.size() > 0) { @@ -92,6 +95,13 @@ public class PaymentSettings extends SettingsPreferenceFragment implements emptyImage.setVisibility(View.VISIBLE); getListView().setVisibility(View.GONE); } else { + CheckBoxPreference foreground = new CheckBoxPreference(getActivity()); + boolean foregroundMode = mPaymentBackend.isForegroundMode(); + foreground.setPersistent(false); + foreground.setTitle(getString(R.string.nfc_payment_favor_foreground)); + foreground.setChecked(foregroundMode); + foreground.setOnPreferenceChangeListener(this); + screen.addPreference(foreground); emptyText.setVisibility(View.GONE); learnMore.setVisibility(View.GONE); emptyImage.setVisibility(View.GONE); @@ -207,14 +217,25 @@ public class PaymentSettings extends SettingsPreferenceFragment implements protected void onBindView(View view) { super.onBindView(view); - view.setOnClickListener(listener); - view.setTag(appInfo); - RadioButton radioButton = (RadioButton) view.findViewById(android.R.id.button1); radioButton.setChecked(appInfo.isDefault); + radioButton.setOnClickListener(listener); + radioButton.setTag(appInfo); ImageView banner = (ImageView) view.findViewById(R.id.banner); banner.setImageDrawable(appInfo.banner); + banner.setOnClickListener(listener); + banner.setTag(appInfo); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference instanceof CheckBoxPreference) { + mPaymentBackend.setForegroundMode(((Boolean) newValue).booleanValue()); + return true; + } else { + return false; } } } diff --git a/src/com/android/settings/notification/AppNotificationSettings.java b/src/com/android/settings/notification/AppNotificationSettings.java new file mode 100644 index 0000000..0eeefa9 --- /dev/null +++ b/src/com/android/settings/notification/AppNotificationSettings.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.notification.NotificationAppList.AppRow; +import com.android.settings.notification.NotificationAppList.Backend; + +/** These settings are per app, so should not be returned in global search results. */ +public class AppNotificationSettings extends SettingsPreferenceFragment { + private static final String TAG = "AppNotificationSettings"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final String KEY_BLOCK = "block"; + private static final String KEY_PRIORITY = "priority"; + private static final String KEY_SENSITIVE = "sensitive"; + + static final String EXTRA_HAS_SETTINGS_INTENT = "has_settings_intent"; + static final String EXTRA_SETTINGS_INTENT = "settings_intent"; + + private final Backend mBackend = new Backend(); + + private Context mContext; + private SwitchPreference mBlock; + private SwitchPreference mPriority; + private SwitchPreference mSensitive; + private AppRow mAppRow; + private boolean mCreated; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (DEBUG) Log.d(TAG, "onActivityCreated mCreated=" + mCreated); + if (mCreated) { + Log.w(TAG, "onActivityCreated: ignoring duplicate call"); + return; + } + mCreated = true; + if (mAppRow == null) return; + final View content = getActivity().findViewById(R.id.main_content); + final ViewGroup contentParent = (ViewGroup) content.getParent(); + final View bar = getActivity().getLayoutInflater().inflate(R.layout.app_notification_header, + contentParent, false); + + final ImageView appIcon = (ImageView) bar.findViewById(R.id.app_icon); + appIcon.setImageDrawable(mAppRow.icon); + + final TextView appName = (TextView) bar.findViewById(R.id.app_name); + appName.setText(mAppRow.label); + + final View appSettings = bar.findViewById(R.id.app_settings); + if (mAppRow.settingsIntent == null) { + appSettings.setVisibility(View.GONE); + } else { + appSettings.setClickable(true); + appSettings.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mContext.startActivity(mAppRow.settingsIntent); + } + }); + } + contentParent.addView(bar, 0); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getActivity(); + Intent intent = getActivity().getIntent(); + if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent); + if (intent == null) { + Log.w(TAG, "No intent"); + toastAndFinish(); + return; + } + + final int uid = intent.getIntExtra(Settings.EXTRA_APP_UID, -1); + final String pkg = intent.getStringExtra(Settings.EXTRA_APP_PACKAGE); + if (uid == -1 || TextUtils.isEmpty(pkg)) { + Log.w(TAG, "Missing extras: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + ", " + + Settings.EXTRA_APP_UID + " was " + uid); + toastAndFinish(); + return; + } + + if (DEBUG) Log.d(TAG, "Load details for pkg=" + pkg + " uid=" + uid); + final PackageManager pm = getPackageManager(); + final PackageInfo info = findPackageInfo(pm, pkg, uid); + if (info == null) { + Log.w(TAG, "Failed to find package info: " + Settings.EXTRA_APP_PACKAGE + " was " + pkg + + ", " + Settings.EXTRA_APP_UID + " was " + uid); + toastAndFinish(); + return; + } + + addPreferencesFromResource(R.xml.app_notification_settings); + mBlock = (SwitchPreference) findPreference(KEY_BLOCK); + mPriority = (SwitchPreference) findPreference(KEY_PRIORITY); + mSensitive = (SwitchPreference) findPreference(KEY_SENSITIVE); + + final boolean secure = new LockPatternUtils(getActivity()).isSecure(); + final boolean enabled = getLockscreenNotificationsEnabled(); + final boolean allowPrivate = getLockscreenAllowPrivateNotifications(); + if (!secure || !enabled || !allowPrivate) { + getPreferenceScreen().removePreference(mSensitive); + } + + mAppRow = NotificationAppList.loadAppRow(pm, info.applicationInfo, mBackend); + if (intent.hasExtra(EXTRA_HAS_SETTINGS_INTENT)) { + // use settings intent from extra + if (intent.getBooleanExtra(EXTRA_HAS_SETTINGS_INTENT, false)) { + mAppRow.settingsIntent = intent.getParcelableExtra(EXTRA_SETTINGS_INTENT); + } + } else { + // load settings intent + ArrayMap<String, AppRow> rows = new ArrayMap<String, AppRow>(); + rows.put(mAppRow.pkg, mAppRow); + NotificationAppList.collectConfigActivities(getPackageManager(), rows); + } + + mBlock.setChecked(mAppRow.banned); + mPriority.setChecked(mAppRow.priority); + if (mSensitive != null) { + mSensitive.setChecked(mAppRow.sensitive); + } + + mBlock.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean block = (Boolean) newValue; + return mBackend.setNotificationsBanned(pkg, uid, block); + } + }); + + mPriority.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean priority = (Boolean) newValue; + return mBackend.setHighPriority(pkg, uid, priority); + } + }); + + if (mSensitive != null) { + mSensitive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean sensitive = (Boolean) newValue; + return mBackend.setSensitive(pkg, uid, sensitive); + } + }); + } + + // Users cannot block notifications from system/signature packages + if (Utils.isSystemPackage(pm, info)) { + getPreferenceScreen().removePreference(mBlock); + mPriority.setDependency(null); // don't have it depend on a preference that's gone + } + } + + private boolean getLockscreenNotificationsEnabled() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0) != 0; + } + + private boolean getLockscreenAllowPrivateNotifications() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0) != 0; + } + + private void toastAndFinish() { + Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show(); + getActivity().finish(); + } + + private static PackageInfo findPackageInfo(PackageManager pm, String pkg, int uid) { + final String[] packages = pm.getPackagesForUid(uid); + if (packages != null && pkg != null) { + final int N = packages.length; + for (int i = 0; i < N; i++) { + final String p = packages[i]; + if (pkg.equals(p)) { + try { + return pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to load package " + pkg, e); + } + } + } + } + return null; + } +} diff --git a/src/com/android/settings/notification/ConditionProviderSettings.java b/src/com/android/settings/notification/ConditionProviderSettings.java new file mode 100644 index 0000000..259e53c --- /dev/null +++ b/src/com/android/settings/notification/ConditionProviderSettings.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.service.notification.ConditionProviderService; + +import com.android.settings.R; + +public class ConditionProviderSettings extends ManagedServiceSettings { + private static final String TAG = ConditionProviderSettings.class.getSimpleName(); + private static final Config CONFIG = getConditionProviderConfig(); + + private static Config getConditionProviderConfig() { + final Config c = new Config(); + c.tag = TAG; + c.setting = Settings.Secure.ENABLED_CONDITION_PROVIDERS; + c.intentAction = ConditionProviderService.SERVICE_INTERFACE; + c.permission = android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE; + c.noun = "condition provider"; + c.warningDialogTitle = R.string.condition_provider_security_warning_title; + c.warningDialogSummary = R.string.condition_provider_security_warning_summary; + c.emptyText = R.string.no_condition_providers; + return c; + } + + @Override + protected Config getConfig() { + return CONFIG; + } + + public static int getProviderCount(PackageManager pm) { + return getServicesCount(CONFIG, pm); + } + + public static int getEnabledProviderCount(Context context) { + return getEnabledServicesCount(CONFIG, context); + } +} diff --git a/src/com/android/settings/notification/DropDownPreference.java b/src/com/android/settings/notification/DropDownPreference.java new file mode 100644 index 0000000..1d1b366 --- /dev/null +++ b/src/com/android/settings/notification/DropDownPreference.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.AdapterView.OnItemSelectedListener; + +import java.util.ArrayList; + +public class DropDownPreference extends Preference { + private final Context mContext; + private final ArrayAdapter<String> mAdapter; + private final Spinner mSpinner; + private final ArrayList<Object> mValues = new ArrayList<Object>(); + + private Callback mCallback; + + public DropDownPreference(Context context) { + this(context, null); + } + + public DropDownPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mAdapter = new ArrayAdapter<String>(mContext, + android.R.layout.simple_spinner_dropdown_item); + + mSpinner = new Spinner(mContext); + + mSpinner.setVisibility(View.INVISIBLE); + mSpinner.setAdapter(mAdapter); + mSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View v, int position, long id) { + setSelectedItem(position); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + // noop + } + }); + setPersistent(false); + setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + mSpinner.performClick(); + return true; + } + }); + } + + public void setDropDownWidth(int dimenResId) { + mSpinner.setDropDownWidth(mContext.getResources().getDimensionPixelSize(dimenResId)); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public void setSelectedItem(int position) { + final Object value = mValues.get(position); + if (mCallback != null && !mCallback.onItemSelected(position, value)) { + return; + } + mSpinner.setSelection(position); + setSummary(mAdapter.getItem(position)); + final boolean disableDependents = value == null; + notifyDependencyChange(disableDependents); + } + + public void setSelectedValue(Object value) { + final int i = mValues.indexOf(value); + if (i > -1) { + setSelectedItem(i); + } + } + + public void addItem(int captionResid, Object value) { + addItem(mContext.getResources().getString(captionResid), value); + } + + public void addItem(String caption, Object value) { + mAdapter.add(caption); + mValues.add(value); + } + + public void clearItems(){ + mAdapter.clear(); + mValues.clear(); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + if (view.equals(mSpinner.getParent())) return; + if (mSpinner.getParent() != null) { + ((ViewGroup)mSpinner.getParent()).removeView(mSpinner); + } + final ViewGroup vg = (ViewGroup)view; + vg.addView(mSpinner, 0); + final ViewGroup.LayoutParams lp = mSpinner.getLayoutParams(); + lp.width = 0; + mSpinner.setLayoutParams(lp); + } + + public interface Callback { + boolean onItemSelected(int pos, Object value); + } +} diff --git a/src/com/android/settings/NotificationAccessSettings.java b/src/com/android/settings/notification/ManagedServiceSettings.java index 07d4353..7be644e 100644 --- a/src/com/android/settings/NotificationAccessSettings.java +++ b/src/com/android/settings/notification/ManagedServiceSettings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2014 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. @@ -14,56 +14,59 @@ * limitations under the License. */ -package com.android.settings; +package com.android.settings.notification; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; +import android.app.ListFragment; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentResolver; +import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.net.Uri; -import android.os.Handler; -import android.service.notification.NotificationListenerService; -import android.util.Slog; -import android.widget.ArrayAdapter; - -import android.app.ListFragment; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.Handler; import android.provider.Settings; +import android.util.Slog; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; +import com.android.settings.R; + import java.util.HashSet; import java.util.List; -public class NotificationAccessSettings extends ListFragment { - static final String TAG = NotificationAccessSettings.class.getSimpleName(); +public abstract class ManagedServiceSettings extends ListFragment { private static final boolean SHOW_PACKAGE_NAME = false; + private final Config mConfig; private PackageManager mPM; private ContentResolver mCR; - private final HashSet<ComponentName> mEnabledListeners = new HashSet<ComponentName>(); - private ListenerListAdapter mList; + private final HashSet<ComponentName> mEnabledServices = new HashSet<ComponentName>(); + private ServiceListAdapter mListAdapter; + + abstract protected Config getConfig(); - private final Uri ENABLED_NOTIFICATION_LISTENERS_URI - = Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + public ManagedServiceSettings() { + mConfig = getConfig(); + } private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { @Override @@ -79,18 +82,18 @@ public class NotificationAccessSettings extends ListFragment { } }; - public class ListenerWarningDialogFragment extends DialogFragment { + public class ScaryWarningDialogFragment extends DialogFragment { static final String KEY_COMPONENT = "c"; static final String KEY_LABEL = "l"; - public ListenerWarningDialogFragment setListenerInfo(ComponentName cn, String label) { + public ScaryWarningDialogFragment setServiceInfo(ComponentName cn, String label) { Bundle args = new Bundle(); args.putString(KEY_COMPONENT, cn.flattenToString()); args.putString(KEY_LABEL, label); setArguments(args); - return this; } + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -98,20 +101,17 @@ public class NotificationAccessSettings extends ListFragment { final String label = args.getString(KEY_LABEL); final ComponentName cn = ComponentName.unflattenFromString(args.getString(KEY_COMPONENT)); - final String title = getResources().getString( - R.string.notification_listener_security_warning_title, label); - final String summary = getResources().getString( - R.string.notification_listener_security_warning_summary, label); + final String title = getResources().getString(mConfig.warningDialogTitle, label); + final String summary = getResources().getString(mConfig.warningDialogSummary, label); return new AlertDialog.Builder(getActivity()) .setMessage(summary) .setTitle(title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setCancelable(true) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { - mEnabledListeners.add(cn); - saveEnabledListeners(); + mEnabledServices.add(cn); + saveEnabledServices(); } }) .setNegativeButton(android.R.string.cancel, @@ -130,13 +130,16 @@ public class NotificationAccessSettings extends ListFragment { mPM = getActivity().getPackageManager(); mCR = getActivity().getContentResolver(); - mList = new ListenerListAdapter(getActivity()); + mListAdapter = new ServiceListAdapter(getActivity()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.notification_access_settings, container, false); + View v = inflater.inflate(R.layout.managed_service_settings, container, false); + TextView empty = (TextView) v.findViewById(android.R.id.empty); + empty.setText(mConfig.emptyText); + return v; } @Override @@ -153,7 +156,8 @@ public class NotificationAccessSettings extends ListFragment { filter.addDataScheme("package"); getActivity().registerReceiver(mPackageReceiver, filter); - mCR.registerContentObserver(ENABLED_NOTIFICATION_LISTENERS_URI, false, mSettingsObserver); + mCR.registerContentObserver(Settings.Secure.getUriFor(mConfig.setting), + false, mSettingsObserver); } @Override @@ -164,24 +168,23 @@ public class NotificationAccessSettings extends ListFragment { mCR.unregisterContentObserver(mSettingsObserver); } - void loadEnabledListeners() { - mEnabledListeners.clear(); - final String flat = Settings.Secure.getString(mCR, - Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + private void loadEnabledServices() { + mEnabledServices.clear(); + final String flat = Settings.Secure.getString(mCR, mConfig.setting); if (flat != null && !"".equals(flat)) { final String[] names = flat.split(":"); for (int i = 0; i < names.length; i++) { final ComponentName cn = ComponentName.unflattenFromString(names[i]); if (cn != null) { - mEnabledListeners.add(cn); + mEnabledServices.add(cn); } } } } - void saveEnabledListeners() { + private void saveEnabledServices() { StringBuilder sb = null; - for (ComponentName cn : mEnabledListeners) { + for (ComponentName cn : mEnabledServices) { if (sb == null) { sb = new StringBuilder(); } else { @@ -190,32 +193,39 @@ public class NotificationAccessSettings extends ListFragment { sb.append(cn.flattenToString()); } Settings.Secure.putString(mCR, - Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + mConfig.setting, sb != null ? sb.toString() : ""); } - void updateList() { - loadEnabledListeners(); + private void updateList() { + loadEnabledServices(); - getListeners(mList, mPM); - mList.sort(new PackageItemInfo.DisplayNameComparator(mPM)); + getServices(mConfig, mListAdapter, mPM); + mListAdapter.sort(new PackageItemInfo.DisplayNameComparator(mPM)); - getListView().setAdapter(mList); + getListView().setAdapter(mListAdapter); } - static int getListenersCount(PackageManager pm) { - return getListeners(null, pm); + protected static int getEnabledServicesCount(Config config, Context context) { + final String flat = Settings.Secure.getString(context.getContentResolver(), config.setting); + if (flat == null || "".equals(flat)) return 0; + final String[] components = flat.split(":"); + return components.length; } - private static int getListeners(ArrayAdapter<ServiceInfo> adapter, PackageManager pm) { - int listeners = 0; + protected static int getServicesCount(Config c, PackageManager pm) { + return getServices(c, null, pm); + } + + private static int getServices(Config c, ArrayAdapter<ServiceInfo> adapter, PackageManager pm) { + int services = 0; if (adapter != null) { adapter.clear(); } final int user = ActivityManager.getCurrentUser(); List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser( - new Intent(NotificationListenerService.SERVICE_INTERFACE), + new Intent(c.intentAction), PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, user); @@ -223,54 +233,53 @@ public class NotificationAccessSettings extends ListFragment { ResolveInfo resolveInfo = installedServices.get(i); ServiceInfo info = resolveInfo.serviceInfo; - if (!android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals( - info.permission)) { - Slog.w(TAG, "Skipping notification listener service " + if (!c.permission.equals(info.permission)) { + Slog.w(c.tag, "Skipping " + c.noun + " service " + info.packageName + "/" + info.name + ": it does not require the permission " - + android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE); + + c.permission); continue; } if (adapter != null) { adapter.add(info); } - listeners++; + services++; } - return listeners; + return services; } - boolean isListenerEnabled(ServiceInfo info) { + private boolean isServiceEnabled(ServiceInfo info) { final ComponentName cn = new ComponentName(info.packageName, info.name); - return mEnabledListeners.contains(cn); + return mEnabledServices.contains(cn); } @Override public void onListItemClick(ListView l, View v, int position, long id) { - ServiceInfo info = mList.getItem(position); + ServiceInfo info = mListAdapter.getItem(position); final ComponentName cn = new ComponentName(info.packageName, info.name); - if (mEnabledListeners.contains(cn)) { + if (mEnabledServices.contains(cn)) { // the simple version: disabling - mEnabledListeners.remove(cn); - saveEnabledListeners(); + mEnabledServices.remove(cn); + saveEnabledServices(); } else { // show a scary dialog - new ListenerWarningDialogFragment() - .setListenerInfo(cn, info.loadLabel(mPM).toString()) + new ScaryWarningDialogFragment() + .setServiceInfo(cn, info.loadLabel(mPM).toString()) .show(getFragmentManager(), "dialog"); } } - static class ViewHolder { + private static class ViewHolder { ImageView icon; TextView name; CheckBox checkbox; TextView description; } - class ListenerListAdapter extends ArrayAdapter<ServiceInfo> { + private class ServiceListAdapter extends ArrayAdapter<ServiceInfo> { final LayoutInflater mInflater; - ListenerListAdapter(Context context) { + ServiceListAdapter(Context context) { super(context, 0, 0); mInflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -296,7 +305,7 @@ public class NotificationAccessSettings extends ListFragment { } public View newView(ViewGroup parent) { - View v = mInflater.inflate(R.layout.notification_listener_item, parent, false); + View v = mInflater.inflate(R.layout.managed_service_item, parent, false); ViewHolder h = new ViewHolder(); h.icon = (ImageView) v.findViewById(R.id.icon); h.name = (TextView) v.findViewById(R.id.name); @@ -318,7 +327,18 @@ public class NotificationAccessSettings extends ListFragment { } else { vh.description.setVisibility(View.GONE); } - vh.checkbox.setChecked(isListenerEnabled(info)); + vh.checkbox.setChecked(isServiceEnabled(info)); } } + + protected static class Config { + String tag; + String setting; + String intentAction; + String permission; + String noun; + int warningDialogTitle; + int warningDialogSummary; + int emptyText; + } } diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java new file mode 100644 index 0000000..ced71a4 --- /dev/null +++ b/src/com/android/settings/notification/NotificationAccessSettings.java @@ -0,0 +1,55 @@ +/* + * 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.notification; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; + +import com.android.settings.R; + +public class NotificationAccessSettings extends ManagedServiceSettings { + private static final String TAG = NotificationAccessSettings.class.getSimpleName(); + private static final Config CONFIG = getNotificationListenerConfig(); + + private static Config getNotificationListenerConfig() { + final Config c = new Config(); + c.tag = TAG; + c.setting = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS; + c.intentAction = NotificationListenerService.SERVICE_INTERFACE; + c.permission = android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE; + c.noun = "notification listener"; + c.warningDialogTitle = R.string.notification_listener_security_warning_title; + c.warningDialogSummary = R.string.notification_listener_security_warning_summary; + c.emptyText = R.string.no_notification_listeners; + return c; + } + + @Override + protected Config getConfig() { + return CONFIG; + } + + public static int getListenersCount(PackageManager pm) { + return getServicesCount(CONFIG, pm); + } + + public static int getEnabledListenersCount(Context context) { + return getEnabledServicesCount(CONFIG, context); + } +} diff --git a/src/com/android/settings/notification/NotificationAppList.java b/src/com/android/settings/notification/NotificationAppList.java new file mode 100644 index 0000000..3c44196 --- /dev/null +++ b/src/com/android/settings/notification/NotificationAppList.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2014 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.notification; + +import static com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT; +import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT; + +import android.animation.LayoutTransition; +import android.app.INotificationManager; +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.util.ArrayMap; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.SectionIndexer; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.settings.PinnedHeaderListFragment; +import com.android.settings.R; +import com.android.settings.Settings.NotificationAppListActivity; +import com.android.settings.UserSpinnerAdapter; +import com.android.settings.Utils; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Just a sectioned list of installed applications, nothing else to index **/ +public class NotificationAppList extends PinnedHeaderListFragment + implements OnItemSelectedListener { + private static final String TAG = "NotificationAppList"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final String EMPTY_SUBTITLE = ""; + private static final String SECTION_BEFORE_A = "*"; + private static final String SECTION_AFTER_Z = "**"; + private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT + = new Intent(Intent.ACTION_MAIN) + .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES); + + private final Handler mHandler = new Handler(); + private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>(); + private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>(); + private final ArrayList<String> mSections = new ArrayList<String>(); + + private Context mContext; + private LayoutInflater mInflater; + private NotificationAppAdapter mAdapter; + private Signature[] mSystemSignature; + private Parcelable mListViewState; + private Backend mBackend = new Backend(); + private UserSpinnerAdapter mProfileSpinnerAdapter; + + private PackageManager mPM; + private UserManager mUM; + private LauncherApps mLauncherApps; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getActivity(); + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mAdapter = new NotificationAppAdapter(mContext); + mUM = UserManager.get(mContext); + mPM = mContext.getPackageManager(); + mLauncherApps = (LauncherApps) mContext.getSystemService(Context.LAUNCHER_APPS_SERVICE); + getActivity().setTitle(R.string.app_notifications_title); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.notification_app_list, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(mUM, mContext); + if (mProfileSpinnerAdapter != null) { + Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate( + R.layout.spinner_view, null); + spinner.setAdapter(mProfileSpinnerAdapter); + spinner.setOnItemSelectedListener(this); + setPinnedHeaderView(spinner); + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + repositionScrollbar(); + getListView().setAdapter(mAdapter); + } + + @Override + public void onPause() { + super.onPause(); + if (DEBUG) Log.d(TAG, "Saving listView state"); + mListViewState = getListView().onSaveInstanceState(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mListViewState = null; // you're dead to me + } + + @Override + public void onResume() { + super.onResume(); + loadAppsList(); + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); + if (selectedUser.getIdentifier() != UserHandle.myUserId()) { + Intent intent = new Intent(getActivity(), NotificationAppListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(intent, selectedUser); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + public void setBackend(Backend backend) { + mBackend = backend; + } + + private void loadAppsList() { + AsyncTask.execute(mCollectAppsRunnable); + } + + private String getSection(CharSequence label) { + if (label == null || label.length() == 0) return SECTION_BEFORE_A; + final char c = Character.toUpperCase(label.charAt(0)); + if (c < 'A') return SECTION_BEFORE_A; + if (c > 'Z') return SECTION_AFTER_Z; + return Character.toString(c); + } + + private void repositionScrollbar() { + final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + getListView().getScrollBarSize(), + getResources().getDisplayMetrics()); + final View parent = (View)getView().getParent(); + final int eat = Math.min(sbWidthPx, parent.getPaddingEnd()); + if (eat <= 0) return; + if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d", + eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection())); + parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(), + parent.getPaddingEnd() - eat, parent.getPaddingBottom()); + } + + private static class ViewHolder { + ViewGroup row; + ImageView icon; + TextView title; + TextView subtitle; + View rowDivider; + } + + private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer { + public NotificationAppAdapter(Context context) { + super(context, 0, 0); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + Row r = getItem(position); + return r instanceof AppRow ? 1 : 0; + } + + public View getView(int position, View convertView, ViewGroup parent) { + Row r = getItem(position); + View v; + if (convertView == null) { + v = newView(parent, r); + } else { + v = convertView; + } + bindView(v, r, false /*animate*/); + return v; + } + + public View newView(ViewGroup parent, Row r) { + if (!(r instanceof AppRow)) { + return mInflater.inflate(R.layout.notification_app_section, parent, false); + } + final View v = mInflater.inflate(R.layout.notification_app, parent, false); + final ViewHolder vh = new ViewHolder(); + vh.row = (ViewGroup) v; + vh.row.setLayoutTransition(new LayoutTransition()); + vh.row.setLayoutTransition(new LayoutTransition()); + vh.icon = (ImageView) v.findViewById(android.R.id.icon); + vh.title = (TextView) v.findViewById(android.R.id.title); + vh.subtitle = (TextView) v.findViewById(android.R.id.text1); + vh.rowDivider = v.findViewById(R.id.row_divider); + v.setTag(vh); + return v; + } + + private void enableLayoutTransitions(ViewGroup vg, boolean enabled) { + if (enabled) { + vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING); + vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING); + } else { + vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING); + vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING); + } + } + + public void bindView(final View view, Row r, boolean animate) { + if (!(r instanceof AppRow)) { + // it's a section row + final TextView tv = (TextView)view.findViewById(android.R.id.title); + tv.setText(r.section); + return; + } + + final AppRow row = (AppRow)r; + final ViewHolder vh = (ViewHolder) view.getTag(); + enableLayoutTransitions(vh.row, animate); + vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE); + vh.row.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg) + .putExtra(Settings.EXTRA_APP_UID, row.uid) + .putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null) + .putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent)); + } + }); + enableLayoutTransitions(vh.row, animate); + vh.icon.setImageDrawable(row.icon); + vh.title.setText(row.label); + final String sub = getSubtitle(row); + vh.subtitle.setText(sub); + vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE); + } + + private String getSubtitle(AppRow row) { + if (row.banned) { + return mContext.getString(R.string.app_notification_row_banned); + } + if (!row.priority && !row.sensitive) { + return EMPTY_SUBTITLE; + } + final String priString = mContext.getString(R.string.app_notification_row_priority); + final String senString = mContext.getString(R.string.app_notification_row_sensitive); + if (row.priority != row.sensitive) { + return row.priority ? priString : senString; + } + return priString + mContext.getString(R.string.summary_divider_text) + senString; + } + + @Override + public Object[] getSections() { + return mSections.toArray(new Object[mSections.size()]); + } + + @Override + public int getPositionForSection(int sectionIndex) { + final String section = mSections.get(sectionIndex); + final int n = getCount(); + for (int i = 0; i < n; i++) { + final Row r = getItem(i); + if (r.section.equals(section)) { + return i; + } + } + return 0; + } + + @Override + public int getSectionForPosition(int position) { + Row row = getItem(position); + return mSections.indexOf(row.section); + } + } + + private static class Row { + public String section; + } + + public static class AppRow extends Row { + public String pkg; + public int uid; + public Drawable icon; + public CharSequence label; + public Intent settingsIntent; + public boolean banned; + public boolean priority; + public boolean sensitive; + public boolean first; // first app in section + } + + private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() { + private final Collator sCollator = Collator.getInstance(); + @Override + public int compare(AppRow lhs, AppRow rhs) { + return sCollator.compare(lhs.label, rhs.label); + } + }; + + + public static AppRow loadAppRow(PackageManager pm, ApplicationInfo app, + Backend backend) { + final AppRow row = new AppRow(); + row.pkg = app.packageName; + row.uid = app.uid; + try { + row.label = app.loadLabel(pm); + } catch (Throwable t) { + Log.e(TAG, "Error loading application label for " + row.pkg, t); + row.label = row.pkg; + } + row.icon = app.loadIcon(pm); + row.banned = backend.getNotificationsBanned(row.pkg, row.uid); + row.priority = backend.getHighPriority(row.pkg, row.uid); + row.sensitive = backend.getSensitive(row.pkg, row.uid); + return row; + } + + public static List<ResolveInfo> queryNotificationConfigActivities(PackageManager pm) { + if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is " + + APP_NOTIFICATION_PREFS_CATEGORY_INTENT); + final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( + APP_NOTIFICATION_PREFS_CATEGORY_INTENT, + 0 //PackageManager.MATCH_DEFAULT_ONLY + ); + return resolveInfos; + } + public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) { + final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm); + applyConfigActivities(pm, rows, resolveInfos); + } + + public static void applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows, + List<ResolveInfo> resolveInfos) { + if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" + + (resolveInfos.size() == 0 ? " ;_;" : "")); + for (ResolveInfo ri : resolveInfos) { + final ActivityInfo activityInfo = ri.activityInfo; + final ApplicationInfo appInfo = activityInfo.applicationInfo; + final AppRow row = rows.get(appInfo.packageName); + if (row == null) { + Log.v(TAG, "Ignoring notification preference activity (" + + activityInfo.name + ") for unknown package " + + activityInfo.packageName); + continue; + } + if (row.settingsIntent != null) { + Log.v(TAG, "Ignoring duplicate notification preference activity (" + + activityInfo.name + ") for package " + + activityInfo.packageName); + continue; + } + row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT) + .setClassName(activityInfo.packageName, activityInfo.name); + } + } + + private final Runnable mCollectAppsRunnable = new Runnable() { + @Override + public void run() { + synchronized (mRows) { + final long start = SystemClock.uptimeMillis(); + if (DEBUG) Log.d(TAG, "Collecting apps..."); + mRows.clear(); + mSortedRows.clear(); + + // collect all launchable apps, plus any packages that have notification settings + final List<ApplicationInfo> appInfos = new ArrayList<ApplicationInfo>(); + + final List<LauncherActivityInfo> lais + = mLauncherApps.getActivityList(null /* all */, + UserHandle.getCallingUserHandle()); + if (DEBUG) Log.d(TAG, " launchable activities:"); + for (LauncherActivityInfo lai : lais) { + if (DEBUG) Log.d(TAG, " " + lai.getComponentName().toString()); + appInfos.add(lai.getApplicationInfo()); + } + + final List<ResolveInfo> resolvedConfigActivities + = queryNotificationConfigActivities(mPM); + if (DEBUG) Log.d(TAG, " config activities:"); + for (ResolveInfo ri : resolvedConfigActivities) { + if (DEBUG) Log.d(TAG, " " + + ri.activityInfo.packageName + "/" + ri.activityInfo.name); + appInfos.add(ri.activityInfo.applicationInfo); + } + + for (ApplicationInfo info : appInfos) { + final String key = info.packageName; + if (mRows.containsKey(key)) { + // we already have this app, thanks + continue; + } + + final AppRow row = loadAppRow(mPM, info, mBackend); + mRows.put(key, row); + } + + // add config activities to the list + applyConfigActivities(mPM, mRows, resolvedConfigActivities); + + // sort rows + mSortedRows.addAll(mRows.values()); + Collections.sort(mSortedRows, mRowComparator); + // compute sections + mSections.clear(); + String section = null; + for (AppRow r : mSortedRows) { + r.section = getSection(r.label); + if (!r.section.equals(section)) { + section = r.section; + mSections.add(section); + } + } + mHandler.post(mRefreshAppsListRunnable); + final long elapsed = SystemClock.uptimeMillis() - start; + if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms"); + } + } + }; + + private void refreshDisplayedItems() { + if (DEBUG) Log.d(TAG, "Refreshing apps..."); + mAdapter.clear(); + synchronized (mSortedRows) { + String section = null; + final int N = mSortedRows.size(); + boolean first = true; + for (int i = 0; i < N; i++) { + final AppRow row = mSortedRows.get(i); + if (!row.section.equals(section)) { + section = row.section; + Row r = new Row(); + r.section = section; + mAdapter.add(r); + first = true; + } + row.first = first; + mAdapter.add(row); + first = false; + } + } + if (mListViewState != null) { + if (DEBUG) Log.d(TAG, "Restoring listView state"); + getListView().onRestoreInstanceState(mListViewState); + mListViewState = null; + } + if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items"); + } + + private final Runnable mRefreshAppsListRunnable = new Runnable() { + @Override + public void run() { + refreshDisplayedItems(); + } + }; + + public static class Backend { + static INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + + public boolean setNotificationsBanned(String pkg, int uid, boolean banned) { + try { + sINM.setNotificationsEnabledForPackage(pkg, uid, !banned); + return true; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean getNotificationsBanned(String pkg, int uid) { + try { + final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid); + return !enabled; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean getHighPriority(String pkg, int uid) { + try { + return sINM.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean setHighPriority(String pkg, int uid, boolean highPriority) { + try { + sINM.setPackagePriority(pkg, uid, + highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT); + return true; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean getSensitive(String pkg, int uid) { + try { + return sINM.getPackageVisibilityOverride(pkg, uid) == Notification.VISIBILITY_PRIVATE; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean setSensitive(String pkg, int uid, boolean sensitive) { + try { + sINM.setPackageVisibilityOverride(pkg, uid, + sensitive ? Notification.VISIBILITY_PRIVATE + : NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); + return true; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + } +} diff --git a/src/com/android/settings/notification/NotificationSettings.java b/src/com/android/settings/notification/NotificationSettings.java new file mode 100644 index 0000000..6899440 --- /dev/null +++ b/src/com/android/settings/notification/NotificationSettings.java @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.media.AudioManager; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Vibrator; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceCategory; +import android.preference.SeekBarVolumizer; +import android.preference.TwoStatePreference; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.util.Log; + +import android.widget.SeekBar; +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class NotificationSettings extends SettingsPreferenceFragment implements Indexable { + private static final String TAG = "NotificationSettings"; + + private static final String KEY_SOUND = "sound"; + private static final String KEY_MEDIA_VOLUME = "media_volume"; + private static final String KEY_ALARM_VOLUME = "alarm_volume"; + private static final String KEY_RING_VOLUME = "ring_volume"; + private static final String KEY_NOTIFICATION_VOLUME = "notification_volume"; + private static final String KEY_PHONE_RINGTONE = "ringtone"; + private static final String KEY_NOTIFICATION_RINGTONE = "notification_ringtone"; + private static final String KEY_VIBRATE_WHEN_RINGING = "vibrate_when_ringing"; + private static final String KEY_NOTIFICATION = "notification"; + private static final String KEY_NOTIFICATION_PULSE = "notification_pulse"; + private static final String KEY_LOCK_SCREEN_NOTIFICATIONS = "lock_screen_notifications"; + private static final String KEY_NOTIFICATION_ACCESS = "manage_notification_access"; + + private static final int SAMPLE_CUTOFF = 2000; // manually cap sample playback at 2 seconds + + private final VolumePreferenceCallback mVolumeCallback = new VolumePreferenceCallback(); + private final H mHandler = new H(); + private final SettingsObserver mSettingsObserver = new SettingsObserver(); + + private Context mContext; + private PackageManager mPM; + private boolean mVoiceCapable; + private Vibrator mVibrator; + private VolumeSeekBarPreference mRingOrNotificationPreference; + + private Preference mPhoneRingtonePreference; + private Preference mNotificationRingtonePreference; + private TwoStatePreference mVibrateWhenRinging; + private TwoStatePreference mNotificationPulse; + private DropDownPreference mLockscreen; + private Preference mNotificationAccess; + private boolean mSecure; + private int mLockscreenSelectedValue; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getActivity(); + mPM = mContext.getPackageManager(); + mVoiceCapable = Utils.isVoiceCapable(mContext); + mSecure = new LockPatternUtils(getActivity()).isSecure(); + + mVibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE); + if (mVibrator != null && !mVibrator.hasVibrator()) { + mVibrator = null; + } + + addPreferencesFromResource(R.xml.notification_settings); + + final PreferenceCategory sound = (PreferenceCategory) findPreference(KEY_SOUND); + initVolumePreference(KEY_MEDIA_VOLUME, AudioManager.STREAM_MUSIC); + initVolumePreference(KEY_ALARM_VOLUME, AudioManager.STREAM_ALARM); + if (mVoiceCapable) { + mRingOrNotificationPreference = + initVolumePreference(KEY_RING_VOLUME, AudioManager.STREAM_RING); + sound.removePreference(sound.findPreference(KEY_NOTIFICATION_VOLUME)); + } else { + mRingOrNotificationPreference = + initVolumePreference(KEY_NOTIFICATION_VOLUME, AudioManager.STREAM_NOTIFICATION); + sound.removePreference(sound.findPreference(KEY_RING_VOLUME)); + } + initRingtones(sound); + initVibrateWhenRinging(sound); + + final PreferenceCategory notification = (PreferenceCategory) + findPreference(KEY_NOTIFICATION); + initPulse(notification); + initLockscreenNotifications(notification); + + mNotificationAccess = findPreference(KEY_NOTIFICATION_ACCESS); + refreshNotificationListeners(); + } + + @Override + public void onResume() { + super.onResume(); + refreshNotificationListeners(); + lookupRingtoneNames(); + mSettingsObserver.register(true); + } + + @Override + public void onPause() { + super.onPause(); + mVolumeCallback.stopSample(); + mSettingsObserver.register(false); + } + + // === Volumes === + + private VolumeSeekBarPreference initVolumePreference(String key, int stream) { + final VolumeSeekBarPreference volumePref = (VolumeSeekBarPreference) findPreference(key); + volumePref.setCallback(mVolumeCallback); + volumePref.setStream(stream); + return volumePref; + } + + private void updateRingOrNotificationIcon(int progress) { + mRingOrNotificationPreference.showIcon(progress > 0 + ? R.drawable.ring_notif + : (mVibrator == null + ? R.drawable.ring_notif_mute + : R.drawable.ring_notif_vibrate)); + } + + private final class VolumePreferenceCallback implements VolumeSeekBarPreference.Callback { + private SeekBarVolumizer mCurrent; + + @Override + public void onSampleStarting(SeekBarVolumizer sbv) { + if (mCurrent != null && mCurrent != sbv) { + mCurrent.stopSample(); + } + mCurrent = sbv; + if (mCurrent != null) { + mHandler.removeMessages(H.STOP_SAMPLE); + mHandler.sendEmptyMessageDelayed(H.STOP_SAMPLE, SAMPLE_CUTOFF); + } + } + + @Override + public void onStreamValueChanged(int stream, int progress) { + if (stream == AudioManager.STREAM_RING) { + mHandler.removeMessages(H.UPDATE_RINGER_ICON); + mHandler.obtainMessage(H.UPDATE_RINGER_ICON, progress, 0).sendToTarget(); + } + } + + public void stopSample() { + if (mCurrent != null) { + mCurrent.stopSample(); + } + } + }; + + + // === Phone & notification ringtone === + + private void initRingtones(PreferenceCategory root) { + mPhoneRingtonePreference = root.findPreference(KEY_PHONE_RINGTONE); + if (mPhoneRingtonePreference != null && !mVoiceCapable) { + root.removePreference(mPhoneRingtonePreference); + mPhoneRingtonePreference = null; + } + mNotificationRingtonePreference = root.findPreference(KEY_NOTIFICATION_RINGTONE); + } + + private void lookupRingtoneNames() { + AsyncTask.execute(mLookupRingtoneNames); + } + + private final Runnable mLookupRingtoneNames = new Runnable() { + @Override + public void run() { + if (mPhoneRingtonePreference != null) { + final CharSequence summary = updateRingtoneName( + mContext, RingtoneManager.TYPE_RINGTONE); + if (summary != null) { + mHandler.obtainMessage(H.UPDATE_PHONE_RINGTONE, summary).sendToTarget(); + } + } + if (mNotificationRingtonePreference != null) { + final CharSequence summary = updateRingtoneName( + mContext, RingtoneManager.TYPE_NOTIFICATION); + if (summary != null) { + mHandler.obtainMessage(H.UPDATE_NOTIFICATION_RINGTONE, summary).sendToTarget(); + } + } + } + }; + + private static CharSequence updateRingtoneName(Context context, int type) { + if (context == null) { + Log.e(TAG, "Unable to update ringtone name, no context provided"); + return null; + } + 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 { + Cursor cursor = null; + try { + if (MediaStore.AUTHORITY.equals(ringtoneUri.getAuthority())) { + // Fetch the ringtone title from the media provider + cursor = context.getContentResolver().query(ringtoneUri, + new String[] { MediaStore.Audio.Media.TITLE }, null, null, null); + } else if (ContentResolver.SCHEME_CONTENT.equals(ringtoneUri.getScheme())) { + cursor = context.getContentResolver().query(ringtoneUri, + new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null); + } + if (cursor != null) { + if (cursor.moveToFirst()) { + summary = cursor.getString(0); + } + } + } catch (SQLiteException sqle) { + // Unknown title for the ringtone + } catch (IllegalArgumentException iae) { + // Some other error retrieving the column from the provider + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + return summary; + } + + // === Vibrate when ringing === + + private void initVibrateWhenRinging(PreferenceCategory root) { + mVibrateWhenRinging = (TwoStatePreference) root.findPreference(KEY_VIBRATE_WHEN_RINGING); + if (mVibrateWhenRinging == null) { + Log.i(TAG, "Preference not found: " + KEY_VIBRATE_WHEN_RINGING); + return; + } + if (!mVoiceCapable) { + root.removePreference(mVibrateWhenRinging); + mVibrateWhenRinging = null; + return; + } + mVibrateWhenRinging.setPersistent(false); + updateVibrateWhenRinging(); + mVibrateWhenRinging.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean val = (Boolean) newValue; + return Settings.System.putInt(getContentResolver(), + Settings.System.VIBRATE_WHEN_RINGING, + val ? 1 : 0); + } + }); + } + + private void updateVibrateWhenRinging() { + if (mVibrateWhenRinging == null) return; + mVibrateWhenRinging.setChecked(Settings.System.getInt(getContentResolver(), + Settings.System.VIBRATE_WHEN_RINGING, 0) != 0); + } + + // === Pulse notification light === + + private void initPulse(PreferenceCategory parent) { + mNotificationPulse = (TwoStatePreference) parent.findPreference(KEY_NOTIFICATION_PULSE); + if (mNotificationPulse == null) { + Log.i(TAG, "Preference not found: " + KEY_NOTIFICATION_PULSE); + return; + } + if (!getResources() + .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed)) { + parent.removePreference(mNotificationPulse); + } else { + updatePulse(); + mNotificationPulse.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean val = (Boolean)newValue; + return Settings.System.putInt(getContentResolver(), + Settings.System.NOTIFICATION_LIGHT_PULSE, + val ? 1 : 0); + } + }); + } + } + + private void updatePulse() { + if (mNotificationPulse == null) { + return; + } + try { + mNotificationPulse.setChecked(Settings.System.getInt(getContentResolver(), + Settings.System.NOTIFICATION_LIGHT_PULSE) == 1); + } catch (Settings.SettingNotFoundException snfe) { + Log.e(TAG, Settings.System.NOTIFICATION_LIGHT_PULSE + " not found"); + } + } + + // === Lockscreen (public / private) notifications === + + private void initLockscreenNotifications(PreferenceCategory parent) { + mLockscreen = (DropDownPreference) parent.findPreference(KEY_LOCK_SCREEN_NOTIFICATIONS); + if (mLockscreen == null) { + Log.i(TAG, "Preference not found: " + KEY_LOCK_SCREEN_NOTIFICATIONS); + return; + } + + mLockscreen.addItem(R.string.lock_screen_notifications_summary_show, + R.string.lock_screen_notifications_summary_show); + if (mSecure) { + mLockscreen.addItem(R.string.lock_screen_notifications_summary_hide, + R.string.lock_screen_notifications_summary_hide); + } + mLockscreen.addItem(R.string.lock_screen_notifications_summary_disable, + R.string.lock_screen_notifications_summary_disable); + updateLockscreenNotifications(); + mLockscreen.setCallback(new DropDownPreference.Callback() { + @Override + public boolean onItemSelected(int pos, Object value) { + final int val = (Integer) value; + if (val == mLockscreenSelectedValue) { + return true; + } + final boolean enabled = val != R.string.lock_screen_notifications_summary_disable; + final boolean show = val == R.string.lock_screen_notifications_summary_show; + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, show ? 1 : 0); + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, enabled ? 1 : 0); + mLockscreenSelectedValue = val; + return true; + } + }); + } + + private void updateLockscreenNotifications() { + if (mLockscreen == null) { + return; + } + final boolean enabled = getLockscreenNotificationsEnabled(); + final boolean allowPrivate = !mSecure || getLockscreenAllowPrivateNotifications(); + mLockscreenSelectedValue = !enabled ? R.string.lock_screen_notifications_summary_disable : + allowPrivate ? R.string.lock_screen_notifications_summary_show : + R.string.lock_screen_notifications_summary_hide; + mLockscreen.setSelectedValue(mLockscreenSelectedValue); + } + + private boolean getLockscreenNotificationsEnabled() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0) != 0; + } + + private boolean getLockscreenAllowPrivateNotifications() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0) != 0; + } + + // === Notification listeners === + + private void refreshNotificationListeners() { + if (mNotificationAccess != null) { + final int total = NotificationAccessSettings.getListenersCount(mPM); + if (total == 0) { + getPreferenceScreen().removePreference(mNotificationAccess); + } else { + final int n = NotificationAccessSettings.getEnabledListenersCount(mContext); + if (n == 0) { + mNotificationAccess.setSummary(getResources().getString( + R.string.manage_notification_access_summary_zero)); + } else { + mNotificationAccess.setSummary(String.format(getResources().getQuantityString( + R.plurals.manage_notification_access_summary_nonzero, + n, n))); + } + } + } + } + + // === Callbacks === + + private final class SettingsObserver extends ContentObserver { + private final Uri VIBRATE_WHEN_RINGING_URI = + Settings.System.getUriFor(Settings.System.VIBRATE_WHEN_RINGING); + private final Uri NOTIFICATION_LIGHT_PULSE_URI = + Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE); + private final Uri LOCK_SCREEN_PRIVATE_URI = + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + private final Uri LOCK_SCREEN_SHOW_URI = + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS); + + public SettingsObserver() { + super(mHandler); + } + + public void register(boolean register) { + final ContentResolver cr = getContentResolver(); + if (register) { + cr.registerContentObserver(VIBRATE_WHEN_RINGING_URI, false, this); + cr.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI, false, this); + cr.registerContentObserver(LOCK_SCREEN_PRIVATE_URI, false, this); + cr.registerContentObserver(LOCK_SCREEN_SHOW_URI, false, this); + } else { + cr.unregisterContentObserver(this); + } + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (VIBRATE_WHEN_RINGING_URI.equals(uri)) { + updateVibrateWhenRinging(); + } + if (NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) { + updatePulse(); + } + if (LOCK_SCREEN_PRIVATE_URI.equals(uri) || LOCK_SCREEN_SHOW_URI.equals(uri)) { + updateLockscreenNotifications(); + } + } + } + + private final class H extends Handler { + private static final int UPDATE_PHONE_RINGTONE = 1; + private static final int UPDATE_NOTIFICATION_RINGTONE = 2; + private static final int STOP_SAMPLE = 3; + private static final int UPDATE_RINGER_ICON = 4; + + private H() { + super(Looper.getMainLooper()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case UPDATE_PHONE_RINGTONE: + mPhoneRingtonePreference.setSummary((CharSequence) msg.obj); + break; + case UPDATE_NOTIFICATION_RINGTONE: + mNotificationRingtonePreference.setSummary((CharSequence) msg.obj); + break; + case STOP_SAMPLE: + mVolumeCallback.stopSample(); + break; + case UPDATE_RINGER_ICON: + updateRingOrNotificationIcon(msg.arg1); + break; + } + } + } + + // === Indexing === + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.notification_settings; + return Arrays.asList(sir); + } + + public List<String> getNonIndexableKeys(Context context) { + final ArrayList<String> rt = new ArrayList<String>(); + if (Utils.isVoiceCapable(context)) { + rt.add(KEY_NOTIFICATION_VOLUME); + } else { + rt.add(KEY_RING_VOLUME); + rt.add(KEY_PHONE_RINGTONE); + rt.add(KEY_VIBRATE_WHEN_RINGING); + } + return rt; + } + }; +} diff --git a/src/com/android/settings/NotificationStation.java b/src/com/android/settings/notification/NotificationStation.java index 8d75579..c61f91e 100644 --- a/src/com/android/settings/NotificationStation.java +++ b/src/com/android/settings/notification/NotificationStation.java @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.android.settings; +package com.android.settings.notification; import android.app.Activity; import android.app.ActivityManager; import android.app.INotificationManager; import android.app.Notification; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -35,8 +33,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.service.notification.INotificationListener; -import android.service.notification.IStatusBarNotificationHolder; +import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.LayoutInflater; @@ -49,17 +46,31 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; + import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class NotificationStation extends SettingsPreferenceFragment { private static final String TAG = NotificationStation.class.getSimpleName(); - static final boolean DEBUG = true; - private static final String PACKAGE_SCHEME = "package"; - private static final boolean SHOW_HISTORICAL_NOTIFICATIONS = true; - private final PackageReceiver mPackageReceiver = new PackageReceiver(); + private static final boolean DEBUG = false; + + private static class HistoricalNotificationInfo { + public String pkg; + public Drawable pkgicon; + public CharSequence pkgname; + public Drawable icon; + public CharSequence title; + public int priority; + public int user; + public long timestamp; + public boolean active; + } + private PackageManager mPm; private INotificationManager mNoMan; @@ -70,23 +81,17 @@ public class NotificationStation extends SettingsPreferenceFragment { } }; - private INotificationListener.Stub mListener = new INotificationListener.Stub() { - @Override - public void onListenerConnected(String[] notificationKeys) throws RemoteException { - // noop - } + private NotificationListenerService mListener = new NotificationListenerService() { @Override - public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder) - throws RemoteException { - Log.v(TAG, "onNotificationPosted: " + sbnHolder.get()); + public void onNotificationPosted(StatusBarNotification notification) { + logd("onNotificationPosted: %s", notification); final Handler h = getListView().getHandler(); h.removeCallbacks(mRefreshListRunnable); h.postDelayed(mRefreshListRunnable, 100); } @Override - public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder) - throws RemoteException { + public void onNotificationRemoved(StatusBarNotification notification) { final Handler h = getListView().getHandler(); h.removeCallbacks(mRefreshListRunnable); h.postDelayed(mRefreshListRunnable, 100); @@ -114,25 +119,21 @@ public class NotificationStation extends SettingsPreferenceFragment { mNoMan = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); try { - mNoMan.registerListener(mListener, - new ComponentName(mContext.getPackageName(), - this.getClass().getCanonicalName()), - ActivityManager.getCurrentUser()); + mListener.registerAsSystemService(mContext, new ComponentName(mContext.getPackageName(), + this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); } catch (RemoteException e) { - // well, that didn't work out + Log.e(TAG, "Cannot register listener", e); } } @Override - public void onCreate(Bundle icicle) { - logd("onCreate(%s)", icicle); - super.onCreate(icicle); - Activity activity = getActivity(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); + public void onDetach() { + try { + mListener.unregisterAsSystemService(); + } catch (RemoteException e) { + Log.e(TAG, "Cannot unregister listener", e); + } + super.onDetach(); } @Override @@ -141,36 +142,17 @@ public class NotificationStation extends SettingsPreferenceFragment { super.onActivityCreated(savedInstanceState); ListView listView = getListView(); - -// TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); -// emptyView.setText(R.string.screensaver_settings_disabled_prompt); -// listView.setEmptyView(emptyView); + Utils.forceCustomPadding(listView, false /* non additive padding */); mAdapter = new NotificationHistoryAdapter(mContext); listView.setAdapter(mAdapter); } @Override - public void onPause() { - logd("onPause()"); - super.onPause(); - mContext.unregisterReceiver(mPackageReceiver); - } - - @Override public void onResume() { logd("onResume()"); super.onResume(); refreshList(); - - // listen for package changes - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addAction(Intent.ACTION_PACKAGE_REPLACED); - filter.addDataScheme(PACKAGE_SCHEME); - mContext.registerReceiver(mPackageReceiver , filter); } private void refreshList() { @@ -184,27 +166,18 @@ public class NotificationStation extends SettingsPreferenceFragment { } private static void logd(String msg, Object... args) { - if (DEBUG) + if (DEBUG) { Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args)); - } - - private static class HistoricalNotificationInfo { - public String pkg; - public Drawable pkgicon; - public CharSequence pkgname; - public Drawable icon; - public CharSequence title; - public int priority; - public int user; - public long timestamp; - public boolean active; + } } private List<HistoricalNotificationInfo> loadNotifications() { final int currentUserId = ActivityManager.getCurrentUser(); try { - StatusBarNotification[] active = mNoMan.getActiveNotifications(mContext.getPackageName()); - StatusBarNotification[] dismissed = mNoMan.getHistoricalNotifications(mContext.getPackageName(), 50); + StatusBarNotification[] active = mNoMan.getActiveNotifications( + mContext.getPackageName()); + StatusBarNotification[] dismissed = mNoMan.getHistoricalNotifications( + mContext.getPackageName(), 50); List<HistoricalNotificationInfo> list = new ArrayList<HistoricalNotificationInfo>(active.length + dismissed.length); @@ -219,9 +192,11 @@ public class NotificationStation extends SettingsPreferenceFragment { info.pkgicon = loadPackageIconDrawable(info.pkg, info.user); info.pkgname = loadPackageName(info.pkg); if (sbn.getNotification().extras != null) { - info.title = sbn.getNotification().extras.getString(Notification.EXTRA_TITLE); + info.title = sbn.getNotification().extras.getString( + Notification.EXTRA_TITLE); if (info.title == null || "".equals(info.title)) { - info.title = sbn.getNotification().extras.getString(Notification.EXTRA_TEXT); + info.title = sbn.getNotification().extras.getString( + Notification.EXTRA_TEXT); } } if (info.title == null || "".equals(info.title)) { @@ -246,7 +221,7 @@ public class NotificationStation extends SettingsPreferenceFragment { return list; } catch (RemoteException e) { - e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + Log.e(TAG, "Cannot load Notifications: ", e); } return null; } @@ -261,7 +236,7 @@ public class NotificationStation extends SettingsPreferenceFragment { } r = mPm.getResourcesForApplicationAsUser(pkg, userId); } catch (PackageManager.NameNotFoundException ex) { - Log.e(TAG, "Icon package not found: " + pkg); + Log.e(TAG, "Icon package not found: " + pkg, ex); return null; } } else { @@ -275,6 +250,7 @@ public class NotificationStation extends SettingsPreferenceFragment { try { icon = mPm.getApplicationIcon(pkg); } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Cannot get application icon", e); } return icon; @@ -286,6 +262,7 @@ public class NotificationStation extends SettingsPreferenceFragment { PackageManager.GET_UNINSTALLED_PACKAGES); if (info != null) return mPm.getApplicationLabel(info); } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Cannot load package name", e); } return pkg; } @@ -302,7 +279,7 @@ public class NotificationStation extends SettingsPreferenceFragment { } catch (RuntimeException e) { Log.w(TAG, "Icon not found in " + (pkg != null ? resId : "<system>") - + ": " + Integer.toHexString(resId)); + + ": " + Integer.toHexString(resId), e); } return null; @@ -320,6 +297,7 @@ public class NotificationStation extends SettingsPreferenceFragment { public View getView(int position, View convertView, ViewGroup parent) { final HistoricalNotificationInfo info = getItem(position); logd("getView(%s/%s)", info.pkg, info.title); + final View row = convertView != null ? convertView : createRow(parent); row.setTag(info); @@ -332,19 +310,10 @@ public class NotificationStation extends SettingsPreferenceFragment { } ((DateTimeView) row.findViewById(R.id.timestamp)).setTime(info.timestamp); - - // bind caption ((TextView) row.findViewById(android.R.id.title)).setText(info.title); - - // app name ((TextView) row.findViewById(R.id.pkgname)).setText(info.pkgname); - // extra goodies -- not implemented yet -// ((TextView) row.findViewById(R.id.extra)).setText( -// ... -// ); row.findViewById(R.id.extra).setVisibility(View.GONE); - row.setAlpha(info.active ? 1.0f : 0.5f); // set up click handler @@ -355,38 +324,11 @@ public class NotificationStation extends SettingsPreferenceFragment { startApplicationDetailsActivity(info.pkg); }}); -// // bind radio button -// RadioButton radioButton = (RadioButton) row.findViewById(android.R.id.button1); -// radioButton.setChecked(dreamInfo.isActive); -// radioButton.setOnTouchListener(new OnTouchListener() { -// @Override -// public boolean onTouch(View v, MotionEvent event) { -// row.onTouchEvent(event); -// return false; -// }}); - - // bind settings button + divider -// boolean showSettings = info. -// settingsComponentName != null; -// View settingsDivider = row.findViewById(R.id.divider); -// settingsDivider.setVisibility(false ? View.VISIBLE : View.INVISIBLE); -// -// ImageView settingsButton = (ImageView) row.findViewById(android.R.id.button2); -// settingsButton.setVisibility(false ? View.VISIBLE : View.INVISIBLE); -// settingsButton.setAlpha(info.isActive ? 1f : Utils.DISABLED_ALPHA); -// settingsButton.setEnabled(info.isActive); -// settingsButton.setOnClickListener(new OnClickListener(){ -// @Override -// public void onClick(View v) { -// mBackend.launchSettings((DreamInfo) row.getTag()); -// }}); - return row; } private View createRow(ViewGroup parent) { - final View row = mInflater.inflate(R.layout.notification_log_row, parent, false); - return row; + return mInflater.inflate(R.layout.notification_log_row, parent, false); } } @@ -397,12 +339,4 @@ public class NotificationStation extends SettingsPreferenceFragment { intent.setComponent(intent.resolveActivity(mPm)); startActivity(intent); } - - private class PackageReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - logd("PackageReceiver.onReceive"); - //refreshList(); - } - } } diff --git a/src/com/android/settings/notification/OtherSoundSettings.java b/src/com/android/settings/notification/OtherSoundSettings.java new file mode 100644 index 0000000..8528ec7 --- /dev/null +++ b/src/com/android/settings/notification/OtherSoundSettings.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2014 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.notification; + +import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; +import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Vibrator; +import android.provider.SearchIndexableResource; +import android.provider.Settings.Global; +import android.provider.Settings.System; +import android.telephony.TelephonyManager; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class OtherSoundSettings extends SettingsPreferenceFragment implements Indexable { + private static final String TAG = "OtherSoundSettings"; + + private static final int DEFAULT_ON = 1; + + private static final int EMERGENCY_TONE_SILENT = 0; + private static final int EMERGENCY_TONE_ALERT = 1; + private static final int EMERGENCY_TONE_VIBRATE = 2; + private static final int DEFAULT_EMERGENCY_TONE = EMERGENCY_TONE_SILENT; + + private static final int DOCK_AUDIO_MEDIA_DISABLED = 0; + private static final int DOCK_AUDIO_MEDIA_ENABLED = 1; + private static final int DEFAULT_DOCK_AUDIO_MEDIA = DOCK_AUDIO_MEDIA_DISABLED; + + private static final String KEY_DIAL_PAD_TONES = "dial_pad_tones"; + private static final String KEY_SCREEN_LOCKING_SOUNDS = "screen_locking_sounds"; + private static final String KEY_DOCKING_SOUNDS = "docking_sounds"; + private static final String KEY_TOUCH_SOUNDS = "touch_sounds"; + private static final String KEY_VIBRATE_ON_TOUCH = "vibrate_on_touch"; + private static final String KEY_DOCK_AUDIO_MEDIA = "dock_audio_media"; + private static final String KEY_EMERGENCY_TONE = "emergency_tone"; + + private static final SettingPref PREF_DIAL_PAD_TONES = new SettingPref( + TYPE_SYSTEM, KEY_DIAL_PAD_TONES, System.DTMF_TONE_WHEN_DIALING, DEFAULT_ON) { + @Override + public boolean isApplicable(Context context) { + return Utils.isVoiceCapable(context); + } + }; + + private static final SettingPref PREF_SCREEN_LOCKING_SOUNDS = new SettingPref( + TYPE_SYSTEM, KEY_SCREEN_LOCKING_SOUNDS, System.LOCKSCREEN_SOUNDS_ENABLED, DEFAULT_ON); + + private static final SettingPref PREF_DOCKING_SOUNDS = new SettingPref( + TYPE_GLOBAL, KEY_DOCKING_SOUNDS, Global.DOCK_SOUNDS_ENABLED, DEFAULT_ON) { + @Override + public boolean isApplicable(Context context) { + return hasDockSettings(context); + } + }; + + private static final SettingPref PREF_TOUCH_SOUNDS = new SettingPref( + TYPE_SYSTEM, KEY_TOUCH_SOUNDS, System.SOUND_EFFECTS_ENABLED, DEFAULT_ON) { + @Override + protected boolean setSetting(Context context, int value) { + final AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (value != 0) { + am.loadSoundEffects(); + } else { + am.unloadSoundEffects(); + } + return super.setSetting(context, value); + } + }; + + private static final SettingPref PREF_VIBRATE_ON_TOUCH = new SettingPref( + TYPE_SYSTEM, KEY_VIBRATE_ON_TOUCH, System.HAPTIC_FEEDBACK_ENABLED, DEFAULT_ON) { + @Override + public boolean isApplicable(Context context) { + return hasHaptic(context); + } + }; + + private static final SettingPref PREF_DOCK_AUDIO_MEDIA = new SettingPref( + TYPE_GLOBAL, KEY_DOCK_AUDIO_MEDIA, Global.DOCK_AUDIO_MEDIA_ENABLED, + DEFAULT_DOCK_AUDIO_MEDIA, DOCK_AUDIO_MEDIA_DISABLED, DOCK_AUDIO_MEDIA_ENABLED) { + @Override + public boolean isApplicable(Context context) { + return hasDockSettings(context); + } + + @Override + protected String getCaption(Resources res, int value) { + switch(value) { + case DOCK_AUDIO_MEDIA_DISABLED: + return res.getString(R.string.dock_audio_media_disabled); + case DOCK_AUDIO_MEDIA_ENABLED: + return res.getString(R.string.dock_audio_media_enabled); + default: + throw new IllegalArgumentException(); + } + } + }; + + private static final SettingPref PREF_EMERGENCY_TONE = new SettingPref( + TYPE_GLOBAL, KEY_EMERGENCY_TONE, Global.EMERGENCY_TONE, DEFAULT_EMERGENCY_TONE, + EMERGENCY_TONE_ALERT, EMERGENCY_TONE_VIBRATE, EMERGENCY_TONE_SILENT) { + @Override + public boolean isApplicable(Context context) { + final int activePhoneType = TelephonyManager.getDefault().getCurrentPhoneType(); + return activePhoneType == TelephonyManager.PHONE_TYPE_CDMA; + } + + @Override + protected String getCaption(Resources res, int value) { + switch(value) { + case EMERGENCY_TONE_SILENT: + return res.getString(R.string.emergency_tone_silent); + case EMERGENCY_TONE_ALERT: + return res.getString(R.string.emergency_tone_alert); + case EMERGENCY_TONE_VIBRATE: + return res.getString(R.string.emergency_tone_vibrate); + default: + throw new IllegalArgumentException(); + } + } + }; + + private static final SettingPref[] PREFS = { + PREF_DIAL_PAD_TONES, + PREF_SCREEN_LOCKING_SOUNDS, + PREF_DOCKING_SOUNDS, + PREF_TOUCH_SOUNDS, + PREF_VIBRATE_ON_TOUCH, + PREF_DOCK_AUDIO_MEDIA, + PREF_EMERGENCY_TONE, + }; + + private final SettingsObserver mSettingsObserver = new SettingsObserver(); + + private Context mContext; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.other_sound_settings); + + mContext = getActivity(); + + for (SettingPref pref : PREFS) { + pref.init(this); + } + } + + @Override + public void onResume() { + super.onResume(); + mSettingsObserver.register(true); + } + + @Override + public void onPause() { + super.onPause(); + mSettingsObserver.register(false); + } + + private static boolean hasDockSettings(Context context) { + return context.getResources().getBoolean(R.bool.has_dock_settings); + } + + private static boolean hasHaptic(Context context) { + final Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + return vibrator != null && vibrator.hasVibrator(); + } + + // === Callbacks === + + private final class SettingsObserver extends ContentObserver { + public SettingsObserver() { + super(new Handler()); + } + + public void register(boolean register) { + final ContentResolver cr = getContentResolver(); + if (register) { + for (SettingPref pref : PREFS) { + cr.registerContentObserver(pref.getUri(), false, this); + } + } else { + cr.unregisterContentObserver(this); + } + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + for (SettingPref pref : PREFS) { + if (pref.getUri().equals(uri)) { + pref.update(mContext); + return; + } + } + } + } + + // === Indexing === + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.other_sound_settings; + return Arrays.asList(sir); + } + + public List<String> getNonIndexableKeys(Context context) { + final ArrayList<String> rt = new ArrayList<String>(); + for (SettingPref pref : PREFS) { + if (!pref.isApplicable(context)) { + rt.add(pref.getKey()); + } + } + return rt; + } + }; +} diff --git a/src/com/android/settings/notification/RedactionInterstitial.java b/src/com/android/settings/notification/RedactionInterstitial.java new file mode 100644 index 0000000..2bfad1a --- /dev/null +++ b/src/com/android/settings/notification/RedactionInterstitial.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014 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.notification; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; + +public class RedactionInterstitial extends SettingsActivity { + + @Override + public Intent getIntent() { + Intent modIntent = new Intent(super.getIntent()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, RedactionInterstitialFragment.class.getName()); + return modIntent; + } + + @Override + protected boolean isValidFragment(String fragmentName) { + return RedactionInterstitialFragment.class.getName().equals(fragmentName); + } + + public static Intent createStartIntent(Context ctx) { + return new Intent(ctx, RedactionInterstitial.class) + .putExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, true) + .putExtra(EXTRA_PREFS_SET_BACK_TEXT, (String) null) + .putExtra(EXTRA_PREFS_SET_NEXT_TEXT, ctx.getString( + R.string.app_notifications_dialog_done)); + } + + public static class RedactionInterstitialFragment extends SettingsPreferenceFragment + implements View.OnClickListener { + + private RadioButton mShowAllButton; + private RadioButton mRedactSensitiveButton; + private RadioButton mHideAllButton; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.redaction_interstitial, container, false); + mShowAllButton = (RadioButton) view.findViewById(R.id.show_all); + mRedactSensitiveButton = (RadioButton) view.findViewById(R.id.redact_sensitive); + mHideAllButton = (RadioButton) view.findViewById(R.id.hide_all); + + mShowAllButton.setOnClickListener(this); + mRedactSensitiveButton.setOnClickListener(this); + mHideAllButton.setOnClickListener(this); + return view; + } + + @Override + public void onResume() { + super.onResume(); + loadFromSettings(); + } + + private void loadFromSettings() { + final boolean enabled = Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0) != 0; + final boolean show = Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1) != 0; + mShowAllButton.setChecked(enabled && show); + mRedactSensitiveButton.setChecked(enabled && !show); + mHideAllButton.setChecked(!enabled); + } + + @Override + public void onClick(View v) { + final boolean show = (v == mShowAllButton); + final boolean enabled = (v != mHideAllButton); + + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, show ? 1 : 0); + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, enabled ? 1 : 0); + } + } +} diff --git a/src/com/android/settings/notification/RedactionSettingsStandalone.java b/src/com/android/settings/notification/RedactionSettingsStandalone.java new file mode 100644 index 0000000..26c05c1 --- /dev/null +++ b/src/com/android/settings/notification/RedactionSettingsStandalone.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 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.notification; + +import com.android.settings.R; + +import android.content.Intent; +import com.android.settings.SettingsActivity; +import com.android.settings.notification.RedactionInterstitial.RedactionInterstitialFragment; + +/** Wrapper to allow external activites to jump directly to the {@link RedactionInterstitial} */ +public class RedactionSettingsStandalone extends SettingsActivity { + + @Override + public Intent getIntent() { + Intent modIntent = new Intent(super.getIntent()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, RedactionInterstitialFragment.class.getName()) + .putExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, true) + .putExtra(EXTRA_PREFS_SET_BACK_TEXT, (String) null) + .putExtra(EXTRA_PREFS_SET_NEXT_TEXT, getString( + R.string.app_notifications_dialog_done)); + return modIntent; + } + + @Override + protected boolean isValidFragment(String fragmentName) { + return RedactionInterstitialFragment.class.getName().equals(fragmentName); + } +} diff --git a/src/com/android/settings/notification/SettingPref.java b/src/com/android/settings/notification/SettingPref.java new file mode 100644 index 0000000..a06c35a --- /dev/null +++ b/src/com/android/settings/notification/SettingPref.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.preference.Preference; +import android.preference.TwoStatePreference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.provider.Settings.Global; +import android.provider.Settings.System; + +import com.android.settings.SettingsPreferenceFragment; + +/** Helper to manage a two-state or dropdown preference bound to a global or system setting. */ +public class SettingPref { + public static final int TYPE_GLOBAL = 1; + public static final int TYPE_SYSTEM = 2; + + protected final int mType; + private final String mKey; + protected final String mSetting; + protected final int mDefault; + private final int[] mValues; + private final Uri mUri; + + protected TwoStatePreference mTwoState; + protected DropDownPreference mDropDown; + + public SettingPref(int type, String key, String setting, int def, int... values) { + mType = type; + mKey = key; + mSetting = setting; + mDefault = def; + mValues = values; + mUri = getUriFor(mType, mSetting); + } + + public boolean isApplicable(Context context) { + return true; + } + + protected String getCaption(Resources res, int value) { + throw new UnsupportedOperationException(); + } + + public Preference init(SettingsPreferenceFragment settings) { + final Context context = settings.getActivity(); + Preference p = settings.getPreferenceScreen().findPreference(mKey); + if (p != null && !isApplicable(context)) { + settings.getPreferenceScreen().removePreference(p); + p = null; + } + if (p instanceof TwoStatePreference) { + mTwoState = (TwoStatePreference) p; + } else if (p instanceof DropDownPreference) { + mDropDown = (DropDownPreference) p; + for (int value : mValues) { + mDropDown.addItem(getCaption(context.getResources(), value), value); + } + } + update(context); + if (mTwoState != null) { + p.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + setSetting(context, (Boolean) newValue ? 1 : 0); + return true; + } + }); + return mTwoState; + } + if (mDropDown != null) { + mDropDown.setCallback(new DropDownPreference.Callback() { + @Override + public boolean onItemSelected(int pos, Object value) { + return setSetting(context, (Integer) value); + } + }); + return mDropDown; + } + return null; + } + + protected boolean setSetting(Context context, int value) { + return putInt(mType, context.getContentResolver(), mSetting, value); + } + + public Uri getUri() { + return mUri; + } + + public String getKey() { + return mKey; + } + + public void update(Context context) { + final int val = getInt(mType, context.getContentResolver(), mSetting, mDefault); + if (mTwoState != null) { + mTwoState.setChecked(val != 0); + } else if (mDropDown != null) { + mDropDown.setSelectedValue(val); + } + } + + private static Uri getUriFor(int type, String setting) { + switch(type) { + case TYPE_GLOBAL: + return Global.getUriFor(setting); + case TYPE_SYSTEM: + return System.getUriFor(setting); + } + throw new IllegalArgumentException(); + } + + protected static boolean putInt(int type, ContentResolver cr, String setting, int value) { + switch(type) { + case TYPE_GLOBAL: + return Global.putInt(cr, setting, value); + case TYPE_SYSTEM: + return System.putInt(cr, setting, value); + } + throw new IllegalArgumentException(); + } + + protected static int getInt(int type, ContentResolver cr, String setting, int def) { + switch(type) { + case TYPE_GLOBAL: + return Global.getInt(cr, setting, def); + case TYPE_SYSTEM: + return System.getInt(cr, setting, def); + } + throw new IllegalArgumentException(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java new file mode 100644 index 0000000..f94e6a1 --- /dev/null +++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.ContentResolver; +import android.content.Context; +import android.media.AudioManager; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.preference.SeekBarPreference; +import android.preference.SeekBarVolumizer; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; + +import com.android.settings.R; + +/** A slider preference that directly controls an audio stream volume (no dialog) **/ +public class VolumeSeekBarPreference extends SeekBarPreference + implements PreferenceManager.OnActivityStopListener { + private static final String TAG = "VolumeSeekBarPreference"; + + private int mStream; + private SeekBar mSeekBar; + private SeekBarVolumizer mVolumizer; + private Callback mCallback; + private ImageView mIconView; + + public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public VolumeSeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public VolumeSeekBarPreference(Context context) { + this(context, null); + } + + public void setStream(int stream) { + mStream = stream; + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public void onActivityStop() { + if (mVolumizer != null) { + mVolumizer.stop(); + } + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + if (mStream == 0) { + Log.w(TAG, "No stream found, not binding volumizer"); + return; + } + getPreferenceManager().registerOnActivityStopListener(this); + final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); + if (seekBar == mSeekBar) return; + mSeekBar = seekBar; + final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() { + @Override + public void onSampleStarting(SeekBarVolumizer sbv) { + if (mCallback != null) { + mCallback.onSampleStarting(sbv); + } + } + }; + final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null; + if (mVolumizer == null) { + mVolumizer = new SeekBarVolumizer(getContext(), mStream, sampleUri, sbvc) { + // we need to piggyback on SBV's SeekBar listener to update our icon + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + super.onProgressChanged(seekBar, progress, fromTouch); + mCallback.onStreamValueChanged(mStream, progress); + } + }; + } + mVolumizer.setSeekBar(mSeekBar); + mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon); + mCallback.onStreamValueChanged(mStream, mSeekBar.getProgress()); + } + + // during initialization, this preference is the SeekBar listener + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + super.onProgressChanged(seekBar, progress, fromTouch); + mCallback.onStreamValueChanged(mStream, progress); + } + + public void showIcon(int resId) { + // Instead of using setIcon, which will trigger listeners, this just decorates the + // preference temporarily with a new icon. + if (mIconView != null) { + mIconView.setImageResource(resId); + } + } + + private Uri getMediaVolumeUri() { + return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + getContext().getPackageName() + + "/" + R.raw.media_volume); + } + + public interface Callback { + void onSampleStarting(SeekBarVolumizer sbv); + void onStreamValueChanged(int stream, int progress); + } +} diff --git a/src/com/android/settings/notification/ZenModeAutomaticConditionSelection.java b/src/com/android/settings/notification/ZenModeAutomaticConditionSelection.java new file mode 100644 index 0000000..0e77632 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeAutomaticConditionSelection.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.animation.LayoutTransition; +import android.app.INotificationManager; +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.util.ArraySet; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; + +import com.android.settings.R; + +public class ZenModeAutomaticConditionSelection extends LinearLayout { + private static final String TAG = "ZenModeAutomaticConditionSelection"; + private static final boolean DEBUG = true; + + private final INotificationManager mNoMan; + private final H mHandler = new H(); + private final Context mContext; + private final ArraySet<Uri> mSelectedConditions = new ArraySet<Uri>(); + + public ZenModeAutomaticConditionSelection(Context context) { + super(context); + mContext = context; + setOrientation(VERTICAL); + setLayoutTransition(new LayoutTransition()); + final int p = mContext.getResources().getDimensionPixelSize(R.dimen.content_margin_left); + setPadding(p, p, p, 0); + mNoMan = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + refreshSelectedConditions(); + } + + private void refreshSelectedConditions() { + try { + final Condition[] automatic = mNoMan.getAutomaticZenModeConditions(); + mSelectedConditions.clear(); + if (automatic != null) { + for (Condition c : automatic) { + mSelectedConditions.add(c.id); + } + } + } catch (RemoteException e) { + Log.w(TAG, "Error calling getAutomaticZenModeConditions", e); + } + } + + private CheckBox newCheckBox(Object tag) { + final CheckBox button = new CheckBox(mContext); + button.setTag(tag); + button.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setSelectedCondition((Uri)button.getTag(), isChecked); + } + }); + addView(button); + return button; + } + + private void setSelectedCondition(Uri conditionId, boolean selected) { + if (DEBUG) Log.d(TAG, "setSelectedCondition conditionId=" + conditionId + + " selected=" + selected); + if (selected) { + mSelectedConditions.add(conditionId); + } else { + mSelectedConditions.remove(conditionId); + } + final Uri[] automatic = new Uri[mSelectedConditions.size()]; + for (int i = 0; i < automatic.length; i++) { + automatic[i] = mSelectedConditions.valueAt(i); + } + try { + mNoMan.setAutomaticZenModeConditions(automatic); + } catch (RemoteException e) { + Log.w(TAG, "Error calling setAutomaticZenModeConditions", e); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + requestZenModeConditions(Condition.FLAG_RELEVANT_ALWAYS); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + requestZenModeConditions(0 /*none*/); + } + + protected void requestZenModeConditions(int relevance) { + if (DEBUG) Log.d(TAG, "requestZenModeConditions " + Condition.relevanceToString(relevance)); + try { + mNoMan.requestZenModeConditions(mListener, relevance); + } catch (RemoteException e) { + Log.w(TAG, "Error calling requestZenModeConditions", e); + } + } + + protected void handleConditions(Condition[] conditions) { + for (final Condition c : conditions) { + CheckBox v = (CheckBox) findViewWithTag(c.id); + if (c.state != Condition.STATE_ERROR) { + if (v == null) { + v = newCheckBox(c.id); + } + } + if (v != null) { + v.setText(c.summary); + v.setEnabled(c.state != Condition.STATE_ERROR); + v.setChecked(mSelectedConditions.contains(c.id)); + } + } + } + + private final IConditionListener mListener = new IConditionListener.Stub() { + @Override + public void onConditionsReceived(Condition[] conditions) { + if (conditions == null || conditions.length == 0) return; + mHandler.obtainMessage(H.CONDITIONS, conditions).sendToTarget(); + } + }; + + private final class H extends Handler { + private static final int CONDITIONS = 1; + + @Override + public void handleMessage(Message msg) { + if (msg.what == CONDITIONS) handleConditions((Condition[])msg.obj); + } + } +} diff --git a/src/com/android/settings/notification/ZenModeConditionSelection.java b/src/com/android/settings/notification/ZenModeConditionSelection.java new file mode 100644 index 0000000..856a7f6 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeConditionSelection.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.animation.LayoutTransition; +import android.app.INotificationManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.service.notification.ZenModeConfig; +import android.util.Log; +import android.widget.CompoundButton; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +public class ZenModeConditionSelection extends RadioGroup { + private static final String TAG = "ZenModeConditionSelection"; + private static final boolean DEBUG = true; + + private final INotificationManager mNoMan; + private final H mHandler = new H(); + private final Context mContext; + private final List<Condition> mConditions; + private Condition mCondition; + + public ZenModeConditionSelection(Context context) { + super(context); + mContext = context; + mConditions = new ArrayList<Condition>(); + setLayoutTransition(new LayoutTransition()); + final int p = mContext.getResources().getDimensionPixelSize(R.dimen.content_margin_left); + setPadding(p, p, p, 0); + mNoMan = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + final RadioButton b = newRadioButton(null); + b.setText(mContext.getString(com.android.internal.R.string.zen_mode_forever)); + b.setChecked(true); + for (int i = ZenModeConfig.MINUTE_BUCKETS.length - 1; i >= 0; --i) { + handleCondition(ZenModeConfig.toTimeCondition(ZenModeConfig.MINUTE_BUCKETS[i])); + } + } + + private RadioButton newRadioButton(Condition condition) { + final RadioButton button = new RadioButton(mContext); + button.setTag(condition); + button.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + setCondition((Condition) button.getTag()); + } + } + }); + addView(button); + return button; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + requestZenModeConditions(Condition.FLAG_RELEVANT_NOW); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + requestZenModeConditions(0 /*none*/); + } + + protected void requestZenModeConditions(int relevance) { + if (DEBUG) Log.d(TAG, "requestZenModeConditions " + Condition.relevanceToString(relevance)); + try { + mNoMan.requestZenModeConditions(mListener, relevance); + } catch (RemoteException e) { + // noop + } + } + + protected void handleConditions(Condition[] conditions) { + for (Condition c : conditions) { + handleCondition(c); + } + } + + protected void handleCondition(Condition c) { + if (mConditions.contains(c)) return; + RadioButton v = (RadioButton) findViewWithTag(c.id); + if (c.state == Condition.STATE_TRUE || c.state == Condition.STATE_UNKNOWN) { + if (v == null) { + v = newRadioButton(c); + } + } + if (v != null) { + v.setText(c.summary); + v.setEnabled(c.state == Condition.STATE_TRUE); + } + mConditions.add(c); + } + + protected void setCondition(Condition c) { + if (DEBUG) Log.d(TAG, "setCondition " + c); + mCondition = c; + } + + public void confirmCondition() { + if (DEBUG) Log.d(TAG, "confirmCondition " + mCondition); + try { + mNoMan.setZenModeCondition(mCondition); + } catch (RemoteException e) { + // noop + } + } + + private final IConditionListener mListener = new IConditionListener.Stub() { + @Override + public void onConditionsReceived(Condition[] conditions) { + if (conditions == null || conditions.length == 0) return; + mHandler.obtainMessage(H.CONDITIONS, conditions).sendToTarget(); + } + }; + + private final class H extends Handler { + private static final int CONDITIONS = 1; + + @Override + public void handleMessage(Message msg) { + if (msg.what == CONDITIONS) handleConditions((Condition[]) msg.obj); + } + } +} diff --git a/src/com/android/settings/notification/ZenModeDowntimeDaysSelection.java b/src/com/android/settings/notification/ZenModeDowntimeDaysSelection.java new file mode 100644 index 0000000..3361ad0 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeDowntimeDaysSelection.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 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.notification; + +import android.content.Context; +import android.service.notification.ZenModeConfig; +import android.util.SparseBooleanArray; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ScrollView; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.LinearLayout; + +import com.android.settings.R; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class ZenModeDowntimeDaysSelection extends ScrollView { + public static final int[] DAYS = { + Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, + Calendar.SATURDAY, Calendar.SUNDAY + }; + private static final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("EEEE"); + + private final SparseBooleanArray mDays = new SparseBooleanArray(); + private final LinearLayout mLayout; + + public ZenModeDowntimeDaysSelection(Context context, String mode) { + super(context); + mLayout = new LinearLayout(mContext); + final int hPad = context.getResources().getDimensionPixelSize(R.dimen.zen_downtime_margin); + mLayout.setPadding(hPad, 0, hPad, 0); + addView(mLayout); + final int[] days = ZenModeConfig.tryParseDays(mode); + if (days != null) { + for (int i = 0; i < days.length; i++) { + mDays.put(days[i], true); + } + } + mLayout.setOrientation(LinearLayout.VERTICAL); + final Calendar c = Calendar.getInstance(); + final LayoutInflater inflater = LayoutInflater.from(context); + for (int i = 0; i < DAYS.length; i++) { + final int day = DAYS[i]; + final CheckBox checkBox = (CheckBox) inflater.inflate(R.layout.zen_downtime_day, + this, false); + c.set(Calendar.DAY_OF_WEEK, day); + checkBox.setText(DAY_FORMAT.format(c.getTime())); + checkBox.setChecked(mDays.get(day)); + checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mDays.put(day, isChecked); + onChanged(getMode()); + } + }); + mLayout.addView(checkBox); + } + } + + private String getMode() { + final StringBuilder sb = new StringBuilder(ZenModeConfig.SLEEP_MODE_DAYS_PREFIX); + boolean empty = true; + for (int i = 0; i < mDays.size(); i++) { + final int day = mDays.keyAt(i); + if (!mDays.valueAt(i)) continue; + if (empty) { + empty = false; + } else { + sb.append(','); + } + sb.append(day); + } + return empty ? null : sb.toString(); + } + + protected void onChanged(String mode) { + // event hook for subclasses + } +} diff --git a/src/com/android/settings/notification/ZenModeSettings.java b/src/com/android/settings/notification/ZenModeSettings.java new file mode 100644 index 0000000..7fd3aa1 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeSettings.java @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2014 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.notification; + +import static com.android.settings.notification.ZenModeDowntimeDaysSelection.DAYS; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.INotificationManager; +import android.app.TimePickerDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.ServiceManager; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.Settings.Global; +import android.service.notification.Condition; +import android.service.notification.ZenModeConfig; +import android.text.format.DateFormat; +import android.util.Log; +import android.util.SparseArray; +import android.widget.ScrollView; +import android.widget.TimePicker; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.notification.DropDownPreference.Callback; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Objects; + +public class ZenModeSettings extends SettingsPreferenceFragment implements Indexable { + private static final String TAG = "ZenModeSettings"; + private static final boolean DEBUG = true; + + private static final String KEY_ZEN_MODE = "zen_mode"; + private static final String KEY_IMPORTANT = "important"; + private static final String KEY_CALLS = "phone_calls"; + private static final String KEY_MESSAGES = "messages"; + private static final String KEY_STARRED = "starred"; + private static final String KEY_EVENTS = "events"; + private static final String KEY_ALARM_INFO = "alarm_info"; + + private static final String KEY_DOWNTIME = "downtime"; + private static final String KEY_DAYS = "days"; + private static final String KEY_START_TIME = "start_time"; + private static final String KEY_END_TIME = "end_time"; + + private static final String KEY_AUTOMATION = "automation"; + private static final String KEY_ENTRY = "entry"; + private static final String KEY_CONDITION_PROVIDERS = "manage_condition_providers"; + + private static final SettingPrefWithCallback PREF_ZEN_MODE = new SettingPrefWithCallback( + SettingPref.TYPE_GLOBAL, KEY_ZEN_MODE, Global.ZEN_MODE, Global.ZEN_MODE_OFF, + Global.ZEN_MODE_OFF, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Global.ZEN_MODE_NO_INTERRUPTIONS) { + protected String getCaption(Resources res, int value) { + switch (value) { + case Global.ZEN_MODE_NO_INTERRUPTIONS: + return res.getString(R.string.zen_mode_option_no_interruptions); + case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + return res.getString(R.string.zen_mode_option_important_interruptions); + default: + return res.getString(R.string.zen_mode_option_off); + } + } + }; + + private static final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("EEE"); + + private static SparseArray<String> allKeyTitles(Context context) { + final SparseArray<String> rt = new SparseArray<String>(); + rt.put(R.string.zen_mode_important_category, KEY_IMPORTANT); + if (Utils.isVoiceCapable(context)) { + rt.put(R.string.zen_mode_phone_calls, KEY_CALLS); + rt.put(R.string.zen_mode_option_title, KEY_ZEN_MODE); + } else { + rt.put(R.string.zen_mode_option_title_novoice, KEY_ZEN_MODE); + } + rt.put(R.string.zen_mode_messages, KEY_MESSAGES); + rt.put(R.string.zen_mode_from_starred, KEY_STARRED); + rt.put(R.string.zen_mode_events, KEY_EVENTS); + rt.put(R.string.zen_mode_alarm_info, KEY_ALARM_INFO); + rt.put(R.string.zen_mode_downtime_category, KEY_DOWNTIME); + rt.put(R.string.zen_mode_downtime_days, KEY_DAYS); + rt.put(R.string.zen_mode_start_time, KEY_START_TIME); + rt.put(R.string.zen_mode_end_time, KEY_END_TIME); + rt.put(R.string.zen_mode_automation_category, KEY_AUTOMATION); + rt.put(R.string.manage_condition_providers, KEY_CONDITION_PROVIDERS); + return rt; + } + + private final Handler mHandler = new Handler(); + private final SettingsObserver mSettingsObserver = new SettingsObserver(); + + private Context mContext; + private PackageManager mPM; + private ZenModeConfig mConfig; + private boolean mDisableListeners; + private SwitchPreference mCalls; + private SwitchPreference mMessages; + private DropDownPreference mStarred; + private SwitchPreference mEvents; + private Preference mDays; + private TimePickerPreference mStart; + private TimePickerPreference mEnd; + private PreferenceCategory mAutomationCategory; + private Preference mEntry; + private Preference mConditionProviders; + private AlertDialog mDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getActivity(); + mPM = mContext.getPackageManager(); + + addPreferencesFromResource(R.xml.zen_mode_settings); + final PreferenceScreen root = getPreferenceScreen(); + + mConfig = getZenModeConfig(); + if (DEBUG) Log.d(TAG, "Loaded mConfig=" + mConfig); + + final Preference zenMode = PREF_ZEN_MODE.init(this); + PREF_ZEN_MODE.setCallback(new SettingPrefWithCallback.Callback() { + @Override + public void onSettingSelected(int value) { + if (value != Global.ZEN_MODE_OFF) { + showConditionSelection(value); + } + } + }); + if (!Utils.isVoiceCapable(mContext)) { + zenMode.setTitle(R.string.zen_mode_option_title_novoice); + } + + final PreferenceCategory important = + (PreferenceCategory) root.findPreference(KEY_IMPORTANT); + + mCalls = (SwitchPreference) important.findPreference(KEY_CALLS); + if (Utils.isVoiceCapable(mContext)) { + mCalls.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mDisableListeners) return true; + final boolean val = (Boolean) newValue; + if (val == mConfig.allowCalls) return true; + if (DEBUG) Log.d(TAG, "onPrefChange allowCalls=" + val); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.allowCalls = val; + return setZenModeConfig(newConfig); + } + }); + } else { + important.removePreference(mCalls); + mCalls = null; + } + + mMessages = (SwitchPreference) important.findPreference(KEY_MESSAGES); + mMessages.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mDisableListeners) return true; + final boolean val = (Boolean) newValue; + if (val == mConfig.allowMessages) return true; + if (DEBUG) Log.d(TAG, "onPrefChange allowMessages=" + val); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.allowMessages = val; + return setZenModeConfig(newConfig); + } + }); + + mStarred = (DropDownPreference) important.findPreference(KEY_STARRED); + mStarred.addItem(R.string.zen_mode_from_anyone, ZenModeConfig.SOURCE_ANYONE); + mStarred.addItem(R.string.zen_mode_from_starred, ZenModeConfig.SOURCE_STAR); + mStarred.addItem(R.string.zen_mode_from_contacts, ZenModeConfig.SOURCE_CONTACT); + mStarred.setCallback(new DropDownPreference.Callback() { + @Override + public boolean onItemSelected(int pos, Object newValue) { + if (mDisableListeners) return true; + final int val = (Integer) newValue; + if (val == mConfig.allowFrom) return true; + if (DEBUG) Log.d(TAG, "onPrefChange allowFrom=" + + ZenModeConfig.sourceToString(val)); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.allowFrom = val; + return setZenModeConfig(newConfig); + } + }); + important.addPreference(mStarred); + + mEvents = (SwitchPreference) important.findPreference(KEY_EVENTS); + mEvents.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mDisableListeners) return true; + final boolean val = (Boolean) newValue; + if (val == mConfig.allowEvents) return true; + if (DEBUG) Log.d(TAG, "onPrefChange allowEvents=" + val); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.allowEvents = val; + return setZenModeConfig(newConfig); + } + }); + + final PreferenceCategory downtime = (PreferenceCategory) root.findPreference(KEY_DOWNTIME); + + mDays = downtime.findPreference(KEY_DAYS); + mDays.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(mContext) + .setTitle(R.string.zen_mode_downtime_days) + .setView(new ZenModeDowntimeDaysSelection(mContext, mConfig.sleepMode) { + @Override + protected void onChanged(String mode) { + if (mDisableListeners) return; + if (Objects.equals(mode, mConfig.sleepMode)) return; + if (DEBUG) Log.d(TAG, "days.onChanged sleepMode=" + mode); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.sleepMode = mode; + setZenModeConfig(newConfig); + } + }) + .setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + updateDays(); + } + }) + .setPositiveButton(R.string.done_button, null) + .show(); + return true; + } + }); + + final FragmentManager mgr = getFragmentManager(); + + mStart = new TimePickerPreference(mContext, mgr); + mStart.setKey(KEY_START_TIME); + mStart.setTitle(R.string.zen_mode_start_time); + mStart.setCallback(new TimePickerPreference.Callback() { + @Override + public boolean onSetTime(int hour, int minute) { + if (mDisableListeners) return true; + if (!ZenModeConfig.isValidHour(hour)) return false; + if (!ZenModeConfig.isValidMinute(minute)) return false; + if (hour == mConfig.sleepStartHour && minute == mConfig.sleepStartMinute) { + return true; + } + if (DEBUG) Log.d(TAG, "onPrefChange sleepStart h=" + hour + " m=" + minute); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.sleepStartHour = hour; + newConfig.sleepStartMinute = minute; + return setZenModeConfig(newConfig); + } + }); + downtime.addPreference(mStart); + mStart.setDependency(mDays.getKey()); + + mEnd = new TimePickerPreference(mContext, mgr); + mEnd.setKey(KEY_END_TIME); + mEnd.setTitle(R.string.zen_mode_end_time); + mEnd.setCallback(new TimePickerPreference.Callback() { + @Override + public boolean onSetTime(int hour, int minute) { + if (mDisableListeners) return true; + if (!ZenModeConfig.isValidHour(hour)) return false; + if (!ZenModeConfig.isValidMinute(minute)) return false; + if (hour == mConfig.sleepEndHour && minute == mConfig.sleepEndMinute) { + return true; + } + if (DEBUG) Log.d(TAG, "onPrefChange sleepEnd h=" + hour + " m=" + minute); + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.sleepEndHour = hour; + newConfig.sleepEndMinute = minute; + return setZenModeConfig(newConfig); + } + }); + downtime.addPreference(mEnd); + mEnd.setDependency(mDays.getKey()); + + mAutomationCategory = (PreferenceCategory) findPreference(KEY_AUTOMATION); + mEntry = findPreference(KEY_ENTRY); + mEntry.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(mContext) + .setTitle(R.string.zen_mode_entry_conditions_title) + .setView(new ZenModeAutomaticConditionSelection(mContext)) + .setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + refreshAutomationSection(); + } + }) + .setPositiveButton(R.string.dlg_ok, null) + .show(); + return true; + } + }); + mConditionProviders = findPreference(KEY_CONDITION_PROVIDERS); + + updateControls(); + } + + private void updateDays() { + if (mConfig != null) { + final int[] days = ZenModeConfig.tryParseDays(mConfig.sleepMode); + if (days != null && days.length != 0) { + final StringBuilder sb = new StringBuilder(); + final Calendar c = Calendar.getInstance(); + for (int i = 0; i < DAYS.length; i++) { + final int day = DAYS[i]; + for (int j = 0; j < days.length; j++) { + if (day == days[j]) { + c.set(Calendar.DAY_OF_WEEK, day); + if (sb.length() > 0) { + sb.append(mContext.getString(R.string.summary_divider_text)); + } + sb.append(DAY_FORMAT.format(c.getTime())); + break; + } + } + } + if (sb.length() > 0) { + mDays.setSummary(sb); + mDays.notifyDependencyChange(false); + return; + } + } + } + mDays.setSummary(R.string.zen_mode_downtime_days_none); + mDays.notifyDependencyChange(true); + } + + private void updateEndSummary() { + final int startMin = 60 * mConfig.sleepStartHour + mConfig.sleepStartMinute; + final int endMin = 60 * mConfig.sleepEndHour + mConfig.sleepEndMinute; + final boolean nextDay = startMin >= endMin; + mEnd.setSummaryFormat(nextDay ? R.string.zen_mode_end_time_summary_format : 0); + } + + private void updateControls() { + mDisableListeners = true; + if (mCalls != null) { + mCalls.setChecked(mConfig.allowCalls); + } + mMessages.setChecked(mConfig.allowMessages); + mStarred.setSelectedValue(mConfig.allowFrom); + mEvents.setChecked(mConfig.allowEvents); + updateStarredEnabled(); + updateDays(); + mStart.setTime(mConfig.sleepStartHour, mConfig.sleepStartMinute); + mEnd.setTime(mConfig.sleepEndHour, mConfig.sleepEndMinute); + mDisableListeners = false; + refreshAutomationSection(); + updateEndSummary(); + } + + private void updateStarredEnabled() { + mStarred.setEnabled(mConfig.allowCalls || mConfig.allowMessages); + } + + private void refreshAutomationSection() { + if (mConditionProviders != null) { + final int total = ConditionProviderSettings.getProviderCount(mPM); + if (total == 0) { + getPreferenceScreen().removePreference(mAutomationCategory); + } else { + final int n = ConditionProviderSettings.getEnabledProviderCount(mContext); + if (n == 0) { + mConditionProviders.setSummary(getResources().getString( + R.string.manage_condition_providers_summary_zero)); + } else { + mConditionProviders.setSummary(String.format(getResources().getQuantityString( + R.plurals.manage_condition_providers_summary_nonzero, + n, n))); + } + final String entrySummary = getEntryConditionSummary(); + if (n == 0 || entrySummary == null) { + mEntry.setSummary(R.string.zen_mode_entry_conditions_summary_none); + } else { + mEntry.setSummary(entrySummary); + } + } + } + } + + private String getEntryConditionSummary() { + final INotificationManager nm = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + try { + final Condition[] automatic = nm.getAutomaticZenModeConditions(); + if (automatic == null || automatic.length == 0) { + return null; + } + final String divider = getString(R.string.summary_divider_text); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < automatic.length; i++) { + if (i > 0) sb.append(divider); + sb.append(automatic[i].summary); + } + return sb.toString(); + } catch (Exception e) { + Log.w(TAG, "Error calling getAutomaticZenModeConditions", e); + return null; + } + } + + @Override + public void onResume() { + super.onResume(); + updateControls(); + mSettingsObserver.register(); + } + + @Override + public void onPause() { + super.onPause(); + mSettingsObserver.unregister(); + } + + private void updateZenModeConfig() { + final ZenModeConfig config = getZenModeConfig(); + if (Objects.equals(config, mConfig)) return; + mConfig = config; + if (DEBUG) Log.d(TAG, "updateZenModeConfig mConfig=" + mConfig); + updateControls(); + } + + private ZenModeConfig getZenModeConfig() { + final INotificationManager nm = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + try { + return nm.getZenModeConfig(); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return new ZenModeConfig(); + } + } + + private boolean setZenModeConfig(ZenModeConfig config) { + final INotificationManager nm = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + try { + final boolean success = nm.setZenModeConfig(config); + if (success) { + mConfig = config; + if (DEBUG) Log.d(TAG, "Saved mConfig=" + mConfig); + updateEndSummary(); + updateStarredEnabled(); + } + return success; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + protected void putZenModeSetting(int value) { + Global.putInt(getContentResolver(), Global.ZEN_MODE, value); + } + + protected void showConditionSelection(final int newSettingsValue) { + if (mDialog != null) return; + + final ZenModeConditionSelection zenModeConditionSelection = + new ZenModeConditionSelection(mContext); + DialogInterface.OnClickListener positiveListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + zenModeConditionSelection.confirmCondition(); + mDialog = null; + } + }; + final int oldSettingsValue = PREF_ZEN_MODE.getValue(mContext); + ScrollView scrollView = new ScrollView(mContext); + scrollView.addView(zenModeConditionSelection); + mDialog = new AlertDialog.Builder(getActivity()) + .setTitle(PREF_ZEN_MODE.getCaption(getResources(), newSettingsValue)) + .setView(scrollView) + .setPositiveButton(R.string.okay, positiveListener) + .setNegativeButton(R.string.cancel_all_caps, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + cancelDialog(oldSettingsValue); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + cancelDialog(oldSettingsValue); + } + }).create(); + mDialog.show(); + } + + protected void cancelDialog(int oldSettingsValue) { + // If not making a decision, reset drop down to current setting. + PREF_ZEN_MODE.setValueWithoutCallback(mContext, oldSettingsValue); + mDialog = null; + } + + // Enable indexing of searchable data + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final SparseArray<String> keyTitles = allKeyTitles(context); + final int N = keyTitles.size(); + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(N); + final Resources res = context.getResources(); + for (int i = 0; i < N; i++) { + final SearchIndexableRaw data = new SearchIndexableRaw(context); + data.key = keyTitles.valueAt(i); + data.title = res.getString(keyTitles.keyAt(i)); + data.screenTitle = res.getString(R.string.zen_mode_settings_title); + result.add(data); + } + return result; + } + + public List<String> getNonIndexableKeys(Context context) { + final ArrayList<String> rt = new ArrayList<String>(); + if (!Utils.isVoiceCapable(context)) { + rt.add(KEY_CALLS); + } + return rt; + } + }; + + private static class SettingPrefWithCallback extends SettingPref { + + private Callback mCallback; + private int mValue; + + public SettingPrefWithCallback(int type, String key, String setting, int def, + int... values) { + super(type, key, setting, def, values); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public void update(Context context) { + // Avoid callbacks from non-user changes. + mValue = getValue(context); + super.update(context); + } + + @Override + protected boolean setSetting(Context context, int value) { + if (value == mValue) return true; + mValue = value; + if (mCallback != null) { + mCallback.onSettingSelected(value); + } + return super.setSetting(context, value); + } + + @Override + public Preference init(SettingsPreferenceFragment settings) { + Preference ret = super.init(settings); + mValue = getValue(settings.getActivity()); + + return ret; + } + + public boolean setValueWithoutCallback(Context context, int value) { + // Set the current value ahead of time, this way we won't trigger a callback. + mValue = value; + return putInt(mType, context.getContentResolver(), mSetting, value); + } + + public int getValue(Context context) { + return getInt(mType, context.getContentResolver(), mSetting, mDefault); + } + + public interface Callback { + void onSettingSelected(int value); + } + } + + private final class SettingsObserver extends ContentObserver { + private final Uri ZEN_MODE_URI = Global.getUriFor(Global.ZEN_MODE); + private final Uri ZEN_MODE_CONFIG_ETAG_URI = Global.getUriFor(Global.ZEN_MODE_CONFIG_ETAG); + + public SettingsObserver() { + super(mHandler); + } + + public void register() { + getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this); + getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, this); + } + + public void unregister() { + getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (ZEN_MODE_URI.equals(uri)) { + PREF_ZEN_MODE.update(mContext); + } + if (ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) { + updateZenModeConfig(); + } + } + } + + private static class TimePickerPreference extends Preference { + private final Context mContext; + + private int mSummaryFormat; + private int mHourOfDay; + private int mMinute; + private Callback mCallback; + + public TimePickerPreference(Context context, final FragmentManager mgr) { + super(context); + mContext = context; + setPersistent(false); + setOnPreferenceClickListener(new OnPreferenceClickListener(){ + @Override + public boolean onPreferenceClick(Preference preference) { + final TimePickerFragment frag = new TimePickerFragment(); + frag.pref = TimePickerPreference.this; + frag.show(mgr, TimePickerPreference.class.getName()); + return true; + } + }); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public void setSummaryFormat(int resId) { + mSummaryFormat = resId; + updateSummary(); + } + + public void setTime(int hourOfDay, int minute) { + if (mCallback != null && !mCallback.onSetTime(hourOfDay, minute)) return; + mHourOfDay = hourOfDay; + mMinute = minute; + updateSummary(); + } + + private void updateSummary() { + final Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, mHourOfDay); + c.set(Calendar.MINUTE, mMinute); + String time = DateFormat.getTimeFormat(mContext).format(c.getTime()); + if (mSummaryFormat != 0) { + time = mContext.getResources().getString(mSummaryFormat, time); + } + setSummary(time); + } + + public static class TimePickerFragment extends DialogFragment implements + TimePickerDialog.OnTimeSetListener { + public TimePickerPreference pref; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final boolean usePref = pref != null && pref.mHourOfDay >= 0 && pref.mMinute >= 0; + final Calendar c = Calendar.getInstance(); + final int hour = usePref ? pref.mHourOfDay : c.get(Calendar.HOUR_OF_DAY); + final int minute = usePref ? pref.mMinute : c.get(Calendar.MINUTE); + return new TimePickerDialog(getActivity(), this, hour, minute, + DateFormat.is24HourFormat(getActivity())); + } + + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + if (pref != null) { + pref.setTime(hourOfDay, minute); + } + } + } + + public interface Callback { + boolean onSetTime(int hour, int minute); + } + } +} diff --git a/src/com/android/settings/print/PrintJobSettingsFragment.java b/src/com/android/settings/print/PrintJobSettingsFragment.java index f420a82..34db97b 100644 --- a/src/com/android/settings/print/PrintJobSettingsFragment.java +++ b/src/com/android/settings/print/PrintJobSettingsFragment.java @@ -80,7 +80,7 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( ActivityManager.getCurrentUser()); - getActivity().setTitle(R.string.print_print_job); + getActivity().getActionBar().setTitle(R.string.print_print_job); processArguments(); @@ -112,13 +112,18 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (!mPrintJob.getInfo().isCancelling()) { + PrintJob printJob = getPrintJob(); + if (printJob == null) { + return; + } + + if (!printJob.getInfo().isCancelling()) { MenuItem cancel = menu.add(0, MENU_ITEM_ID_CANCEL, Menu.NONE, getString(R.string.print_cancel)); cancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } - if (mPrintJob.isFailed()) { + if (printJob.isFailed()) { MenuItem restart = menu.add(0, MENU_ITEM_ID_RESTART, Menu.NONE, getString(R.string.print_restart)); restart.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); @@ -129,13 +134,13 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_ITEM_ID_CANCEL: { - mPrintJob.cancel(); + getPrintJob().cancel(); finish(); return true; } case MENU_ITEM_ID_RESTART: { - mPrintJob.restart(); + getPrintJob().restart(); finish(); return true; } @@ -152,25 +157,32 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { } } + private PrintJob getPrintJob() { + if (mPrintJob == null) { + mPrintJob = mPrintManager.getPrintJob(mPrintJobId); + } + return mPrintJob; + } + private void updateUi() { - mPrintJob = mPrintManager.getPrintJob(mPrintJobId); + PrintJob printJob = getPrintJob(); - if (mPrintJob == null) { + if (printJob == null) { finish(); return; } - if (mPrintJob.isCancelled() || mPrintJob.isCompleted()) { + if (printJob.isCancelled() || printJob.isCompleted()) { finish(); return; } - PrintJobInfo info = mPrintJob.getInfo(); + PrintJobInfo info = printJob.getInfo(); switch (info.getState()) { case PrintJobInfo.STATE_QUEUED: case PrintJobInfo.STATE_STARTED: { - if (!mPrintJob.getInfo().isCancelling()) { + if (!printJob.getInfo().isCancelling()) { mPrintJobPreference.setTitle(getString( R.string.print_printing_state_title_template, info.getLabel())); } else { @@ -185,7 +197,7 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { } break; case PrintJobInfo.STATE_BLOCKED: { - if (!mPrintJob.getInfo().isCancelling()) { + if (!printJob.getInfo().isCancelling()) { mPrintJobPreference.setTitle(getString( R.string.print_blocked_state_title_template, info.getLabel())); } else { @@ -203,12 +215,12 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { switch (info.getState()) { case PrintJobInfo.STATE_QUEUED: case PrintJobInfo.STATE_STARTED: { - mPrintJobPreference.setIcon(com.android.internal.R.drawable.ic_print); + mPrintJobPreference.setIcon(R.drawable.ic_print); } break; case PrintJobInfo.STATE_FAILED: case PrintJobInfo.STATE_BLOCKED: { - mPrintJobPreference.setIcon(com.android.internal.R.drawable.ic_print_error); + mPrintJobPreference.setIcon(R.drawable.ic_print_error); } break; } diff --git a/src/com/android/settings/print/PrintServiceSettingsFragment.java b/src/com/android/settings/print/PrintServiceSettingsFragment.java index 326dd78..49fd6df 100644 --- a/src/com/android/settings/print/PrintServiceSettingsFragment.java +++ b/src/com/android/settings/print/PrintServiceSettingsFragment.java @@ -16,7 +16,6 @@ package com.android.settings.print; -import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -38,16 +37,13 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.preference.PreferenceActivity; import android.print.PrintManager; import android.print.PrinterDiscoverySession; import android.print.PrinterDiscoverySession.OnPrintersChangeListener; import android.print.PrinterId; import android.print.PrinterInfo; -import android.provider.Settings; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -55,29 +51,31 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.widget.BaseAdapter; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.ListView; import android.widget.SearchView; +import android.widget.Switch; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.print.PrintSettingsFragment.ToggleSwitch; -import com.android.settings.print.PrintSettingsFragment.ToggleSwitch.OnBeforeCheckedChangeListener; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; + +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.ToggleSwitch; + /** * Fragment with print service settings. */ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment - implements DialogInterface.OnClickListener { + implements DialogInterface.OnClickListener, SwitchBar.OnSwitchChangeListener { private static final int LOADER_ID_PRINTERS_LOADER = 1; @@ -113,6 +111,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } }; + private SwitchBar mSwitchBar; private ToggleSwitch mToggleSwitch; private String mPreferenceKey; @@ -130,12 +129,14 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment private PrintersAdapter mPrintersAdapter; + // 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; + private int mLastUnfilteredItemCount; private boolean mServiceEnabled; - private AnnounceFilterResult mAnnounceFilterResult; - @Override public void onResume() { super.onResume(); @@ -147,9 +148,6 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public void onPause() { mSettingsContentObserver.unregister(getContentResolver()); - if (mAnnounceFilterResult != null) { - mAnnounceFilterResult.remove(); - } super.onPause(); } @@ -162,20 +160,23 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public void onDestroyView() { - getActivity().getActionBar().setCustomView(null); - mToggleSwitch.setOnBeforeCheckedChangeListener(null); + if (mOldActivityTitle != null) { + getActivity().getActionBar().setTitle(mOldActivityTitle); + } super.onDestroyView(); + mSwitchBar.removeOnSwitchChangeListener(this); + mSwitchBar.hide(); } private void onPreferenceToggled(String preferenceKey, boolean enabled) { ComponentName service = ComponentName.unflattenFromString(preferenceKey); - List<ComponentName> services = SettingsUtils.readEnabledPrintServices(getActivity()); + List<ComponentName> services = PrintSettingsUtils.readEnabledPrintServices(getActivity()); if (enabled) { services.add(service); } else { services.remove(service); } - SettingsUtils.writeEnabledPrintServices(getActivity(), services); + PrintSettingsUtils.writeEnabledPrintServices(getActivity(), services); } @Override @@ -192,7 +193,6 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } return new AlertDialog.Builder(getActivity()) .setTitle(title) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(message) .setCancelable(true) .setPositiveButton(android.R.string.ok, this) @@ -206,13 +206,13 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment switch (which) { case DialogInterface.BUTTON_POSITIVE: checked = true; - mToggleSwitch.setCheckedInternal(checked); + mSwitchBar.setCheckedInternal(checked); getArguments().putBoolean(PrintSettingsFragment.EXTRA_CHECKED, checked); onPreferenceToggled(mPreferenceKey, checked); break; case DialogInterface.BUTTON_NEGATIVE: checked = false; - mToggleSwitch.setCheckedInternal(checked); + mSwitchBar.setCheckedInternal(checked); getArguments().putBoolean(PrintSettingsFragment.EXTRA_CHECKED, checked); onPreferenceToggled(mPreferenceKey, checked); break; @@ -233,7 +233,8 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); - emptyView.setContentDescription(getString(R.string.print_service_disabled)); + ImageView iconView = (ImageView) emptyView.findViewById(R.id.icon); + iconView.setContentDescription(getString(R.string.print_service_disabled)); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_service_disabled); contentRoot.addView(emptyView); @@ -259,7 +260,8 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); - emptyView.setContentDescription(getString(R.string.print_no_printers_found)); + ImageView iconView = (ImageView) emptyView.findViewById(R.id.icon); + iconView.setContentDescription(getString(R.string.print_no_printers_found)); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_no_printers_found); contentRoot.addView(emptyView); @@ -269,13 +271,13 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } private void updateUiForServiceState() { - List<ComponentName> services = SettingsUtils.readEnabledPrintServices(getActivity()); + List<ComponentName> services = PrintSettingsUtils.readEnabledPrintServices(getActivity()); mServiceEnabled = services.contains(mComponentName); if (mServiceEnabled) { - mToggleSwitch.setCheckedInternal(true); + mSwitchBar.setCheckedInternal(true); mPrintersAdapter.enable(); } else { - mToggleSwitch.setCheckedInternal(false); + mSwitchBar.setCheckedInternal(false); mPrintersAdapter.disable(); } getActivity().invalidateOptionsMenu(); @@ -285,13 +287,19 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment mPrintersAdapter = new PrintersAdapter(); mPrintersAdapter.registerDataSetObserver(mDataObserver); - mToggleSwitch = createAndAddActionBarToggleSwitch(getActivity()); - mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { + final SettingsActivity activity = (SettingsActivity) getActivity(); + + mSwitchBar = activity.getSwitchBar(); + mSwitchBar.addOnSwitchChangeListener(this); + mSwitchBar.show(); + + mToggleSwitch = mSwitchBar.getSwitch(); + mToggleSwitch.setOnBeforeCheckedChangeListener(new ToggleSwitch.OnBeforeCheckedChangeListener() { @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { if (checked) { if (!TextUtils.isEmpty(mEnableWarningMessage)) { - toggleSwitch.setCheckedInternal(false); + mSwitchBar.setCheckedInternal(false); getArguments().putBoolean(PrintSettingsFragment.EXTRA_CHECKED, false); showDialog(DIALOG_ID_ENABLE_WARNING); return true; @@ -303,17 +311,17 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment return false; } }); - mToggleSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - updateEmptyView(); - } - }); getListView().setSelector(new ColorDrawable(Color.TRANSPARENT)); getListView().setAdapter(mPrintersAdapter); } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + updateEmptyView(); + } + private void updateUiForArguments() { Bundle arguments = getArguments(); @@ -322,17 +330,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment // Enabled. final boolean enabled = arguments.getBoolean(PrintSettingsFragment.EXTRA_CHECKED); - mToggleSwitch.setCheckedInternal(enabled); - - // Title. - PreferenceActivity activity = (PreferenceActivity) getActivity(); - if (!activity.onIsMultiPane() || activity.onIsHidingHeaders()) { - // PreferenceActivity allows passing as an extra only title by its - // resource id but we do not have the resource id for the print - // service label. Therefore, we do it ourselves. - String title = arguments.getString(PrintSettingsFragment.EXTRA_TITLE); - getActivity().setTitle(title); - } + mSwitchBar.setCheckedInternal(enabled); // Settings title and intent. String settingsTitle = arguments.getString(PrintSettingsFragment.EXTRA_SETTINGS_TITLE); @@ -446,20 +444,6 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } } - private ToggleSwitch createAndAddActionBarToggleSwitch(Activity activity) { - ToggleSwitch toggleSwitch = new ToggleSwitch(activity); - final int padding = activity.getResources().getDimensionPixelSize( - R.dimen.action_bar_switch_padding); - toggleSwitch.setPaddingRelative(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.END)); - return toggleSwitch; - } - private static abstract class SettingsContentObserver extends ContentObserver { public SettingsContentObserver(Handler handler) { @@ -467,8 +451,8 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } public void register(ContentResolver contentResolver) { - contentResolver.registerContentObserver(Settings.Secure.getUriFor( - Settings.Secure.ENABLED_PRINT_SERVICES), false, this); + contentResolver.registerContentObserver(android.provider.Settings.Secure.getUriFor( + android.provider.Settings.Secure.ENABLED_PRINT_SERVICES), false, this); } public void unregister(ContentResolver contentResolver) { @@ -479,39 +463,6 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment public abstract void onChange(boolean selfChange, Uri uri); } - private final class AnnounceFilterResult implements Runnable { - private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec - - public void post() { - remove(); - getListView().postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); - } - - public void remove() { - getListView().removeCallbacks(this); - } - - @Override - public void run() { - final int count = getListView().getAdapter().getCount(); - final String text; - if (count <= 0) { - text = getString(R.string.print_no_printers_found); - } else { - text = getActivity().getResources().getQuantityString( - R.plurals.print_search_result_count_utterance, count, count); - } - getListView().announceForAccessibility(text); - } - } - - private void announceSearchResult() { - if (mAnnounceFilterResult == null) { - mAnnounceFilterResult = new AnnounceFilterResult(); - } - mAnnounceFilterResult.post(); - } - private final class PrintersAdapter extends BaseAdapter implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>, Filterable { private final Object mLock = new Object(); @@ -563,9 +514,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override @SuppressWarnings("unchecked") protected void publishResults(CharSequence constraint, FilterResults results) { - final boolean resultCountChanged; synchronized (mLock) { - final int oldPrinterCount = mFilteredPrinters.size(); mLastSearchString = constraint; mFilteredPrinters.clear(); if (results == null) { @@ -574,10 +523,6 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment List<PrinterInfo> printers = (List<PrinterInfo>) results.values; mFilteredPrinters.addAll(printers); } - resultCountChanged = (oldPrinterCount != mFilteredPrinters.size()); - } - if (resultCountChanged) { - announceSearchResult(); } notifyDataSetChanged(); } @@ -794,7 +739,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } }); } - mDiscoverySession.startPrinterDisovery(null); + mDiscoverySession.startPrinterDiscovery(null); } } } diff --git a/src/com/android/settings/print/PrintSettingsFragment.java b/src/com/android/settings/print/PrintSettingsFragment.java index df38db4..4a34875 100644 --- a/src/com/android/settings/print/PrintSettingsFragment.java +++ b/src/com/android/settings/print/PrintSettingsFragment.java @@ -31,6 +31,9 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.Process; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; @@ -40,6 +43,7 @@ import android.print.PrintJobInfo; import android.print.PrintManager; import android.print.PrintManager.PrintJobStateChangeListener; import android.printservice.PrintServiceInfo; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.text.TextUtils; import android.text.format.DateUtils; @@ -49,24 +53,32 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.Switch; +import android.widget.AdapterView; import android.widget.TextView; import com.android.internal.content.PackageMonitor; +import com.android.settings.UserSpinnerAdapter; +import com.android.settings.UserSpinnerAdapter.UserDetails; import com.android.settings.DialogCreatable; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.Spinner; + /** * Fragment with the top level print settings. */ -public class PrintSettingsFragment extends SettingsPreferenceFragment implements DialogCreatable { - - static final char ENABLED_PRINT_SERVICES_SEPARATOR = ':'; +public class PrintSettingsFragment extends SettingsPreferenceFragment + implements DialogCreatable, Indexable, OnItemSelectedListener { private static final int LOADER_ID_PRINT_JOBS_LOADER = 1; @@ -111,6 +123,7 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements private PreferenceCategory mPrintServicesCategory; private PrintJobsController mPrintJobsController; + private UserSpinnerAdapter mProfileSpinnerAdapter; @Override public void onCreate(Bundle icicle) { @@ -119,7 +132,7 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements mActivePrintJobsCategory = (PreferenceCategory) findPreference( PRINT_JOBS_CATEGORY); - mPrintServicesCategory= (PreferenceCategory) findPreference( + mPrintServicesCategory = (PreferenceCategory) findPreference( PRINT_SERVICES_CATEGORY); getPreferenceScreen().removePreference(mActivePrintJobsCategory); @@ -152,8 +165,8 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements Settings.Secure.PRINT_SERVICE_SEARCH_URI); if (!TextUtils.isEmpty(searchUri)) { MenuItem menuItem = menu.add(R.string.print_menu_item_add_service); - menuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); - menuItem.setIntent(new Intent(Intent.ACTION_VIEW,Uri.parse(searchUri))); + menuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER); + menuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri))); } } @@ -162,11 +175,21 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements super.onViewCreated(view, savedInstanceState); ViewGroup contentRoot = (ViewGroup) getListView().getParent(); View emptyView = getActivity().getLayoutInflater().inflate( - R.layout.empty_print_state, contentRoot, false); + R.layout.empty_print_state, contentRoot, false); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_no_services_installed); contentRoot.addView(emptyView); getListView().setEmptyView(emptyView); + + final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); + mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, getActivity()); + if (mProfileSpinnerAdapter != null) { + Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate( + R.layout.spinner_view, null); + spinner.setAdapter(mProfileSpinnerAdapter); + spinner.setOnItemSelectedListener(this); + setPinnedHeaderView(spinner); + } } private void updateServicesPreferences() { @@ -178,7 +201,7 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements mPrintServicesCategory.removeAll(); } - List<ComponentName> enabledServices = SettingsUtils + List<ComponentName> enabledServices = PrintSettingsUtils .readEnabledPrintServices(getActivity()); List<ResolveInfo> installedServices = getActivity().getPackageManager() @@ -269,10 +292,26 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements } } + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); + if (selectedUser.getIdentifier() != UserHandle.myUserId()) { + Intent intent = new Intent(Settings.ACTION_PRINT_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + getActivity().startActivityAsUser(intent, selectedUser); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + // Nothing to do + } + private class SettingsPackageMonitor extends PackageMonitor { @Override public void onPackageAdded(String packageName, int uid) { - mHandler.obtainMessage().sendToTarget(); + mHandler.obtainMessage().sendToTarget(); } @Override @@ -291,36 +330,6 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements } } - 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); - } - } - private static abstract class SettingsContentObserver extends ContentObserver { public SettingsContentObserver(Handler handler) { @@ -414,12 +423,12 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements switch (printJob.getState()) { case PrintJobInfo.STATE_QUEUED: case PrintJobInfo.STATE_STARTED: { - preference.setIcon(com.android.internal.R.drawable.ic_print); + preference.setIcon(R.drawable.ic_print); } break; case PrintJobInfo.STATE_FAILED: case PrintJobInfo.STATE_BLOCKED: { - preference.setIcon(com.android.internal.R.drawable.ic_print_error); + preference.setIcon(R.drawable.ic_print_error); } break; } @@ -443,7 +452,7 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements private static final boolean DEBUG = false; - private List <PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); + private List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); private final PrintManager mPrintManager; @@ -453,7 +462,7 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements super(context); mPrintManager = ((PrintManager) context.getSystemService( Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( - ActivityManager.getCurrentUser()); + ActivityManager.getCurrentUser()); } @Override @@ -544,4 +553,53 @@ public class PrintSettingsFragment extends SettingsPreferenceFragment implements return false; } } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>(); + + PackageManager packageManager = context.getPackageManager(); + PrintManager printManager = (PrintManager) context.getSystemService( + Context.PRINT_SERVICE); + + String screenTitle = context.getResources().getString(R.string.print_settings); + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = screenTitle; + data.screenTitle = screenTitle; + indexables.add(data); + + // Indexing all services, regardless if enabled. + List<PrintServiceInfo> services = printManager.getInstalledPrintServices(); + final int serviceCount = services.size(); + for (int i = 0; i < serviceCount; i++) { + PrintServiceInfo service = services.get(i); + + ComponentName componentName = new ComponentName( + service.getResolveInfo().serviceInfo.packageName, + service.getResolveInfo().serviceInfo.name); + + data = new SearchIndexableRaw(context); + data.key = componentName.flattenToString(); + data.title = service.getResolveInfo().loadLabel(packageManager).toString(); + data.summaryOn = context.getString(R.string.print_feature_state_on); + data.summaryOff = context.getString(R.string.print_feature_state_off); + data.screenTitle = screenTitle; + indexables.add(data); + } + + return indexables; + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + List<SearchIndexableResource> indexables = new ArrayList<SearchIndexableResource>(); + SearchIndexableResource indexable = new SearchIndexableResource(context); + indexable.xmlResId = R.xml.print_settings; + indexables.add(indexable); + return indexables; + } + }; } diff --git a/src/com/android/settings/print/SettingsUtils.java b/src/com/android/settings/print/PrintSettingsUtils.java index 37827e6..24f20d5 100644 --- a/src/com/android/settings/print/SettingsUtils.java +++ b/src/com/android/settings/print/PrintSettingsUtils.java @@ -26,11 +26,11 @@ import java.util.ArrayList;import java.util.List; /** * Helper methods for reading and writing to print settings. */ -public class SettingsUtils { +public class PrintSettingsUtils { private static final char ENABLED_PRINT_SERVICES_SEPARATOR = ':'; - private SettingsUtils() { + private PrintSettingsUtils() { /* do nothing */ } diff --git a/src/com/android/settings/quicklaunch/BookmarkPicker.java b/src/com/android/settings/quicklaunch/BookmarkPicker.java index 7152abb..32594b6 100644 --- a/src/com/android/settings/quicklaunch/BookmarkPicker.java +++ b/src/com/android/settings/quicklaunch/BookmarkPicker.java @@ -97,7 +97,7 @@ public class BookmarkPicker extends ListActivity implements SimpleAdapter.ViewBi updateListAndAdapter(); } - + @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, DISPLAY_MODE_LAUNCH, 0, R.string.quick_launch_display_mode_applications) diff --git a/src/com/android/settings/quicklaunch/QuickLaunchSettings.java b/src/com/android/settings/quicklaunch/QuickLaunchSettings.java index 5654323..1367018 100644 --- a/src/com/android/settings/quicklaunch/QuickLaunchSettings.java +++ b/src/com/android/settings/quicklaunch/QuickLaunchSettings.java @@ -16,6 +16,7 @@ package com.android.settings.quicklaunch; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; @@ -27,7 +28,6 @@ import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.provider.Settings.Bookmarks; @@ -40,6 +40,7 @@ import android.view.View; import android.widget.AdapterView; import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; import java.net.URISyntaxException; @@ -49,7 +50,7 @@ import java.net.URISyntaxException; * Shows a list of possible shortcuts, the current application each is bound to, * and allows choosing a new bookmark for a shortcut. */ -public class QuickLaunchSettings extends PreferenceActivity implements +public class QuickLaunchSettings extends SettingsPreferenceFragment implements AdapterView.OnItemLongClickListener, DialogInterface.OnClickListener { private static final String TAG = "QuickLaunchSettings"; @@ -91,7 +92,7 @@ public class QuickLaunchSettings extends PreferenceActivity implements private static final String CLEAR_DIALOG_SHORTCUT = "CLEAR_DIALOG_SHORTCUT"; @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.quick_launch_settings); @@ -100,35 +101,47 @@ public class QuickLaunchSettings extends PreferenceActivity implements mShortcutToPreference = new SparseArray<ShortcutPreference>(); mBookmarksObserver = new BookmarksObserver(mUiHandler); initShortcutPreferences(); - mBookmarksCursor = managedQuery(Bookmarks.CONTENT_URI, sProjection, null, null); - getListView().setOnItemLongClickListener(this); + mBookmarksCursor = getActivity().getContentResolver().query(Bookmarks.CONTENT_URI, + sProjection, null, null, null); } @Override - protected void onResume() { + public void onResume() { super.onResume(); + mBookmarksCursor = getActivity().getContentResolver().query(Bookmarks.CONTENT_URI, + sProjection, null, null, null); getContentResolver().registerContentObserver(Bookmarks.CONTENT_URI, true, mBookmarksObserver); refreshShortcuts(); } @Override - protected void onPause() { + public void onPause() { super.onPause(); getContentResolver().unregisterContentObserver(mBookmarksObserver); } @Override - protected void onRestoreInstanceState(Bundle state) { - super.onRestoreInstanceState(state); - - // Restore the clear dialog's info - mClearDialogBookmarkTitle = state.getString(CLEAR_DIALOG_BOOKMARK_TITLE); - mClearDialogShortcut = (char) state.getInt(CLEAR_DIALOG_SHORTCUT, 0); + public void onStop() { + super.onStop(); + mBookmarksCursor.close(); + } + + @Override + public void onActivityCreated(Bundle state) { + super.onActivityCreated(state); + + getListView().setOnItemLongClickListener(this); + + if (state != null) { + // Restore the clear dialog's info + mClearDialogBookmarkTitle = state.getString(CLEAR_DIALOG_BOOKMARK_TITLE); + mClearDialogShortcut = (char) state.getInt(CLEAR_DIALOG_SHORTCUT, 0); + } } @Override - protected void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Save the clear dialog's info @@ -137,14 +150,13 @@ public class QuickLaunchSettings extends PreferenceActivity implements } @Override - protected Dialog onCreateDialog(int id) { + public Dialog onCreateDialog(int id) { switch (id) { case DIALOG_CLEAR_SHORTCUT: { // Create the dialog for clearing a shortcut - return new AlertDialog.Builder(this) + return new AlertDialog.Builder(getActivity()) .setTitle(getString(R.string.quick_launch_clear_dialog_title)) - .setIconAttribute(android.R.attr.alertDialogIcon) .setMessage(getString(R.string.quick_launch_clear_dialog_message, mClearDialogShortcut, mClearDialogBookmarkTitle)) .setPositiveButton(R.string.quick_launch_clear_ok_button, this) @@ -156,18 +168,6 @@ public class QuickLaunchSettings extends PreferenceActivity implements return super.onCreateDialog(id); } - @Override - protected void onPrepareDialog(int id, Dialog dialog) { - switch (id) { - - case DIALOG_CLEAR_SHORTCUT: { - AlertDialog alertDialog = (AlertDialog) dialog; - alertDialog.setMessage(getString(R.string.quick_launch_clear_dialog_message, - mClearDialogShortcut, mClearDialogBookmarkTitle)); - } - } - } - private void showClearDialog(ShortcutPreference pref) { if (!pref.hasBookmark()) return; @@ -197,7 +197,7 @@ public class QuickLaunchSettings extends PreferenceActivity implements // Open the screen to pick a bookmark for this shortcut ShortcutPreference pref = (ShortcutPreference) preference; - Intent intent = new Intent(this, BookmarkPicker.class); + Intent intent = new Intent(getActivity(), BookmarkPicker.class); intent.putExtra(BookmarkPicker.EXTRA_SHORTCUT, pref.getShortcut()); startActivityForResult(intent, REQUEST_PICK_BOOKMARK); @@ -214,8 +214,8 @@ public class QuickLaunchSettings extends PreferenceActivity implements } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != RESULT_OK) { + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { return; } @@ -253,7 +253,7 @@ public class QuickLaunchSettings extends PreferenceActivity implements } private ShortcutPreference createPreference(char shortcut) { - ShortcutPreference pref = new ShortcutPreference(QuickLaunchSettings.this, shortcut); + ShortcutPreference pref = new ShortcutPreference(getActivity(), shortcut); mShortcutGroup.addPreference(pref); mShortcutToPreference.put(shortcut, pref); return pref; @@ -303,7 +303,7 @@ public class QuickLaunchSettings extends PreferenceActivity implements if (shortcut == 0) continue; ShortcutPreference pref = getOrCreatePreference(shortcut); - CharSequence title = Bookmarks.getTitle(this, c); + CharSequence title = Bookmarks.getTitle(getActivity(), c); /* * The title retrieved from Bookmarks.getTitle() will be in diff --git a/src/com/android/settings/search/BaseSearchIndexProvider.java b/src/com/android/settings/search/BaseSearchIndexProvider.java new file mode 100644 index 0000000..0fe1944 --- /dev/null +++ b/src/com/android/settings/search/BaseSearchIndexProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 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.search; + +import android.content.Context; +import android.provider.SearchIndexableResource; + +import java.util.Collections; +import java.util.List; + +/** + * A basic SearchIndexProvider that returns no data to index. + */ +public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { + + private static final List<String> EMPTY_LIST = Collections.<String>emptyList(); + + public BaseSearchIndexProvider() { + } + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) { + return null; + } + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + return null; + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + return EMPTY_LIST; + } +} diff --git a/src/com/android/settings/search/DynamicIndexableContentMonitor.java b/src/com/android/settings/search/DynamicIndexableContentMonitor.java new file mode 100644 index 0000000..12bb6ef --- /dev/null +++ b/src/com/android/settings/search/DynamicIndexableContentMonitor.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2014 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.search; + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.print.PrintManager; +import android.printservice.PrintService; +import android.printservice.PrintServiceInfo; +import android.provider.UserDictionary; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import com.android.internal.content.PackageMonitor; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.print.PrintSettingsFragment; + +import java.util.ArrayList; +import java.util.List; + +public final class DynamicIndexableContentMonitor extends PackageMonitor implements + InputManager.InputDeviceListener { + + private static final long DELAY_PROCESS_PACKAGE_CHANGE = 2000; + + private static final int MSG_PACKAGE_AVAILABLE = 1; + private static final int MSG_PACKAGE_UNAVAILABLE = 2; + + private final List<String> mAccessibilityServices = new ArrayList<String>(); + private final List<String> mPrintServices = new ArrayList<String>(); + private final List<String> mImeServices = new ArrayList<String>(); + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PACKAGE_AVAILABLE: { + String packageName = (String) msg.obj; + handlePackageAvailable(packageName); + } break; + + case MSG_PACKAGE_UNAVAILABLE: { + String packageName = (String) msg.obj; + handlePackageUnavailable(packageName); + } break; + } + } + }; + + private final ContentObserver mUserDictionaryContentObserver = + new UserDictionaryContentObserver(mHandler); + + private Context mContext; + private boolean mHasFeaturePrinting; + private boolean mHasFeatureIme; + + private static Intent getAccessibilityServiceIntent(String packageName) { + final Intent intent = new Intent(AccessibilityService.SERVICE_INTERFACE); + intent.setPackage(packageName); + return intent; + } + + private static Intent getPrintServiceIntent(String packageName) { + final Intent intent = new Intent(PrintService.SERVICE_INTERFACE); + intent.setPackage(packageName); + return intent; + } + + private static Intent getIMEServiceIntent(String packageName) { + final Intent intent = new Intent("android.view.InputMethod"); + intent.setPackage(packageName); + return intent; + } + + public void register(Context context) { + mContext = context; + + mHasFeaturePrinting = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_PRINTING); + mHasFeatureIme = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_INPUT_METHODS); + + // Cache accessibility service packages to know when they go away. + AccessibilityManager accessibilityManager = (AccessibilityManager) + mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + List<AccessibilityServiceInfo> accessibilityServices = accessibilityManager + .getInstalledAccessibilityServiceList(); + final int accessibilityServiceCount = accessibilityServices.size(); + for (int i = 0; i < accessibilityServiceCount; i++) { + AccessibilityServiceInfo accessibilityService = accessibilityServices.get(i); + ResolveInfo resolveInfo = accessibilityService.getResolveInfo(); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + continue; + } + mAccessibilityServices.add(resolveInfo.serviceInfo.packageName); + } + + if (mHasFeaturePrinting) { + // Cache print service packages to know when they go away. + PrintManager printManager = (PrintManager) + mContext.getSystemService(Context.PRINT_SERVICE); + List<PrintServiceInfo> printServices = printManager.getInstalledPrintServices(); + final int serviceCount = printServices.size(); + for (int i = 0; i < serviceCount; i++) { + PrintServiceInfo printService = printServices.get(i); + ResolveInfo resolveInfo = printService.getResolveInfo(); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + continue; + } + mPrintServices.add(resolveInfo.serviceInfo.packageName); + } + } + + // Cache IME service packages to know when they go away. + if (mHasFeatureIme) { + InputMethodManager imeManager = (InputMethodManager) + mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + List<InputMethodInfo> inputMethods = imeManager.getInputMethodList(); + final int inputMethodCount = inputMethods.size(); + for (int i = 0; i < inputMethodCount; i++) { + InputMethodInfo inputMethod = inputMethods.get(i); + ServiceInfo serviceInfo = inputMethod.getServiceInfo(); + if (serviceInfo == null) continue; + mImeServices.add(serviceInfo.packageName); + } + + // Watch for related content URIs. + mContext.getContentResolver().registerContentObserver( + UserDictionary.Words.CONTENT_URI, true, mUserDictionaryContentObserver); + } + + // Watch for input device changes. + InputManager inputManager = (InputManager) context.getSystemService( + Context.INPUT_SERVICE); + inputManager.registerInputDeviceListener(this, mHandler); + + // Start tracking packages. + register(context, Looper.getMainLooper(), UserHandle.CURRENT, false); + } + + public void unregister() { + super.unregister(); + + InputManager inputManager = (InputManager) mContext.getSystemService( + Context.INPUT_SERVICE); + inputManager.unregisterInputDeviceListener(this); + + if (mHasFeatureIme) { + mContext.getContentResolver().unregisterContentObserver( + mUserDictionaryContentObserver); + } + + mAccessibilityServices.clear(); + mPrintServices.clear(); + mImeServices.clear(); + } + + // Covers installed, appeared external storage with the package, upgraded. + @Override + public void onPackageAppeared(String packageName, int uid) { + postMessage(MSG_PACKAGE_AVAILABLE, packageName); + } + + // Covers uninstalled, removed external storage with the package. + @Override + public void onPackageDisappeared(String packageName, int uid) { + postMessage(MSG_PACKAGE_UNAVAILABLE, packageName); + } + + // Covers enabled, disabled. + @Override + public void onPackageModified(String packageName) { + super.onPackageModified(packageName); + final int state = mContext.getPackageManager().getApplicationEnabledSetting( + packageName); + if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT + || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + postMessage(MSG_PACKAGE_AVAILABLE, packageName); + } else { + postMessage(MSG_PACKAGE_UNAVAILABLE, packageName); + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), false, true); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + onInputDeviceChanged(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + + private void postMessage(int what, String packageName) { + Message message = mHandler.obtainMessage(what, packageName); + mHandler.sendMessageDelayed(message, DELAY_PROCESS_PACKAGE_CHANGE); + } + + private void handlePackageAvailable(String packageName) { + if (!mAccessibilityServices.contains(packageName)) { + final Intent intent = getAccessibilityServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mAccessibilityServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + AccessibilitySettings.class.getName(), false, true); + } + } + + if (mHasFeaturePrinting) { + if (!mPrintServices.contains(packageName)) { + final Intent intent = getPrintServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mPrintServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + PrintSettingsFragment.class.getName(), false, true); + } + } + } + + if (mHasFeatureIme) { + if (!mImeServices.contains(packageName)) { + Intent intent = getIMEServiceIntent(packageName); + if (!mContext.getPackageManager().queryIntentServices(intent, 0).isEmpty()) { + mImeServices.add(packageName); + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), false, true); + } + } + } + } + + private void handlePackageUnavailable(String packageName) { + final int accessibilityIndex = mAccessibilityServices.indexOf(packageName); + if (accessibilityIndex >= 0) { + mAccessibilityServices.remove(accessibilityIndex); + Index.getInstance(mContext).updateFromClassNameResource( + AccessibilitySettings.class.getName(), true, true); + } + + if (mHasFeaturePrinting) { + final int printIndex = mPrintServices.indexOf(packageName); + if (printIndex >= 0) { + mPrintServices.remove(printIndex); + Index.getInstance(mContext).updateFromClassNameResource( + PrintSettingsFragment.class.getName(), true, true); + } + } + + if (mHasFeatureIme) { + final int imeIndex = mImeServices.indexOf(packageName); + if (imeIndex >= 0) { + mImeServices.remove(imeIndex); + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + } + } + + private final class UserDictionaryContentObserver extends ContentObserver { + + public UserDictionaryContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (UserDictionary.Words.CONTENT_URI.equals(uri)) { + Index.getInstance(mContext).updateFromClassNameResource( + InputMethodAndLanguageSettings.class.getName(), true, true); + } + }; + } +} diff --git a/src/com/android/settings/search/Index.java b/src/com/android/settings/search/Index.java new file mode 100644 index 0000000..3957cf6 --- /dev/null +++ b/src/com/android/settings/search/Index.java @@ -0,0 +1,1320 @@ +/* + * Copyright (C) 2014 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.search; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.SearchIndexableData; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesContract; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import com.android.settings.R; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; + +import static com.android.settings.search.IndexDatabaseHelper.Tables; +import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; + +public class Index { + + private static final String LOG_TAG = "Index"; + + // Those indices should match the indices of SELECT_COLUMNS ! + public static final int COLUMN_INDEX_RANK = 0; + public static final int COLUMN_INDEX_TITLE = 1; + public static final int COLUMN_INDEX_SUMMARY_ON = 2; + public static final int COLUMN_INDEX_SUMMARY_OFF = 3; + public static final int COLUMN_INDEX_ENTRIES = 4; + public static final int COLUMN_INDEX_KEYWORDS = 5; + public static final int COLUMN_INDEX_CLASS_NAME = 6; + public static final int COLUMN_INDEX_SCREEN_TITLE = 7; + public static final int COLUMN_INDEX_ICON = 8; + public static final int COLUMN_INDEX_INTENT_ACTION = 9; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 10; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 11; + public static final int COLUMN_INDEX_ENABLED = 12; + public static final int COLUMN_INDEX_KEY = 13; + public static final int COLUMN_INDEX_USER_ID = 14; + + public static final String ENTRIES_SEPARATOR = "|"; + + // If you change the order of columns here, you SHOULD change the COLUMN_INDEX_XXX values + private static final String[] SELECT_COLUMNS = new String[] { + IndexColumns.DATA_RANK, // 0 + IndexColumns.DATA_TITLE, // 1 + IndexColumns.DATA_SUMMARY_ON, // 2 + IndexColumns.DATA_SUMMARY_OFF, // 3 + IndexColumns.DATA_ENTRIES, // 4 + IndexColumns.DATA_KEYWORDS, // 5 + IndexColumns.CLASS_NAME, // 6 + IndexColumns.SCREEN_TITLE, // 7 + IndexColumns.ICON, // 8 + IndexColumns.INTENT_ACTION, // 9 + IndexColumns.INTENT_TARGET_PACKAGE, // 10 + IndexColumns.INTENT_TARGET_CLASS, // 11 + IndexColumns.ENABLED, // 12 + IndexColumns.DATA_KEY_REF // 13 + }; + + private static final String[] MATCH_COLUMNS_PRIMARY = { + IndexColumns.DATA_TITLE, + IndexColumns.DATA_TITLE_NORMALIZED, + IndexColumns.DATA_KEYWORDS + }; + + private static final String[] MATCH_COLUMNS_SECONDARY = { + IndexColumns.DATA_SUMMARY_ON, + IndexColumns.DATA_SUMMARY_ON_NORMALIZED, + IndexColumns.DATA_SUMMARY_OFF, + IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, + IndexColumns.DATA_ENTRIES + }; + + // Max number of saved search queries (who will be used for proposing suggestions) + private static long MAX_SAVED_SEARCH_QUERY = 64; + // Max number of proposed suggestions + private static final int MAX_PROPOSED_SUGGESTIONS = 5; + + private static final String BASE_AUTHORITY = "com.android.settings"; + + private static final String EMPTY = ""; + private static final String NON_BREAKING_HYPHEN = "\u2011"; + private static final String HYPHEN = "-"; + + private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = + "SEARCH_INDEX_DATA_PROVIDER"; + + private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; + private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; + private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; + + private static final List<String> EMPTY_LIST = Collections.<String>emptyList(); + + private static Index sInstance; + + private static final Pattern REMOVE_DIACRITICALS_PATTERN + = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); + + /** + * A private class to describe the update data for the Index database + */ + private static class UpdateData { + public List<SearchIndexableData> dataToUpdate; + public List<SearchIndexableData> dataToDelete; + public Map<String, List<String>> nonIndexableKeys; + + public boolean forceUpdate = false; + + public UpdateData() { + dataToUpdate = new ArrayList<SearchIndexableData>(); + dataToDelete = new ArrayList<SearchIndexableData>(); + nonIndexableKeys = new HashMap<String, List<String>>(); + } + + public UpdateData(UpdateData other) { + dataToUpdate = new ArrayList<SearchIndexableData>(other.dataToUpdate); + dataToDelete = new ArrayList<SearchIndexableData>(other.dataToDelete); + nonIndexableKeys = new HashMap<String, List<String>>(other.nonIndexableKeys); + forceUpdate = other.forceUpdate; + } + + public UpdateData copy() { + return new UpdateData(this); + } + + public void clear() { + dataToUpdate.clear(); + dataToDelete.clear(); + nonIndexableKeys.clear(); + forceUpdate = false; + } + } + + private final AtomicBoolean mIsAvailable = new AtomicBoolean(false); + private final UpdateData mDataToProcess = new UpdateData(); + private Context mContext; + private final String mBaseAuthority; + + /** + * A basic singleton + */ + public static Index getInstance(Context context) { + if (sInstance == null) { + sInstance = new Index(context, BASE_AUTHORITY); + } else { + sInstance.setContext(context); + } + return sInstance; + } + + public Index(Context context, String baseAuthority) { + mContext = context; + mBaseAuthority = baseAuthority; + } + + public void setContext(Context context) { + mContext = context; + } + + public boolean isAvailable() { + return mIsAvailable.get(); + } + + public Cursor search(String query) { + final SQLiteDatabase database = getReadableDatabase(); + final Cursor[] cursors = new Cursor[2]; + + final String primarySql = buildSearchSQL(query, MATCH_COLUMNS_PRIMARY, true); + Log.d(LOG_TAG, "Search primary query: " + primarySql); + cursors[0] = database.rawQuery(primarySql, null); + + // We need to use an EXCEPT operator as negate MATCH queries do not work. + StringBuilder sql = new StringBuilder( + buildSearchSQL(query, MATCH_COLUMNS_SECONDARY, false)); + sql.append(" EXCEPT "); + sql.append(primarySql); + + final String secondarySql = sql.toString(); + Log.d(LOG_TAG, "Search secondary query: " + secondarySql); + cursors[1] = database.rawQuery(secondarySql, null); + + return new MergeCursor(cursors); + } + + public Cursor getSuggestions(String query) { + final String sql = buildSuggestionsSQL(query); + Log.d(LOG_TAG, "Suggestions query: " + sql); + return getReadableDatabase().rawQuery(sql, null); + } + + private String buildSuggestionsSQL(String query) { + StringBuilder sb = new StringBuilder(); + + sb.append("SELECT "); + sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); + sb.append(" FROM "); + sb.append(Tables.TABLE_SAVED_QUERIES); + + if (TextUtils.isEmpty(query)) { + sb.append(" ORDER BY rowId DESC"); + } else { + sb.append(" WHERE "); + sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); + sb.append(" LIKE "); + sb.append("'"); + sb.append(query); + sb.append("%"); + sb.append("'"); + } + + sb.append(" LIMIT "); + sb.append(MAX_PROPOSED_SUGGESTIONS); + + return sb.toString(); + } + + public long addSavedQuery(String query){ + final SaveSearchQueryTask task = new SaveSearchQueryTask(); + task.execute(query); + try { + return task.get(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); + return -1 ; + } catch (ExecutionException e) { + Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); + return -1; + } + } + + public void update() { + final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); + List<ResolveInfo> list = + mContext.getPackageManager().queryIntentContentProviders(intent, 0); + + final int size = list.size(); + for (int n = 0; n < size; n++) { + final ResolveInfo info = list.get(n); + if (!isWellKnownProvider(info)) { + continue; + } + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.packageName; + + addIndexablesFromRemoteProvider(packageName, authority); + addNonIndexablesKeysFromRemoteProvider(packageName, authority); + } + + updateInternal(); + } + + private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { + try { + final int baseRank = Ranking.getBaseRankForAuthority(authority); + + final Context context = mBaseAuthority.equals(authority) ? + mContext : mContext.createPackageContext(packageName, 0); + + final Uri uriForResources = buildUriForXmlResources(authority); + addIndexablesForXmlResourceUri(context, packageName, uriForResources, + SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank); + + final Uri uriForRawData = buildUriForRawData(authority); + addIndexablesForRawDataUri(context, packageName, uriForRawData, + SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank); + return true; + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return false; + } + } + + private void addNonIndexablesKeysFromRemoteProvider(String packageName, + String authority) { + final List<String> keys = + getNonIndexablesKeysFromRemoteProvider(packageName, authority); + addNonIndexableKeys(packageName, keys); + } + + private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName, + String authority) { + try { + final Context packageContext = mContext.createPackageContext(packageName, 0); + + final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); + return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, + SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return EMPTY_LIST; + } + } + + private List<String> getNonIndexablesKeys(Context packageContext, Uri uri, + String[] projection) { + + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + + if (cursor == null) { + Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); + return EMPTY_LIST; + } + + List<String> result = new ArrayList<String>(); + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); + result.add(key); + } + } + return result; + } finally { + cursor.close(); + } + } + + public void addIndexableData(SearchIndexableData data) { + synchronized (mDataToProcess) { + mDataToProcess.dataToUpdate.add(data); + } + } + + public void addIndexableData(SearchIndexableResource[] array) { + synchronized (mDataToProcess) { + final int count = array.length; + for (int n = 0; n < count; n++) { + mDataToProcess.dataToUpdate.add(array[n]); + } + } + } + + public void deleteIndexableData(SearchIndexableData data) { + synchronized (mDataToProcess) { + mDataToProcess.dataToDelete.add(data); + } + } + + public void addNonIndexableKeys(String authority, List<String> keys) { + synchronized (mDataToProcess) { + mDataToProcess.nonIndexableKeys.put(authority, keys); + } + } + + /** + * Only allow a "well known" SearchIndexablesProvider. The provider should: + * + * - have read/write {@link android.Manifest.permission#READ_SEARCH_INDEXABLES} + * - be from a privileged package + */ + private boolean isWellKnownProvider(ResolveInfo info) { + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.applicationInfo.packageName; + + if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { + return false; + } + + final String readPermission = info.providerInfo.readPermission; + final String writePermission = info.providerInfo.writePermission; + + if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { + return false; + } + + if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || + !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { + return false; + } + + return isPrivilegedPackage(packageName); + } + + private boolean isPrivilegedPackage(String packageName) { + final PackageManager pm = mContext.getPackageManager(); + try { + PackageInfo packInfo = pm.getPackageInfo(packageName, 0); + return ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private void updateFromRemoteProvider(String packageName, String authority) { + if (addIndexablesFromRemoteProvider(packageName, authority)) { + updateInternal(); + } + } + + /** + * Update the Index for a specific class name resources + * + * @param className the class name (typically a fragment name). + * @param rebuild true means that you want to delete the data from the Index first. + * @param includeInSearchResults true means that you want the bit "enabled" set so that the + * data will be seen included into the search results + */ + public void updateFromClassNameResource(String className, boolean rebuild, + boolean includeInSearchResults) { + if (className == null) { + throw new IllegalArgumentException("class name cannot be null!"); + } + final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className); + if (res == null ) { + Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className); + return; + } + res.context = mContext; + res.enabled = includeInSearchResults; + if (rebuild) { + deleteIndexableData(res); + } + addIndexableData(res); + mDataToProcess.forceUpdate = true; + updateInternal(); + res.enabled = false; + } + + public void updateFromSearchIndexableData(SearchIndexableData data) { + addIndexableData(data); + mDataToProcess.forceUpdate = true; + updateInternal(); + } + + private SQLiteDatabase getReadableDatabase() { + return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); + } + + private SQLiteDatabase getWritableDatabase() { + return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); + } + + private static Uri buildUriForXmlResources(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.INDEXABLES_XML_RES_PATH); + } + + private static Uri buildUriForRawData(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.INDEXABLES_RAW_PATH); + } + + private static Uri buildUriForNonIndexableKeys(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); + } + + private void updateInternal() { + synchronized (mDataToProcess) { + final UpdateIndexTask task = new UpdateIndexTask(); + UpdateData copy = mDataToProcess.copy(); + task.execute(copy); + mDataToProcess.clear(); + } + } + + private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, + Uri uri, String[] projection, int baseRank) { + + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + + if (cursor == null) { + Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); + return; + } + + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); + + final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); + final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); + + final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); + final String targetPackage = cursor.getString( + COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); + final String targetClass = cursor.getString( + COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); + + SearchIndexableResource sir = new SearchIndexableResource(packageContext); + sir.rank = rank; + sir.xmlResId = xmlResId; + sir.className = className; + sir.packageName = packageName; + sir.iconResId = iconResId; + sir.intentAction = action; + sir.intentTargetPackage = targetPackage; + sir.intentTargetClass = targetClass; + + addIndexableData(sir); + } + } + } finally { + cursor.close(); + } + } + + private void addIndexablesForRawDataUri(Context packageContext, String packageName, + Uri uri, String[] projection, int baseRank) { + + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + + if (cursor == null) { + Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); + return; + } + + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); + final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); + final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); + final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); + final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); + + final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); + + final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); + final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); + + final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); + final String targetPackage = cursor.getString( + COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); + final String targetClass = cursor.getString( + COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); + + final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); + final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); + + SearchIndexableRaw data = new SearchIndexableRaw(packageContext); + data.rank = rank; + data.title = title; + data.summaryOn = summaryOn; + data.summaryOff = summaryOff; + data.entries = entries; + data.keywords = keywords; + data.screenTitle = screenTitle; + data.className = className; + data.packageName = packageName; + data.iconResId = iconResId; + data.intentAction = action; + data.intentTargetPackage = targetPackage; + data.intentTargetClass = targetClass; + data.key = key; + data.userId = userId; + + addIndexableData(data); + } + } + } finally { + cursor.close(); + } + } + + private String buildSearchSQL(String query, String[] colums, boolean withOrderBy) { + StringBuilder sb = new StringBuilder(); + sb.append(buildSearchSQLForColumn(query, colums)); + if (withOrderBy) { + sb.append(" ORDER BY "); + sb.append(IndexColumns.DATA_RANK); + } + return sb.toString(); + } + + private String buildSearchSQLForColumn(String query, String[] columnNames) { + StringBuilder sb = new StringBuilder(); + sb.append("SELECT "); + for (int n = 0; n < SELECT_COLUMNS.length; n++) { + sb.append(SELECT_COLUMNS[n]); + if (n < SELECT_COLUMNS.length - 1) { + sb.append(", "); + } + } + sb.append(" FROM "); + sb.append(Tables.TABLE_PREFS_INDEX); + sb.append(" WHERE "); + sb.append(buildSearchWhereStringForColumns(query, columnNames)); + + return sb.toString(); + } + + private String buildSearchWhereStringForColumns(String query, String[] columnNames) { + final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX); + sb.append(" MATCH "); + DatabaseUtils.appendEscapedSQLString(sb, + buildSearchMatchStringForColumns(query, columnNames)); + sb.append(" AND "); + sb.append(IndexColumns.LOCALE); + sb.append(" = "); + DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString()); + sb.append(" AND "); + sb.append(IndexColumns.ENABLED); + sb.append(" = 1"); + return sb.toString(); + } + + private String buildSearchMatchStringForColumns(String query, String[] columnNames) { + final String value = query + "*"; + StringBuilder sb = new StringBuilder(); + final int count = columnNames.length; + for (int n = 0; n < count; n++) { + sb.append(columnNames[n]); + sb.append(":"); + sb.append(value); + if (n < count - 1) { + sb.append(" OR "); + } + } + return sb.toString(); + } + + private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, + SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) { + if (data instanceof SearchIndexableResource) { + indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); + } else if (data instanceof SearchIndexableRaw) { + indexOneRaw(database, localeStr, (SearchIndexableRaw) data); + } + } + + private void indexOneRaw(SQLiteDatabase database, String localeStr, + SearchIndexableRaw raw) { + // Should be the same locale as the one we are processing + if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { + return; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + raw.className, + raw.screenTitle, + raw.iconResId, + raw.rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + + private static boolean isIndexableClass(final Class<?> clazz) { + return (clazz != null) && Indexable.class.isAssignableFrom(clazz); + } + + private static Class<?> getIndexableClass(String className) { + final Class<?> clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + Log.d(LOG_TAG, "Cannot find class: " + className); + return null; + } + return isIndexableClass(clazz) ? clazz : null; + } + + private void indexOneResource(SQLiteDatabase database, String localeStr, + SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) { + + if (sir == null) { + Log.e(LOG_TAG, "Cannot index a null resource!"); + return; + } + + final List<String> nonIndexableKeys = new ArrayList<String>(); + + if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) { + List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName); + if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) { + nonIndexableKeys.addAll(resNonIndxableKeys); + } + + indexFromResource(sir.context, database, localeStr, + sir.xmlResId, sir.className, sir.iconResId, sir.rank, + sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass, + nonIndexableKeys); + } else { + if (TextUtils.isEmpty(sir.className)) { + Log.w(LOG_TAG, "Cannot index an empty Search Provider name!"); + return; + } + + final Class<?> clazz = getIndexableClass(sir.className); + if (clazz == null) { + Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className + + "' should implement the " + Indexable.class.getName() + " interface!"); + return; + } + + // Will be non null only for a Local provider implementing a + // SEARCH_INDEX_DATA_PROVIDER field + final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz); + if (provider != null) { + List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context); + if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) { + nonIndexableKeys.addAll(providerNonIndexableKeys); + } + + indexFromProvider(mContext, database, localeStr, provider, sir.className, + sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys); + } + } + } + + private Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { + try { + final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); + return (Indexable.SearchIndexProvider) f.get(null); + } catch (NoSuchFieldException e) { + Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (SecurityException se) { + Log.d(LOG_TAG, + "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalAccessException e) { + Log.d(LOG_TAG, + "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalArgumentException e) { + Log.d(LOG_TAG, + "Illegal argument when accessing field '" + + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } + return null; + } + + private void indexFromResource(Context context, SQLiteDatabase database, String localeStr, + int xmlResId, String fragmentName, int iconResId, int rank, + String intentAction, String intentTargetPackage, String intentTargetClass, + List<String> nonIndexableKeys) { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getXml(xmlResId); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Parse next until start tag is found + } + + String nodeName = parser.getName(); + if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { + throw new RuntimeException( + "XML document must start with <PreferenceScreen> tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + final int outerDepth = parser.getDepth(); + final AttributeSet attrs = Xml.asAttributeSet(parser); + + final String screenTitle = getDataTitle(context, attrs); + + String key = getDataKey(context, attrs); + + String title; + String summary; + String keywords; + + // Insert rows for the main PreferenceScreen node. Rewrite the data for removing + // hyphens. + if (!nonIndexableKeys.contains(key)) { + title = getDataTitle(context, attrs); + summary = getDataSummary(context, attrs); + keywords = getDataKeywords(context, attrs); + + updateOneRowWithFilteredData(database, localeStr, title, summary, null, null, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, true, + key, -1 /* default user id */); + } + + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + nodeName = parser.getName(); + + key = getDataKey(context, attrs); + if (nonIndexableKeys.contains(key)) { + continue; + } + + title = getDataTitle(context, attrs); + keywords = getDataKeywords(context, attrs); + + if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { + summary = getDataSummary(context, attrs); + + String entries = null; + + if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { + entries = getDataEntries(context, attrs); + } + + // Insert rows for the child nodes of PreferenceScreen + updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } else { + String summaryOn = getDataSummaryOn(context, attrs); + String summaryOff = getDataSummaryOff(context, attrs); + + if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { + summaryOn = getDataSummary(context, attrs); + } + + updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff, + null, fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } + } + + } catch (XmlPullParserException e) { + throw new RuntimeException("Error parsing PreferenceScreen", e); + } catch (IOException e) { + throw new RuntimeException("Error parsing PreferenceScreen", e); + } finally { + if (parser != null) parser.close(); + } + } + + private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr, + Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, + boolean enabled, List<String> nonIndexableKeys) { + + if (provider == null) { + Log.w(LOG_TAG, "Cannot find provider: " + className); + return; + } + + final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, enabled); + + if (rawList != null) { + final int rawSize = rawList.size(); + for (int i = 0; i < rawSize; i++) { + SearchIndexableRaw raw = rawList.get(i); + + // Should be the same locale as the one we are processing + if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { + continue; + } + + if (nonIndexableKeys.contains(raw.key)) { + continue; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + className, + raw.screenTitle, + iconResId, + rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + } + + final List<SearchIndexableResource> resList = + provider.getXmlResourcesToIndex(context, enabled); + if (resList != null) { + final int resSize = resList.size(); + for (int i = 0; i < resSize; i++) { + SearchIndexableResource item = resList.get(i); + + // Should be the same locale as the one we are processing + if (!item.locale.toString().equalsIgnoreCase(localeStr)) { + continue; + } + + final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId; + final int itemRank = (item.rank == 0) ? rank : item.rank; + String itemClassName = (TextUtils.isEmpty(item.className)) + ? className : item.className; + + indexFromResource(context, database, localeStr, + item.xmlResId, itemClassName, itemIconResId, itemRank, + item.intentAction, item.intentTargetPackage, + item.intentTargetClass, nonIndexableKeys); + } + } + } + + private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale, + String title, String summaryOn, String summaryOff, String entries, + String className, + String screenTitle, int iconResId, int rank, String keywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + final String updatedTitle = normalizeHyphen(title); + final String updatedSummaryOn = normalizeHyphen(summaryOn); + final String updatedSummaryOff = normalizeHyphen(summaryOff); + + final String normalizedTitle = normalizeString(updatedTitle); + final String normalizedSummaryOn = normalizeString(updatedSummaryOn); + final String normalizedSummaryOff = normalizeString(updatedSummaryOff); + + updateOneRow(database, locale, + updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn, + updatedSummaryOff, normalizedSummaryOff, entries, + className, screenTitle, iconResId, + rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled, + key, userId); + } + + private static String normalizeHyphen(String input) { + return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; + } + + private static String normalizeString(String input) { + final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; + final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); + + return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); + } + + private void updateOneRow(SQLiteDatabase database, String locale, + String updatedTitle, String normalizedTitle, + String updatedSummaryOn, String normalizedSummaryOn, + String updatedSummaryOff, String normalizedSummaryOff, String entries, + String className, String screenTitle, int iconResId, int rank, String keywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + if (TextUtils.isEmpty(updatedTitle)) { + return; + } + + // The DocID should contains more than the title string itself (you may have two settings + // with the same title). So we need to use a combination of the title and the screenTitle. + StringBuilder sb = new StringBuilder(updatedTitle); + sb.append(screenTitle); + int docId = sb.toString().hashCode(); + + ContentValues values = new ContentValues(); + values.put(IndexColumns.DOCID, docId); + values.put(IndexColumns.LOCALE, locale); + values.put(IndexColumns.DATA_RANK, rank); + values.put(IndexColumns.DATA_TITLE, updatedTitle); + values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle); + values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn); + values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn); + values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff); + values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff); + values.put(IndexColumns.DATA_ENTRIES, entries); + values.put(IndexColumns.DATA_KEYWORDS, keywords); + values.put(IndexColumns.CLASS_NAME, className); + values.put(IndexColumns.SCREEN_TITLE, screenTitle); + values.put(IndexColumns.INTENT_ACTION, intentAction); + values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage); + values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass); + values.put(IndexColumns.ICON, iconResId); + values.put(IndexColumns.ENABLED, enabled); + values.put(IndexColumns.DATA_KEY_REF, key); + values.put(IndexColumns.USER_ID, userId); + + database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values); + } + + private String getDataKey(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_key); + } + + private String getDataTitle(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_title); + } + + private String getDataSummary(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_summary); + } + + private String getDataSummaryOn(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOn); + } + + private String getDataSummaryOff(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOff); + } + + private String getDataEntries(Context context, AttributeSet attrs) { + return getDataEntries(context, attrs, + com.android.internal.R.styleable.ListPreference, + com.android.internal.R.styleable.ListPreference_entries); + } + + private String getDataKeywords(Context context, AttributeSet attrs) { + return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); + } + + private String getData(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + CharSequence data = null; + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + data = context.getText(tv.resourceId); + } else { + data = tv.string; + } + } + return (data != null) ? data.toString() : null; + } + + private String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + String[] data = null; + if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { + if (tv.resourceId != 0) { + data = context.getResources().getStringArray(tv.resourceId); + } + } + final int count = (data == null ) ? 0 : data.length; + if (count == 0) { + return null; + } + final StringBuilder result = new StringBuilder(); + for (int n = 0; n < count; n++) { + result.append(data[n]); + result.append(ENTRIES_SEPARATOR); + } + return result.toString(); + } + + private int getResId(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + return tv.resourceId; + } else { + return 0; + } + } + + /** + * A private class for updating the Index database + */ + private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + mIsAvailable.set(false); + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + mIsAvailable.set(true); + } + + @Override + protected Void doInBackground(UpdateData... params) { + final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate; + final List<SearchIndexableData> dataToDelete = params[0].dataToDelete; + final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys; + + final boolean forceUpdate = params[0].forceUpdate; + + final SQLiteDatabase database = getWritableDatabase(); + final String localeStr = Locale.getDefault().toString(); + + try { + database.beginTransaction(); + if (dataToDelete.size() > 0) { + processDataToDelete(database, localeStr, dataToDelete); + } + if (dataToUpdate.size() > 0) { + processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, + forceUpdate); + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + + return null; + } + + private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, + List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, + boolean forceUpdate) { + + if (!forceUpdate && isLocaleAlreadyIndexed(database, localeStr)) { + Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); + return true; + } + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToUpdate.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToUpdate.get(n); + try { + indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); + } catch (Exception e) { + Log.e(LOG_TAG, + "Cannot index: " + data.className + " for locale: " + localeStr, e); + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private boolean processDataToDelete(SQLiteDatabase database, String localeStr, + List<SearchIndexableData> dataToDelete) { + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToDelete.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToDelete.get(n); + if (data == null) { + continue; + } + if (!TextUtils.isEmpty(data.className)) { + delete(database, IndexColumns.CLASS_NAME, data.className); + } else { + if (data instanceof SearchIndexableRaw) { + final SearchIndexableRaw raw = (SearchIndexableRaw) data; + if (!TextUtils.isEmpty(raw.title)) { + delete(database, IndexColumns.DATA_TITLE, raw.title); + } + } + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private int delete(SQLiteDatabase database, String columName, String value) { + final String whereClause = columName + "=?"; + final String[] whereArgs = new String[] { value }; + + return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs); + } + + private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, String locale) { + Cursor cursor = null; + boolean result = false; + final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE); + sb.append(" = "); + DatabaseUtils.appendEscapedSQLString(sb, locale); + try { + // We care only for 1 row + cursor = database.query(Tables.TABLE_PREFS_INDEX, null, + sb.toString(), null, null, null, null, "1"); + final int count = cursor.getCount(); + result = (count >= 1); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + } + + /** + * A basic AsyncTask for saving a Search query into the database + */ + private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> { + + @Override + protected Long doInBackground(String... params) { + final long now = new Date().getTime(); + + final ContentValues values = new ContentValues(); + values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]); + values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now); + + final SQLiteDatabase database = getWritableDatabase(); + + long lastInsertedRowId = -1; + try { + // First, delete all saved queries that are the same + database.delete(Tables.TABLE_SAVED_QUERIES, + IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?", + new String[] { params[0] }); + + // Second, insert the saved query + lastInsertedRowId = + database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values); + + // Last, remove "old" saved queries + final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY; + if (delta > 0) { + int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?", + new String[] { Long.toString(delta) }); + Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)"); + } + } catch (Exception e) { + Log.d(LOG_TAG, "Cannot update saved Search queries", e); + } + + return lastInsertedRowId; + } + } +} diff --git a/src/com/android/settings/search/IndexDatabaseHelper.java b/src/com/android/settings/search/IndexDatabaseHelper.java new file mode 100644 index 0000000..152cbf3 --- /dev/null +++ b/src/com/android/settings/search/IndexDatabaseHelper.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014 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.search; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; +import android.util.Log; + +public class IndexDatabaseHelper extends SQLiteOpenHelper { + + private static final String TAG = "IndexDatabaseHelper"; + + private static final String DATABASE_NAME = "search_index.db"; + private static final int DATABASE_VERSION = 115; + + public interface Tables { + public static final String TABLE_PREFS_INDEX = "prefs_index"; + public static final String TABLE_META_INDEX = "meta_index"; + public static final String TABLE_SAVED_QUERIES = "saved_queries"; + } + + public interface IndexColumns { + public static final String DOCID = "docid"; + public static final String LOCALE = "locale"; + public static final String DATA_RANK = "data_rank"; + public static final String DATA_TITLE = "data_title"; + public static final String DATA_TITLE_NORMALIZED = "data_title_normalized"; + public static final String DATA_SUMMARY_ON = "data_summary_on"; + public static final String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized"; + public static final String DATA_SUMMARY_OFF = "data_summary_off"; + public static final String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized"; + public static final String DATA_ENTRIES = "data_entries"; + public static final String DATA_KEYWORDS = "data_keywords"; + public static final String CLASS_NAME = "class_name"; + public static final String SCREEN_TITLE = "screen_title"; + public static final String INTENT_ACTION = "intent_action"; + public static final String INTENT_TARGET_PACKAGE = "intent_target_package"; + public static final String INTENT_TARGET_CLASS = "intent_target_class"; + public static final String ICON = "icon"; + public static final String ENABLED = "enabled"; + public static final String DATA_KEY_REF = "data_key_reference"; + public static final String USER_ID = "user_id"; + } + + public interface MetaColumns { + public static final String BUILD = "build"; + } + + public interface SavedQueriesColums { + public static final String QUERY = "query"; + public static final String TIME_STAMP = "timestamp"; + } + + private static final String CREATE_INDEX_TABLE = + "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" + + "(" + + IndexColumns.LOCALE + + ", " + + IndexColumns.DATA_RANK + + ", " + + IndexColumns.DATA_TITLE + + ", " + + IndexColumns.DATA_TITLE_NORMALIZED + + ", " + + IndexColumns.DATA_SUMMARY_ON + + ", " + + IndexColumns.DATA_SUMMARY_ON_NORMALIZED + + ", " + + IndexColumns.DATA_SUMMARY_OFF + + ", " + + IndexColumns.DATA_SUMMARY_OFF_NORMALIZED + + ", " + + IndexColumns.DATA_ENTRIES + + ", " + + IndexColumns.DATA_KEYWORDS + + ", " + + IndexColumns.SCREEN_TITLE + + ", " + + IndexColumns.CLASS_NAME + + ", " + + IndexColumns.ICON + + ", " + + IndexColumns.INTENT_ACTION + + ", " + + IndexColumns.INTENT_TARGET_PACKAGE + + ", " + + IndexColumns.INTENT_TARGET_CLASS + + ", " + + IndexColumns.ENABLED + + ", " + + IndexColumns.DATA_KEY_REF + + ", " + + IndexColumns.USER_ID + + ");"; + + private static final String CREATE_META_TABLE = + "CREATE TABLE " + Tables.TABLE_META_INDEX + + "(" + + MetaColumns.BUILD + " VARCHAR(32) NOT NULL" + + ")"; + + private static final String CREATE_SAVED_QUERIES_TABLE = + "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES + + "(" + + SavedQueriesColums.QUERY + " VARCHAR(64) NOT NULL" + + ", " + + SavedQueriesColums.TIME_STAMP + " INTEGER" + + ")"; + + private static final String INSERT_BUILD_VERSION = + "INSERT INTO " + Tables.TABLE_META_INDEX + + " VALUES ('" + Build.VERSION.INCREMENTAL + "');"; + + private static final String SELECT_BUILD_VERSION = + "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;"; + + private static IndexDatabaseHelper sSingleton; + + public static synchronized IndexDatabaseHelper getInstance(Context context) { + if (sSingleton == null) { + sSingleton = new IndexDatabaseHelper(context); + } + return sSingleton; + } + + public IndexDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + bootstrapDB(db); + } + + private void bootstrapDB(SQLiteDatabase db) { + db.execSQL(CREATE_INDEX_TABLE); + db.execSQL(CREATE_META_TABLE); + db.execSQL(CREATE_SAVED_QUERIES_TABLE); + db.execSQL(INSERT_BUILD_VERSION); + Log.i(TAG, "Bootstrapped database"); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + + Log.i(TAG, "Using schema version: " + db.getVersion()); + + if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) { + Log.w(TAG, "Index needs to be rebuilt as build-version is not the same"); + // We need to drop the tables and recreate them + reconstruct(db); + } else { + Log.i(TAG, "Index is fine"); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < DATABASE_VERSION) { + Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + + "Index needs to be rebuilt for schema version '" + newVersion + "'."); + // We need to drop the tables and recreate them + reconstruct(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + + "Index needs to be rebuilt for schema version '" + newVersion + "'."); + // We need to drop the tables and recreate them + reconstruct(db); + } + + private void reconstruct(SQLiteDatabase db) { + dropTables(db); + bootstrapDB(db); + } + + private String getBuildVersion(SQLiteDatabase db) { + String version = null; + Cursor cursor = null; + try { + cursor = db.rawQuery(SELECT_BUILD_VERSION, null); + if (cursor.moveToFirst()) { + version = cursor.getString(0); + } + } + catch (Exception e) { + Log.e(TAG, "Cannot get build version from Index metadata"); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + return version; + } + + private void dropTables(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); + } +} diff --git a/src/com/android/settings/search/Indexable.java b/src/com/android/settings/search/Indexable.java new file mode 100644 index 0000000..19f88ae --- /dev/null +++ b/src/com/android/settings/search/Indexable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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.search; + +import android.content.Context; +import android.provider.SearchIndexableResource; + +import java.util.List; + +/** + * Interface for classes whose instances can provide data for indexing. + * + * Classes implementing the Indexable interface must have a static field called + * <code>SEARCH_INDEX_DATA_PROVIDER</code>, which is an object implementing the + * {@link Indexable.SearchIndexProvider} interface. + * + * See {@link android.provider.SearchIndexableResource} and {@link SearchIndexableRaw}. + * + */ +public interface Indexable { + + public interface SearchIndexProvider { + /** + * Return a list of references for indexing. + * + * See {@link android.provider.SearchIndexableResource} + * + * + * @param context the context. + * @param enabled hint telling if the data needs to be considered into the search results + * or not. + * @return a list of {@link android.provider.SearchIndexableResource} references. + * Can be null. + */ + List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled); + + /** + * Return a list of raw data for indexing. See {@link SearchIndexableRaw} + * + * @param context the context. + * @param enabled hint telling if the data needs to be considered into the search results + * or not. + * @return a list of {@link SearchIndexableRaw} references. Can be null. + */ + List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled); + + /** + * Return a list of data keys that cannot be indexed. See {@link SearchIndexableRaw} + * + * @param context the context. + * @return a list of {@link SearchIndexableRaw} references. Can be null. + */ + List<String> getNonIndexableKeys(Context context); + } +} diff --git a/src/com/android/settings/search/Ranking.java b/src/com/android/settings/search/Ranking.java new file mode 100644 index 0000000..2c76002 --- /dev/null +++ b/src/com/android/settings/search/Ranking.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 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.search; + +import com.android.settings.ChooseLockGeneric; +import com.android.settings.DataUsageSummary; +import com.android.settings.DateTimeSettings; +import com.android.settings.DevelopmentSettings; +import com.android.settings.DeviceInfoSettings; +import com.android.settings.DisplaySettings; +import com.android.settings.HomeSettings; +import com.android.settings.ScreenPinningSettings; +import com.android.settings.PrivacySettings; +import com.android.settings.SecuritySettings; +import com.android.settings.WallpaperTypeSettings; +import com.android.settings.WirelessSettings; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.deviceinfo.Memory; +import com.android.settings.fuelgauge.BatterySaverSettings; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.location.LocationSettings; +import com.android.settings.net.DataUsageMeteredSettings; +import com.android.settings.notification.NotificationSettings; +import com.android.settings.notification.OtherSoundSettings; +import com.android.settings.notification.ZenModeSettings; +import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.sim.SimSettings; +import com.android.settings.users.UserSettings; +import com.android.settings.voice.VoiceInputSettings; +import com.android.settings.wifi.AdvancedWifiSettings; +import com.android.settings.wifi.SavedAccessPointsWifiSettings; +import com.android.settings.wifi.WifiSettings; + +import java.util.HashMap; + +/** + * Utility class for dealing with Search Ranking. + */ +public final class Ranking { + + public static final int RANK_WIFI = 1; + public static final int RANK_BT = 2; + public static final int RANK_SIM = 3; + public static final int RANK_DATA_USAGE = 4; + public static final int RANK_WIRELESS = 5; + public static final int RANK_HOME = 6; + public static final int RANK_DISPLAY = 7; + public static final int RANK_WALLPAPER = 8; + public static final int RANK_NOTIFICATIONS = 9; + public static final int RANK_MEMORY = 10; + public static final int RANK_POWER_USAGE = 11; + public static final int RANK_USERS = 12; + public static final int RANK_LOCATION = 13; + public static final int RANK_SECURITY = 14; + public static final int RANK_IME = 15; + public static final int RANK_PRIVACY = 16; + public static final int RANK_DATE_TIME = 17; + public static final int RANK_ACCESSIBILITY = 18; + public static final int RANK_PRINTING = 19; + public static final int RANK_DEVELOPEMENT = 20; + public static final int RANK_DEVICE_INFO = 21; + + public static final int RANK_UNDEFINED = -1; + public static final int RANK_OTHERS = 1024; + public static final int BASE_RANK_DEFAULT = 2048; + + public static int sCurrentBaseRank = BASE_RANK_DEFAULT; + + private static HashMap<String, Integer> sRankMap = new HashMap<String, Integer>(); + private static HashMap<String, Integer> sBaseRankMap = new HashMap<String, Integer>(); + + static { + // Wi-Fi + sRankMap.put(WifiSettings.class.getName(), RANK_WIFI); + sRankMap.put(AdvancedWifiSettings.class.getName(), RANK_WIFI); + sRankMap.put(SavedAccessPointsWifiSettings.class.getName(), RANK_WIFI); + + // BT + sRankMap.put(BluetoothSettings.class.getName(), RANK_BT); + + // SIM Cards + sRankMap.put(SimSettings.class.getName(), RANK_SIM); + + // DataUsage + sRankMap.put(DataUsageSummary.class.getName(), RANK_DATA_USAGE); + sRankMap.put(DataUsageMeteredSettings.class.getName(), RANK_DATA_USAGE); + + // Other wireless settinfs + sRankMap.put(WirelessSettings.class.getName(), RANK_WIRELESS); + + // Home + sRankMap.put(HomeSettings.class.getName(), RANK_HOME); + + // Display + sRankMap.put(DisplaySettings.class.getName(), RANK_DISPLAY); + + // Wallpapers + sRankMap.put(WallpaperTypeSettings.class.getName(), RANK_WALLPAPER); + + // Notifications + sRankMap.put(NotificationSettings.class.getName(), RANK_NOTIFICATIONS); + sRankMap.put(OtherSoundSettings.class.getName(), RANK_NOTIFICATIONS); + sRankMap.put(ZenModeSettings.class.getName(), RANK_NOTIFICATIONS); + + // Memory + sRankMap.put(Memory.class.getName(), RANK_MEMORY); + + // Battery + sRankMap.put(PowerUsageSummary.class.getName(), RANK_POWER_USAGE); + sRankMap.put(BatterySaverSettings.class.getName(), RANK_POWER_USAGE); + + // Users + sRankMap.put(UserSettings.class.getName(), RANK_USERS); + + // Location + sRankMap.put(LocationSettings.class.getName(), RANK_LOCATION); + + // Security + sRankMap.put(SecuritySettings.class.getName(), RANK_SECURITY); + sRankMap.put(ChooseLockGeneric.ChooseLockGenericFragment.class.getName(), RANK_SECURITY); + sRankMap.put(ScreenPinningSettings.class.getName(), RANK_SECURITY); + + // IMEs + sRankMap.put(InputMethodAndLanguageSettings.class.getName(), RANK_IME); + sRankMap.put(VoiceInputSettings.class.getName(), RANK_IME); + + // Privacy + sRankMap.put(PrivacySettings.class.getName(), RANK_PRIVACY); + + // Date / Time + sRankMap.put(DateTimeSettings.class.getName(), RANK_DATE_TIME); + + // Accessibility + sRankMap.put(AccessibilitySettings.class.getName(), RANK_ACCESSIBILITY); + + // Print + sRankMap.put(PrintSettingsFragment.class.getName(), RANK_PRINTING); + + // Development + sRankMap.put(DevelopmentSettings.class.getName(), RANK_DEVELOPEMENT); + + // Device infos + sRankMap.put(DeviceInfoSettings.class.getName(), RANK_DEVICE_INFO); + + sBaseRankMap.put("com.android.settings", 0); + } + + public static int getRankForClassName(String className) { + Integer rank = sRankMap.get(className); + return (rank != null) ? (int) rank: RANK_OTHERS; + } + + public static int getBaseRankForAuthority(String authority) { + synchronized (sBaseRankMap) { + Integer base = sBaseRankMap.get(authority); + if (base != null) { + return base; + } + sCurrentBaseRank++; + sBaseRankMap.put(authority, sCurrentBaseRank); + return sCurrentBaseRank; + } + } +} diff --git a/src/com/android/settings/search/SearchIndexableRaw.java b/src/com/android/settings/search/SearchIndexableRaw.java new file mode 100644 index 0000000..b8a1699 --- /dev/null +++ b/src/com/android/settings/search/SearchIndexableRaw.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 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.search; + +import android.content.Context; +import android.provider.SearchIndexableData; + +/** + * Indexable raw data for Search. + * + * This is the raw data used by the Indexer and should match its data model. + * + * See {@link Indexable} and {@link android.provider.SearchIndexableResource}. + */ +public class SearchIndexableRaw extends SearchIndexableData { + + /** + * Title's raw data. + */ + public String title; + + /** + * Summary's raw data when the data is "ON". + */ + public String summaryOn; + + /** + * Summary's raw data when the data is "OFF". + */ + public String summaryOff; + + /** + * Entries associated with the raw data (when the data can have several values). + */ + public String entries; + + /** + * Keywords' raw data. + */ + public String keywords; + + /** + * Fragment's or Activity's title associated with the raw data. + */ + public String screenTitle; + + public SearchIndexableRaw(Context context) { + super(context); + } +} diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java new file mode 100644 index 0000000..7b3fa77 --- /dev/null +++ b/src/com/android/settings/search/SearchIndexableResources.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2014 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.search; + +import android.provider.SearchIndexableResource; + +import com.android.settings.DataUsageSummary; +import com.android.settings.DateTimeSettings; +import com.android.settings.DevelopmentSettings; +import com.android.settings.DeviceInfoSettings; +import com.android.settings.DisplaySettings; +import com.android.settings.HomeSettings; +import com.android.settings.ScreenPinningSettings; +import com.android.settings.PrivacySettings; +import com.android.settings.R; +import com.android.settings.SecuritySettings; +import com.android.settings.WallpaperTypeSettings; +import com.android.settings.WirelessSettings; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.bluetooth.BluetoothSettings; +import com.android.settings.deviceinfo.Memory; +import com.android.settings.fuelgauge.BatterySaverSettings; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.inputmethod.InputMethodAndLanguageSettings; +import com.android.settings.location.LocationSettings; +import com.android.settings.net.DataUsageMeteredSettings; +import com.android.settings.notification.NotificationSettings; +import com.android.settings.notification.OtherSoundSettings; +import com.android.settings.notification.ZenModeSettings; +import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.sim.SimSettings; +import com.android.settings.users.UserSettings; +import com.android.settings.voice.VoiceInputSettings; +import com.android.settings.wifi.AdvancedWifiSettings; +import com.android.settings.wifi.SavedAccessPointsWifiSettings; +import com.android.settings.wifi.WifiSettings; + +import java.util.Collection; +import java.util.HashMap; + +public final class SearchIndexableResources { + + public static int NO_DATA_RES_ID = 0; + + private static HashMap<String, SearchIndexableResource> sResMap = + new HashMap<String, SearchIndexableResource>(); + + static { + sResMap.put(WifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WifiSettings.class.getName()), + NO_DATA_RES_ID, + WifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(AdvancedWifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(AdvancedWifiSettings.class.getName()), + R.xml.wifi_advanced_settings, + AdvancedWifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(SavedAccessPointsWifiSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SavedAccessPointsWifiSettings.class.getName()), + R.xml.wifi_display_saved_access_points, + SavedAccessPointsWifiSettings.class.getName(), + R.drawable.ic_settings_wireless)); + + sResMap.put(BluetoothSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(BluetoothSettings.class.getName()), + NO_DATA_RES_ID, + BluetoothSettings.class.getName(), + R.drawable.ic_settings_bluetooth2)); + + sResMap.put(SimSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SimSettings.class.getName()), + NO_DATA_RES_ID, + SimSettings.class.getName(), + R.drawable.ic_sim_sd)); + + sResMap.put(DataUsageSummary.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DataUsageSummary.class.getName()), + NO_DATA_RES_ID, + DataUsageSummary.class.getName(), + R.drawable.ic_settings_data_usage)); + + sResMap.put(DataUsageMeteredSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DataUsageMeteredSettings.class.getName()), + NO_DATA_RES_ID, + DataUsageMeteredSettings.class.getName(), + R.drawable.ic_settings_data_usage)); + + sResMap.put(WirelessSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WirelessSettings.class.getName()), + NO_DATA_RES_ID, + WirelessSettings.class.getName(), + R.drawable.ic_settings_more)); + + sResMap.put(HomeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(HomeSettings.class.getName()), + NO_DATA_RES_ID, + HomeSettings.class.getName(), + R.drawable.ic_settings_home)); + + sResMap.put(DisplaySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DisplaySettings.class.getName()), + NO_DATA_RES_ID, + DisplaySettings.class.getName(), + R.drawable.ic_settings_display)); + + sResMap.put(WallpaperTypeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(WallpaperTypeSettings.class.getName()), + NO_DATA_RES_ID, + WallpaperTypeSettings.class.getName(), + R.drawable.ic_settings_display)); + + sResMap.put(NotificationSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(NotificationSettings.class.getName()), + NO_DATA_RES_ID, + NotificationSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(OtherSoundSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(OtherSoundSettings.class.getName()), + NO_DATA_RES_ID, + OtherSoundSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(ZenModeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(ZenModeSettings.class.getName()), + NO_DATA_RES_ID, + ZenModeSettings.class.getName(), + R.drawable.ic_settings_notifications)); + + sResMap.put(Memory.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(Memory.class.getName()), + NO_DATA_RES_ID, + Memory.class.getName(), + R.drawable.ic_settings_storage)); + + sResMap.put(PowerUsageSummary.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PowerUsageSummary.class.getName()), + R.xml.power_usage_summary, + PowerUsageSummary.class.getName(), + R.drawable.ic_settings_battery)); + + sResMap.put(BatterySaverSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(BatterySaverSettings.class.getName()), + R.xml.battery_saver_settings, + BatterySaverSettings.class.getName(), + R.drawable.ic_settings_battery)); + + sResMap.put(UserSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(UserSettings.class.getName()), + R.xml.user_settings, + UserSettings.class.getName(), + R.drawable.ic_settings_multiuser)); + + sResMap.put(LocationSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(LocationSettings.class.getName()), + R.xml.location_settings, + LocationSettings.class.getName(), + R.drawable.ic_settings_location)); + + sResMap.put(SecuritySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(SecuritySettings.class.getName()), + NO_DATA_RES_ID, + SecuritySettings.class.getName(), + R.drawable.ic_settings_security)); + + sResMap.put(ScreenPinningSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(ScreenPinningSettings.class.getName()), + NO_DATA_RES_ID, + ScreenPinningSettings.class.getName(), + R.drawable.ic_settings_security)); + + sResMap.put(InputMethodAndLanguageSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(InputMethodAndLanguageSettings.class.getName()), + NO_DATA_RES_ID, + InputMethodAndLanguageSettings.class.getName(), + R.drawable.ic_settings_language)); + + sResMap.put(VoiceInputSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(VoiceInputSettings.class.getName()), + NO_DATA_RES_ID, + VoiceInputSettings.class.getName(), + R.drawable.ic_settings_language)); + + sResMap.put(PrivacySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PrivacySettings.class.getName()), + NO_DATA_RES_ID, + PrivacySettings.class.getName(), + R.drawable.ic_settings_backup)); + + sResMap.put(DateTimeSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DateTimeSettings.class.getName()), + R.xml.date_time_prefs, + DateTimeSettings.class.getName(), + R.drawable.ic_settings_date_time)); + + sResMap.put(AccessibilitySettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(AccessibilitySettings.class.getName()), + NO_DATA_RES_ID, + AccessibilitySettings.class.getName(), + R.drawable.ic_settings_accessibility)); + + sResMap.put(PrintSettingsFragment.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(PrintSettingsFragment.class.getName()), + NO_DATA_RES_ID, + PrintSettingsFragment.class.getName(), + R.drawable.ic_settings_print)); + + sResMap.put(DevelopmentSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DevelopmentSettings.class.getName()), + NO_DATA_RES_ID, + DevelopmentSettings.class.getName(), + R.drawable.ic_settings_development)); + + sResMap.put(DeviceInfoSettings.class.getName(), + new SearchIndexableResource( + Ranking.getRankForClassName(DeviceInfoSettings.class.getName()), + NO_DATA_RES_ID, + DeviceInfoSettings.class.getName(), + R.drawable.ic_settings_about)); + } + + private SearchIndexableResources() { + } + + public static int size() { + return sResMap.size(); + } + + public static SearchIndexableResource getResourceByName(String className) { + return sResMap.get(className); + } + + public static Collection<SearchIndexableResource> values() { + return sResMap.values(); + } +} diff --git a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java new file mode 100644 index 0000000..c0afcaf --- /dev/null +++ b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 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.search; + +import android.database.Cursor; +import android.database.MatrixCursor; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesProvider; + +import java.util.Collection; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; + +import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; +import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; + +public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { + private static final String TAG = "SettingsSearchIndexablesProvider"; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryXmlResources(String[] projection) { + MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); + Collection<SearchIndexableResource> values = SearchIndexableResources.values(); + for (SearchIndexableResource val : values) { + Object[] ref = new Object[7]; + ref[COLUMN_INDEX_XML_RES_RANK] = val.rank; + ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId; + ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className; + ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId; + ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = null; // intent action + ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package + ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class + cursor.addRow(ref); + } + return cursor; + } + + @Override + public Cursor queryRawData(String[] projection) { + MatrixCursor result = new MatrixCursor(INDEXABLES_RAW_COLUMNS); + return result; + } + + @Override + public Cursor queryNonIndexableKeys(String[] projection) { + MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); + return cursor; + } +} diff --git a/src/com/android/settings/sim/SimSettings.java b/src/com/android/settings/sim/SimSettings.java new file mode 100644 index 0000000..836ea24 --- /dev/null +++ b/src/com/android/settings/sim/SimSettings.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2014 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.sim; + +import android.provider.SearchIndexableResource; +import com.android.settings.R; + +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceScreen; +import android.telephony.SubInfoRecord; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telecom.PhoneAccount; +import android.telephony.CellInfo; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.TelephonyIntents; +import com.android.settings.RestrictedSettingsFragment; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; +import com.android.settings.notification.DropDownPreference; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.Indexable.SearchIndexProvider; +import com.android.settings.search.SearchIndexableRaw; + +import java.util.ArrayList; +import java.util.List; + +public class SimSettings extends RestrictedSettingsFragment implements Indexable { + private static final String TAG = "SimSettings"; + + private static final String DISALLOW_CONFIG_SIM = "no_config_sim"; + private static final String SIM_CARD_CATEGORY = "sim_cards"; + private static final String KEY_CELLULAR_DATA = "sim_cellular_data"; + private static final String KEY_CALLS = "sim_calls"; + private static final String KEY_SMS = "sim_sms"; + private static final String KEY_ACTIVITIES = "activities"; + + /** + * By UX design we have use only one Subscription Information(SubInfo) record per SIM slot. + * mAvalableSubInfos is the list of SubInfos we present to the user. + * mSubInfoList is the list of all SubInfos. + */ + private List<SubInfoRecord> mAvailableSubInfos = null; + private List<SubInfoRecord> mSubInfoList = null; + + private SubInfoRecord mCellularData = null; + private SubInfoRecord mCalls = null; + private SubInfoRecord mSMS = null; + + private int mNumSims; + + public SimSettings() { + super(DISALLOW_CONFIG_SIM); + } + + @Override + public void onCreate(final Bundle bundle) { + super.onCreate(bundle); + + if (mSubInfoList == null) { + mSubInfoList = SubscriptionManager.getActiveSubInfoList(); + } + + createPreferences(); + updateAllOptions(); + } + + private void createPreferences() { + final TelephonyManager tm = + (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); + + addPreferencesFromResource(R.xml.sim_settings); + + final PreferenceCategory simCards = (PreferenceCategory)findPreference(SIM_CARD_CATEGORY); + + final int numSlots = tm.getSimCount(); + mAvailableSubInfos = new ArrayList<SubInfoRecord>(numSlots); + mNumSims = 0; + for (int i = 0; i < numSlots; ++i) { + final SubInfoRecord sir = findRecordBySlotId(i); + simCards.addPreference(new SimPreference(getActivity(), sir, i)); + mAvailableSubInfos.add(sir); + if (sir != null) { + mNumSims++; + } + } + + updateActivitesCategory(); + } + + private void updateAllOptions() { + updateSimSlotValues(); + updateActivitesCategory(); + } + + private void updateSimSlotValues() { + SubscriptionManager.getAllSubInfoList(); + final PreferenceCategory simCards = (PreferenceCategory)findPreference(SIM_CARD_CATEGORY); + final PreferenceScreen prefScreen = getPreferenceScreen(); + + final int prefSize = prefScreen.getPreferenceCount(); + for (int i = 0; i < prefSize; ++i) { + Preference pref = prefScreen.getPreference(i); + if (pref instanceof SimPreference) { + ((SimPreference)pref).update(); + } + } + } + + private void updateActivitesCategory() { + createDropDown((DropDownPreference) findPreference(KEY_CELLULAR_DATA)); + createDropDown((DropDownPreference) findPreference(KEY_CALLS)); + createDropDown((DropDownPreference) findPreference(KEY_SMS)); + + updateCellularDataValues(); + updateCallValues(); + updateSmsValues(); + } + + /** + * finds a record with subId. + * Since the number of SIMs are few, an array is fine. + */ + private SubInfoRecord findRecordBySubId(final long subId) { + final int availableSubInfoLength = mAvailableSubInfos.size(); + + for (int i = 0; i < availableSubInfoLength; ++i) { + final SubInfoRecord sir = mAvailableSubInfos.get(i); + if (sir != null && sir.subId == subId) { + return sir; + } + } + + return null; + } + + /** + * finds a record with slotId. + * Since the number of SIMs are few, an array is fine. + */ + private SubInfoRecord findRecordBySlotId(final int slotId) { + if (mSubInfoList != null){ + final int availableSubInfoLength = mSubInfoList.size(); + + for (int i = 0; i < availableSubInfoLength; ++i) { + final SubInfoRecord sir = mSubInfoList.get(i); + if (sir.slotId == slotId) { + //Right now we take the first subscription on a SIM. + return sir; + } + } + } + + return null; + } + + private void updateSmsValues() { + final DropDownPreference simPref = (DropDownPreference) findPreference(KEY_SMS); + final SubInfoRecord sir = findRecordBySubId(SubscriptionManager.getDefaultSmsSubId()); + if (sir != null) { + simPref.setSelectedItem(sir.slotId + 1); + } + simPref.setEnabled(mNumSims > 1); + } + + private void updateCellularDataValues() { + final DropDownPreference simPref = (DropDownPreference) findPreference(KEY_CELLULAR_DATA); + final SubInfoRecord sir = findRecordBySubId(SubscriptionManager.getDefaultDataSubId()); + if (sir != null) { + simPref.setSelectedItem(sir.slotId); + } + simPref.setEnabled(mNumSims > 1); + } + + private void updateCallValues() { + final DropDownPreference simPref = (DropDownPreference) findPreference(KEY_CALLS); + final SubInfoRecord sir = findRecordBySubId(SubscriptionManager.getDefaultVoiceSubId()); + if (sir != null) { + simPref.setSelectedItem(sir.slotId + 1); + } + simPref.setEnabled(mNumSims > 1); + } + + @Override + public void onResume() { + super.onResume(); + updateAllOptions(); + } + + @Override + public boolean onPreferenceTreeClick(final PreferenceScreen preferenceScreen, + final Preference preference) { + if (preference instanceof SimPreference) { + ((SimPreference)preference).createEditDialog((SimPreference)preference); + } + + return true; + } + + public void createDropDown(DropDownPreference preference) { + final DropDownPreference simPref = preference; + final String keyPref = simPref.getKey(); + final boolean askFirst = keyPref.equals(KEY_CALLS) || keyPref.equals(KEY_SMS); + + simPref.clearItems(); + + if (askFirst) { + simPref.addItem(getResources().getString( + R.string.sim_calls_ask_first_prefs_title), null); + } + + final int subAvailableSize = mAvailableSubInfos.size(); + for (int i = 0; i < subAvailableSize; ++i) { + final SubInfoRecord sir = mAvailableSubInfos.get(i); + if(sir != null){ + simPref.addItem(sir.displayName, sir); + } + } + + simPref.setCallback(new DropDownPreference.Callback() { + @Override + public boolean onItemSelected(int pos, Object value) { + final long subId = value == null ? 0 : ((SubInfoRecord)value).subId; + + if (simPref.getKey().equals(KEY_CELLULAR_DATA)) { + SubscriptionManager.setDefaultDataSubId(subId); + } else if (simPref.getKey().equals(KEY_CALLS)) { + SubscriptionManager.setDefaultVoiceSubId(subId); + } else if (simPref.getKey().equals(KEY_SMS)) { + // TODO: uncomment once implemented. Bug: 16520931 + // SubscriptionManager.setDefaultSMSSubId(subId); + } + + return true; + } + }); + } + + private void setActivity(Preference preference, SubInfoRecord sir) { + final String key = preference.getKey(); + + if (key.equals(KEY_CELLULAR_DATA)) { + mCellularData = sir; + } else if (key.equals(KEY_CALLS)) { + mCalls = sir; + } else if (key.equals(KEY_SMS)) { + mSMS = sir; + } + + updateActivitesCategory(); + } + + private class SimPreference extends Preference{ + private SubInfoRecord mSubInfoRecord; + private int mSlotId; + + public SimPreference(Context context, SubInfoRecord subInfoRecord, int slotId) { + super(context); + + mSubInfoRecord = subInfoRecord; + mSlotId = slotId; + setKey("sim" + mSlotId); + update(); + } + + public void update() { + final Resources res = getResources(); + + setTitle(res.getString(R.string.sim_card_number_title, mSlotId + 1)); + if (mSubInfoRecord != null) { + setSummary(res.getString(R.string.sim_settings_summary, + mSubInfoRecord.displayName, mSubInfoRecord.number)); + setEnabled(true); + } else { + setSummary(R.string.sim_slot_empty); + setFragment(null); + setEnabled(false); + } + } + + public void createEditDialog(SimPreference simPref) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + final View dialogLayout = getActivity().getLayoutInflater().inflate( + R.layout.multi_sim_dialog, null); + builder.setView(dialogLayout); + + EditText nameText = (EditText)dialogLayout.findViewById(R.id.sim_name); + nameText.setText(mSubInfoRecord.displayName); + + TextView numberView = (TextView)dialogLayout.findViewById(R.id.number); + numberView.setText(mSubInfoRecord.number); + + TextView carrierView = (TextView)dialogLayout.findViewById(R.id.carrier); + carrierView.setText(mSubInfoRecord.displayName); + + builder.setTitle(R.string.sim_editor_title); + + builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + final EditText nameText = (EditText)dialogLayout.findViewById(R.id.sim_name); + final Spinner displayNumbers = + (Spinner)dialogLayout.findViewById(R.id.display_numbers); + + SubscriptionManager.setDisplayNumberFormat( + displayNumbers.getSelectedItemPosition() == 0 + ? SubscriptionManager.DISPLAY_NUMBER_LAST + : SubscriptionManager.DISPLAY_NUMBER_FIRST, mSubInfoRecord.subId); + + mSubInfoRecord.displayName = nameText.getText().toString(); + SubscriptionManager.setDisplayName(mSubInfoRecord.displayName, + mSubInfoRecord.subId); + + updateAllOptions(); + update(); + } + }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + dialog.dismiss(); + } + }); + + builder.create().show(); + } + } + + /** + * For search + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + ArrayList<SearchIndexableResource> result = + new ArrayList<SearchIndexableResource>(); + + if (Utils.showSimCardTile(context)) { + SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.sim_settings; + result.add(sir); + } + + return result; + } + }; + +} diff --git a/src/com/android/settings/tts/TextToSpeechSettings.java b/src/com/android/settings/tts/TextToSpeechSettings.java index 0ff7f4f..b16ab56 100644 --- a/src/com/android/settings/tts/TextToSpeechSettings.java +++ b/src/com/android/settings/tts/TextToSpeechSettings.java @@ -20,6 +20,7 @@ import static android.provider.Settings.Secure.TTS_DEFAULT_RATE; import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState; @@ -27,12 +28,11 @@ import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; -import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; @@ -46,6 +46,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; +import java.util.Objects; import java.util.Set; public class TextToSpeechSettings extends SettingsPreferenceFragment implements @@ -103,7 +105,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private TextToSpeech mTts = null; private TtsEngines mEnginesHelper = null; - private String mSampleText = ""; + private String mSampleText = null; /** * Default locale used by selected TTS engine, null if not connected to any engine. @@ -164,6 +166,9 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements setTtsUtteranceProgressListener(); initSettings(); + + // Prevent restarting the TTS connection on rotation + setRetainInstance(true); } @Override @@ -212,7 +217,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements // Set up the default rate. try { - mDefaultRate = Settings.Secure.getInt(resolver, TTS_DEFAULT_RATE); + mDefaultRate = android.provider.Settings.Secure.getInt(resolver, TTS_DEFAULT_RATE); } catch (SettingNotFoundException e) { // Default rate setting not found, initialize it mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE; @@ -222,12 +227,12 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements mCurrentEngine = mTts.getCurrentEngine(); - PreferenceActivity preferenceActivity = null; - if (getActivity() instanceof PreferenceActivity) { - preferenceActivity = (PreferenceActivity) getActivity(); + SettingsActivity activity = null; + if (getActivity() instanceof SettingsActivity) { + activity = (SettingsActivity) getActivity(); } else { throw new IllegalStateException("TextToSpeechSettings used outside a " + - "PreferenceActivity"); + "Settings"); } mEnginePreferenceCategory.removeAll(); @@ -235,7 +240,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements List<EngineInfo> engines = mEnginesHelper.getEngines(); for (EngineInfo engine : engines) { TtsEnginePreference enginePref = new TtsEnginePreference(getActivity(), engine, - this, preferenceActivity); + this, activity); mEnginePreferenceCategory.addPreference(enginePref); } @@ -264,10 +269,16 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements return; } - mCurrentDefaultLocale = defaultLocale; + // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize, + // we may end up with English (USA)and German (DEU). + final Locale oldDefaultLocale = mCurrentDefaultLocale; + mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString()); + if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) { + mSampleText = null; + } int defaultAvailable = mTts.setLanguage(defaultLocale); - if (evaluateDefaultLocale()) { + if (evaluateDefaultLocale() && mSampleText == null) { getSampleText(); } } @@ -278,25 +289,32 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) { return false; } - int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale); - // Check if language is listed in CheckVoices Action result as available voice. - String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language(); boolean notInAvailableLangauges = true; - if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) { - defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country(); - } - if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) { - defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant(); - } + try { + // Check if language is listed in CheckVoices Action result as available voice. + String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language(); + if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) { + defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country(); + } + if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) { + defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant(); + } - for (String loc : mAvailableStrLocals) { - if (loc.equalsIgnoreCase(defaultLocaleStr)) { - notInAvailableLangauges = false; - break; + for (String loc : mAvailableStrLocals) { + if (loc.equalsIgnoreCase(defaultLocaleStr)) { + notInAvailableLangauges = false; + break; + } } + } catch (MissingResourceException e) { + if (DBG) Log.wtf(TAG, "MissingResourceException", e); + updateEngineStatus(R.string.tts_status_not_supported); + updateWidgetState(false); + return false; } + int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale); if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED || defaultAvailable == TextToSpeech.LANG_MISSING_DATA || notInAvailableLangauges) { @@ -315,7 +333,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements } } - /** * Ask the current default engine to return a string of sample text to be * spoken to the user. @@ -358,16 +375,21 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private String getDefaultSampleString() { if (mTts != null && mTts.getLanguage() != null) { - final String currentLang = mTts.getLanguage().getISO3Language(); - String[] strings = getActivity().getResources().getStringArray( - R.array.tts_demo_strings); - String[] langs = getActivity().getResources().getStringArray( - R.array.tts_demo_string_langs); - - for (int i = 0; i < strings.length; ++i) { - if (langs[i].equals(currentLang)) { - return strings[i]; + try { + final String currentLang = mTts.getLanguage().getISO3Language(); + String[] strings = getActivity().getResources().getStringArray( + R.array.tts_demo_strings); + String[] langs = getActivity().getResources().getStringArray( + R.array.tts_demo_string_langs); + + for (int i = 0; i < strings.length; ++i) { + if (langs[i].equals(currentLang)) { + return strings[i]; + } } + } catch (MissingResourceException e) { + if (DBG) Log.wtf(TAG, "MissingResourceException", e); + // Ignore and fall back to default sample string } } return getString(R.string.tts_default_sample_string); @@ -422,7 +444,8 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements // Default rate mDefaultRate = Integer.parseInt((String) objValue); try { - Settings.Secure.putInt(getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate); + android.provider.Settings.Secure.putInt(getContentResolver(), + TTS_DEFAULT_RATE, mDefaultRate); if (mTts != null) { mTts.setSpeechRate(mDefaultRate / 100.0f); } @@ -466,11 +489,10 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements private void displayNetworkAlert() { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(android.R.string.dialog_alert_title); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getActivity().getString(R.string.tts_engine_network_required)); - builder.setCancelable(false); - builder.setPositiveButton(android.R.string.ok, null); + builder.setTitle(android.R.string.dialog_alert_title) + .setMessage(getActivity().getString(R.string.tts_engine_network_required)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.create(); dialog.show(); @@ -565,7 +587,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment implements return; } - Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); + android.provider.Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); mAvailableStrLocals = data.getStringArrayListExtra( TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); diff --git a/src/com/android/settings/tts/TtsEnginePreference.java b/src/com/android/settings/tts/TtsEnginePreference.java index 486fdf8..ae921f8 100644 --- a/src/com/android/settings/tts/TtsEnginePreference.java +++ b/src/com/android/settings/tts/TtsEnginePreference.java @@ -22,7 +22,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.speech.tts.TextToSpeech.EngineInfo; import android.util.Log; import android.view.View; @@ -33,6 +32,7 @@ import android.widget.RadioButton; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.Utils; @@ -63,7 +63,7 @@ public class TtsEnginePreference extends Preference { * The preference activity that owns this preference. Required * for instantiating the engine specific settings screen. */ - private final PreferenceActivity mPreferenceActivity; + private final SettingsActivity mSettingsActivity; /** * The engine information for the engine this preference represents. @@ -95,12 +95,12 @@ public class TtsEnginePreference extends Preference { }; public TtsEnginePreference(Context context, EngineInfo info, RadioButtonGroupState state, - PreferenceActivity prefActivity) { + SettingsActivity prefActivity) { super(context); setLayoutResource(R.layout.preference_tts_engine); mSharedState = state; - mPreferenceActivity = prefActivity; + mSettingsActivity = prefActivity; mEngineInfo = info; mPreventRadioButtonCallbacks = false; @@ -156,10 +156,10 @@ public class TtsEnginePreference extends Preference { } // Note that we use this instead of the (easier to use) - // PreferenceActivity.startPreferenceFragment because the + // SettingsActivity.startPreferenceFragment because the // title will not be updated correctly in the fragment // breadcrumb since it isn't inflated from the XML layout. - mPreferenceActivity.startPreferencePanel( + mSettingsActivity.startPreferencePanel( TtsEngineSettingsFragment.class.getName(), args, 0, mEngineInfo.label, null, 0); } @@ -198,13 +198,12 @@ public class TtsEnginePreference extends Preference { Log.i(TAG, "Displaying data alert for :" + mEngineInfo.name); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setTitle(android.R.string.dialog_alert_title); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getContext().getString( - R.string.tts_engine_security_warning, mEngineInfo.label)); - builder.setCancelable(true); - builder.setPositiveButton(android.R.string.ok, positiveOnClickListener); - builder.setNegativeButton(android.R.string.cancel, negativeOnClickListener); + builder.setTitle(android.R.string.dialog_alert_title) + .setMessage(getContext().getString( + R.string.tts_engine_security_warning, mEngineInfo.label)) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, positiveOnClickListener) + .setNegativeButton(android.R.string.cancel, negativeOnClickListener); AlertDialog dialog = builder.create(); dialog.show(); diff --git a/src/com/android/settings/tts/TtsEngineSettingsFragment.java b/src/com/android/settings/tts/TtsEngineSettingsFragment.java index bb5ac7a..2449353 100644 --- a/src/com/android/settings/tts/TtsEngineSettingsFragment.java +++ b/src/com/android/settings/tts/TtsEngineSettingsFragment.java @@ -52,6 +52,10 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem private static final String KEY_ENGINE_SETTINGS = "tts_engine_settings"; private static final String KEY_INSTALL_DATA = "tts_install_data"; + private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries"; + private static final String STATE_KEY_LOCALE_ENTRY_VALUES= "locale_entry_values"; + private static final String STATE_KEY_LOCALE_VALUE = "locale_value"; + private static final int VOICE_DATA_INTEGRITY_CHECK = 1977; private TtsEngines mEnginesHelper; @@ -64,7 +68,6 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem private TextToSpeech mTts; private int mSelectedLocaleIndex = -1; - private int mSystemLocaleIndex = -1; private final TextToSpeech.OnInitListener mTtsInitListener = new TextToSpeech.OnInitListener() { @Override @@ -120,10 +123,26 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem mEngineSettingsPreference.setEnabled(false); } mInstallVoicesPreference.setEnabled(false); - mLocalePreference.setEnabled(false); - mLocalePreference.setEntries(new CharSequence[0]); - mLocalePreference.setEntryValues(new CharSequence[0]); + if (savedInstanceState == null) { + mLocalePreference.setEnabled(false); + mLocalePreference.setEntries(new CharSequence[0]); + mLocalePreference.setEntryValues(new CharSequence[0]); + } else { + // Repopulate mLocalePreference with saved state. Will be updated later with + // up-to-date values when checkTtsData() calls back with results. + final CharSequence[] entries = + savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRIES); + final CharSequence[] entryValues = + savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES); + final CharSequence value = + savedInstanceState.getCharSequence(STATE_KEY_LOCALE_VALUE); + + mLocalePreference.setEntries(entries); + mLocalePreference.setEntryValues(entryValues); + mLocalePreference.setValue(value != null ? value.toString() : null); + mLocalePreference.setEnabled(entries.length > 0); + } mVoiceDataDetails = getArguments().getParcelable(TtsEnginePreference.FRAGMENT_ARGS_VOICES); @@ -144,6 +163,19 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem super.onDestroy(); } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // Save the mLocalePreference values, so we can repopulate it with entries. + outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRIES, + mLocalePreference.getEntries()); + outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES, + mLocalePreference.getEntryValues()); + outState.putCharSequence(STATE_KEY_LOCALE_VALUE, + mLocalePreference.getValue()); + } + private final void checkTtsData() { Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); intent.setPackage(getEngineName()); @@ -158,12 +190,22 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { - mVoiceDataDetails = data; - updateVoiceDetails(); + if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) { + updateVoiceDetails(data); + } else { + Log.e(TAG, "CheckVoiceData activity failed"); + } } } - private void updateVoiceDetails() { + private void updateVoiceDetails(Intent data) { + if (data == null){ + Log.e(TAG, "Engine failed voice data integrity check (null return)" + + mTts.getCurrentEngine()); + return; + } + mVoiceDataDetails = data; + if (DBG) Log.d(TAG, "Parsing voice data details, data: " + mVoiceDataDetails.toUri(0)); final ArrayList<String> available = mVoiceDataDetails.getStringArrayListExtra( @@ -191,53 +233,44 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem mLocalePreference.setEnabled(false); return; } - String currentLocale = mEnginesHelper.getLocalePrefForEngine( - getEngineName()); + Locale currentLocale = null; + if (!mEnginesHelper.isLocaleSetToDefaultForEngine(getEngineName())) { + currentLocale = mEnginesHelper.getLocalePrefForEngine(getEngineName()); + } - ArrayList<Pair<String, String>> entryPairs = - new ArrayList<Pair<String, String>>(availableLangs.size()); + ArrayList<Pair<String, Locale>> entryPairs = + new ArrayList<Pair<String, Locale>>(availableLangs.size()); 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){ - entryPairs.add(new Pair<String, String>( - loc.getDisplayName(), availableLangs.get(i))); + Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i)); + if (locale != null){ + entryPairs.add(new Pair<String, Locale>( + locale.getDisplayName(), locale)); } } // Sort it - Collections.sort(entryPairs, new Comparator<Pair<String, String>>() { + Collections.sort(entryPairs, new Comparator<Pair<String, Locale>>() { @Override - public int compare(Pair<String, String> lhs, Pair<String, String> rhs) { + public int compare(Pair<String, Locale> lhs, Pair<String, Locale> rhs) { return lhs.first.compareToIgnoreCase(rhs.first); } }); - String defaultLocaleStr = mEnginesHelper.getDefaultLocale(); - // Get two arrays out of one of pairs - mSelectedLocaleIndex = -1; - mSystemLocaleIndex = -1; - CharSequence[] entries = new CharSequence[availableLangs.size()]; - CharSequence[] entryValues = new CharSequence[availableLangs.size()]; - int i = 0; - for (Pair<String, String> entry : entryPairs) { - if (entry.second.equalsIgnoreCase(currentLocale)) { + mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value + CharSequence[] entries = new CharSequence[availableLangs.size()+1]; + CharSequence[] entryValues = new CharSequence[availableLangs.size()+1]; + + entries[0] = getActivity().getString(R.string.tts_lang_use_system); + entryValues[0] = ""; + + int i = 1; + for (Pair<String, Locale> entry : entryPairs) { + if (entry.second.equals(currentLocale)) { mSelectedLocaleIndex = i; } - if (entry.second.equalsIgnoreCase(defaultLocaleStr)) { - mSystemLocaleIndex = i; - } entries[i] = entry.first; - entryValues[i++] = entry.second; + entryValues[i++] = entry.second.toString(); } mLocalePreference.setEntries(entries); @@ -264,7 +297,6 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem private void installVoiceData() { if (TextUtils.isEmpty(getEngineName())) return; Intent intent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setPackage(getEngineName()); try { Log.v(TAG, "Installing voice data: " + intent.toUri(0)); @@ -290,16 +322,19 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference == mLocalePreference) { - updateLanguageTo((String) newValue); + String localeString = (String) newValue; + updateLanguageTo((!TextUtils.isEmpty(localeString) ? + mEnginesHelper.parseLocaleString(localeString) : null)); return true; } return false; } - private void updateLanguageTo(String locale) { + private void updateLanguageTo(Locale locale) { int selectedLocaleIndex = -1; + String localeString = (locale != null) ? locale.toString() : ""; for (int i=0; i < mLocalePreference.getEntryValues().length; i++) { - if (locale.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) { + if (localeString.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) { selectedLocaleIndex = i; break; } @@ -312,18 +347,11 @@ public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implem mLocalePreference.setSummary(mLocalePreference.getEntries()[selectedLocaleIndex]); mSelectedLocaleIndex = selectedLocaleIndex; - if (mSelectedLocaleIndex == mSystemLocaleIndex) { - // Use empty locale, it will default to the system language - mEnginesHelper.updateLocalePrefForEngine(getEngineName(), ""); - } else { - mEnginesHelper.updateLocalePrefForEngine(getEngineName(), locale); - } + mEnginesHelper.updateLocalePrefForEngine(getEngineName(), locale); if (getEngineName().equals(mTts.getCurrentEngine())) { - String[] localeArray = TtsEngines.parseLocalePref(locale); - if (localeArray != null) { - mTts.setLanguage(new Locale(localeArray[0], localeArray[1], localeArray[2])); - } + // Null locale means "use system default" + mTts.setLanguage((locale != null) ? locale : Locale.getDefault()); } } diff --git a/src/com/android/settings/users/AppRestrictionsFragment.java b/src/com/android/settings/users/AppRestrictionsFragment.java index fcaf18f..d717489 100644 --- a/src/com/android/settings/users/AppRestrictionsFragment.java +++ b/src/com/android/settings/users/AppRestrictionsFragment.java @@ -17,7 +17,6 @@ package com.android.settings.users; import android.app.Activity; -import android.app.AppGlobals; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -58,13 +57,13 @@ import android.view.View.OnClickListener; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.ViewGroup; -import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Switch; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.drawable.CircleFramedDrawable; import java.util.ArrayList; import java.util.Collections; @@ -161,34 +160,17 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen private boolean panelOpen; private boolean immutable; private List<Preference> mChildren = new ArrayList<Preference>(); - private final ColorFilter grayscaleFilter; AppRestrictionsPreference(Context context, OnClickListener listener) { super(context); setLayoutResource(R.layout.preference_app_restrictions); this.listener = listener; - - ColorMatrix colorMatrix = new ColorMatrix(); - colorMatrix.setSaturation(0f); - float[] matrix = colorMatrix.getArray(); - matrix[18] = 0.5f; - grayscaleFilter = new ColorMatrixColorFilter(colorMatrix); } private void setSettingsEnabled(boolean enable) { hasSettings = enable; } - @Override - public void setChecked(boolean checked) { - if (checked) { - getIcon().setColorFilter(null); - } else { - getIcon().setColorFilter(grayscaleFilter); - } - super.setChecked(checked); - } - void setRestrictions(ArrayList<RestrictionEntry> restrictions) { this.restrictions = restrictions; } @@ -248,6 +230,8 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen final Switch toggle = (Switch) widget.getChildAt(0); toggle.setEnabled(!isImmutable()); toggle.setTag(this); + toggle.setClickable(true); + toggle.setFocusable(true); toggle.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -346,7 +330,7 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen return getPreferenceScreen(); } - protected Drawable getCircularUserIcon() { + Drawable getCircularUserIcon() { Bitmap userIcon = mUserManager.getUserIcon(mUser.getIdentifier()); if (userIcon == null) { return null; @@ -387,12 +371,12 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen Log.d(TAG, "Installing " + packageName); } } - if (info != null && (info.flags&ApplicationInfo.FLAG_BLOCKED) != 0 + if (info != null && (info.flags&ApplicationInfo.FLAG_HIDDEN) != 0 && (info.flags&ApplicationInfo.FLAG_INSTALLED) != 0) { disableUiForPackage(packageName); - mIPm.setApplicationBlockedSettingAsUser(packageName, false, userId); + mIPm.setApplicationHiddenSettingAsUser(packageName, false, userId); if (DEBUG) { - Log.d(TAG, "Unblocking " + packageName); + Log.d(TAG, "Unhiding " + packageName); } } } catch (RemoteException re) { @@ -410,9 +394,9 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen } } else { disableUiForPackage(packageName); - mIPm.setApplicationBlockedSettingAsUser(packageName, true, userId); + mIPm.setApplicationHiddenSettingAsUser(packageName, true, userId); if (DEBUG) { - Log.d(TAG, "Blocking " + packageName); + Log.d(TAG, "Hiding " + packageName); } } } @@ -654,9 +638,9 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen private boolean isAppEnabledForUser(PackageInfo pi) { if (pi == null) return false; final int flags = pi.applicationInfo.flags; - // Return true if it is installed and not blocked + // Return true if it is installed and not hidden return ((flags&ApplicationInfo.FLAG_INSTALLED) != 0 - && (flags&ApplicationInfo.FLAG_BLOCKED) == 0); + && (flags&ApplicationInfo.FLAG_HIDDEN) == 0); } private void populateApps() { @@ -684,7 +668,7 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen app.masterEntry.activityName)); } p.setKey(getKeyForPackage(packageName)); - p.setSettingsEnabled(hasSettings || isSettingsApp); + p.setSettingsEnabled((hasSettings || isSettingsApp) && app.masterEntry == null); p.setPersistent(false); p.setOnPreferenceChangeListener(this); p.setOnPreferenceClickListener(this); @@ -703,7 +687,8 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen // Get and populate the defaults, since the user is not going to be // able to toggle this app ON (it's ON by default and immutable). // Only do this for restricted profiles, not single-user restrictions - if (hasSettings) { + // Also don't do this for slave icons + if (hasSettings && app.masterEntry == null) { requestRestrictionsForApp(packageName, p, false); } } else if (!mNewUser && isAppEnabledForUser(pi)) { @@ -1008,6 +993,7 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen + entry.getKey()); mAppList.addPreference(p); p.setOnPreferenceChangeListener(AppRestrictionsFragment.this); + p.setIcon(R.drawable.empty_icon); preference.mChildren.add(p); count++; } diff --git a/src/com/android/settings/users/EditUserInfoController.java b/src/com/android/settings/users/EditUserInfoController.java new file mode 100644 index 0000000..0f844a7 --- /dev/null +++ b/src/com/android/settings/users/EditUserInfoController.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2013 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.users; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.Fragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.UserInfo; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.drawable.CircleFramedDrawable; + +/** + * This class encapsulates a Dialog for editing the user nickname and photo. + */ +public class EditUserInfoController { + + private static final String KEY_AWAITING_RESULT = "awaiting_result"; + private static final String KEY_SAVED_PHOTO = "pending_photo"; + + private Dialog mEditUserInfoDialog; + private Bitmap mSavedPhoto; + private EditUserPhotoController mEditUserPhotoController; + private UserHandle mUser; + private UserManager mUserManager; + private boolean mWaitingForActivityResult = false; + + public interface OnContentChangedCallback { + public void onPhotoChanged(Drawable photo); + public void onLabelChanged(CharSequence label); + } + + public void clear() { + mEditUserInfoDialog = null; + mSavedPhoto = null; + } + + public Dialog getDialog() { + return mEditUserInfoDialog; + } + + public void onRestoreInstanceState(Bundle icicle) { + mSavedPhoto = (Bitmap) icicle.getParcelable(KEY_SAVED_PHOTO); + mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false); + } + + public void onSaveInstanceState(Bundle outState) { + if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() + && mEditUserPhotoController != null) { + outState.putParcelable(KEY_SAVED_PHOTO, + mEditUserPhotoController.getNewUserPhotoBitmap()); + } + if (mWaitingForActivityResult) { + outState.putBoolean(KEY_AWAITING_RESULT, + mWaitingForActivityResult); + } + } + + public void startingActivityForResult() { + mWaitingForActivityResult = true; + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mWaitingForActivityResult = false; + + if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() + && mEditUserPhotoController.onActivityResult(requestCode, resultCode, data)) { + return; + } + } + + Drawable getCircularUserIcon(Activity activity) { + Bitmap userIcon = mUserManager.getUserIcon(mUser.getIdentifier()); + if (userIcon == null) { + return null; + } + CircleFramedDrawable circularIcon = + CircleFramedDrawable.getInstance(activity, userIcon); + return circularIcon; + } + + public Dialog createDialog(final Fragment fragment, final Drawable currentUserIcon, + final CharSequence currentUserName, + int titleResId, final OnContentChangedCallback callback, UserHandle user) { + Activity activity = fragment.getActivity(); + mUser = user; + if (mUserManager == null) { + mUserManager = UserManager.get(activity); + } + LayoutInflater inflater = activity.getLayoutInflater(); + View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); + + UserInfo info = mUserManager.getUserInfo(mUser.getIdentifier()); + + final EditText userNameView = (EditText) content.findViewById(R.id.user_name); + userNameView.setText(info.name); + + final ImageView userPhotoView = (ImageView) content.findViewById(R.id.user_photo); + Drawable drawable = null; + if (mSavedPhoto != null) { + drawable = CircleFramedDrawable.getInstance(activity, mSavedPhoto); + } else { + drawable = currentUserIcon; + if (drawable == null) { + drawable = getCircularUserIcon(activity); + } + } + userPhotoView.setImageDrawable(drawable); + mEditUserPhotoController = new EditUserPhotoController(fragment, userPhotoView, + mSavedPhoto, drawable, mWaitingForActivityResult); + mEditUserInfoDialog = new AlertDialog.Builder(activity) + .setTitle(R.string.profile_info_settings_title) + .setView(content) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + // Update the name if changed. + CharSequence userName = userNameView.getText(); + if (!TextUtils.isEmpty(userName)) { + if (currentUserName == null + || !userName.toString().equals(currentUserName.toString())) { + if (callback != null) { + callback.onLabelChanged(userName.toString()); + } + mUserManager.setUserName(mUser.getIdentifier(), + userName.toString()); + } + } + // Update the photo if changed. + Drawable drawable = mEditUserPhotoController.getNewUserPhotoDrawable(); + Bitmap bitmap = mEditUserPhotoController.getNewUserPhotoBitmap(); + if (drawable != null && bitmap != null + && !drawable.equals(currentUserIcon)) { + if (callback != null) { + callback.onPhotoChanged(drawable); + } + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + mUserManager.setUserIcon(mUser.getIdentifier(), + mEditUserPhotoController.getNewUserPhotoBitmap()); + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + fragment.getActivity().removeDialog( + RestrictedProfileSettings.DIALOG_ID_EDIT_USER_INFO); + } + clear(); + } + }) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + clear(); + } + }) + .create(); + + // Make sure the IME is up. + mEditUserInfoDialog.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + return mEditUserInfoDialog; + } +} diff --git a/src/com/android/settings/users/EditUserPhotoController.java b/src/com/android/settings/users/EditUserPhotoController.java new file mode 100644 index 0000000..82e550e --- /dev/null +++ b/src/com/android/settings/users/EditUserPhotoController.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2013 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.users; + +import android.app.Activity; +import android.app.Fragment; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Bitmap.Config; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.provider.ContactsContract.DisplayPhoto; +import android.support.v4.content.FileProvider; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListPopupWindow; + +import com.android.settings.R; +import com.android.settings.drawable.CircleFramedDrawable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class EditUserPhotoController { + private static final String TAG = "EditUserPhotoController"; + + private static final int POPUP_LIST_ITEM_ID_CHOOSE_PHOTO = 1; + private static final int POPUP_LIST_ITEM_ID_TAKE_PHOTO = 2; + + // It seems that this class generates custom request codes and they may + // collide with ours, these values are very unlikely to have a conflict. + private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; + private static final int REQUEST_CODE_TAKE_PHOTO = 1002; + private static final int REQUEST_CODE_CROP_PHOTO = 1003; + + private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; + private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; + + private final int mPhotoSize; + + private final Context mContext; + private final Fragment mFragment; + private final ImageView mImageView; + + private final Uri mCropPictureUri; + private final Uri mTakePictureUri; + + private Bitmap mNewUserPhotoBitmap; + private Drawable mNewUserPhotoDrawable; + + public EditUserPhotoController(Fragment fragment, ImageView view, + Bitmap bitmap, Drawable drawable, boolean waiting) { + mContext = view.getContext(); + mFragment = fragment; + mImageView = view; + mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); + mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); + mPhotoSize = getPhotoSize(mContext); + mImageView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showUpdatePhotoPopup(); + } + }); + mNewUserPhotoBitmap = bitmap; + mNewUserPhotoDrawable = drawable; + } + + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + return false; + } + final Uri pictureUri = data != null && data.getData() != null + ? data.getData() : mTakePictureUri; + switch (requestCode) { + case REQUEST_CODE_CROP_PHOTO: + onPhotoCropped(pictureUri, true); + return true; + case REQUEST_CODE_TAKE_PHOTO: + case REQUEST_CODE_CHOOSE_PHOTO: + cropPhoto(pictureUri); + return true; + } + return false; + } + + public Bitmap getNewUserPhotoBitmap() { + return mNewUserPhotoBitmap; + } + + public Drawable getNewUserPhotoDrawable() { + return mNewUserPhotoDrawable; + } + + private void showUpdatePhotoPopup() { + final boolean canTakePhoto = canTakePhoto(); + final boolean canChoosePhoto = canChoosePhoto(); + + if (!canTakePhoto && !canChoosePhoto) { + return; + } + + Context context = mImageView.getContext(); + final List<EditUserPhotoController.AdapterItem> items = new ArrayList<EditUserPhotoController.AdapterItem>(); + + if (canTakePhoto()) { + String title = mImageView.getContext().getString( R.string.user_image_take_photo); + EditUserPhotoController.AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_TAKE_PHOTO); + items.add(item); + } + + if (canChoosePhoto) { + String title = context.getString(R.string.user_image_choose_photo); + EditUserPhotoController.AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_CHOOSE_PHOTO); + items.add(item); + } + + final ListPopupWindow listPopupWindow = new ListPopupWindow(context); + + listPopupWindow.setAnchorView(mImageView); + listPopupWindow.setModal(true); + listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); + + ListAdapter adapter = new ArrayAdapter<EditUserPhotoController.AdapterItem>(context, + R.layout.edit_user_photo_popup_item, items); + listPopupWindow.setAdapter(adapter); + + final int width = Math.max(mImageView.getWidth(), context.getResources() + .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); + listPopupWindow.setWidth(width); + + listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + EditUserPhotoController.AdapterItem item = items.get(position); + switch (item.id) { + case POPUP_LIST_ITEM_ID_CHOOSE_PHOTO: { + choosePhoto(); + listPopupWindow.dismiss(); + } break; + case POPUP_LIST_ITEM_ID_TAKE_PHOTO: { + takePhoto(); + listPopupWindow.dismiss(); + } break; + } + } + }); + + listPopupWindow.show(); + } + + private boolean canTakePhoto() { + return mImageView.getContext().getPackageManager().queryIntentActivities( + new Intent(MediaStore.ACTION_IMAGE_CAPTURE), + PackageManager.MATCH_DEFAULT_ONLY).size() > 0; + } + + private boolean canChoosePhoto() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + return mImageView.getContext().getPackageManager().queryIntentActivities( + intent, 0).size() > 0; + } + + private void takePhoto() { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + appendOutputExtra(intent, mTakePictureUri); + mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); + } + + private void choosePhoto() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); + intent.setType("image/*"); + appendOutputExtra(intent, mTakePictureUri); + mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); + } + + private void cropPhoto(Uri pictureUri) { + // TODO: Use a public intent, when there is one. + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(pictureUri, "image/*"); + appendOutputExtra(intent, mCropPictureUri); + appendCropExtras(intent); + if (intent.resolveActivity(mContext.getPackageManager()) != null) { + mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); + } else { + onPhotoCropped(pictureUri, false); + } + } + + private void appendOutputExtra(Intent intent, Uri pictureUri) { + intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); + } + + private void appendCropExtras(Intent intent) { + intent.putExtra("crop", "true"); + intent.putExtra("scale", true); + intent.putExtra("scaleUpIfNeeded", true); + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + intent.putExtra("outputX", mPhotoSize); + intent.putExtra("outputY", mPhotoSize); + } + + private void onPhotoCropped(final Uri data, final boolean cropped) { + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(Void... params) { + if (cropped) { + InputStream imageStream = null; + try { + imageStream = mContext.getContentResolver() + .openInputStream(data); + return BitmapFactory.decodeStream(imageStream); + } catch (FileNotFoundException fe) { + Log.w(TAG, "Cannot find image file", fe); + return null; + } finally { + if (imageStream != null) { + try { + imageStream.close(); + } catch (IOException ioe) { + Log.w(TAG, "Cannot close image stream", ioe); + } + } + } + } else { + // Scale and crop to a square aspect ratio + Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, + Config.ARGB_8888); + Canvas canvas = new Canvas(croppedImage); + Bitmap fullImage = null; + try { + InputStream imageStream = mContext.getContentResolver() + .openInputStream(data); + fullImage = BitmapFactory.decodeStream(imageStream); + } catch (FileNotFoundException fe) { + return null; + } + if (fullImage != null) { + final int squareSize = Math.min(fullImage.getWidth(), + fullImage.getHeight()); + final int left = (fullImage.getWidth() - squareSize) / 2; + final int top = (fullImage.getHeight() - squareSize) / 2; + Rect rectSource = new Rect(left, top, + left + squareSize, top + squareSize); + Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); + Paint paint = new Paint(); + canvas.drawBitmap(fullImage, rectSource, rectDest, paint); + return croppedImage; + } else { + // Bah! Got nothin. + return null; + } + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null) { + mNewUserPhotoBitmap = bitmap; + mNewUserPhotoDrawable = CircleFramedDrawable + .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); + mImageView.setImageDrawable(mNewUserPhotoDrawable); + } + new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); + new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + + private static int getPhotoSize(Context context) { + Cursor cursor = context.getContentResolver().query( + DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, + new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); + try { + cursor.moveToFirst(); + return cursor.getInt(0); + } finally { + cursor.close(); + } + } + + private Uri createTempImageUri(Context context, String fileName, boolean purge) { + final File folder = context.getCacheDir(); + folder.mkdirs(); + final File fullPath = new File(folder, fileName); + if (purge) { + fullPath.delete(); + } + final Uri fileUri = + FileProvider.getUriForFile(context, RestrictedProfileSettings.FILE_PROVIDER_AUTHORITY, fullPath); + return fileUri; + } + + private static final class AdapterItem { + final String title; + final int id; + + public AdapterItem(String title, int id) { + this.title = title; + this.id = id; + } + + @Override + public String toString() { + return title; + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/users/RestrictedProfileSettings.java b/src/com/android/settings/users/RestrictedProfileSettings.java index c293536..8db911c 100644 --- a/src/com/android/settings/users/RestrictedProfileSettings.java +++ b/src/com/android/settings/users/RestrictedProfileSettings.java @@ -16,77 +16,44 @@ package com.android.settings.users; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.content.ClipData; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.UserInfo; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; -import android.provider.MediaStore; -import android.provider.ContactsContract.DisplayPhoto; -import android.support.v4.content.FileProvider; -import android.text.TextUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.ListPopupWindow; import android.widget.TextView; import com.android.settings.R; +import com.android.settings.Utils; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.ArrayList; import java.util.List; -public class RestrictedProfileSettings extends AppRestrictionsFragment { +public class RestrictedProfileSettings extends AppRestrictionsFragment + implements EditUserInfoController.OnContentChangedCallback { - private static final String KEY_SAVED_PHOTO = "pending_photo"; - private static final String KEY_AWAITING_RESULT = "awaiting_result"; - private static final int DIALOG_ID_EDIT_USER_INFO = 1; public static final String FILE_PROVIDER_AUTHORITY = "com.android.settings.files"; + static final int DIALOG_ID_EDIT_USER_INFO = 1; + private static final int DIALOG_CONFIRM_REMOVE = 2; private View mHeaderView; private ImageView mUserIconView; private TextView mUserNameView; + private ImageView mDeleteButton; - private Dialog mEditUserInfoDialog; - private EditUserPhotoController mEditUserPhotoController; - private Bitmap mSavedPhoto; - private boolean mWaitingForActivityResult; + private EditUserInfoController mEditUserInfoController = + new EditUserInfoController(); @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); if (icicle != null) { - mSavedPhoto = (Bitmap) icicle.getParcelable(KEY_SAVED_PHOTO); - mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false); + mEditUserInfoController.onRestoreInstanceState(icicle); } init(icicle); @@ -97,10 +64,12 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment { if (mHeaderView == null) { mHeaderView = LayoutInflater.from(getActivity()).inflate( R.layout.user_info_header, null); - ((ViewGroup) getListView().getParent()).addView(mHeaderView, 0); + setPinnedHeaderView(mHeaderView); mHeaderView.setOnClickListener(this); mUserIconView = (ImageView) mHeaderView.findViewById(android.R.id.icon); mUserNameView = (TextView) mHeaderView.findViewById(android.R.id.title); + mDeleteButton = (ImageView) mHeaderView.findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); getListView().setFastScrollEnabled(true); } // This is going to bind the preferences. @@ -110,14 +79,7 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment { @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() - && mEditUserPhotoController != null) { - outState.putParcelable(KEY_SAVED_PHOTO, - mEditUserPhotoController.getNewUserPhotoBitmap()); - } - if (mWaitingForActivityResult) { - outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); - } + mEditUserInfoController.onSaveInstanceState(outState); } @Override @@ -147,25 +109,23 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment { @Override public void startActivityForResult(Intent intent, int requestCode) { - mWaitingForActivityResult = true; + mEditUserInfoController.startingActivityForResult(); super.startActivityForResult(intent, requestCode); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - mWaitingForActivityResult = false; - if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() - && mEditUserPhotoController.onActivityResult(requestCode, resultCode, data)) { - return; - } + mEditUserInfoController.onActivityResult(requestCode, resultCode, data); } @Override public void onClick(View view) { if (view == mHeaderView) { showDialog(DIALOG_ID_EDIT_USER_INFO); + } else if (view == mDeleteButton) { + showDialog(DIALOG_CONFIRM_REMOVE); } else { super.onClick(view); // in AppRestrictionsFragment } @@ -174,373 +134,40 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment { @Override public Dialog onCreateDialog(int dialogId) { if (dialogId == DIALOG_ID_EDIT_USER_INFO) { - if (mEditUserInfoDialog != null) { - return mEditUserInfoDialog; - } - - LayoutInflater inflater = getActivity().getLayoutInflater(); - View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); - - UserInfo info = mUserManager.getUserInfo(mUser.getIdentifier()); - - final EditText userNameView = (EditText) content.findViewById(R.id.user_name); - userNameView.setText(info.name); - - final ImageView userPhotoView = (ImageView) content.findViewById(R.id.user_photo); - Drawable drawable = null; - if (mSavedPhoto != null) { - drawable = CircleFramedDrawable.getInstance(getActivity(), mSavedPhoto); - } else { - drawable = mUserIconView.getDrawable(); - if (drawable == null) { - drawable = getCircularUserIcon(); - } - } - userPhotoView.setImageDrawable(drawable); - mEditUserPhotoController = new EditUserPhotoController(this, userPhotoView, - mSavedPhoto, drawable, mWaitingForActivityResult); - - mEditUserInfoDialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.profile_info_settings_title) - .setIconAttribute(R.drawable.ic_settings_multiuser) - .setView(content) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - // Update the name if changed. - CharSequence userName = userNameView.getText(); - if (!TextUtils.isEmpty(userName)) { - CharSequence oldUserName = mUserNameView.getText(); - if (oldUserName == null - || !userName.toString().equals(oldUserName.toString())) { - ((TextView) mHeaderView.findViewById(android.R.id.title)) - .setText(userName.toString()); - mUserManager.setUserName(mUser.getIdentifier(), - userName.toString()); + return mEditUserInfoController.createDialog(this, mUserIconView.getDrawable(), + mUserNameView.getText(), R.string.profile_info_settings_title, + this, mUser); + } else if (dialogId == DIALOG_CONFIRM_REMOVE) { + Dialog dlg = + Utils.createRemoveConfirmationDialog(getActivity(), mUser.getIdentifier(), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + removeUser(); } } - // Update the photo if changed. - Drawable drawable = mEditUserPhotoController.getNewUserPhotoDrawable(); - Bitmap bitmap = mEditUserPhotoController.getNewUserPhotoBitmap(); - if (drawable != null && bitmap != null - && !drawable.equals(mUserIconView.getDrawable())) { - mUserIconView.setImageDrawable(drawable); - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - mUserManager.setUserIcon(mUser.getIdentifier(), - mEditUserPhotoController.getNewUserPhotoBitmap()); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - removeDialog(DIALOG_ID_EDIT_USER_INFO); - } - clearEditUserInfoDialog(); - } - }) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - clearEditUserInfoDialog(); - } - }) - .create(); - - // Make sure the IME is up. - mEditUserInfoDialog.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - - return mEditUserInfoDialog; + ); + return dlg; } return null; } - private void clearEditUserInfoDialog() { - mEditUserInfoDialog = null; - mSavedPhoto = null; - } - - private static class EditUserPhotoController { - private static final int POPUP_LIST_ITEM_ID_CHOOSE_PHOTO = 1; - private static final int POPUP_LIST_ITEM_ID_TAKE_PHOTO = 2; - - // It seems that this class generates custom request codes and they may - // collide with ours, these values are very unlikely to have a conflict. - private static final int REQUEST_CODE_CHOOSE_PHOTO = 1; - private static final int REQUEST_CODE_TAKE_PHOTO = 2; - private static final int REQUEST_CODE_CROP_PHOTO = 3; - - private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; - private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; - - private final int mPhotoSize; - - private final Context mContext; - private final Fragment mFragment; - private final ImageView mImageView; - - private final Uri mCropPictureUri; - private final Uri mTakePictureUri; - - private Bitmap mNewUserPhotoBitmap; - private Drawable mNewUserPhotoDrawable; - - public EditUserPhotoController(Fragment fragment, ImageView view, - Bitmap bitmap, Drawable drawable, boolean waiting) { - mContext = view.getContext(); - mFragment = fragment; - mImageView = view; - mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); - mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); - mPhotoSize = getPhotoSize(mContext); - mImageView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - showUpdatePhotoPopup(); - } - }); - mNewUserPhotoBitmap = bitmap; - mNewUserPhotoDrawable = drawable; - } - - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return false; - } - final Uri pictureUri = data != null && data.getData() != null - ? data.getData() : mTakePictureUri; - switch (requestCode) { - case REQUEST_CODE_CROP_PHOTO: - onPhotoCropped(pictureUri, true); - return true; - case REQUEST_CODE_TAKE_PHOTO: - case REQUEST_CODE_CHOOSE_PHOTO: - cropPhoto(pictureUri); - return true; - } - return false; - } - - public Bitmap getNewUserPhotoBitmap() { - return mNewUserPhotoBitmap; - } - - public Drawable getNewUserPhotoDrawable() { - return mNewUserPhotoDrawable; - } - - private void showUpdatePhotoPopup() { - final boolean canTakePhoto = canTakePhoto(); - final boolean canChoosePhoto = canChoosePhoto(); - - if (!canTakePhoto && !canChoosePhoto) { - return; - } - - Context context = mImageView.getContext(); - final List<AdapterItem> items = new ArrayList<AdapterItem>(); - - if (canTakePhoto()) { - String title = mImageView.getContext().getString( R.string.user_image_take_photo); - AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_TAKE_PHOTO); - items.add(item); - } - - if (canChoosePhoto) { - String title = context.getString(R.string.user_image_choose_photo); - AdapterItem item = new AdapterItem(title, POPUP_LIST_ITEM_ID_CHOOSE_PHOTO); - items.add(item); - } - - final ListPopupWindow listPopupWindow = new ListPopupWindow(context); - - listPopupWindow.setAnchorView(mImageView); - listPopupWindow.setModal(true); - listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); - - ListAdapter adapter = new ArrayAdapter<AdapterItem>(context, - R.layout.edit_user_photo_popup_item, items); - listPopupWindow.setAdapter(adapter); - - final int width = Math.max(mImageView.getWidth(), context.getResources() - .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); - listPopupWindow.setWidth(width); - - listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - AdapterItem item = items.get(position); - switch (item.id) { - case POPUP_LIST_ITEM_ID_CHOOSE_PHOTO: { - choosePhoto(); - listPopupWindow.dismiss(); - } break; - case POPUP_LIST_ITEM_ID_TAKE_PHOTO: { - takePhoto(); - listPopupWindow.dismiss(); - } break; - } - } - }); - - listPopupWindow.show(); - } - - private boolean canTakePhoto() { - return mImageView.getContext().getPackageManager().queryIntentActivities( - new Intent(MediaStore.ACTION_IMAGE_CAPTURE), - PackageManager.MATCH_DEFAULT_ONLY).size() > 0; - } - - private boolean canChoosePhoto() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - return mImageView.getContext().getPackageManager().queryIntentActivities( - intent, 0).size() > 0; - } - - private void takePhoto() { - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - appendOutputExtra(intent, mTakePictureUri); - mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); - } - - private void choosePhoto() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); - intent.setType("image/*"); - appendOutputExtra(intent, mTakePictureUri); - mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); - } - - private void cropPhoto(Uri pictureUri) { - // TODO: Use a public intent, when there is one. - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(pictureUri, "image/*"); - appendOutputExtra(intent, mCropPictureUri); - appendCropExtras(intent); - if (intent.resolveActivity(mContext.getPackageManager()) != null) { - mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); - } else { - onPhotoCropped(pictureUri, false); - } - } - - private void appendOutputExtra(Intent intent, Uri pictureUri) { - intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); - } - - private void appendCropExtras(Intent intent) { - intent.putExtra("crop", "true"); - intent.putExtra("scale", true); - intent.putExtra("scaleUpIfNeeded", true); - intent.putExtra("aspectX", 1); - intent.putExtra("aspectY", 1); - intent.putExtra("outputX", mPhotoSize); - intent.putExtra("outputY", mPhotoSize); - } - - private void onPhotoCropped(final Uri data, final boolean cropped) { - new AsyncTask<Void, Void, Bitmap>() { - @Override - protected Bitmap doInBackground(Void... params) { - if (cropped) { - try { - InputStream imageStream = mContext.getContentResolver() - .openInputStream(data); - return BitmapFactory.decodeStream(imageStream); - } catch (FileNotFoundException fe) { - return null; - } - } else { - // Scale and crop to a square aspect ratio - Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, - Config.ARGB_8888); - Canvas canvas = new Canvas(croppedImage); - Bitmap fullImage = null; - try { - InputStream imageStream = mContext.getContentResolver() - .openInputStream(data); - fullImage = BitmapFactory.decodeStream(imageStream); - } catch (FileNotFoundException fe) { - return null; - } - if (fullImage != null) { - final int squareSize = Math.min(fullImage.getWidth(), - fullImage.getHeight()); - final int left = (fullImage.getWidth() - squareSize) / 2; - final int top = (fullImage.getHeight() - squareSize) / 2; - Rect rectSource = new Rect(left, top, - left + squareSize, top + squareSize); - Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); - Paint paint = new Paint(); - canvas.drawBitmap(fullImage, rectSource, rectDest, paint); - return croppedImage; - } else { - // Bah! Got nothin. - return null; - } - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null) { - mNewUserPhotoBitmap = bitmap; - mNewUserPhotoDrawable = CircleFramedDrawable - .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); - mImageView.setImageDrawable(mNewUserPhotoDrawable); - } - new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); - new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - - private static int getPhotoSize(Context context) { - Cursor cursor = context.getContentResolver().query( - DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, - new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); - try { - cursor.moveToFirst(); - return cursor.getInt(0); - } finally { - cursor.close(); - } - } - - private Uri createTempImageUri(Context context, String fileName, boolean purge) { - final File folder = context.getCacheDir(); - folder.mkdirs(); - final File fullPath = new File(folder, fileName); - if (purge) { - fullPath.delete(); - } - final Uri fileUri = - FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, fullPath); - return fileUri; - } - - private static final class AdapterItem { - final String title; - final int id; - - public AdapterItem(String title, int id) { - this.title = title; - this.id = id; + private void removeUser() { + getView().post(new Runnable() { + public void run() { + mUserManager.removeUser(mUser.getIdentifier()); + finishFragment(); } + }); + } - @Override - public String toString() { - return title; - } - } + @Override + public void onPhotoChanged(Drawable photo) { + mUserIconView.setImageDrawable(photo); } + @Override + public void onLabelChanged(CharSequence label) { + mUserNameView.setText(label); + } } diff --git a/src/com/android/settings/users/RestrictionUtils.java b/src/com/android/settings/users/RestrictionUtils.java index 3ee6d51..e8d46e9 100644 --- a/src/com/android/settings/users/RestrictionUtils.java +++ b/src/com/android/settings/users/RestrictionUtils.java @@ -84,8 +84,8 @@ public class RestrictionUtils { userRestrictions.putBoolean(entry.getKey(), !entry.getSelectedState()); if (entry.getKey().equals(UserManager.DISALLOW_SHARE_LOCATION) && !entry.getSelectedState()) { - Secure.putStringForUser(context.getContentResolver(), - Secure.LOCATION_PROVIDERS_ALLOWED, "", user.getIdentifier()); + Secure.putIntForUser(context.getContentResolver(), + Secure.LOCATION_MODE, Secure.LOCATION_MODE_OFF, user.getIdentifier()); } } um.setUserRestrictions(userRestrictions, user); diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java new file mode 100644 index 0000000..366b628 --- /dev/null +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 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.users; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.UserInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.preference.Preference; +import android.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.Utils; + +import java.util.List; + +/** + * Settings screen for configuring a specific user. It can contain user restrictions + * and deletion controls. It is shown when you tap on the settings icon in the + * user management (UserSettings) screen. + * + * Arguments to this fragment must include the userId of the user (in EXTRA_USER_ID) for whom + * to display controls, or should contain the EXTRA_USER_GUEST = true. + */ +public class UserDetailsSettings extends SettingsPreferenceFragment + implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener { + + private static final String TAG = UserDetailsSettings.class.getSimpleName(); + + private static final String KEY_ENABLE_TELEPHONY = "enable_calling"; + private static final String KEY_REMOVE_USER = "remove_user"; + + /** Integer extra containing the userId to manage */ + static final String EXTRA_USER_ID = "user_id"; + /** Boolean extra to indicate guest preferences */ + static final String EXTRA_USER_GUEST = "guest_user"; + + private static final int DIALOG_CONFIRM_REMOVE = 1; + private static final int DIALOG_CONFIRM_ENABLE_CALLING = 2; + private static final int DIALOG_CONFIRM_ENABLE_CALLING_SMS = 3; + + private UserManager mUserManager; + private SwitchPreference mPhonePref; + private Preference mRemoveUserPref; + + private UserInfo mUserInfo; + private boolean mGuestUser; + private Bundle mDefaultGuestRestrictions; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + final Context context = getActivity(); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + + addPreferencesFromResource(R.xml.user_details_settings); + mPhonePref = (SwitchPreference) findPreference(KEY_ENABLE_TELEPHONY); + mRemoveUserPref = findPreference(KEY_REMOVE_USER); + + mGuestUser = getArguments().getBoolean(EXTRA_USER_GUEST, false); + + if (!mGuestUser) { + // Regular user. Get the user id from the caller. + final int userId = getArguments().getInt(EXTRA_USER_ID, -1); + if (userId == -1) { + throw new RuntimeException("Arguments to this fragment must contain the user id"); + } + mUserInfo = mUserManager.getUserInfo(userId); + mPhonePref.setChecked(!mUserManager.hasUserRestriction( + UserManager.DISALLOW_OUTGOING_CALLS, new UserHandle(userId))); + mRemoveUserPref.setOnPreferenceClickListener(this); + } else { + // These are not for an existing user, just general Guest settings. + removePreference(KEY_REMOVE_USER); + // Default title is for calling and SMS. Change to calling-only here + mPhonePref.setTitle(R.string.user_enable_calling); + mDefaultGuestRestrictions = mUserManager.getDefaultGuestRestrictions(); + mPhonePref.setChecked( + !mDefaultGuestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS)); + } + if (mUserManager.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER)) { + removePreference(KEY_REMOVE_USER); + } + mPhonePref.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference == mRemoveUserPref) { + if (UserHandle.myUserId() != UserHandle.USER_OWNER) { + throw new RuntimeException("Only the owner can remove a user"); + } + showDialog(DIALOG_CONFIRM_REMOVE); + return true; + } + return false; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mGuestUser) { + // TODO: Show confirmation dialog: b/15761405 + mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_OUTGOING_CALLS, + !((Boolean) newValue)); + // SMS is always disabled for guest + mDefaultGuestRestrictions.putBoolean(UserManager.DISALLOW_SMS, true); + mUserManager.setDefaultGuestRestrictions(mDefaultGuestRestrictions); + // Update the guest's restrictions, if there is a guest + List<UserInfo> users = mUserManager.getUsers(true); + for (UserInfo user: users) { + if (user.isGuest()) { + UserHandle userHandle = new UserHandle(user.id); + Bundle userRestrictions = mUserManager.getUserRestrictions(userHandle); + userRestrictions.putAll(mDefaultGuestRestrictions); + mUserManager.setUserRestrictions(userRestrictions, userHandle); + } + } + } else { + // TODO: Show confirmation dialog: b/15761405 + UserHandle userHandle = new UserHandle(mUserInfo.id); + mUserManager.setUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS, + !((Boolean) newValue), userHandle); + mUserManager.setUserRestriction(UserManager.DISALLOW_SMS, + !((Boolean) newValue), userHandle); + } + return true; + } + + @Override + public Dialog onCreateDialog(int dialogId) { + Context context = getActivity(); + if (context == null) return null; + switch (dialogId) { + case DIALOG_CONFIRM_REMOVE: { + Dialog dlg = Utils.createRemoveConfirmationDialog(getActivity(), mUserInfo.id, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + removeUser(); + } + }); + return dlg; + } + case DIALOG_CONFIRM_ENABLE_CALLING: + case DIALOG_CONFIRM_ENABLE_CALLING_SMS: + // TODO: b/15761405 + } + return null; + } + + void removeUser() { + mUserManager.removeUser(mUserInfo.id); + finishFragment(); + } +} diff --git a/src/com/android/settings/users/UserPreference.java b/src/com/android/settings/users/UserPreference.java index 9f53aa5..23359ec 100644 --- a/src/com/android/settings/users/UserPreference.java +++ b/src/com/android/settings/users/UserPreference.java @@ -16,7 +16,6 @@ package com.android.settings.users; -import com.android.internal.util.CharSequences; import com.android.settings.R; import android.content.Context; @@ -30,13 +29,12 @@ import android.view.View.OnClickListener; public class UserPreference extends Preference { public static final int USERID_UNKNOWN = -10; + public static final int USERID_GUEST_DEFAULTS = -11; private OnClickListener mDeleteClickListener; private OnClickListener mSettingsClickListener; private int mSerialNumber = -1; private int mUserId = USERID_UNKNOWN; - private boolean mRestricted; - private boolean mSelf; static final int SETTINGS_ID = R.id.manage_user; static final int DELETE_ID = R.id.trash_user; @@ -58,11 +56,13 @@ public class UserPreference extends Preference { @Override protected void onBindView(View view) { + UserManager um = (UserManager) getContext().getSystemService(Context.USER_SERVICE); View deleteDividerView = view.findViewById(R.id.divider_delete); View manageDividerView = view.findViewById(R.id.divider_manage); View deleteView = view.findViewById(R.id.trash_user); if (deleteView != null) { - if (mDeleteClickListener != null) { + if (mDeleteClickListener != null + && !um.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER)) { deleteView.setOnClickListener(mDeleteClickListener); deleteView.setTag(this); } else { @@ -90,7 +90,11 @@ public class UserPreference extends Preference { if (mUserId == UserHandle.myUserId()) return Integer.MIN_VALUE; if (mSerialNumber < 0) { // If the userId is unknown - if (mUserId == USERID_UNKNOWN) return Integer.MAX_VALUE; + if (mUserId == USERID_UNKNOWN) { + return Integer.MAX_VALUE; + } else if (mUserId == USERID_GUEST_DEFAULTS) { + return Integer.MAX_VALUE - 1; + } mSerialNumber = ((UserManager) getContext().getSystemService(Context.USER_SERVICE)) .getUserSerialNumber(mUserId); if (mSerialNumber < 0) return mUserId; diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index bbae37d..b95c397 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -16,16 +16,13 @@ package com.android.settings.users; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; import android.app.ActivityManagerNative; import android.app.AlertDialog; import android.app.Dialog; +import android.app.Fragment; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -36,7 +33,6 @@ import android.content.SharedPreferences; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; @@ -47,10 +43,8 @@ import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; +import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; import android.util.SparseArray; @@ -61,17 +55,33 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.SimpleAdapter; +import com.android.internal.util.UserIcons; import com.android.internal.widget.LockPatternUtils; import com.android.settings.ChooseLockGeneric; import com.android.settings.OwnerInfoSettings; import com.android.settings.R; -import com.android.settings.RestrictedSettingsFragment; import com.android.settings.SelectableEditTextPreference; +import com.android.settings.SettingsActivity; +import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.drawable.CircleFramedDrawable; -public class UserSettings extends RestrictedSettingsFragment +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Screen that manages the list of users on the device. + * Guest user is an always visible entry, even if the guest is not currently + * active/created. It is meant for controlling properties of a guest user. + * + * The first one is always the current user. + * Owner is the primary user. + */ +public class UserSettings extends SettingsPreferenceFragment implements OnPreferenceClickListener, OnClickListener, DialogInterface.OnDismissListener, - Preference.OnPreferenceChangeListener { + Preference.OnPreferenceChangeListener, + EditUserInfoController.OnContentChangedCallback { private static final String TAG = "UserSettings"; @@ -85,6 +95,7 @@ public class UserSettings extends RestrictedSettingsFragment private static final String KEY_ADD_USER = "user_add"; private static final int MENU_REMOVE_USER = Menu.FIRST; + private static final int MENU_ADD_ON_LOCKSCREEN = Menu.FIRST + 1; private static final int DIALOG_CONFIRM_REMOVE = 1; private static final int DIALOG_ADD_USER = 2; @@ -93,6 +104,8 @@ public class UserSettings extends RestrictedSettingsFragment private static final int DIALOG_USER_CANNOT_MANAGE = 5; private static final int DIALOG_CHOOSE_USER_TYPE = 6; private static final int DIALOG_NEED_LOCKSCREEN = 7; + private static final int DIALOG_CONFIRM_EXIT_GUEST = 8; + private static final int DIALOG_USER_PROFILE_EDITOR = 9; private static final int MESSAGE_UPDATE_LIST = 1; private static final int MESSAGE_SETUP_USER = 2; @@ -106,17 +119,6 @@ public class UserSettings extends RestrictedSettingsFragment private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED = "key_add_user_long_message_displayed"; - static final int[] USER_DRAWABLES = { - R.drawable.avatar_default_1, - R.drawable.avatar_default_2, - R.drawable.avatar_default_3, - R.drawable.avatar_default_4, - R.drawable.avatar_default_5, - R.drawable.avatar_default_6, - R.drawable.avatar_default_7, - R.drawable.avatar_default_8 - }; - private static final String KEY_TITLE = "title"; private static final String KEY_SUMMARY = "summary"; @@ -127,16 +129,20 @@ public class UserSettings extends RestrictedSettingsFragment private int mRemovingUserId = -1; private int mAddedUserId = 0; private boolean mAddingUser; - private boolean mProfileExists; + private boolean mEnabled = true; + private boolean mCanAddRestrictedProfile = true; private final Object mUserLock = new Object(); private UserManager mUserManager; private SparseArray<Bitmap> mUserIcons = new SparseArray<Bitmap>(); private boolean mIsOwner = UserHandle.myUserId() == UserHandle.USER_OWNER; + private boolean mIsGuest; - public UserSettings() { - super(RestrictedSettingsFragment.RESTRICTIONS_PIN_SET); - } + private EditUserInfoController mEditUserInfoController = + new EditUserInfoController(); + + // A place to cache the generated default avatar + private Drawable mDefaultIconDrawable; private Handler mHandler = new Handler() { @Override @@ -181,34 +187,59 @@ public class UserSettings extends RestrictedSettingsFragment if (icicle.containsKey(SAVE_REMOVING_USER)) { mRemovingUserId = icicle.getInt(SAVE_REMOVING_USER); } + mEditUserInfoController.onRestoreInstanceState(icicle); + } + final Context context = getActivity(); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + boolean hasMultipleUsers = mUserManager.getUserCount() > 1; + if ((!UserManager.supportsMultipleUsers() && !hasMultipleUsers) + || Utils.isMonkeyRunning()) { + mEnabled = false; + return; } - mUserManager = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); + final int myUserId = UserHandle.myUserId(); + mIsGuest = mUserManager.getUserInfo(myUserId).isGuest(); + addPreferencesFromResource(R.xml.user_settings); mUserListCategory = (PreferenceGroup) findPreference(KEY_USER_LIST); - mMePreference = new UserPreference(getActivity(), null, UserHandle.myUserId(), - mUserManager.isLinkedUser() ? null : this, null); + mMePreference = new UserPreference(context, null /* attrs */, myUserId, + null /* settings icon handler */, + null /* delete icon handler */); mMePreference.setKey(KEY_USER_ME); mMePreference.setOnPreferenceClickListener(this); if (mIsOwner) { mMePreference.setSummary(R.string.user_owner); } mAddUser = findPreference(KEY_ADD_USER); - mAddUser.setOnPreferenceClickListener(this); - if (!mIsOwner || UserManager.getMaxSupportedUsers() < 2) { + if (!mIsOwner || UserManager.getMaxSupportedUsers() < 2 + || !UserManager.supportsMultipleUsers() + || mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER)) { removePreference(KEY_ADD_USER); + } else { + mAddUser.setOnPreferenceClickListener(this); + DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( + Context.DEVICE_POLICY_SERVICE); + // No restricted profiles for tablets with a device owner, or phones. + if (dpm.getDeviceOwner() != null || Utils.isVoiceCapable(context)) { + mCanAddRestrictedProfile = false; + mAddUser.setTitle(R.string.user_add_user_menu); + } } loadProfile(); setHasOptionsMenu(true); IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_INFO_CHANGED); - getActivity().registerReceiverAsUser(mUserChangeReceiver, UserHandle.ALL, filter, null, + context.registerReceiverAsUser(mUserChangeReceiver, UserHandle.ALL, filter, null, mHandler); } @Override public void onResume() { super.onResume(); + + if (!mEnabled) return; + loadProfile(); updateUserList(); } @@ -216,26 +247,43 @@ public class UserSettings extends RestrictedSettingsFragment @Override public void onDestroy() { super.onDestroy(); + + if (!mEnabled) return; + getActivity().unregisterReceiver(mUserChangeReceiver); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - + mEditUserInfoController.onSaveInstanceState(outState); outState.putInt(SAVE_ADDING_USER, mAddedUserId); outState.putInt(SAVE_REMOVING_USER, mRemovingUserId); } @Override + public void startActivityForResult(Intent intent, int requestCode) { + mEditUserInfoController.startingActivityForResult(); + super.startActivityForResult(intent, requestCode); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + int pos = 0; UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); if (!mIsOwner && !um.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER)) { String nickname = mUserManager.getUserName(); - MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, 0, + MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); removeThisUser.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } + if (mIsOwner && !um.hasUserRestriction(UserManager.DISALLOW_ADD_USER)) { + MenuItem allowAddOnLockscreen = menu.add(0, MENU_ADD_ON_LOCKSCREEN, pos++, + R.string.user_add_on_lockscreen_menu); + allowAddOnLockscreen.setCheckable(true); + allowAddOnLockscreen.setChecked(Settings.Global.getInt(getContentResolver(), + Settings.Global.ADD_USERS_WHEN_LOCKED, 0) == 1); + } super.onCreateOptionsMenu(menu, inflater); } @@ -245,13 +293,28 @@ public class UserSettings extends RestrictedSettingsFragment if (itemId == MENU_REMOVE_USER) { onRemoveUserClicked(UserHandle.myUserId()); return true; + } else if (itemId == MENU_ADD_ON_LOCKSCREEN) { + final boolean isChecked = item.isChecked(); + Settings.Global.putInt(getContentResolver(), Settings.Global.ADD_USERS_WHEN_LOCKED, + isChecked ? 0 : 1); + item.setChecked(!isChecked); + return true; } else { return super.onOptionsItemSelected(item); } } + /** + * Loads profile information for the current user. + */ private void loadProfile() { - mProfileExists = false; + if (mIsGuest) { + // No need to load profile information + mMePreference.setIcon(getEncircledDefaultIcon()); + mMePreference.setTitle(R.string.user_exit_guest_title); + return; + } + new AsyncTask<Void, Void, String>() { @Override protected void onPostExecute(String result) { @@ -264,11 +327,7 @@ public class UserSettings extends RestrictedSettingsFragment if (user.iconPath == null || user.iconPath.equals("")) { assignProfilePhoto(user); } - String profileName = getProfileName(); - if (profileName == null) { - profileName = user.name; - } - return profileName; + return user.name; } }.execute(); } @@ -303,9 +362,9 @@ public class UserSettings extends RestrictedSettingsFragment if (requestCode == REQUEST_CHOOSE_LOCK) { if (resultCode != Activity.RESULT_CANCELED && hasLockscreenSecurity()) { addUserNow(USER_TYPE_RESTRICTED_PROFILE); - } else { - showDialog(DIALOG_NEED_LOCKSCREEN); } + } else { + mEditUserInfoController.onActivityResult(requestCode, resultCode, data); } } @@ -338,19 +397,18 @@ public class UserSettings extends RestrictedSettingsFragment } private UserInfo createLimitedUser() { - UserInfo newUserInfo = mUserManager.createUser( + UserInfo newUserInfo = mUserManager.createSecondaryUser( getResources().getString(R.string.user_new_profile_name), UserInfo.FLAG_RESTRICTED); int userId = newUserInfo.id; UserHandle user = new UserHandle(userId); mUserManager.setUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS, true, user); + // Change the setting before applying the DISALLOW_SHARE_LOCATION restriction, otherwise + // the putIntForUser() will fail. + Secure.putIntForUser(getContentResolver(), + Secure.LOCATION_MODE, Secure.LOCATION_MODE_OFF, userId); mUserManager.setUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, true, user); - Secure.putStringForUser(getContentResolver(), - Secure.LOCATION_PROVIDERS_ALLOWED, "", userId); - Bitmap bitmap = BitmapFactory.decodeResource(getResources(), - UserSettings.USER_DRAWABLES[ - userId % UserSettings.USER_DRAWABLES.length]); - mUserManager.setUserIcon(userId, bitmap); + assignDefaultPhoto(newUserInfo); // Add shared accounts AccountManager am = AccountManager.get(getActivity()); Account [] accounts = am.getAccounts(); @@ -363,7 +421,7 @@ public class UserSettings extends RestrictedSettingsFragment } private UserInfo createTrustedUser() { - UserInfo newUserInfo = mUserManager.createUser( + UserInfo newUserInfo = mUserManager.createSecondaryUser( getResources().getString(R.string.user_new_user_name), 0); if (newUserInfo != null) { assignDefaultPhoto(newUserInfo); @@ -372,12 +430,20 @@ public class UserSettings extends RestrictedSettingsFragment } private void onManageUserClicked(int userId, boolean newUser) { + if (userId == UserPreference.USERID_GUEST_DEFAULTS) { + Bundle extras = new Bundle(); + extras.putBoolean(UserDetailsSettings.EXTRA_USER_GUEST, true); + ((SettingsActivity) getActivity()).startPreferencePanel( + UserDetailsSettings.class.getName(), + extras, R.string.user_guest, null, null, 0); + return; + } UserInfo info = mUserManager.getUserInfo(userId); if (info.isRestricted() && mIsOwner) { Bundle extras = new Bundle(); extras.putInt(RestrictedProfileSettings.EXTRA_USER_ID, userId); extras.putBoolean(RestrictedProfileSettings.EXTRA_NEW_USER, newUser); - ((PreferenceActivity) getActivity()).startPreferencePanel( + ((SettingsActivity) getActivity()).startPreferencePanel( RestrictedProfileSettings.class.getName(), extras, R.string.user_restrictions_title, null, null, 0); @@ -390,9 +456,19 @@ public class UserSettings extends RestrictedSettingsFragment int titleResId = info.id == UserHandle.USER_OWNER ? R.string.owner_info_settings_title : (info.isRestricted() ? R.string.profile_info_settings_title : R.string.user_info_settings_title); - ((PreferenceActivity) getActivity()).startPreferencePanel( + ((SettingsActivity) getActivity()).startPreferencePanel( OwnerInfoSettings.class.getName(), extras, titleResId, null, null, 0); + } else if (mIsOwner) { + Bundle extras = new Bundle(); + extras.putInt(UserDetailsSettings.EXTRA_USER_ID, userId); + ((SettingsActivity) getActivity()).startPreferencePanel( + UserDetailsSettings.class.getName(), + extras, + -1, /* No title res id */ + info.name, /* title */ + null, /* resultTo */ + 0 /* resultRequestCode */); } } @@ -418,25 +494,14 @@ public class UserSettings extends RestrictedSettingsFragment if (context == null) return null; switch (dialogId) { case DIALOG_CONFIRM_REMOVE: { - Dialog dlg = new AlertDialog.Builder(getActivity()) - .setTitle(UserHandle.myUserId() == mRemovingUserId - ? R.string.user_confirm_remove_self_title - : (mUserManager.getUserInfo(mRemovingUserId).isRestricted() - ? R.string.user_profile_confirm_remove_title - : R.string.user_confirm_remove_title)) - .setMessage(UserHandle.myUserId() == mRemovingUserId - ? R.string.user_confirm_remove_self_message - : (mUserManager.getUserInfo(mRemovingUserId).isRestricted() - ? R.string.user_profile_confirm_remove_message - : R.string.user_confirm_remove_message)) - .setPositiveButton(R.string.user_delete_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - removeUserNow(); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); + Dialog dlg = + Utils.createRemoveConfirmationDialog(getActivity(), mRemovingUserId, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + removeUserNow(); + } + } + ); return dlg; } case DIALOG_USER_CANNOT_MANAGE: @@ -537,6 +602,31 @@ public class UserSettings extends RestrictedSettingsFragment .create(); return dlg; } + case DIALOG_CONFIRM_EXIT_GUEST: { + Dialog dlg = new AlertDialog.Builder(context) + .setTitle(R.string.user_exit_guest_confirm_title) + .setMessage(R.string.user_exit_guest_confirm_message) + .setPositiveButton(R.string.user_exit_guest_dialog_remove, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + exitGuest(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + return dlg; + } + case DIALOG_USER_PROFILE_EDITOR: { + Dialog dlg = mEditUserInfoController.createDialog( + (Fragment) this, + mMePreference.getIcon(), + mMePreference.getTitle(), + R.string.profile_info_settings_title, + this /* callback */, + android.os.Process.myUserHandle()); + return dlg; + } default: return null; } @@ -604,23 +694,54 @@ public class UserSettings extends RestrictedSettingsFragment } } + /** + * Erase the current user (guest) and switch to another user. + */ + private void exitGuest() { + // Just to be safe + if (!mIsGuest) { + return; + } + removeThisUser(); + } + private void updateUserList() { if (getActivity() == null) return; List<UserInfo> users = mUserManager.getUsers(true); + final Context context = getActivity(); mUserListCategory.removeAll(); mUserListCategory.setOrderingAsAdded(false); mUserListCategory.addPreference(mMePreference); + final boolean voiceCapable = Utils.isVoiceCapable(context); final ArrayList<Integer> missingIcons = new ArrayList<Integer>(); for (UserInfo user : users) { + if (user.isManagedProfile()) { + // Managed profiles appear under Accounts Settings instead + continue; + } Preference pref; if (user.id == UserHandle.myUserId()) { pref = mMePreference; + } else if (user.isGuest()) { + // Skip over Guest. We add generic Guest settings after this loop + continue; } else { - pref = new UserPreference(getActivity(), null, user.id, - mIsOwner && user.isRestricted() ? this : null, - mIsOwner ? this : null); + // With Telephony: + // Secondary user: Settings + // Guest: Settings + // Restricted Profile: There is no Restricted Profile + // Without Telephony: + // Secondary user: Delete + // Guest: Nothing + // Restricted Profile: Settings + final boolean showSettings = mIsOwner && (voiceCapable || user.isRestricted()); + final boolean showDelete = mIsOwner + && (!voiceCapable && !user.isRestricted() && !user.isGuest()); + pref = new UserPreference(context, null, user.id, + showSettings ? this : null, + showDelete ? this : null); pref.setOnPreferenceClickListener(this); pref.setKey("id=" + user.id); mUserListCategory.addPreference(pref); @@ -630,37 +751,69 @@ public class UserSettings extends RestrictedSettingsFragment pref.setTitle(user.name); } if (!isInitialized(user)) { - pref.setSummary(user.isRestricted() - ? R.string.user_summary_restricted_not_set_up - : R.string.user_summary_not_set_up); + if (user.isRestricted()) { + pref.setSummary(R.string.user_summary_restricted_not_set_up); + } else { + pref.setSummary(R.string.user_summary_not_set_up); + } } else if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_profile); } if (user.iconPath != null) { if (mUserIcons.get(user.id) == null) { + // Icon not loaded yet, print a placeholder missingIcons.add(user.id); - pref.setIcon(encircle(R.drawable.avatar_default_1)); + pref.setIcon(getEncircledDefaultIcon()); } else { setPhotoId(pref, user); } + } else { + // Icon not available yet, print a placeholder + pref.setIcon(getEncircledDefaultIcon()); } } + // Add a temporary entry for the user being created if (mAddingUser) { Preference pref = new UserPreference(getActivity(), null, UserPreference.USERID_UNKNOWN, null, null); pref.setEnabled(false); pref.setTitle(R.string.user_new_user_name); - pref.setIcon(encircle(R.drawable.avatar_default_1)); + pref.setIcon(getEncircledDefaultIcon()); + mUserListCategory.addPreference(pref); + } + + boolean showGuestPreference = !mIsGuest; + // If user has DISALLOW_ADD_USER don't allow creating a guest either. + if (showGuestPreference && mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER)) { + showGuestPreference = false; + // If guest already exists, no user creation needed. + for (UserInfo user : users) { + if (user.isGuest()) { + showGuestPreference = true; + break; + } + } + } + if (showGuestPreference) { + // Add a virtual Guest user for guest defaults + Preference pref = new UserPreference(getActivity(), null, + UserPreference.USERID_GUEST_DEFAULTS, + mIsOwner && voiceCapable? this : null /* settings icon handler */, + null /* delete icon handler */); + pref.setTitle(R.string.user_guest); + pref.setIcon(getEncircledDefaultIcon()); + pref.setOnPreferenceClickListener(this); mUserListCategory.addPreference(pref); } + getActivity().invalidateOptionsMenu(); // Load the icons if (missingIcons.size() > 0) { loadIconsAsync(missingIcons); } - boolean moreUsers = mUserManager.getMaxSupportedUsers() > users.size(); + boolean moreUsers = mUserManager.canAddMoreUsers(); mAddUser.setEnabled(moreUsers); } @@ -676,6 +829,10 @@ public class UserSettings extends RestrictedSettingsFragment protected Void doInBackground(List<Integer>... values) { for (int userId : values[0]) { Bitmap bitmap = mUserManager.getUserIcon(userId); + if (bitmap == null) { + bitmap = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(userId, + /* light= */ false)); + } mUserIcons.append(userId, bitmap); } return null; @@ -689,20 +846,20 @@ public class UserSettings extends RestrictedSettingsFragment } } - private String getProfileName() { - String name = Utils.getMeProfileName(getActivity(), true); - if (name != null) { - mProfileExists = true; - } - return name; - } - private void assignDefaultPhoto(UserInfo user) { - Bitmap bitmap = BitmapFactory.decodeResource(getResources(), - USER_DRAWABLES[user.id % USER_DRAWABLES.length]); + Bitmap bitmap = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon(user.id, + /* light= */ false)); mUserManager.setUserIcon(user.id, bitmap); } + private Drawable getEncircledDefaultIcon() { + if (mDefaultIconDrawable == null) { + mDefaultIconDrawable = encircle(UserIcons.convertToBitmap( + UserIcons.getDefaultUserIcon(UserHandle.USER_NULL, /* light= */ false))); + } + return mDefaultIconDrawable; + } + private void setPhotoId(Preference pref, UserInfo user) { Bitmap bitmap = mUserIcons.get(user.id); if (bitmap != null) { @@ -719,51 +876,67 @@ public class UserSettings extends RestrictedSettingsFragment @Override public boolean onPreferenceClick(Preference pref) { if (pref == mMePreference) { - Intent editProfile; - if (!mProfileExists) { - editProfile = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); - // TODO: Make this a proper API - editProfile.putExtra("newLocalProfile", true); - } else { - editProfile = new Intent(Intent.ACTION_EDIT, ContactsContract.Profile.CONTENT_URI); + if (mIsGuest) { + showDialog(DIALOG_CONFIRM_EXIT_GUEST); + return true; } - // To make sure that it returns back here when done - // TODO: Make this a proper API - editProfile.putExtra("finishActivityOnSaveCompleted", true); - // If this is a limited user, launch the user info settings instead of profile editor if (mUserManager.isLinkedUser()) { onManageUserClicked(UserHandle.myUserId(), false); } else { - startActivity(editProfile); + showDialog(DIALOG_USER_PROFILE_EDITOR); } } else if (pref instanceof UserPreference) { int userId = ((UserPreference) pref).getUserId(); - // Get the latest status of the user - UserInfo user = mUserManager.getUserInfo(userId); - if (UserHandle.myUserId() != UserHandle.USER_OWNER) { - showDialog(DIALOG_USER_CANNOT_MANAGE); + if (userId == UserPreference.USERID_GUEST_DEFAULTS) { + createAndSwitchToGuestUser(); } else { + // Get the latest status of the user + UserInfo user = mUserManager.getUserInfo(userId); if (!isInitialized(user)) { mHandler.sendMessage(mHandler.obtainMessage( MESSAGE_SETUP_USER, user.id, user.serialNumber)); - } else if (user.isRestricted()) { - onManageUserClicked(user.id, false); + } else { + switchUserNow(userId); } } } else if (pref == mAddUser) { - showDialog(DIALOG_CHOOSE_USER_TYPE); + // If we allow both types, show a picker, otherwise directly go to + // flow for full user. + if (mCanAddRestrictedProfile) { + showDialog(DIALOG_CHOOSE_USER_TYPE); + } else { + onAddUserClicked(USER_TYPE_USER); + } } return false; } - private boolean isInitialized(UserInfo user) { - return (user.flags & UserInfo.FLAG_INITIALIZED) != 0; + private void createAndSwitchToGuestUser() { + List<UserInfo> users = mUserManager.getUsers(); + for (UserInfo user : users) { + if (user.isGuest()) { + switchUserNow(user.id); + return; + } + } + // No guest user. Create one, if there's no restriction. + // If it is not the primary user, then adding users from lockscreen must be enabled + if (mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER) + || (!mIsOwner && Settings.Global.getInt(getContentResolver(), + Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 1)) { + Log.i(TAG, "Blocking guest creation because it is restricted"); + return; + } + UserInfo guestUser = mUserManager.createGuest(getActivity(), + getResources().getString(R.string.user_guest)); + if (guestUser != null) { + switchUserNow(guestUser.id); + } } - private Drawable encircle(int iconResId) { - Bitmap icon = BitmapFactory.decodeResource(getResources(), iconResId); - return encircle(icon); + private boolean isInitialized(UserInfo user) { + return (user.flags & UserInfo.FLAG_INITIALIZED) != 0; } private Drawable encircle(Bitmap icon) { @@ -812,4 +985,14 @@ public class UserSettings extends RestrictedSettingsFragment public int getHelpResource() { return R.string.help_url_users; } + + @Override + public void onPhotoChanged(Drawable photo) { + mMePreference.setIcon(photo); + } + + @Override + public void onLabelChanged(CharSequence label) { + mMePreference.setTitle(label); + } } diff --git a/src/com/android/settings/users/UserUtils.java b/src/com/android/settings/users/UserUtils.java deleted file mode 100644 index 946d871..0000000 --- a/src/com/android/settings/users/UserUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2012 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.users; - -import android.content.Context; -import android.content.pm.UserInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.UserManager; - -public class UserUtils { - public static Drawable getUserIcon(Context context, UserManager um, UserInfo user, Resources res) { - if (user.iconPath == null) return null; - Bitmap icon = um.getUserIcon(user.id); - if (icon == null) return null; - return CircleFramedDrawable.getInstance(context, icon); - } -} diff --git a/src/com/android/settings/voice/VoiceInputHelper.java b/src/com/android/settings/voice/VoiceInputHelper.java new file mode 100644 index 0000000..63b891a --- /dev/null +++ b/src/com/android/settings/voice/VoiceInputHelper.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 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.voice; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.provider.Settings; +import android.service.voice.VoiceInteractionService; +import android.service.voice.VoiceInteractionServiceInfo; +import android.speech.RecognitionService; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class VoiceInputHelper { + static final String TAG = "VoiceInputHelper"; + final Context mContext; + + final List<ResolveInfo> mAvailableVoiceInteractions; + final List<ResolveInfo> mAvailableRecognition; + + static public class BaseInfo implements Comparable { + public final ServiceInfo service; + public final ComponentName componentName; + public final String key; + public final ComponentName settings; + public final CharSequence label; + public final String labelStr; + public final CharSequence appLabel; + + public BaseInfo(PackageManager pm, ServiceInfo _service, String _settings) { + service = _service; + componentName = new ComponentName(_service.packageName, _service.name); + key = componentName.flattenToShortString(); + settings = _settings != null + ? new ComponentName(_service.packageName, _settings) : null; + label = _service.loadLabel(pm); + labelStr = label.toString(); + appLabel = _service.applicationInfo.loadLabel(pm); + } + + @Override + public int compareTo(Object another) { + return labelStr.compareTo(((BaseInfo)another).labelStr); + } + } + + static public class InteractionInfo extends BaseInfo { + public final VoiceInteractionServiceInfo serviceInfo; + + public InteractionInfo(PackageManager pm, VoiceInteractionServiceInfo _service) { + super(pm, _service.getServiceInfo(), _service.getSettingsActivity()); + serviceInfo = _service; + } + } + + static public class RecognizerInfo extends BaseInfo { + public RecognizerInfo(PackageManager pm, ServiceInfo _service, String _settings) { + super(pm, _service, _settings); + } + } + + final ArrayList<InteractionInfo> mAvailableInteractionInfos = new ArrayList<>(); + final ArrayList<RecognizerInfo> mAvailableRecognizerInfos = new ArrayList<>(); + + ComponentName mCurrentVoiceInteraction; + ComponentName mCurrentRecognizer; + + public VoiceInputHelper(Context context) { + mContext = context; + + mAvailableVoiceInteractions = mContext.getPackageManager().queryIntentServices( + new Intent(VoiceInteractionService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + mAvailableRecognition = mContext.getPackageManager().queryIntentServices( + new Intent(RecognitionService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + } + + public boolean hasItems() { + return mAvailableVoiceInteractions.size() > 0 || mAvailableRecognition.size() > 0; + } + + public void buildUi() { + // Get the currently selected interactor from the secure setting. + String currentSetting = Settings.Secure.getString( + mContext.getContentResolver(), Settings.Secure.VOICE_INTERACTION_SERVICE); + if (currentSetting != null && !currentSetting.isEmpty()) { + mCurrentVoiceInteraction = ComponentName.unflattenFromString(currentSetting); + } else { + mCurrentVoiceInteraction = null; + } + + ArraySet<ComponentName> interactorRecognizers = new ArraySet<>(); + + // Iterate through all the available interactors and load up their info to show + // in the preference. + int size = mAvailableVoiceInteractions.size(); + for (int i = 0; i < size; i++) { + ResolveInfo resolveInfo = mAvailableVoiceInteractions.get(i); + VoiceInteractionServiceInfo info = new VoiceInteractionServiceInfo( + mContext.getPackageManager(), resolveInfo.serviceInfo); + if (info.getParseError() != null) { + Log.w("VoiceInteractionService", "Error in VoiceInteractionService " + + resolveInfo.serviceInfo.packageName + "/" + + resolveInfo.serviceInfo.name + ": " + info.getParseError()); + continue; + } + mAvailableInteractionInfos.add(new InteractionInfo(mContext.getPackageManager(), info)); + if (info.getRecognitionService() != null) { + interactorRecognizers.add(new ComponentName(resolveInfo.serviceInfo.packageName, + info.getRecognitionService())); + } + } + Collections.sort(mAvailableInteractionInfos); + + // Get the currently selected recognizer from the secure setting. + currentSetting = Settings.Secure.getString( + mContext.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE); + if (currentSetting != null && !currentSetting.isEmpty()) { + mCurrentRecognizer = ComponentName.unflattenFromString(currentSetting); + } else { + mCurrentRecognizer = null; + } + + // Iterate through all the available recognizers and load up their info to show + // in the preference. + size = mAvailableRecognition.size(); + for (int i = 0; i < size; i++) { + ResolveInfo resolveInfo = mAvailableRecognition.get(i); + ComponentName comp = new ComponentName(resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name); + if (interactorRecognizers.contains(comp)) { + //continue; + } + ServiceInfo si = resolveInfo.serviceInfo; + XmlResourceParser parser = null; + String settingsActivity = null; + try { + parser = si.loadXmlMetaData(mContext.getPackageManager(), + RecognitionService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA + + " meta-data for " + si.packageName); + } + + Resources res = mContext.getPackageManager().getResourcesForApplication( + si.applicationInfo); + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!"recognition-service".equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with recognition-service tag"); + } + + TypedArray array = res.obtainAttributes(attrs, + com.android.internal.R.styleable.RecognitionService); + settingsActivity = array.getString( + com.android.internal.R.styleable.RecognitionService_settingsActivity); + array.recycle(); + } catch (XmlPullParserException e) { + Log.e(TAG, "error parsing recognition service meta-data", e); + } catch (IOException e) { + Log.e(TAG, "error parsing recognition service meta-data", e); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "error parsing recognition service meta-data", e); + } finally { + if (parser != null) parser.close(); + } + mAvailableRecognizerInfos.add(new RecognizerInfo(mContext.getPackageManager(), + resolveInfo.serviceInfo, settingsActivity)); + } + Collections.sort(mAvailableRecognizerInfos); + } +} diff --git a/src/com/android/settings/voice/VoiceInputPreference.java b/src/com/android/settings/voice/VoiceInputPreference.java new file mode 100644 index 0000000..0ebffbb --- /dev/null +++ b/src/com/android/settings/voice/VoiceInputPreference.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 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.voice; + +import android.app.AlertDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Checkable; +import android.widget.CompoundButton; +import android.widget.RadioButton; + + +import com.android.settings.R; +import com.android.settings.Utils; + +public final class VoiceInputPreference extends Preference { + + private static final String TAG = "VoiceInputPreference"; + + private final CharSequence mLabel; + + private final CharSequence mAppLabel; + + private final CharSequence mAlertText; + + private final ComponentName mSettingsComponent; + + /** + * The shared radio button state, which button is checked etc. + */ + private final RadioButtonGroupState mSharedState; + + /** + * When true, the change callbacks on the radio button will not + * fire. + */ + private volatile boolean mPreventRadioButtonCallbacks; + + private View mSettingsIcon; + private RadioButton mRadioButton; + + private final CompoundButton.OnCheckedChangeListener mRadioChangeListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + onRadioButtonClicked(buttonView, isChecked); + } + }; + + public VoiceInputPreference(Context context, VoiceInputHelper.BaseInfo info, + CharSequence summary, CharSequence alertText, RadioButtonGroupState state) { + super(context); + setLayoutResource(R.layout.preference_tts_engine); + + mSharedState = state; + mLabel = info.label; + mAppLabel = info.appLabel; + mAlertText = alertText; + mSettingsComponent = info.settings; + mPreventRadioButtonCallbacks = false; + + setKey(info.key); + setTitle(info.label); + setSummary(summary); + } + + @Override + public View getView(View convertView, ViewGroup parent) { + if (mSharedState == null) { + throw new IllegalStateException("Call to getView() before a call to" + + "setSharedState()"); + } + + View view = super.getView(convertView, parent); + final RadioButton rb = (RadioButton) view.findViewById(R.id.tts_engine_radiobutton); + rb.setOnCheckedChangeListener(mRadioChangeListener); + + boolean isChecked = getKey().equals(mSharedState.getCurrentKey()); + if (isChecked) { + mSharedState.setCurrentChecked(rb); + } + + mPreventRadioButtonCallbacks = true; + rb.setChecked(isChecked); + mPreventRadioButtonCallbacks = false; + + mRadioButton = rb; + + View textLayout = view.findViewById(R.id.tts_engine_pref_text); + textLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onRadioButtonClicked(rb, !rb.isChecked()); + } + }); + + mSettingsIcon = view.findViewById(R.id.tts_engine_settings); + mSettingsIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(mSettingsComponent); + getContext().startActivity(new Intent(intent)); + } + }); + updateCheckedState(isChecked); + + return view; + } + + private boolean shouldDisplayAlert() { + return mAlertText != null; + } + + private void displayAlert( + final DialogInterface.OnClickListener positiveOnClickListener, + final DialogInterface.OnClickListener negativeOnClickListener) { + Log.i(TAG, "Displaying data alert for :" + getKey()); + + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + String msg = String.format(getContext().getResources().getConfiguration().locale, + mAlertText.toString(), mAppLabel); + builder.setTitle(android.R.string.dialog_alert_title) + .setMessage(msg) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, positiveOnClickListener) + .setNegativeButton(android.R.string.cancel, negativeOnClickListener) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override public void onCancel(DialogInterface dialog) { + negativeOnClickListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE); + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + public void doClick() { + mRadioButton.performClick(); + } + + void updateCheckedState(boolean isChecked) { + if (mSettingsComponent != null) { + mSettingsIcon.setVisibility(View.VISIBLE); + if (isChecked) { + mSettingsIcon.setEnabled(true); + mSettingsIcon.setAlpha(1); + } else { + mSettingsIcon.setEnabled(false); + mSettingsIcon.setAlpha(Utils.DISABLED_ALPHA); + } + } else { + mSettingsIcon.setVisibility(View.GONE); + } + } + + void onRadioButtonClicked(final CompoundButton buttonView, boolean isChecked) { + if (mPreventRadioButtonCallbacks) { + return; + } + if (mSharedState.getCurrentChecked() == buttonView) { + updateCheckedState(isChecked); + return; + } + + if (isChecked) { + // Should we alert user? if that's true, delay making engine current one. + if (shouldDisplayAlert()) { + displayAlert(new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + makeCurrentChecked(buttonView); + } + }, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Undo the click. + buttonView.setChecked(false); + } + } + ); + } else { + // Privileged engine, set it current + makeCurrentChecked(buttonView); + } + } else { + updateCheckedState(isChecked); + } + } + + void makeCurrentChecked(Checkable current) { + if (mSharedState.getCurrentChecked() != null) { + mSharedState.getCurrentChecked().setChecked(false); + } + mSharedState.setCurrentChecked(current); + mSharedState.setCurrentKey(getKey()); + updateCheckedState(true); + callChangeListener(mSharedState.getCurrentKey()); + } + + /** + * Holds all state that is common to this group of radio buttons, such + * as the currently selected key and the currently checked compound button. + * (which corresponds to this key). + */ + public interface RadioButtonGroupState { + String getCurrentKey(); + Checkable getCurrentChecked(); + + void setCurrentKey(String key); + void setCurrentChecked(Checkable current); + } +} diff --git a/src/com/android/settings/voice/VoiceInputSettings.java b/src/com/android/settings/voice/VoiceInputSettings.java new file mode 100644 index 0000000..262f145 --- /dev/null +++ b/src/com/android/settings/voice/VoiceInputSettings.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2014 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.voice; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.preference.Preference; +import android.provider.Settings; +import android.service.voice.VoiceInteractionService; +import android.service.voice.VoiceInteractionServiceInfo; +import android.speech.RecognitionService; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.voice.VoiceInputPreference.RadioButtonGroupState; + +import android.os.Bundle; +import android.preference.PreferenceCategory; +import android.widget.Checkable; + +import java.util.ArrayList; +import java.util.List; + +public class VoiceInputSettings extends SettingsPreferenceFragment implements + Preference.OnPreferenceClickListener, RadioButtonGroupState, Indexable { + + private static final String TAG = "VoiceInputSettings"; + private static final boolean DBG = false; + + /** + * Preference key for the engine selection preference. + */ + private static final String KEY_SERVICE_PREFERENCE_SECTION = + "voice_service_preference_section"; + + private PreferenceCategory mServicePreferenceCategory; + + private CharSequence mInteractorSummary; + private CharSequence mRecognizerSummary; + private CharSequence mInteractorWarning; + + /** + * The currently selected engine. + */ + private String mCurrentKey; + + /** + * The engine checkbox that is currently checked. Saves us a bit of effort + * in deducing the right one from the currently selected engine. + */ + private Checkable mCurrentChecked; + + private VoiceInputHelper mHelper; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.voice_input_settings); + + mServicePreferenceCategory = (PreferenceCategory) findPreference( + KEY_SERVICE_PREFERENCE_SECTION); + + mInteractorSummary = getActivity().getText( + R.string.voice_interactor_preference_summary); + mRecognizerSummary = getActivity().getText( + R.string.voice_recognizer_preference_summary); + mInteractorWarning = getActivity().getText(R.string.voice_interaction_security_warning); + } + + @Override + public void onStart() { + super.onStart(); + initSettings(); + } + + private void initSettings() { + mHelper = new VoiceInputHelper(getActivity()); + mHelper.buildUi(); + + mServicePreferenceCategory.removeAll(); + + if (mHelper.mCurrentVoiceInteraction != null) { + mCurrentKey = mHelper.mCurrentVoiceInteraction.flattenToShortString(); + } else if (mHelper.mCurrentRecognizer != null) { + mCurrentKey = mHelper.mCurrentRecognizer.flattenToShortString(); + } else { + mCurrentKey = null; + } + + for (int i=0; i<mHelper.mAvailableInteractionInfos.size(); i++) { + VoiceInputHelper.InteractionInfo info = mHelper.mAvailableInteractionInfos.get(i); + VoiceInputPreference pref = new VoiceInputPreference(getActivity(), info, + mInteractorSummary, mInteractorWarning, this); + mServicePreferenceCategory.addPreference(pref); + } + + for (int i=0; i<mHelper.mAvailableRecognizerInfos.size(); i++) { + VoiceInputHelper.RecognizerInfo info = mHelper.mAvailableRecognizerInfos.get(i); + VoiceInputPreference pref = new VoiceInputPreference(getActivity(), info, + mRecognizerSummary, null, this); + mServicePreferenceCategory.addPreference(pref); + } + } + + @Override + public Checkable getCurrentChecked() { + return mCurrentChecked; + } + + @Override + public String getCurrentKey() { + return mCurrentKey; + } + + @Override + public void setCurrentChecked(Checkable current) { + mCurrentChecked = current; + } + + @Override + public void setCurrentKey(String key) { + mCurrentKey = key; + for (int i=0; i<mHelper.mAvailableInteractionInfos.size(); i++) { + VoiceInputHelper.InteractionInfo info = mHelper.mAvailableInteractionInfos.get(i); + if (info.key.equals(key)) { + // Put the new value back into secure settings. + Settings.Secure.putString(getActivity().getContentResolver(), + Settings.Secure.VOICE_INTERACTION_SERVICE, key); + // Eventually we will require that an interactor always specify a recognizer + if (info.settings != null) { + Settings.Secure.putString(getActivity().getContentResolver(), + Settings.Secure.VOICE_RECOGNITION_SERVICE, + info.settings.flattenToShortString()); + } + return; + } + } + + for (int i=0; i<mHelper.mAvailableRecognizerInfos.size(); i++) { + VoiceInputHelper.RecognizerInfo info = mHelper.mAvailableRecognizerInfos.get(i); + if (info.key.equals(key)) { + Settings.Secure.putString(getActivity().getContentResolver(), + Settings.Secure.VOICE_INTERACTION_SERVICE, ""); + Settings.Secure.putString(getActivity().getContentResolver(), + Settings.Secure.VOICE_RECOGNITION_SERVICE, key); + return; + } + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference instanceof VoiceInputPreference) { + ((VoiceInputPreference)preference).doClick(); + } + return true; + } + + // For Search + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, + boolean enabled) { + + List<SearchIndexableRaw> indexables = new ArrayList<>(); + + final String screenTitle = context.getString(R.string.voice_input_settings_title); + + SearchIndexableRaw indexable = new SearchIndexableRaw(context); + indexable.key = "voice_service_preference_section_title"; + indexable.title = context.getString(R.string.voice_service_preference_section_title); + indexable.screenTitle = screenTitle; + indexables.add(indexable); + + final List<ResolveInfo> voiceInteractions = + context.getPackageManager().queryIntentServices( + new Intent(VoiceInteractionService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + + final int countInteractions = voiceInteractions.size(); + for (int i = 0; i < countInteractions; i++) { + ResolveInfo info = voiceInteractions.get(i); + VoiceInteractionServiceInfo visInfo = new VoiceInteractionServiceInfo( + context.getPackageManager(), info.serviceInfo); + if (visInfo.getParseError() != null) { + continue; + } + indexables.add(getSearchIndexableRaw(context, info, screenTitle)); + } + + final List<ResolveInfo> recognitions = + context.getPackageManager().queryIntentServices( + new Intent(RecognitionService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + + final int countRecognitions = recognitions.size(); + for (int i = 0; i < countRecognitions; i++) { + ResolveInfo info = recognitions.get(i); + indexables.add(getSearchIndexableRaw(context, info, screenTitle)); + } + + return indexables; + } + + private SearchIndexableRaw getSearchIndexableRaw(Context context, + ResolveInfo info, String screenTitle) { + + ServiceInfo serviceInfo = info.serviceInfo; + ComponentName componentName = new ComponentName(serviceInfo.packageName, + serviceInfo.name); + + SearchIndexableRaw indexable = new SearchIndexableRaw(context); + indexable.key = componentName.flattenToString(); + indexable.title = info.loadLabel(context.getPackageManager()).toString(); + indexable.screenTitle = screenTitle; + + return indexable; + } + }; +} diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index 73aae99..7516392 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -29,8 +29,10 @@ import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemProperties; +import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; import android.security.Credentials; import android.security.KeyStore; import android.text.TextUtils; @@ -43,8 +45,11 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; import android.widget.Toast; import com.android.internal.net.LegacyVpnInfo; @@ -80,14 +85,25 @@ public class VpnSettings extends SettingsPreferenceFragment implements private Handler mUpdater; private LegacyVpnInfo mInfo; + private UserManager mUm; // The key of the profile for the current ContextMenu. private String mSelectedKey; + private boolean mUnavailable; + @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); + mUm = (UserManager) getSystemService(Context.USER_SERVICE); + + if (mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { + mUnavailable = true; + setPreferenceScreen(new PreferenceScreen(getActivity(), null)); + return; + } + setHasOptionsMenu(true); addPreferencesFromResource(R.xml.vpn_settings2); @@ -156,6 +172,15 @@ public class VpnSettings extends SettingsPreferenceFragment implements public void onResume() { super.onResume(); + if (mUnavailable) { + TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); + getListView().setEmptyView(emptyView); + if (emptyView != null) { + emptyView.setText(R.string.vpn_settings_not_available); + } + return; + } + final boolean pickLockdown = getActivity() .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false); if (pickLockdown) { @@ -214,6 +239,10 @@ public class VpnSettings extends SettingsPreferenceFragment implements public void onPause() { super.onPause(); + if (mUnavailable) { + return; + } + // Hide the dialog if there is one. if (mDialog != null) { mDialog.setOnDismissListener(null); @@ -463,7 +492,7 @@ public class VpnSettings extends SettingsPreferenceFragment implements private static class TitleAdapter extends ArrayAdapter<CharSequence> { public TitleAdapter(Context context, List<CharSequence> objects) { - super(context, com.android.internal.R.layout.select_dialog_singlechoice_holo, + super(context, com.android.internal.R.layout.select_dialog_singlechoice_material, android.R.id.text1, objects); } } diff --git a/src/com/android/settings/wfd/WifiDisplaySettings.java b/src/com/android/settings/wfd/WifiDisplaySettings.java index d7f4708..c3f22a7 100755 --- a/src/com/android/settings/wfd/WifiDisplaySettings.java +++ b/src/com/android/settings/wfd/WifiDisplaySettings.java @@ -42,15 +42,12 @@ import android.os.Looper; 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.Html; import android.util.Slog; import android.util.TypedValue; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -59,14 +56,11 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; -import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; -import android.widget.Switch; import android.widget.TextView; import com.android.internal.app.MediaRouteDialogPresenter; -import com.android.settings.ProgressCategory; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; diff --git a/src/com/android/settings/widget/ChartDataUsageView.java b/src/com/android/settings/widget/ChartDataUsageView.java index 4e16bfc..c20a8db 100644 --- a/src/com/android/settings/widget/ChartDataUsageView.java +++ b/src/com/android/settings/widget/ChartDataUsageView.java @@ -50,26 +50,24 @@ public class ChartDataUsageView extends ChartView { private static final int MSG_UPDATE_AXIS = 100; private static final long DELAY_MILLIS = 250; - private static final boolean LIMIT_SWEEPS_TO_VALID_DATA = false; - 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 long mInspectStart; + private long mInspectEnd; + private Handler mHandler; /** Current maximum value of {@link #mVert}. */ private long mVertMax; public interface DataUsageChartListener { - public void onInspectRangeChanged(); public void onWarningChanged(); public void onLimitChanged(); public void requestWarningEdit(); @@ -112,43 +110,27 @@ public class ChartDataUsageView extends ChartView { 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); // mark neighbors for checking touch events against - mSweepLeft.setNeighbors(mSweepRight); - mSweepRight.setNeighbors(mSweepLeft); - mSweepLimit.setNeighbors(mSweepWarning, mSweepLeft, mSweepRight); - mSweepWarning.setNeighbors(mSweepLimit, mSweepLeft, mSweepRight); + mSweepLimit.setNeighbors(mSweepWarning); + mSweepWarning.setNeighbors(mSweepLimit); - mSweepLeft.addOnSweepListener(mHorizListener); - mSweepRight.addOnSweepListener(mHorizListener); mSweepWarning.addOnSweepListener(mVertListener); mSweepLimit.addOnSweepListener(mVertListener); mSweepWarning.setDragInterval(5 * MB_IN_BYTES); mSweepLimit.setDragInterval(5 * MB_IN_BYTES); - // TODO: make time sweeps adjustable through dpad - mSweepLeft.setClickable(false); - mSweepLeft.setFocusable(false); - mSweepRight.setClickable(false); - mSweepRight.setFocusable(false); - // 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); @@ -194,7 +176,7 @@ public class ChartDataUsageView extends ChartView { mSweepLimit.setEnabled(true); mSweepLimit.setValue(policy.limitBytes); } else { - mSweepLimit.setVisibility(View.VISIBLE); + mSweepLimit.setVisibility(View.INVISIBLE); mSweepLimit.setEnabled(false); mSweepLimit.setValue(-1); } @@ -295,23 +277,6 @@ public class ChartDataUsageView extends ChartView { mSeries.setEstimateVisible(estimateVisible); } - private OnSweepListener mHorizListener = new OnSweepListener() { - @Override - public void onSweep(ChartSweepView sweep, boolean sweepDone) { - updatePrimaryRange(); - - // update detail list only when done sweeping - if (sweepDone && mListener != null) { - mListener.onInspectRangeChanged(); - } - } - - @Override - public void requestEdit(ChartSweepView sweep) { - // ignored - } - }; - private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) { if (force || !mHandler.hasMessages(MSG_UPDATE_AXIS, sweep)) { mHandler.sendMessageDelayed( @@ -369,11 +334,11 @@ public class ChartDataUsageView extends ChartView { } public long getInspectStart() { - return mSweepLeft.getValue(); + return mInspectStart; } public long getInspectEnd() { - return mSweepRight.getValue(); + return mInspectEnd; } public long getWarningBytes() { @@ -384,14 +349,6 @@ public class ChartDataUsageView extends ChartView { return mSweepLimit.getLabelValue(); } - private long getHistoryStart() { - return mHistory != null ? mHistory.getStart() : Long.MAX_VALUE; - } - - private long getHistoryEnd() { - return mHistory != null ? mHistory.getEnd() : Long.MIN_VALUE; - } - /** * Set the exact time range that should be displayed, updating how * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the @@ -403,30 +360,8 @@ public class ChartDataUsageView extends ChartView { mSeries.setBounds(visibleStart, visibleEnd); mDetailSeries.setBounds(visibleStart, visibleEnd); - final long historyStart = getHistoryStart(); - final long historyEnd = getHistoryEnd(); - - final long validStart = historyStart == Long.MAX_VALUE ? visibleStart - : Math.max(visibleStart, historyStart); - final long validEnd = historyEnd == Long.MIN_VALUE ? visibleEnd - : Math.min(visibleEnd, historyEnd); - - if (LIMIT_SWEEPS_TO_VALID_DATA) { - // prevent time sweeps from leaving valid data - mSweepLeft.setValidRange(validStart, validEnd); - mSweepRight.setValidRange(validStart, validEnd); - } else { - mSweepLeft.setValidRange(visibleStart, visibleEnd); - mSweepRight.setValidRange(visibleStart, visibleEnd); - } - - // 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); + mInspectStart = visibleStart; + mInspectEnd = visibleEnd; requestLayout(); if (changed) { @@ -440,15 +375,11 @@ public class ChartDataUsageView extends ChartView { } 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); + mSeries.setSecondary(true); } else { - mSeries.setPrimaryRange(left, right); + mSeries.setSecondary(false); } } diff --git a/src/com/android/settings/widget/ChartGridView.java b/src/com/android/settings/widget/ChartGridView.java index ec5882c..4cd6f5f 100644 --- a/src/com/android/settings/widget/ChartGridView.java +++ b/src/com/android/settings/widget/ChartGridView.java @@ -19,22 +19,25 @@ package com.android.settings.widget; import static com.android.settings.DataUsageSummary.formatDateRange; import android.content.Context; +import android.content.res.ColorStateList; 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.Log; import android.util.TypedValue; import android.view.View; import com.android.internal.util.Preconditions; import com.android.settings.R; +import java.util.Locale; + /** * Background of {@link ChartView} that renders grid lines as requested by * {@link ChartAxis#getTickPoints()}. @@ -47,10 +50,13 @@ public class ChartGridView extends View { private Drawable mPrimary; private Drawable mSecondary; private Drawable mBorder; + + private int mLabelSize; private int mLabelColor; - private Layout mLayoutStart; - private Layout mLayoutEnd; + private Layout mLabelStart; + private Layout mLabelMid; + private Layout mLabelEnd; public ChartGridView(Context context) { this(context, null, 0); @@ -71,7 +77,17 @@ public class ChartGridView extends View { 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); + + final int taId = a.getResourceId(R.styleable.ChartGridView_android_textAppearance, -1); + final TypedArray ta = context.obtainStyledAttributes(taId, + com.android.internal.R.styleable.TextAppearance); + mLabelSize = ta.getDimensionPixelSize( + com.android.internal.R.styleable.TextAppearance_textSize, 0); + ta.recycle(); + + final ColorStateList labelColor = a.getColorStateList( + R.styleable.ChartGridView_android_textColor); + mLabelColor = labelColor.getDefaultColor(); a.recycle(); } @@ -83,71 +99,83 @@ public class ChartGridView extends View { void setBounds(long start, long end) { final Context context = getContext(); - mLayoutStart = makeLayout(formatDateRange(context, start, start)); - mLayoutEnd = makeLayout(formatDateRange(context, end, end)); + final long mid = (start + end) / 2; + mLabelStart = makeLabel(formatDateRange(context, start, start)); + mLabelMid = makeLabel(formatDateRange(context, mid, mid)); + mLabelEnd = makeLabel(formatDateRange(context, end, end)); invalidate(); } @Override protected void onDraw(Canvas canvas) { final int width = getWidth(); - final int height = getHeight(); + final int height = getHeight() - getPaddingBottom(); 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); + if (secondary != null) { + final int secondaryHeight = secondary.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); + if (primary != null) { + final int primaryWidth = primary.getIntrinsicWidth(); + final int primaryHeight = primary.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 != null ? mLayoutStart.getHeight() / 8 : 0; + final int padding = mLabelStart != null ? mLabelStart.getHeight() / 8 : 0; - final Layout start = mLayoutStart; + final Layout start = mLabelStart; if (start != null) { - canvas.save(); + final int saveCount = canvas.save(); canvas.translate(0, height + padding); start.draw(canvas); - canvas.restore(); + canvas.restoreToCount(saveCount); + } + + final Layout mid = mLabelMid; + if (mid != null) { + final int saveCount = canvas.save(); + canvas.translate((width - mid.getWidth()) / 2, height + padding); + mid.draw(canvas); + canvas.restoreToCount(saveCount); } - final Layout end = mLayoutEnd; + final Layout end = mLabelEnd; if (end != null) { - canvas.save(); + final int saveCount = canvas.save(); canvas.translate(width - end.getWidth(), height + padding); end.draw(canvas); - canvas.restore(); + canvas.restoreToCount(saveCount); } } - private Layout makeLayout(CharSequence text) { + private Layout makeLabel(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())); + paint.setTextSize(mLabelSize); 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 index 6250a25..7aaba66 100644 --- a/src/com/android/settings/widget/ChartNetworkSeriesView.java +++ b/src/com/android/settings/widget/ChartNetworkSeriesView.java @@ -60,17 +60,17 @@ public class ChartNetworkSeriesView extends View { private Path mPathFill; private Path mPathEstimate; + private int mSafeRegion; + private long mStart; private long mEnd; - private long mPrimaryLeft; - private long mPrimaryRight; - /** Series will be extended to reach this end time. */ private long mEndTime = Long.MIN_VALUE; private boolean mPathValid = false; private boolean mEstimateVisible = false; + private boolean mSecondary = false; private long mMax; private long mMaxEstimate; @@ -93,8 +93,11 @@ public class ChartNetworkSeriesView extends View { final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); final int fillSecondary = a.getColor( R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); + final int safeRegion = a.getDimensionPixelSize( + R.styleable.ChartNetworkSeriesView_safeRegion, 0); setChartColor(stroke, fill, fillSecondary); + setSafeRegion(safeRegion); setWillNotDraw(false); a.recycle(); @@ -134,6 +137,10 @@ public class ChartNetworkSeriesView extends View { mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); } + public void setSafeRegion(int safeRegion) { + mSafeRegion = safeRegion; + } + public void bindNetworkStats(NetworkStatsHistory stats) { mStats = stats; invalidatePath(); @@ -145,14 +152,8 @@ public class ChartNetworkSeriesView extends View { mEnd = end; } - /** - * 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(); + public void setSecondary(boolean secondary) { + mSecondary = secondary; } public void invalidatePath() { @@ -322,9 +323,6 @@ public class ChartNetworkSeriesView extends View { generatePath(); } - final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft); - final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight); - if (mEstimateVisible) { save = canvas.save(); canvas.clipRect(0, 0, getWidth(), getHeight()); @@ -332,21 +330,11 @@ public class ChartNetworkSeriesView extends View { canvas.restoreToCount(save); } - save = canvas.save(); - canvas.clipRect(0, 0, primaryLeftPoint, getHeight()); - canvas.drawPath(mPathFill, mPaintFillSecondary); - canvas.restoreToCount(save); + final Paint paintFill = mSecondary ? mPaintFillSecondary : mPaintFill; save = canvas.save(); - canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight()); - canvas.drawPath(mPathFill, mPaintFillSecondary); + canvas.clipRect(mSafeRegion, 0, getWidth(), getHeight() - mSafeRegion); + canvas.drawPath(mPathFill, paintFill); 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 index 774e5d8..04fc862 100644 --- a/src/com/android/settings/widget/ChartSweepView.java +++ b/src/com/android/settings/widget/ChartSweepView.java @@ -58,6 +58,7 @@ public class ChartSweepView extends View { private Rect mMargins = new Rect(); private float mNeighborMargin; + private int mSafeRegion; private int mFollowAxis; @@ -125,6 +126,7 @@ public class ChartSweepView extends View { setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0)); + setSafeRegion(a.getDimensionPixelSize(R.styleable.ChartSweepView_safeRegion, 0)); setLabelMinSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); @@ -259,7 +261,6 @@ public class ChartSweepView extends View { paint.density = getResources().getDisplayMetrics().density; paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); paint.setColor(mLabelColor); - paint.setShadowLayer(4 * paint.density, 0, 0, Color.BLACK); mLabelTemplate = new SpannableStringBuilder(template); mLabelLayout = new DynamicLayout( @@ -383,6 +384,10 @@ public class ChartSweepView extends View { mNeighborMargin = neighborMargin; } + public void setSafeRegion(int safeRegion) { + mSafeRegion = safeRegion; + } + /** * Set valid range this sweep can move within, defined by the given * {@link ChartSweepView}. The most restrictive combination of all valid @@ -709,7 +714,7 @@ public class ChartSweepView extends View { mLabelLayout.draw(canvas); } canvas.restoreToCount(count); - labelSize = (int) mLabelSize; + labelSize = (int) mLabelSize + mSafeRegion; } else { labelSize = 0; } diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java index 69e6e94..30284bc 100644 --- a/src/com/android/settings/widget/ChartView.java +++ b/src/com/android/settings/widget/ChartView.java @@ -112,12 +112,18 @@ public class ChartView extends FrameLayout { parentRect.set(mContent); - if (child instanceof ChartNetworkSeriesView || child instanceof ChartGridView) { + if (child instanceof ChartNetworkSeriesView) { // 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 ChartGridView) { + // Grid uses some extra room for labels + Gravity.apply(params.gravity, width, height, parentRect, childRect); + child.layout(childRect.left, childRect.top, childRect.right, + childRect.bottom + child.getPaddingBottom()); + } else if (child instanceof ChartSweepView) { layoutSweep((ChartSweepView) child, parentRect, childRect); child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); @@ -154,5 +160,4 @@ public class ChartView extends FrameLayout { parentRect, childRect); } } - } diff --git a/src/com/android/settings/widget/PieChartView.java b/src/com/android/settings/widget/PieChartView.java deleted file mode 100644 index 6070190..0000000 --- a/src/com/android/settings/widget/PieChartView.java +++ /dev/null @@ -1,244 +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.widget; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.Path; -import android.graphics.Path.Direction; -import android.graphics.RadialGradient; -import android.graphics.RectF; -import android.graphics.Shader.TileMode; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; - -import com.google.android.collect.Lists; - -import java.util.ArrayList; - -/** - * Pie chart with multiple items. - */ -public class PieChartView extends View { - public static final String TAG = "PieChartView"; - public static final boolean LOGD = false; - - private static final boolean FILL_GRADIENT = false; - - private ArrayList<Slice> mSlices = Lists.newArrayList(); - - private int mOriginAngle; - private Matrix mMatrix = new Matrix(); - - private Paint mPaintOutline = new Paint(); - - private Path mPathSide = new Path(); - private Path mPathSideOutline = new Path(); - - private Path mPathOutline = new Path(); - - private int mSideWidth; - - public class Slice { - public long value; - - public Path path = new Path(); - public Path pathSide = new Path(); - public Path pathOutline = new Path(); - - public Paint paint; - - public Slice(long value, int color) { - this.value = value; - this.paint = buildFillPaint(color, getResources()); - } - } - - public PieChartView(Context context) { - this(context, null); - } - - public PieChartView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PieChartView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mPaintOutline.setColor(Color.BLACK); - mPaintOutline.setStyle(Style.STROKE); - mPaintOutline.setStrokeWidth(3f * getResources().getDisplayMetrics().density); - mPaintOutline.setAntiAlias(true); - - mSideWidth = (int) (20 * getResources().getDisplayMetrics().density); - - setWillNotDraw(false); - } - - private static Paint buildFillPaint(int color, Resources res) { - final Paint paint = new Paint(); - - paint.setColor(color); - paint.setStyle(Style.FILL_AND_STROKE); - paint.setAntiAlias(true); - - if (FILL_GRADIENT) { - final int width = (int) (280 * res.getDisplayMetrics().density); - paint.setShader(new RadialGradient(0, 0, width, color, darken(color), TileMode.MIRROR)); - } - - return paint; - } - - public void setOriginAngle(int originAngle) { - mOriginAngle = originAngle; - } - - public void addSlice(long value, int color) { - mSlices.add(new Slice(value, color)); - } - - public void removeAllSlices() { - mSlices.clear(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final float centerX = getWidth() / 2; - final float centerY = getHeight() / 2; - - mMatrix.reset(); - mMatrix.postScale(0.665f, 0.95f, centerX, centerY); - mMatrix.postRotate(-40, centerX, centerY); - - generatePath(); - } - - public void generatePath() { - if (LOGD) Log.d(TAG, "generatePath()"); - - long total = 0; - for (Slice slice : mSlices) { - slice.path.reset(); - slice.pathSide.reset(); - slice.pathOutline.reset(); - total += slice.value; - } - - mPathSide.reset(); - mPathSideOutline.reset(); - mPathOutline.reset(); - - // bail when not enough stats to render - if (total == 0) { - invalidate(); - return; - } - - final int width = getWidth(); - final int height = getHeight(); - - final RectF rect = new RectF(0, 0, width, height); - final RectF rectSide = new RectF(); - rectSide.set(rect); - rectSide.offset(-mSideWidth, 0); - - mPathSide.addOval(rectSide, Direction.CW); - mPathSideOutline.addOval(rectSide, Direction.CW); - mPathOutline.addOval(rect, Direction.CW); - - int startAngle = mOriginAngle; - for (Slice slice : mSlices) { - final int sweepAngle = (int) (slice.value * 360 / total); - final int endAngle = startAngle + sweepAngle; - - final float startAngleMod = startAngle % 360; - final float endAngleMod = endAngle % 360; - final boolean startSideVisible = startAngleMod > 90 && startAngleMod < 270; - final boolean endSideVisible = endAngleMod > 90 && endAngleMod < 270; - - // draw slice - slice.path.moveTo(rect.centerX(), rect.centerY()); - slice.path.arcTo(rect, startAngle, sweepAngle); - slice.path.lineTo(rect.centerX(), rect.centerY()); - - if (startSideVisible || endSideVisible) { - - // when start is beyond horizon, push until visible - final float startAngleSide = startSideVisible ? startAngle : 450; - final float endAngleSide = endSideVisible ? endAngle : 270; - final float sweepAngleSide = endAngleSide - startAngleSide; - - // draw slice side - slice.pathSide.moveTo(rect.centerX(), rect.centerY()); - slice.pathSide.arcTo(rect, startAngleSide, 0); - slice.pathSide.rLineTo(-mSideWidth, 0); - slice.pathSide.arcTo(rectSide, startAngleSide, sweepAngleSide); - slice.pathSide.rLineTo(mSideWidth, 0); - slice.pathSide.arcTo(rect, endAngleSide, -sweepAngleSide); - } - - // draw slice outline - slice.pathOutline.moveTo(rect.centerX(), rect.centerY()); - slice.pathOutline.arcTo(rect, startAngle, 0); - if (startSideVisible) { - slice.pathOutline.rLineTo(-mSideWidth, 0); - } - slice.pathOutline.moveTo(rect.centerX(), rect.centerY()); - slice.pathOutline.arcTo(rect, startAngle + sweepAngle, 0); - if (endSideVisible) { - slice.pathOutline.rLineTo(-mSideWidth, 0); - } - - startAngle += sweepAngle; - } - - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - - canvas.concat(mMatrix); - - for (Slice slice : mSlices) { - canvas.drawPath(slice.pathSide, slice.paint); - } - canvas.drawPath(mPathSideOutline, mPaintOutline); - - for (Slice slice : mSlices) { - canvas.drawPath(slice.path, slice.paint); - canvas.drawPath(slice.pathOutline, mPaintOutline); - } - canvas.drawPath(mPathOutline, mPaintOutline); - } - - public static int darken(int color) { - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); - hsv[2] /= 2; - hsv[1] /= 2; - return Color.HSVToColor(hsv); - } - -} diff --git a/src/com/android/settings/widget/SetupWizardIllustration.java b/src/com/android/settings/widget/SetupWizardIllustration.java new file mode 100644 index 0000000..717ec35 --- /dev/null +++ b/src/com/android/settings/widget/SetupWizardIllustration.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2014 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.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; + +import com.android.settings.R; + +/** + * Class to draw the illustration of setup wizard. The aspectRatio attribute determines the aspect + * ratio of the top padding, which is leaving space for the illustration. Draws an illustration + * (foreground) to fit the width of the view and fills the rest with the background. + * + * Copied from com.google.android.setupwizard.util.SetupWizardIllustration + */ +public class SetupWizardIllustration extends FrameLayout { + + private static final String TAG = "SetupWizardIllustration"; + + // Size of the baseline grid in pixels + private float mBaselineGridSize; + private Drawable mBackground; + private Drawable mForeground; + private final Rect mViewBounds = new Rect(); + private final Rect mForegroundBounds = new Rect(); + private float mScale = 1.0f; + private float mAspectRatio = 0.0f; + + public SetupWizardIllustration(Context context) { + this(context, null); + } + + public SetupWizardIllustration(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SetupWizardIllustration(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SetupWizardIllustration(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SetupWizardIllustration, 0, 0); + mAspectRatio = a.getFloat(R.styleable.SetupWizardIllustration_aspectRatio, 0.0f); + a.recycle(); + } + // Number of pixels of the 8dp baseline grid as defined in material design specs + mBaselineGridSize = getResources().getDisplayMetrics().density * 8; + setWillNotDraw(false); + } + + /** + * The background will be drawn to fill up the rest of the view. It will also be scaled by the + * same amount as the foreground so their textures look the same. + */ + @Override + public void setBackground(Drawable background) { + mBackground = background; + } + + /** + * Sets the drawable used as the illustration. THe drawable is expected to have intrinsic + * width and height defined and will be scaled to fit the width of the view. + */ + @Override + public void setForeground(Drawable foreground) { + mForeground = foreground; + } + + @Override + public void onResolveDrawables(int layoutDirection) { + mBackground.setLayoutDirection(layoutDirection); + mForeground.setLayoutDirection(layoutDirection); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mAspectRatio != 0.0f) { + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int illustrationHeight = (int) (parentWidth / mAspectRatio); + illustrationHeight -= illustrationHeight % mBaselineGridSize; + setPaddingRelative(0, illustrationHeight, 0, 0); + } + setOutlineProvider(ViewOutlineProvider.BOUNDS); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int layoutWidth = right - left; + final int layoutHeight = bottom - top; + if (mForeground != null) { + int intrinsicWidth = mForeground.getIntrinsicWidth(); + int intrinsicHeight = mForeground.getIntrinsicHeight(); + final int layoutDirection = getLayoutDirection(); + + mViewBounds.set(0, 0, layoutWidth, layoutHeight); + if (mAspectRatio != 0f) { + mScale = layoutWidth / (float) intrinsicWidth; + intrinsicWidth = layoutWidth; + intrinsicHeight = (int) (intrinsicHeight * mScale); + } + Gravity.apply(Gravity.FILL_HORIZONTAL | Gravity.TOP, intrinsicWidth, intrinsicHeight, + mViewBounds, mForegroundBounds, layoutDirection); + mForeground.setBounds(mForegroundBounds); + } + if (mBackground != null) { + // Scale the bounds by mScale to compensate for the scale done to the canvas before + // drawing. + mBackground.setBounds(0, 0, (int) Math.ceil(layoutWidth / mScale), + (int) Math.ceil((layoutHeight - mForegroundBounds.height()) / mScale)); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + public void onDraw(Canvas canvas) { + if (mBackground != null) { + canvas.save(); + // Draw the background filling parts not covered by the illustration + canvas.translate(0, mForegroundBounds.height()); + // Scale the background so its size matches the foreground + canvas.scale(mScale, mScale, 0, 0); + mBackground.draw(canvas); + canvas.restore(); + } + if (mForeground != null) { + canvas.save(); + // Draw the illustration + mForeground.draw(canvas); + canvas.restore(); + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/settings/widget/StickyHeaderListView.java b/src/com/android/settings/widget/StickyHeaderListView.java new file mode 100644 index 0000000..3d9f158 --- /dev/null +++ b/src/com/android/settings/widget/StickyHeaderListView.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 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.graphics.Canvas; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowInsets; +import android.widget.ListView; + +/** + * This class provides sticky header functionality in a list view, to use with + * SetupWizardIllustration. To use this, add a header tagged with "sticky", or a header tagged with + * "stickyContainer" and one of its child tagged as "sticky". The sticky container will be drawn + * when the sticky element hits the top of the view. + * + * There are a few things to note: + * 1. The two supported scenarios are StickyHeaderListView -> Header (stickyContainer) -> sticky, + * and StickyHeaderListView -> Header (sticky). The arrow (->) represents parent/child + * relationship and must be immediate child. + * 2. The view does not work well with padding. b/16190933 + * 3. If fitsSystemWindows is true, then this will offset the sticking position by the height of + * the system decorations at the top of the screen. + * + * @see SetupWizardIllustration + * @see com.google.android.setupwizard.util.StickyHeaderScrollView + * + * Copied from com.google.android.setupwizard.util.StickyHeaderListView + */ +public class StickyHeaderListView extends ListView { + + private View mSticky; + private View mStickyContainer; + private boolean mDrawScrollBar; + private int mStatusBarInset = 0; + private RectF mStickyRect = new RectF(); + + public StickyHeaderListView(Context context) { + super(context); + } + + public StickyHeaderListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public StickyHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public StickyHeaderListView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (mSticky == null) { + updateStickyView(); + } + } + + public void updateStickyView() { + mSticky = findViewWithTag("sticky"); + mStickyContainer = findViewWithTag("stickyContainer"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mStickyRect.contains(ev.getX(), ev.getY())) { + ev.offsetLocation(-mStickyRect.left, -mStickyRect.top); + return mStickyContainer.dispatchTouchEvent(ev); + } else { + return super.dispatchTouchEvent(ev); + } + } + + @Override + public void draw(Canvas canvas) { + mDrawScrollBar = false; + super.draw(canvas); + if (mSticky != null) { + final int saveCount = canvas.save(); + // The view to draw when sticking to the top + final View drawTarget = mStickyContainer != null ? mStickyContainer : mSticky; + // The offset to draw the view at when sticky + final int drawOffset = mStickyContainer != null ? mSticky.getTop() : 0; + // Position of the draw target, relative to the outside of the scrollView + final int drawTop = drawTarget.getTop(); + if (drawTop + drawOffset < mStatusBarInset || !drawTarget.isShown()) { + // ListView does not translate the canvas, so we can simply draw at the top + canvas.translate(0, -drawOffset + mStatusBarInset); + canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight()); + drawTarget.draw(canvas); + mStickyRect.set(0, -drawOffset + mStatusBarInset, drawTarget.getWidth(), + drawTarget.getHeight() - drawOffset + mStatusBarInset); + } else { + mStickyRect.setEmpty(); + } + canvas.restoreToCount(saveCount); + } + // Draw the scrollbars last so they are on top of the header + mDrawScrollBar = true; + onDrawScrollBars(canvas); + } + + @Override + protected boolean isVerticalScrollBarHidden() { + return super.isVerticalScrollBarHidden() || !mDrawScrollBar; + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + if (getFitsSystemWindows()) { + mStatusBarInset = insets.getSystemWindowInsetTop(); + insets.consumeSystemWindowInsets(false, true, false, false); + } + return insets; + } +} diff --git a/src/com/android/settings/widget/SwitchBar.java b/src/com/android/settings/widget/SwitchBar.java new file mode 100644 index 0000000..c15ac41 --- /dev/null +++ b/src/com/android/settings/widget/SwitchBar.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2014 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.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.LinearLayout; + +import android.widget.Switch; +import android.widget.TextView; +import com.android.settings.R; + +import java.util.ArrayList; + +public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener, + View.OnClickListener { + + public static interface OnSwitchChangeListener { + /** + * Called when the checked state of the Switch has changed. + * + * @param switchView The Switch view whose state has changed. + * @param isChecked The new checked state of switchView. + */ + void onSwitchChanged(Switch switchView, boolean isChecked); + } + + private ToggleSwitch mSwitch; + private TextView mTextView; + + private ArrayList<OnSwitchChangeListener> mSwitchChangeListeners = + new ArrayList<OnSwitchChangeListener>(); + + private static int[] MARGIN_ATTRIBUTES = { + R.attr.switchBarMarginStart, R.attr.switchBarMarginEnd}; + + public SwitchBar(Context context) { + this(context, null); + } + + public SwitchBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SwitchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(R.layout.switch_bar, this); + + final TypedArray a = context.obtainStyledAttributes(attrs, MARGIN_ATTRIBUTES); + int switchBarMarginStart = (int) a.getDimension(0, 0); + int switchBarMarginEnd = (int) a.getDimension(1, 0); + a.recycle(); + + mTextView = (TextView) findViewById(R.id.switch_text); + mTextView.setText(R.string.switch_off_text); + ViewGroup.MarginLayoutParams lp = (MarginLayoutParams) mTextView.getLayoutParams(); + lp.setMarginStart(switchBarMarginStart); + + mSwitch = (ToggleSwitch) findViewById(R.id.switch_widget); + // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch + // on our own + mSwitch.setSaveEnabled(false); + lp = (MarginLayoutParams) mSwitch.getLayoutParams(); + lp.setMarginEnd(switchBarMarginEnd); + + addOnSwitchChangeListener(new OnSwitchChangeListener() { + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + setTextViewLabel(isChecked); + } + }); + + setOnClickListener(this); + + // Default is hide + setVisibility(View.GONE); + } + + public void setTextViewLabel(boolean isChecked) { + mTextView.setText(isChecked ? R.string.switch_on_text : R.string.switch_off_text); + } + + public void setChecked(boolean checked) { + setTextViewLabel(checked); + mSwitch.setChecked(checked); + } + + public void setCheckedInternal(boolean checked) { + setTextViewLabel(checked); + mSwitch.setCheckedInternal(checked); + } + + public boolean isChecked() { + return mSwitch.isChecked(); + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mTextView.setEnabled(enabled); + mSwitch.setEnabled(enabled); + } + + public final ToggleSwitch getSwitch() { + return mSwitch; + } + + public void show() { + if (!isShowing()) { + setVisibility(View.VISIBLE); + mSwitch.setOnCheckedChangeListener(this); + } + } + + public void hide() { + if (isShowing()) { + setVisibility(View.GONE); + mSwitch.setOnCheckedChangeListener(null); + } + } + + public boolean isShowing() { + return (getVisibility() == View.VISIBLE); + } + + @Override + public void onClick(View v) { + final boolean isChecked = !mSwitch.isChecked(); + setChecked(isChecked); + } + + public void propagateChecked(boolean isChecked) { + final int count = mSwitchChangeListeners.size(); + for (int n = 0; n < count; n++) { + mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + propagateChecked(isChecked); + } + + public void addOnSwitchChangeListener(OnSwitchChangeListener listener) { + if (mSwitchChangeListeners.contains(listener)) { + throw new IllegalStateException("Cannot add twice the same OnSwitchChangeListener"); + } + mSwitchChangeListeners.add(listener); + } + + public void removeOnSwitchChangeListener(OnSwitchChangeListener listener) { + if (!mSwitchChangeListeners.contains(listener)) { + throw new IllegalStateException("Cannot remove OnSwitchChangeListener"); + } + mSwitchChangeListeners.remove(listener); + } + + static class SavedState extends BaseSavedState { + boolean checked; + boolean visible; + + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + checked = (Boolean)in.readValue(null); + visible = (Boolean)in.readValue(null); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(checked); + out.writeValue(visible); + } + + @Override + public String toString() { + return "SwitchBar.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " checked=" + checked + + " visible=" + visible + "}"; + } + + 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]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.checked = mSwitch.isChecked(); + ss.visible = isShowing(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + mSwitch.setCheckedInternal(ss.checked); + setTextViewLabel(ss.checked); + setVisibility(ss.visible ? View.VISIBLE : View.GONE); + mSwitch.setOnCheckedChangeListener(ss.visible ? this : null); + + requestLayout(); + } +} diff --git a/src/com/android/settings/accessibility/ToggleSwitch.java b/src/com/android/settings/widget/ToggleSwitch.java index e7c39e4..8232ff1 100644 --- a/src/com/android/settings/accessibility/ToggleSwitch.java +++ b/src/com/android/settings/widget/ToggleSwitch.java @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.widget; import android.content.Context; +import android.util.AttributeSet; import android.widget.Switch; public class ToggleSwitch extends Switch { + private ToggleSwitch.OnBeforeCheckedChangeListener mOnBeforeListener; public static interface OnBeforeCheckedChangeListener { @@ -30,6 +32,18 @@ public class ToggleSwitch extends Switch { super(context); } + public ToggleSwitch(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) { mOnBeforeListener = listener; } diff --git a/src/com/android/settings/wifi/AccessPoint.java b/src/com/android/settings/wifi/AccessPoint.java index c4d1f7c..8e71819 100644 --- a/src/com/android/settings/wifi/AccessPoint.java +++ b/src/com/android/settings/wifi/AccessPoint.java @@ -19,6 +19,8 @@ package com.android.settings.wifi; import com.android.settings.R; import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.net.NetworkInfo.DetailedState; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; @@ -28,12 +30,46 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.preference.Preference; import android.util.Log; +import android.util.LruCache; import android.view.View; import android.widget.ImageView; +import android.widget.TextView; + +import java.util.Map; + class AccessPoint extends Preference { static final String TAG = "Settings.AccessPoint"; + /** + * Lower bound on the 2.4 GHz (802.11b/g/n) WLAN channels + */ + public static final int LOWER_FREQ_24GHZ = 2400; + + /** + * Upper bound on the 2.4 GHz (802.11b/g/n) WLAN channels + */ + public static final int HIGHER_FREQ_24GHZ = 2500; + + /** + * Lower bound on the 5.0 GHz (802.11a/h/j/n/ac) WLAN channels + */ + public static final int LOWER_FREQ_5GHZ = 4900; + + /** + * Upper bound on the 5.0 GHz (802.11a/h/j/n/ac) WLAN channels + */ + public static final int HIGHER_FREQ_5GHZ = 5900; + + /** + * Experimental: we should be able to show the user the list of BSSIDs and bands + * for that SSID. + * For now this data is used only with Verbose Logging so as to show the band and number + * of BSSIDs on which that network is seen. + */ + public LruCache<String, ScanResult> mScanResultCache; + + private static final String KEY_DETAILEDSTATE = "key_detailedstate"; private static final String KEY_WIFIINFO = "key_wifiinfo"; private static final String KEY_SCANRESULT = "key_scanresult"; @@ -44,7 +80,11 @@ class AccessPoint extends Preference { }; private static final int[] STATE_NONE = {}; - /** These values are matched in string arrays -- changes must be kept in sync */ + private static int[] wifi_signal_attributes = { R.attr.wifi_signal }; + + /** + * These values are matched in string arrays -- changes must be kept in sync + */ static final int SECURITY_NONE = 0; static final int SECURITY_WEP = 1; static final int SECURITY_PSK = 2; @@ -60,18 +100,25 @@ class AccessPoint extends Preference { String ssid; String bssid; int security; - int networkId; + int networkId = -1; boolean wpsAvailable = false; + boolean showSummary = true; PskType pskType = PskType.UNKNOWN; private WifiConfiguration mConfig; /* package */ScanResult mScanResult; - private int mRssi; + private int mRssi = Integer.MAX_VALUE; + private long mSeen = 0; + private WifiInfo mInfo; private DetailedState mState; + private static final int VISIBILITY_MAX_AGE_IN_MILLI = 1000000; + private static final int VISIBILITY_OUTDATED_AGE_IN_MILLI = 20000; + private static final int SECOND_TO_MILLI = 1000; + static int getSecurity(WifiConfiguration config) { if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) { return SECURITY_PSK; @@ -142,21 +189,18 @@ class AccessPoint extends Preference { AccessPoint(Context context, WifiConfiguration config) { super(context); - setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); loadConfig(config); refresh(); } AccessPoint(Context context, ScanResult result) { super(context); - setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); loadResult(result); refresh(); } AccessPoint(Context context, Bundle savedState) { super(context); - setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); mConfig = savedState.getParcelable(KEY_CONFIG); if (mConfig != null) { @@ -187,7 +231,6 @@ class AccessPoint extends Preference { bssid = config.BSSID; security = getSecurity(config); networkId = config.networkId; - mRssi = Integer.MAX_VALUE; mConfig = config; } @@ -198,23 +241,48 @@ class AccessPoint extends Preference { wpsAvailable = security != SECURITY_EAP && result.capabilities.contains("WPS"); if (security == SECURITY_PSK) pskType = getPskType(result); - networkId = -1; mRssi = result.level; mScanResult = result; + if (result.seen > mSeen) { + mSeen = result.seen; + } } @Override protected void onBindView(View view) { super.onBindView(view); - ImageView signal = (ImageView) view.findViewById(R.id.signal); - if (mRssi == Integer.MAX_VALUE) { - signal.setImageDrawable(null); + updateIcon(getLevel(), getContext()); + + final TextView summaryView = (TextView) view.findViewById( + com.android.internal.R.id.summary); + summaryView.setVisibility(showSummary ? View.VISIBLE : View.GONE); + + notifyChanged(); + } + + protected void updateIcon(int level, Context context) { + if (level == -1) { + setIcon(null); } else { - signal.setImageLevel(getLevel()); - signal.setImageDrawable(getContext().getTheme().obtainStyledAttributes( - new int[] {R.attr.wifi_signal}).getDrawable(0)); - signal.setImageState((security != SECURITY_NONE) ? - STATE_SECURED : STATE_NONE, true); + Drawable drawable = getIcon(); + + if (drawable == null) { + // To avoid a drawing race condition, we first set the state (SECURE/NONE) and then + // set the icon (drawable) to that state's drawable. + StateListDrawable sld = (StateListDrawable) context.getTheme() + .obtainStyledAttributes(wifi_signal_attributes).getDrawable(0); + // If sld is null then we are indexing and therefore do not have access to + // (nor need to display) the drawable. + if (sld != null) { + sld.setState((security != SECURITY_NONE) ? STATE_SECURED : STATE_NONE); + drawable = sld.getCurrent(); + setIcon(drawable); + } + } + + if (drawable != null) { + drawable.setLevel(level); + } } } @@ -231,6 +299,7 @@ class AccessPoint extends Preference { // Reachable one goes before unreachable one. if (mRssi != Integer.MAX_VALUE && other.mRssi == Integer.MAX_VALUE) return -1; if (mRssi == Integer.MAX_VALUE && other.mRssi != Integer.MAX_VALUE) return 1; + if (mRssi == Integer.MAX_VALUE && other.mRssi != Integer.MAX_VALUE) return 1; // Configured one goes before unconfigured one. if (networkId != WifiConfiguration.INVALID_NETWORK_ID @@ -264,6 +333,16 @@ class AccessPoint extends Preference { } boolean update(ScanResult result) { + if (result.seen > mSeen) { + mSeen = result.seen; + } + if (WifiSettings.mVerboseLogging > 0) { + if (mScanResultCache == null) { + mScanResultCache = new LruCache<String, ScanResult>(32); + } + mScanResultCache.put(result.BSSID, result); + } + if (ssid.equals(result.SSID) && security == getSecurity(result)) { if (WifiManager.compareSignalLevel(result.level, mRssi) > 0) { int oldLevel = getLevel(); @@ -276,6 +355,7 @@ class AccessPoint extends Preference { if (security == SECURITY_PSK) { pskType = getPskType(result); } + mScanResult = result; refresh(); return true; } @@ -334,51 +414,241 @@ class AccessPoint extends Preference { return "\"" + string + "\""; } - /** Updates the title and summary; may indirectly call notifyChanged() */ + /** + * Shows or Hides the Summary of an AccessPoint. + * + * @param showSummary true will show the summary, false will hide the summary + */ + public void setShowSummary(boolean showSummary){ + this.showSummary = showSummary; + } + + /** + * Returns the visibility status of the WifiConfiguration. + * + * @return autojoin debugging information + * TODO: use a string formatter + * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"] + * For instance [-40,5/-30,2] + */ + private String getVisibilityStatus() { + StringBuilder visibility = new StringBuilder(); + StringBuilder scans24GHz = null; + StringBuilder scans5GHz = null; + String bssid = null; + + long now = System.currentTimeMillis(); + + if (mInfo != null) { + bssid = mInfo.getBSSID(); + if (bssid != null) { + visibility.append(" ").append(bssid); + } + visibility.append(" score=").append(mInfo.score); + visibility.append(" "); + visibility.append(String.format("tx=%.1f,", mInfo.txSuccessRate)); + visibility.append(String.format("%.1f,", mInfo.txRetriesRate)); + visibility.append(String.format("%.1f ", mInfo.txBadRate)); + visibility.append(String.format("rx=%.1f", mInfo.rxSuccessRate)); + } + + if (mScanResultCache != null) { + int rssi5 = WifiConfiguration.INVALID_RSSI; + int rssi24 = WifiConfiguration.INVALID_RSSI; + int num5 = 0; + int num24 = 0; + int numBlackListed = 0; + int n24 = 0; // Number scan results we included in the string + int n5 = 0; // Number scan results we included in the string + Map<String, ScanResult> list = mScanResultCache.snapshot(); + // TODO: sort list by RSSI or age + for (ScanResult result : list.values()) { + if (result.seen == 0) + continue; + + if (result.autoJoinStatus != ScanResult.ENABLED) numBlackListed++; + + if (result.frequency >= LOWER_FREQ_5GHZ + && result.frequency <= HIGHER_FREQ_5GHZ) { + // Strictly speaking: [4915, 5825] + // number of known BSSID on 5GHz band + num5 = num5 + 1; + } else if (result.frequency >= LOWER_FREQ_24GHZ + && result.frequency <= HIGHER_FREQ_24GHZ) { + // Strictly speaking: [2412, 2482] + // number of known BSSID on 2.4Ghz band + num24 = num24 + 1; + } + + // Ignore results seen, older than 20 seconds + if (now - result.seen > VISIBILITY_OUTDATED_AGE_IN_MILLI) continue; + + if (result.frequency >= LOWER_FREQ_5GHZ + && result.frequency <= HIGHER_FREQ_5GHZ) { + if (result.level > rssi5) { + rssi5 = result.level; + } + if (n5 < 4) { + if (scans5GHz == null) scans5GHz = new StringBuilder(); + scans5GHz.append(" {").append(result.BSSID); + if (bssid != null && result.BSSID.equals(bssid)) scans5GHz.append("*"); + scans5GHz.append("=").append(result.frequency); + scans5GHz.append(",").append(result.level); + if (result.autoJoinStatus != 0) { + scans5GHz.append(",st=").append(result.autoJoinStatus); + } + if (result.numIpConfigFailures != 0) { + scans5GHz.append(",ipf=").append(result.numIpConfigFailures); + } + scans5GHz.append("}"); + n5++; + } + } else if (result.frequency >= LOWER_FREQ_24GHZ + && result.frequency <= HIGHER_FREQ_24GHZ) { + if (result.level > rssi24) { + rssi24 = result.level; + } + if (n24 < 4) { + if (scans24GHz == null) scans24GHz = new StringBuilder(); + scans24GHz.append(" {").append(result.BSSID); + if (bssid != null && result.BSSID.equals(bssid)) scans24GHz.append("*"); + scans24GHz.append("=").append(result.frequency); + scans24GHz.append(",").append(result.level); + if (result.autoJoinStatus != 0) { + scans24GHz.append(",st=").append(result.autoJoinStatus); + } + if (result.numIpConfigFailures != 0) { + scans24GHz.append(",ipf=").append(result.numIpConfigFailures); + } + scans24GHz.append("}"); + n24++; + } + } + } + visibility.append(" ["); + if (num24 > 0) { + visibility.append("(").append(num24).append(")"); + if (n24 <= 4) { + if (scans24GHz != null) { + visibility.append(scans24GHz.toString()); + } + } else { + visibility.append("max=").append(rssi24); + if (scans24GHz != null) { + visibility.append(",").append(scans24GHz.toString()); + } + } + } + visibility.append(";"); + if (num5 > 0) { + visibility.append("(").append(num5).append(")"); + if (n5 <= 4) { + if (scans5GHz != null) { + visibility.append(scans5GHz.toString()); + } + } else { + visibility.append("max=").append(rssi5); + if (scans5GHz != null) { + visibility.append(",").append(scans5GHz.toString()); + } + } + } + if (numBlackListed > 0) + visibility.append("!").append(numBlackListed); + visibility.append("]"); + } else { + if (mRssi != Integer.MAX_VALUE) { + visibility.append(" rssi="); + visibility.append(mRssi); + if (mScanResult != null) { + visibility.append(", f="); + visibility.append(mScanResult.frequency); + } + } + } + + return visibility.toString(); + } + + /** + * Updates the title and summary; may indirectly call notifyChanged(). + */ private void refresh() { setTitle(ssid); - Context context = getContext(); - if (mConfig != null && mConfig.status == WifiConfiguration.Status.DISABLED) { - switch (mConfig.disableReason) { - case WifiConfiguration.DISABLED_AUTH_FAILURE: - setSummary(context.getString(R.string.wifi_disabled_password_failure)); - break; - case WifiConfiguration.DISABLED_DHCP_FAILURE: - case WifiConfiguration.DISABLED_DNS_FAILURE: - setSummary(context.getString(R.string.wifi_disabled_network_failure)); - break; - case WifiConfiguration.DISABLED_UNKNOWN_REASON: - setSummary(context.getString(R.string.wifi_disabled_generic)); + final Context context = getContext(); + updateIcon(getLevel(), context); + + // Force new summary + setSummary(null); + + // Update to new summary + StringBuilder summary = new StringBuilder(); + + if (mState != null) { // This is the active connection + summary.append(Summary.get(context, mState)); + } else if (mConfig != null && ((mConfig.status == WifiConfiguration.Status.DISABLED && + mConfig.disableReason != WifiConfiguration.DISABLED_UNKNOWN_REASON) + || mConfig.autoJoinStatus + >= WifiConfiguration.AUTO_JOIN_DISABLED_ON_AUTH_FAILURE)) { + if (mConfig.autoJoinStatus + >= WifiConfiguration.AUTO_JOIN_DISABLED_ON_AUTH_FAILURE) { + if (mConfig.disableReason == WifiConfiguration.DISABLED_DHCP_FAILURE) { + summary.append(context.getString(R.string.wifi_disabled_network_failure)); + } else { + summary.append(context.getString(R.string.wifi_disabled_password_failure)); + } + } else { + switch (mConfig.disableReason) { + case WifiConfiguration.DISABLED_AUTH_FAILURE: + summary.append(context.getString(R.string.wifi_disabled_password_failure)); + break; + case WifiConfiguration.DISABLED_DHCP_FAILURE: + case WifiConfiguration.DISABLED_DNS_FAILURE: + summary.append(context.getString(R.string.wifi_disabled_network_failure)); + break; + case WifiConfiguration.DISABLED_UNKNOWN_REASON: + case WifiConfiguration.DISABLED_ASSOCIATION_REJECT: + summary.append(context.getString(R.string.wifi_disabled_generic)); + break; + } } } else if (mRssi == Integer.MAX_VALUE) { // Wifi out of range - setSummary(context.getString(R.string.wifi_not_in_range)); - } else if (mState != null) { // This is the active connection - setSummary(Summary.get(context, mState)); + summary.append(context.getString(R.string.wifi_not_in_range)); } else { // In range, not disabled. - StringBuilder summary = new StringBuilder(); if (mConfig != null) { // Is saved network summary.append(context.getString(R.string.wifi_remembered)); } + } - if (security != SECURITY_NONE) { - String securityStrFormat; - if (summary.length() == 0) { - securityStrFormat = context.getString(R.string.wifi_secured_first_item); - } else { - securityStrFormat = context.getString(R.string.wifi_secured_second_item); - } - summary.append(String.format(securityStrFormat, getSecurityString(true))); + if (WifiSettings.mVerboseLogging > 0) { + //add RSSI/band information for this config, what was seen up to 6 seconds ago + //verbose WiFi Logging is only turned on thru developers settings + if (mInfo != null && mState != null) { // This is the active connection + summary.append(" (f=" + Integer.toString(mInfo.getFrequency()) + ")"); } - - if (mConfig == null && wpsAvailable) { // Only list WPS available for unsaved networks - if (summary.length() == 0) { - summary.append(context.getString(R.string.wifi_wps_available_first_item)); - } else { - summary.append(context.getString(R.string.wifi_wps_available_second_item)); + summary.append(" " + getVisibilityStatus()); + if (mConfig != null && mConfig.autoJoinStatus > 0) { + summary.append(" (" + mConfig.autoJoinStatus); + if (mConfig.blackListTimestamp > 0) { + long now = System.currentTimeMillis(); + long diff = (now - mConfig.blackListTimestamp)/1000; + long sec = diff%60; //seconds + long min = (diff/60)%60; //minutes + long hour = (min/60)%60; //hours + summary.append(", "); + if (hour > 0) summary.append(Long.toString(hour) + "h "); + summary.append( Long.toString(min) + "m "); + summary.append( Long.toString(sec) + "s "); } + summary.append(")"); } + } + + if (summary.length() > 0) { setSummary(summary.toString()); + } else { + showSummary = false; } } diff --git a/src/com/android/settings/wifi/AdvancedWifiSettings.java b/src/com/android/settings/wifi/AdvancedWifiSettings.java index bbcd50d..bda13ff 100644 --- a/src/com/android/settings/wifi/AdvancedWifiSettings.java +++ b/src/com/android/settings/wifi/AdvancedWifiSettings.java @@ -16,18 +16,25 @@ package com.android.settings.wifi; +import android.app.Dialog; +import android.app.DialogFragment; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.NetworkScoreManager; +import android.net.NetworkScorerAppManager; +import android.net.NetworkScorerAppManager.NetworkScorerAppData; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.net.wifi.WifiWatchdogStateMachine; +import android.net.wifi.WpsInfo; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; import android.provider.Settings; import android.provider.Settings.Global; import android.security.Credentials; @@ -39,6 +46,8 @@ import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import java.util.Collection; + public class AdvancedWifiSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener { @@ -48,12 +57,15 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment 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_POOR_NETWORK_DETECTION = "wifi_poor_network_detection"; private static final String KEY_SCAN_ALWAYS_AVAILABLE = "wifi_scan_always_available"; private static final String KEY_INSTALL_CREDENTIALS = "install_credentials"; - private static final String KEY_SUSPEND_OPTIMIZATIONS = "suspend_optimizations"; + private static final String KEY_WIFI_ASSISTANT = "wifi_assistant"; + private static final String KEY_WIFI_DIRECT = "wifi_direct"; + private static final String KEY_WPS_PUSH = "wps_push_button"; + private static final String KEY_WPS_PIN = "wps_pin_entry"; private WifiManager mWifiManager; + private NetworkScoreManager mNetworkScoreManager; private IntentFilter mFilter; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -80,14 +92,15 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment mFilter = new IntentFilter(); mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION); mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + mNetworkScoreManager = + (NetworkScoreManager) getSystemService(Context.NETWORK_SCORE_SERVICE); } @Override public void onResume() { super.onResume(); initPreferences(); - getActivity().registerReceiver(mReceiver, mFilter, - android.Manifest.permission.CHANGE_NETWORK_STATE, null); + getActivity().registerReceiver(mReceiver, mFilter); refreshWifiInfo(); } @@ -98,41 +111,63 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment } private void initPreferences() { - CheckBoxPreference notifyOpenNetworks = - (CheckBoxPreference) findPreference(KEY_NOTIFY_OPEN_NETWORKS); + SwitchPreference notifyOpenNetworks = + (SwitchPreference) findPreference(KEY_NOTIFY_OPEN_NETWORKS); notifyOpenNetworks.setChecked(Settings.Global.getInt(getContentResolver(), Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 0) == 1); notifyOpenNetworks.setEnabled(mWifiManager.isWifiEnabled()); - CheckBoxPreference poorNetworkDetection = - (CheckBoxPreference) findPreference(KEY_POOR_NETWORK_DETECTION); - if (poorNetworkDetection != null) { - if (Utils.isWifiOnly(getActivity())) { - getPreferenceScreen().removePreference(poorNetworkDetection); - } else { - poorNetworkDetection.setChecked(Global.getInt(getContentResolver(), - Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, - WifiWatchdogStateMachine.DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED ? - 1 : 0) == 1); - } - } - - CheckBoxPreference scanAlwaysAvailable = - (CheckBoxPreference) findPreference(KEY_SCAN_ALWAYS_AVAILABLE); + SwitchPreference scanAlwaysAvailable = + (SwitchPreference) findPreference(KEY_SCAN_ALWAYS_AVAILABLE); scanAlwaysAvailable.setChecked(Global.getInt(getContentResolver(), Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1); - Intent intent=new Intent(Credentials.INSTALL_AS_USER_ACTION); + Intent intent = new Intent(Credentials.INSTALL_AS_USER_ACTION); intent.setClassName("com.android.certinstaller", "com.android.certinstaller.CertInstallerMain"); intent.putExtra(Credentials.EXTRA_INSTALL_AS_UID, android.os.Process.WIFI_UID); Preference pref = findPreference(KEY_INSTALL_CREDENTIALS); pref.setIntent(intent); - CheckBoxPreference suspendOptimizations = - (CheckBoxPreference) findPreference(KEY_SUSPEND_OPTIMIZATIONS); - suspendOptimizations.setChecked(Global.getInt(getContentResolver(), - Global.WIFI_SUSPEND_OPTIMIZATIONS_ENABLED, 1) == 1); + final Context context = getActivity(); + NetworkScorerAppData scorer = WifiSettings.getWifiAssistantApp(context); + SwitchPreference wifiAssistant = (SwitchPreference)findPreference(KEY_WIFI_ASSISTANT); + if (scorer != null) { + final boolean checked = NetworkScorerAppManager.getActiveScorer(context) != null; + wifiAssistant.setSummary(getResources().getString( + R.string.wifi_automatically_manage_summary, scorer.mScorerName)); + wifiAssistant.setOnPreferenceChangeListener(this); + wifiAssistant.setChecked(checked); + } else { + if (wifiAssistant != null) { + getPreferenceScreen().removePreference(wifiAssistant); + } + } + + Intent wifiDirectIntent = new Intent(context, + com.android.settings.Settings.WifiP2pSettingsActivity.class); + Preference wifiDirectPref = findPreference(KEY_WIFI_DIRECT); + wifiDirectPref.setIntent(wifiDirectIntent); + + // WpsDialog: Create the dialog like WifiSettings does. + Preference wpsPushPref = findPreference(KEY_WPS_PUSH); + wpsPushPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference arg0) { + WpsFragment wpsFragment = new WpsFragment(WpsInfo.PBC); + wpsFragment.show(getFragmentManager(), KEY_WPS_PUSH); + return true; + } + }); + + // WpsDialog: Create the dialog like WifiSettings does. + Preference wpsPinPref = findPreference(KEY_WPS_PIN); + wpsPinPref.setOnPreferenceClickListener(new OnPreferenceClickListener(){ + public boolean onPreferenceClick(Preference arg0) { + WpsFragment wpsFragment = new WpsFragment(WpsInfo.DISPLAY); + wpsFragment.show(getFragmentManager(), KEY_WPS_PIN); + return true; + } + }); ListPreference frequencyPref = (ListPreference) findPreference(KEY_FREQUENCY_BAND); @@ -154,7 +189,7 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment ListPreference sleepPolicyPref = (ListPreference) findPreference(KEY_SLEEP_POLICY); if (sleepPolicyPref != null) { - if (Utils.isWifiOnly(getActivity())) { + if (Utils.isWifiOnly(context)) { sleepPolicyPref.setEntries(R.array.wifi_sleep_policy_entries_wifi_only); } sleepPolicyPref.setOnPreferenceChangeListener(this); @@ -199,19 +234,11 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment if (KEY_NOTIFY_OPEN_NETWORKS.equals(key)) { Global.putInt(getContentResolver(), Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, - ((CheckBoxPreference) preference).isChecked() ? 1 : 0); - } else if (KEY_POOR_NETWORK_DETECTION.equals(key)) { - Global.putInt(getContentResolver(), - Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED, - ((CheckBoxPreference) preference).isChecked() ? 1 : 0); - } else if (KEY_SUSPEND_OPTIMIZATIONS.equals(key)) { - Global.putInt(getContentResolver(), - Global.WIFI_SUSPEND_OPTIMIZATIONS_ENABLED, - ((CheckBoxPreference) preference).isChecked() ? 1 : 0); + ((SwitchPreference) preference).isChecked() ? 1 : 0); } else if (KEY_SCAN_ALWAYS_AVAILABLE.equals(key)) { Global.putInt(getContentResolver(), Global.WIFI_SCAN_ALWAYS_AVAILABLE, - ((CheckBoxPreference) preference).isChecked() ? 1 : 0); + ((SwitchPreference) preference).isChecked() ? 1 : 0); } else { return super.onPreferenceTreeClick(screen, preference); } @@ -220,6 +247,7 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + final Context context = getActivity(); String key = preference.getKey(); if (KEY_FREQUENCY_BAND.equals(key)) { @@ -228,10 +256,32 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment mWifiManager.setFrequencyBand(value, true); updateFrequencyBandSummary(preference, value); } catch (NumberFormatException e) { - Toast.makeText(getActivity(), R.string.wifi_setting_frequency_band_error, + Toast.makeText(context, R.string.wifi_setting_frequency_band_error, Toast.LENGTH_SHORT).show(); return false; } + } else if (KEY_WIFI_ASSISTANT.equals(key)) { + if (((Boolean)newValue).booleanValue() == false) { + mNetworkScoreManager.setActiveScorer(null); + return true; + } + + NetworkScorerAppData wifiAssistant = WifiSettings.getWifiAssistantApp(context); + Intent intent = new Intent(); + if (wifiAssistant.mConfigurationActivityClassName != null) { + // App has a custom configuration activity; launch that. + // This custom activity will be responsible for launching the system + // dialog. + intent.setClassName(wifiAssistant.mPackageName, + wifiAssistant.mConfigurationActivityClassName); + } else { + // Fall back on the system dialog. + intent.setAction(NetworkScoreManager.ACTION_CHANGE_ACTIVE); + intent.putExtra(NetworkScoreManager.EXTRA_PACKAGE_NAME, + wifiAssistant.mPackageName); + } + + startActivity(intent); } if (KEY_SLEEP_POLICY.equals(key)) { @@ -241,7 +291,7 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment Integer.parseInt(stringValue)); updateSleepPolicySummary(preference, stringValue); } catch (NumberFormatException e) { - Toast.makeText(getActivity(), R.string.wifi_setting_sleep_policy_error, + Toast.makeText(context, R.string.wifi_setting_sleep_policy_error, Toast.LENGTH_SHORT).show(); return false; } @@ -251,17 +301,40 @@ public class AdvancedWifiSettings extends SettingsPreferenceFragment } private void refreshWifiInfo() { + final Context context = getActivity(); 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)); + : context.getString(R.string.status_unavailable)); + wifiMacAddressPref.setSelectable(false); Preference wifiIpAddressPref = findPreference(KEY_CURRENT_IP_ADDRESS); - String ipAddress = Utils.getWifiIpAddresses(getActivity()); + String ipAddress = Utils.getWifiIpAddresses(context); wifiIpAddressPref.setSummary(ipAddress == null ? - getActivity().getString(R.string.status_unavailable) : ipAddress); + context.getString(R.string.status_unavailable) : ipAddress); + wifiIpAddressPref.setSelectable(false); + } + + /* Wrapper class for the WPS dialog to properly handle life cycle events like rotation. */ + public static class WpsFragment extends DialogFragment { + private static int mWpsSetup; + + // Public default constructor is required for rotation. + public WpsFragment() { + super(); + } + + public WpsFragment(int wpsSetup) { + super(); + mWpsSetup = wpsSetup; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new WpsDialog(getActivity(), mWpsSetup); + } } } diff --git a/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java b/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java new file mode 100644 index 0000000..bea720c --- /dev/null +++ b/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2014 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.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; + +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; + +import android.util.Log; +import android.view.View; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * UI to manage saved networks/access points. + */ +public class SavedAccessPointsWifiSettings extends SettingsPreferenceFragment + implements DialogInterface.OnClickListener, Indexable { + private static final String TAG = "SavedAccessPointsWifiSettings"; + + private WifiDialog mDialog; + private WifiManager mWifiManager; + private AccessPoint mDlgAccessPoint; + private Bundle mAccessPointSavedState; + private AccessPoint mSelectedAccessPoint; + + // Instance state key + private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.wifi_display_saved_access_points); + } + + @Override + public void onResume() { + super.onResume(); + initPreferences(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { + mAccessPointSavedState = + savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); + } + } + } + + private void initPreferences() { + PreferenceScreen preferenceScreen = getPreferenceScreen(); + final Context context = getActivity(); + + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + final List<AccessPoint> accessPoints = constructSavedAccessPoints(context, mWifiManager); + + preferenceScreen.removeAll(); + + final int accessPointsSize = accessPoints.size(); + for (int i = 0; i < accessPointsSize; ++i){ + preferenceScreen.addPreference(accessPoints.get(i)); + } + + if(getPreferenceScreen().getPreferenceCount() < 1) { + Log.w(TAG, "Saved networks activity loaded, but there are no saved networks!"); + } + } + + private static List<AccessPoint> constructSavedAccessPoints(Context context, + WifiManager wifiManager){ + List<AccessPoint> accessPoints = new ArrayList<AccessPoint>(); + Map<String, List<ScanResult>> resultsMap = new HashMap<String, List<ScanResult>>(); + + final List<WifiConfiguration> configs = wifiManager.getConfiguredNetworks(); + final List<ScanResult> scanResults = wifiManager.getScanResults(); + + if (configs != null) { + //Construct a Map for quick searching of a wifi network via ssid. + final int scanResultsSize = scanResults.size(); + for (int i = 0; i < scanResultsSize; ++i){ + final ScanResult result = scanResults.get(i); + List<ScanResult> res = resultsMap.get(result.SSID); + + if(res == null){ + res = new ArrayList<ScanResult>(); + resultsMap.put(result.SSID, res); + } + + res.add(result); + } + + final int configsSize = configs.size(); + for (int i = 0; i < configsSize; ++i){ + WifiConfiguration config = configs.get(i); + if (config.selfAdded && config.numAssociation == 0) { + continue; + } + AccessPoint accessPoint = new AccessPoint(context, config); + final List<ScanResult> results = resultsMap.get(accessPoint.ssid); + + accessPoint.setShowSummary(false); + if(results != null){ + final int resultsSize = results.size(); + for (int j = 0; j < resultsSize; ++j){ + accessPoint.update(results.get(j)); + accessPoint.setIcon(null); + } + } + + accessPoints.add(accessPoint); + } + } + + return accessPoints; + } + + private void showDialog(AccessPoint accessPoint, boolean edit) { + if (mDialog != null) { + removeDialog(WifiSettings.WIFI_DIALOG_ID); + mDialog = null; + } + + // Save the access point and edit mode + mDlgAccessPoint = accessPoint; + + showDialog(WifiSettings.WIFI_DIALOG_ID); + } + + @Override + public Dialog onCreateDialog(int dialogId) { + switch (dialogId) { + case WifiSettings.WIFI_DIALOG_ID: + if (mDlgAccessPoint == null) { // For re-launch from saved state + mDlgAccessPoint = new AccessPoint(getActivity(), mAccessPointSavedState); + // Reset the saved access point data + mAccessPointSavedState = null; + } + mSelectedAccessPoint = mDlgAccessPoint; + mDialog = new WifiDialog(getActivity(), this, mDlgAccessPoint, + false /* not editting */, true /* hide the submit button */); + return mDialog; + + } + return super.onCreateDialog(dialogId); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // If the dialog is showing, save its state. + if (mDialog != null && mDialog.isShowing()) { + if (mDlgAccessPoint != null) { + mAccessPointSavedState = new Bundle(); + mDlgAccessPoint.saveWifiState(mAccessPointSavedState); + outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState); + } + } + } + + @Override + public void onClick(DialogInterface dialogInterface, int button) { + if (button == WifiDialog.BUTTON_FORGET && mSelectedAccessPoint != null) { + mWifiManager.forget(mSelectedAccessPoint.networkId, null); + getPreferenceScreen().removePreference(mSelectedAccessPoint); + mSelectedAccessPoint = null; + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (preference instanceof AccessPoint) { + showDialog((AccessPoint) preference, false); + return true; + } else{ + return super.onPreferenceTreeClick(screen, preference); + } + } + + /** + * For search. + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + final Resources res = context.getResources(); + final String title = res.getString(R.string.wifi_saved_access_points_titlebar); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = title; + data.screenTitle = title; + data.enabled = enabled; + result.add(data); + + // Add available Wi-Fi access points + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + final List<AccessPoint> accessPoints = + constructSavedAccessPoints(context, wifiManager); + + final int accessPointsSize = accessPoints.size(); + for (int i = 0; i < accessPointsSize; ++i){ + data = new SearchIndexableRaw(context); + data.title = accessPoints.get(i).getTitle().toString(); + data.screenTitle = title; + data.enabled = enabled; + result.add(data); + } + + return result; + } + }; +} diff --git a/src/com/android/settings/wifi/WifiApDialog.java b/src/com/android/settings/wifi/WifiApDialog.java index 211e85d..fb8026a 100644 --- a/src/com/android/settings/wifi/WifiApDialog.java +++ b/src/com/android/settings/wifi/WifiApDialog.java @@ -47,8 +47,7 @@ public class WifiApDialog extends AlertDialog implements View.OnClickListener, private final DialogInterface.OnClickListener mListener; public static final int OPEN_INDEX = 0; - public static final int WPA_INDEX = 1; - public static final int WPA2_INDEX = 2; + public static final int WPA2_INDEX = 1; private View mView; private TextView mSsid; @@ -68,9 +67,7 @@ public class WifiApDialog extends AlertDialog implements View.OnClickListener, } public static int getSecurityTypeIndex(WifiConfiguration wifiConfig) { - if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) { - return WPA_INDEX; - } else if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) { + if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) { return WPA2_INDEX; } return OPEN_INDEX; @@ -93,15 +90,6 @@ public class WifiApDialog extends AlertDialog implements View.OnClickListener, config.allowedKeyManagement.set(KeyMgmt.NONE); return config; - case WPA_INDEX: - config.allowedKeyManagement.set(KeyMgmt.WPA_PSK); - config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN); - if (mPassword.length() != 0) { - String password = mPassword.getText().toString(); - config.preSharedKey = password; - } - return config; - case WPA2_INDEX: config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK); config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN); @@ -137,8 +125,7 @@ public class WifiApDialog extends AlertDialog implements View.OnClickListener, if (mWifiConfig != null) { mSsid.setText(mWifiConfig.SSID); mSecurity.setSelection(mSecurityTypeIndex); - if (mSecurityTypeIndex == WPA_INDEX || - mSecurityTypeIndex == WPA2_INDEX) { + if (mSecurityTypeIndex == WPA2_INDEX) { mPassword.setText(mWifiConfig.preSharedKey); } } @@ -156,7 +143,7 @@ public class WifiApDialog extends AlertDialog implements View.OnClickListener, private void validate() { if ((mSsid != null && mSsid.length() == 0) || - (((mSecurityTypeIndex == WPA_INDEX) || (mSecurityTypeIndex == WPA2_INDEX))&& + ((mSecurityTypeIndex == WPA2_INDEX)&& mPassword.length() < 8)) { getButton(BUTTON_SUBMIT).setEnabled(false); } else { diff --git a/src/com/android/settings/wifi/WifiApEnabler.java b/src/com/android/settings/wifi/WifiApEnabler.java index 9a3b49d..fc34f3b 100644 --- a/src/com/android/settings/wifi/WifiApEnabler.java +++ b/src/com/android/settings/wifi/WifiApEnabler.java @@ -33,7 +33,7 @@ import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.preference.CheckBoxPreference; +import android.preference.SwitchPreference; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -41,7 +41,7 @@ import android.widget.Toast; public class WifiApEnabler { private final Context mContext; - private final CheckBoxPreference mCheckBox; + private final SwitchPreference mSwitch; private final CharSequence mOriginalSummary; private WifiManager mWifiManager; @@ -66,17 +66,17 @@ public class WifiApEnabler { ConnectivityManager.EXTRA_ERRORED_TETHER); updateTetherState(available.toArray(), active.toArray(), errored.toArray()); } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) { - enableWifiCheckBox(); + enableWifiSwitch(); } } }; - public WifiApEnabler(Context context, CheckBoxPreference checkBox) { + public WifiApEnabler(Context context, SwitchPreference switchPreference) { mContext = context; - mCheckBox = checkBox; - mOriginalSummary = checkBox.getSummary(); - checkBox.setPersistent(false); + mSwitch = switchPreference; + mOriginalSummary = switchPreference.getSummary(); + switchPreference.setPersistent(false); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mCm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -90,21 +90,21 @@ public class WifiApEnabler { public void resume() { mContext.registerReceiver(mReceiver, mIntentFilter); - enableWifiCheckBox(); + enableWifiSwitch(); } public void pause() { mContext.unregisterReceiver(mReceiver); } - private void enableWifiCheckBox() { + private void enableWifiSwitch() { boolean isAirplaneMode = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0; if(!isAirplaneMode) { - mCheckBox.setEnabled(true); + mSwitch.setEnabled(true); } else { - mCheckBox.setSummary(mOriginalSummary); - mCheckBox.setEnabled(false); + mSwitch.setSummary(mOriginalSummary); + mSwitch.setEnabled(false); } } @@ -122,9 +122,9 @@ public class WifiApEnabler { if (mWifiManager.setWifiApEnabled(null, enable)) { /* Disable here, enabled on receiving success broadcast */ - mCheckBox.setEnabled(false); + mSwitch.setEnabled(false); } else { - mCheckBox.setSummary(R.string.wifi_error); + mSwitch.setSummary(R.string.wifi_error); } /** @@ -147,7 +147,7 @@ public class WifiApEnabler { public void updateConfigSummary(WifiConfiguration wifiConfig) { String s = mContext.getString( com.android.internal.R.string.wifi_tether_configure_ssid_default); - mCheckBox.setSummary(String.format( + mSwitch.setSummary(String.format( mContext.getString(R.string.wifi_tether_enabled_subtext), (wifiConfig == null) ? s : wifiConfig.SSID)); } @@ -173,38 +173,38 @@ public class WifiApEnabler { WifiConfiguration wifiConfig = mWifiManager.getWifiApConfiguration(); updateConfigSummary(wifiConfig); } else if (wifiErrored) { - mCheckBox.setSummary(R.string.wifi_error); + mSwitch.setSummary(R.string.wifi_error); } } private void handleWifiApStateChanged(int state) { switch (state) { case WifiManager.WIFI_AP_STATE_ENABLING: - mCheckBox.setSummary(R.string.wifi_tether_starting); - mCheckBox.setEnabled(false); + mSwitch.setSummary(R.string.wifi_tether_starting); + mSwitch.setEnabled(false); break; case WifiManager.WIFI_AP_STATE_ENABLED: /** * Summary on enable is handled by tether * broadcast notice */ - mCheckBox.setChecked(true); + mSwitch.setChecked(true); /* Doesnt need the airplane check */ - mCheckBox.setEnabled(true); + mSwitch.setEnabled(true); break; case WifiManager.WIFI_AP_STATE_DISABLING: - mCheckBox.setSummary(R.string.wifi_tether_stopping); - mCheckBox.setEnabled(false); + mSwitch.setSummary(R.string.wifi_tether_stopping); + mSwitch.setEnabled(false); break; case WifiManager.WIFI_AP_STATE_DISABLED: - mCheckBox.setChecked(false); - mCheckBox.setSummary(mOriginalSummary); - enableWifiCheckBox(); + mSwitch.setChecked(false); + mSwitch.setSummary(mOriginalSummary); + enableWifiSwitch(); break; default: - mCheckBox.setChecked(false); - mCheckBox.setSummary(R.string.wifi_error); - enableWifiCheckBox(); + mSwitch.setChecked(false); + mSwitch.setSummary(R.string.wifi_error); + enableWifiSwitch(); } } } diff --git a/src/com/android/settings/wifi/WifiConfigController.java b/src/com/android/settings/wifi/WifiConfigController.java index 55dc033..e6dd9c7 100644 --- a/src/com/android/settings/wifi/WifiConfigController.java +++ b/src/com/android/settings/wifi/WifiConfigController.java @@ -18,24 +18,28 @@ package com.android.settings.wifi; import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; import android.net.LinkAddress; -import android.net.LinkProperties; import android.net.NetworkInfo.DetailedState; import android.net.NetworkUtils; -import android.net.ProxyProperties; +import android.net.ProxyInfo; import android.net.RouteInfo; +import android.net.StaticIpConfiguration; +import android.net.Uri; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.AuthAlgorithm; -import android.net.wifi.WifiConfiguration.IpAssignment; import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WifiConfiguration.ProxySettings; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiEnterpriseConfig.Eap; import android.net.wifi.WifiEnterpriseConfig.Phase2; import android.net.wifi.WifiInfo; import android.os.Handler; +import android.os.UserHandle; import android.security.Credentials; import android.security.KeyStore; import android.text.Editable; @@ -59,6 +63,7 @@ import com.android.settings.ProxySelector; import com.android.settings.R; import java.net.InetAddress; +import java.net.Inet4Address; import java.util.Iterator; /** @@ -67,36 +72,12 @@ import java.util.Iterator; */ public class WifiConfigController implements TextWatcher, AdapterView.OnItemSelectedListener, OnCheckedChangeListener { + private static final String TAG = "WifiConfigController"; + private final WifiConfigUiBase mConfigUi; private final View mView; private final AccessPoint mAccessPoint; - private boolean mEdit; - - private TextView mSsidView; - - // e.g. AccessPoint.SECURITY_NONE - private int mAccessPointSecurity; - private TextView mPasswordView; - - private String unspecifiedCert = "unspecified"; - private static final int unspecifiedCertIndex = 0; - - /* Phase2 methods supported by PEAP are limited */ - private final ArrayAdapter<String> PHASE2_PEAP_ADAPTER; - /* Full list of phase2 methods */ - private final ArrayAdapter<String> PHASE2_FULL_ADAPTER; - - private Spinner mSecuritySpinner; - private Spinner mEapMethodSpinner; - private Spinner mEapCaCertSpinner; - private Spinner mPhase2Spinner; - // Associated with mPhase2Spinner, one of PHASE2_FULL_ADAPTER or PHASE2_PEAP_ADAPTER - private ArrayAdapter<String> mPhase2Adapter; - private Spinner mEapUserCertSpinner; - private TextView mEapIdentityView; - private TextView mEapAnonymousView; - /* This value comes from "wifi_ip_settings" resource array */ private static final int DHCP = 0; private static final int STATIC_IP = 1; @@ -104,6 +85,7 @@ public class WifiConfigController implements TextWatcher, /* These values come from "wifi_proxy_settings" resource array */ public static final int PROXY_NONE = 0; public static final int PROXY_STATIC = 1; + public static final int PROXY_PAC = 2; /* These values come from "wifi_eap_method" resource array */ public static final int WIFI_EAP_METHOD_PEAP = 0; @@ -116,7 +98,32 @@ public class WifiConfigController implements TextWatcher, public static final int WIFI_PEAP_PHASE2_MSCHAPV2 = 1; public static final int WIFI_PEAP_PHASE2_GTC = 2; - private static final String TAG = "WifiConfigController"; + /* Phase2 methods supported by PEAP are limited */ + private final ArrayAdapter<String> PHASE2_PEAP_ADAPTER; + /* Full list of phase2 methods */ + private final ArrayAdapter<String> PHASE2_FULL_ADAPTER; + + // True when this instance is used in SetupWizard XL context. + private final boolean mInXlSetupWizard; + + private final Handler mTextViewChangedHandler; + + // e.g. AccessPoint.SECURITY_NONE + private int mAccessPointSecurity; + private TextView mPasswordView; + + private String unspecifiedCert = "unspecified"; + private static final int unspecifiedCertIndex = 0; + + private Spinner mSecuritySpinner; + private Spinner mEapMethodSpinner; + private Spinner mEapCaCertSpinner; + private Spinner mPhase2Spinner; + // Associated with mPhase2Spinner, one of PHASE2_FULL_ADAPTER or PHASE2_PEAP_ADAPTER + private ArrayAdapter<String> mPhase2Adapter; + private Spinner mEapUserCertSpinner; + private TextView mEapIdentityView; + private TextView mEapAnonymousView; private Spinner mIpSettingsSpinner; private TextView mIpAddressView; @@ -129,15 +136,18 @@ public class WifiConfigController implements TextWatcher, private TextView mProxyHostView; private TextView mProxyPortView; private TextView mProxyExclusionListView; + private TextView mProxyPacView; private IpAssignment mIpAssignment = IpAssignment.UNASSIGNED; private ProxySettings mProxySettings = ProxySettings.UNASSIGNED; - private LinkProperties mLinkProperties = new LinkProperties(); + private ProxyInfo mHttpProxy = null; + private StaticIpConfiguration mStaticIpConfiguration = null; - // True when this instance is used in SetupWizard XL context. - private final boolean mInXlSetupWizard; + private String[] mLevels; + private boolean mEdit; + private TextView mSsidView; - private final Handler mTextViewChangedHandler; + private Context mContext; public WifiConfigController( WifiConfigUiBase parent, View view, AccessPoint accessPoint, boolean edit) { @@ -151,20 +161,21 @@ public class WifiConfigController implements TextWatcher, mEdit = edit; mTextViewChangedHandler = new Handler(); - final Context context = mConfigUi.getContext(); - final Resources resources = context.getResources(); + mContext = mConfigUi.getContext(); + final Resources res = mContext.getResources(); + mLevels = res.getStringArray(R.array.wifi_signal); PHASE2_PEAP_ADAPTER = new ArrayAdapter<String>( - context, android.R.layout.simple_spinner_item, - context.getResources().getStringArray(R.array.wifi_peap_phase2_entries)); + mContext, android.R.layout.simple_spinner_item, + res.getStringArray(R.array.wifi_peap_phase2_entries)); PHASE2_PEAP_ADAPTER.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); PHASE2_FULL_ADAPTER = new ArrayAdapter<String>( - context, android.R.layout.simple_spinner_item, - context.getResources().getStringArray(R.array.wifi_phase2_entries)); + mContext, android.R.layout.simple_spinner_item, + res.getStringArray(R.array.wifi_phase2_entries)); PHASE2_FULL_ADAPTER.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - unspecifiedCert = context.getString(R.string.wifi_unspecified); + unspecifiedCert = mContext.getString(R.string.wifi_unspecified); mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings); mIpSettingsSpinner.setOnItemSelectedListener(this); mProxySettingsSpinner = (Spinner) mView.findViewById(R.id.proxy_settings); @@ -182,9 +193,9 @@ public class WifiConfigController implements TextWatcher, mView.findViewById(R.id.type_security).setVisibility(View.VISIBLE); // We want custom layout. The content must be same as the other cases. - ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, + ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, R.layout.wifi_setup_custom_list_item_1, android.R.id.text1, - context.getResources().getStringArray(R.array.wifi_security_no_eap)); + res.getStringArray(R.array.wifi_security_no_eap)); mSecuritySpinner.setAdapter(adapter); } else { mView.findViewById(R.id.type).setVisibility(View.VISIBLE); @@ -197,54 +208,34 @@ public class WifiConfigController implements TextWatcher, .setOnCheckedChangeListener(this); - mConfigUi.setSubmitButton(context.getString(R.string.wifi_save)); + mConfigUi.setSubmitButton(res.getString(R.string.wifi_save)); } else { mConfigUi.setTitle(mAccessPoint.ssid); ViewGroup group = (ViewGroup) mView.findViewById(R.id.info); - DetailedState state = mAccessPoint.getState(); - if (state != null) { - addRow(group, R.string.wifi_status, Summary.get(mConfigUi.getContext(), state)); - } - - int level = mAccessPoint.getLevel(); - if (level != -1) { - String[] signal = resources.getStringArray(R.array.wifi_signal); - addRow(group, R.string.wifi_signal, signal[level]); - } - - WifiInfo info = mAccessPoint.getInfo(); - if (info != null && info.getLinkSpeed() != -1) { - addRow(group, R.string.wifi_speed, info.getLinkSpeed() + WifiInfo.LINK_SPEED_UNITS); - } - - addRow(group, R.string.wifi_security, mAccessPoint.getSecurityString(false)); - boolean showAdvancedFields = false; if (mAccessPoint.networkId != INVALID_NETWORK_ID) { WifiConfiguration config = mAccessPoint.getConfig(); - if (config.ipAssignment == IpAssignment.STATIC) { + if (config.getIpAssignment() == IpAssignment.STATIC) { mIpSettingsSpinner.setSelection(STATIC_IP); showAdvancedFields = true; + // Display IP address. + StaticIpConfiguration staticConfig = config.getStaticIpConfiguration(); + if (staticConfig != null && staticConfig.ipAddress != null) { + addRow(group, R.string.wifi_ip_address, + staticConfig.ipAddress.getAddress().getHostAddress()); + } } else { mIpSettingsSpinner.setSelection(DHCP); } - //Display IP addresses - for(InetAddress a : config.linkProperties.getAddresses()) { - addRow(group, R.string.wifi_ip_address, a.getHostAddress()); - } - if (config.proxySettings == ProxySettings.STATIC) { + if (config.getProxySettings() == ProxySettings.STATIC) { mProxySettingsSpinner.setSelection(PROXY_STATIC); showAdvancedFields = true; - } else if (config.proxySettings == ProxySettings.PAC) { - mProxySettingsSpinner.setVisibility(View.GONE); - TextView textView = (TextView)mView.findViewById(R.id.proxy_pac_info); - textView.setVisibility(View.VISIBLE); - textView.setText(context.getString(R.string.proxy_url) + - config.linkProperties.getHttpProxy().getPacFileUrl()); + } else if (config.getProxySettings() == ProxySettings.PAC) { + mProxySettingsSpinner.setSelection(PROXY_PAC); showAdvancedFields = true; } else { mProxySettingsSpinner.setSelection(PROXY_NONE); @@ -265,21 +256,62 @@ public class WifiConfigController implements TextWatcher, } if (mEdit) { - mConfigUi.setSubmitButton(context.getString(R.string.wifi_save)); + mConfigUi.setSubmitButton(res.getString(R.string.wifi_save)); } else { - if (state == null && level != -1) { - mConfigUi.setSubmitButton(context.getString(R.string.wifi_connect)); + final DetailedState state = mAccessPoint.getState(); + final String signalLevel = getSignalString(); + + if (state == null && signalLevel != null) { + mConfigUi.setSubmitButton(res.getString(R.string.wifi_connect)); } else { + if (state != null) { + addRow(group, R.string.wifi_status, Summary.get(mConfigUi.getContext(), + state)); + } + + if (signalLevel != null) { + addRow(group, R.string.wifi_signal, signalLevel); + } + + WifiInfo info = mAccessPoint.getInfo(); + if (info != null && info.getLinkSpeed() != -1) { + addRow(group, R.string.wifi_speed, info.getLinkSpeed() + + WifiInfo.LINK_SPEED_UNITS); + } + + if (info != null && info.getFrequency() != -1) { + final int frequency = info.getFrequency(); + String band = null; + + if (frequency >= AccessPoint.LOWER_FREQ_24GHZ + && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { + band = res.getString(R.string.wifi_band_24ghz); + } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ + && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { + band = res.getString(R.string.wifi_band_5ghz); + } else { + Log.e(TAG, "Unexpected frequency " + frequency); + } + if (band != null) { + addRow(group, R.string.wifi_frequency, band); + } + } + + addRow(group, R.string.wifi_security, mAccessPoint.getSecurityString(false)); mView.findViewById(R.id.ip_fields).setVisibility(View.GONE); } - if (mAccessPoint.networkId != INVALID_NETWORK_ID) { - mConfigUi.setForgetButton(context.getString(R.string.wifi_forget)); + if (mAccessPoint.networkId != INVALID_NETWORK_ID + && ActivityManager.getCurrentUser() == UserHandle.USER_OWNER) { + mConfigUi.setForgetButton(res.getString(R.string.wifi_forget)); } } } - - mConfigUi.setCancelButton(context.getString(R.string.wifi_cancel)); + if (mEdit || (mAccessPoint.getState() == null && mAccessPoint.getLevel() != -1)){ + mConfigUi.setCancelButton(res.getString(R.string.wifi_cancel)); + }else{ + mConfigUi.setCancelButton(res.getString(R.string.wifi_display_options_done)); + } if (mConfigUi.getSubmitButton() != null) { enableSubmitIfAppropriate(); } @@ -292,6 +324,19 @@ public class WifiConfigController implements TextWatcher, group.addView(row); } + private String getSignalString(){ + final int level = mAccessPoint.getLevel(); + + return (level > -1 && level < mLevels.length) ? mLevels[level] : null; + } + + void hideSubmitButton() { + Button submit = mConfigUi.getSubmitButton(); + if (submit == null) return; + + submit.setVisibility(View.GONE); + } + /* show submit button if password, ip and proxy settings are valid */ void enableSubmitIfAppropriate() { Button submit = mConfigUi.getSubmitButton(); @@ -430,31 +475,31 @@ public class WifiConfigController implements TextWatcher, return null; } - config.proxySettings = mProxySettings; - config.ipAssignment = mIpAssignment; - config.linkProperties = new LinkProperties(mLinkProperties); + config.setIpConfiguration( + new IpConfiguration(mIpAssignment, mProxySettings, + mStaticIpConfiguration, mHttpProxy)); return config; } private boolean ipAndProxyFieldsAreValid() { - mLinkProperties.clear(); mIpAssignment = (mIpSettingsSpinner != null && mIpSettingsSpinner.getSelectedItemPosition() == STATIC_IP) ? IpAssignment.STATIC : IpAssignment.DHCP; if (mIpAssignment == IpAssignment.STATIC) { - int result = validateIpConfigFields(mLinkProperties); + mStaticIpConfiguration = new StaticIpConfiguration(); + int result = validateIpConfigFields(mStaticIpConfiguration); if (result != 0) { return false; } } - mProxySettings = (mProxySettingsSpinner != null && - mProxySettingsSpinner.getSelectedItemPosition() == PROXY_STATIC) ? - ProxySettings.STATIC : ProxySettings.NONE; - - if (mProxySettings == ProxySettings.STATIC && mProxyHostView != null) { + final int selectedPosition = mProxySettingsSpinner.getSelectedItemPosition(); + mProxySettings = ProxySettings.NONE; + mHttpProxy = null; + if (selectedPosition == PROXY_STATIC && mProxyHostView != null) { + mProxySettings = ProxySettings.STATIC; String host = mProxyHostView.getText().toString(); String portStr = mProxyPortView.getText().toString(); String exclusionList = mProxyExclusionListView.getText().toString(); @@ -467,25 +512,41 @@ public class WifiConfigController implements TextWatcher, result = R.string.proxy_error_invalid_port; } if (result == 0) { - ProxyProperties proxyProperties= new ProxyProperties(host, port, exclusionList); - mLinkProperties.setHttpProxy(proxyProperties); + mHttpProxy = new ProxyInfo(host, port, exclusionList); } else { return false; } + } else if (selectedPosition == PROXY_PAC && mProxyPacView != null) { + mProxySettings = ProxySettings.PAC; + CharSequence uriSequence = mProxyPacView.getText(); + if (TextUtils.isEmpty(uriSequence)) { + return false; + } + Uri uri = Uri.parse(uriSequence.toString()); + if (uri == null) { + return false; + } + mHttpProxy = new ProxyInfo(uri); } return true; } - private int validateIpConfigFields(LinkProperties linkProperties) { + private Inet4Address getIPv4Address(String text) { + try { + return (Inet4Address) NetworkUtils.numericToInetAddress(text); + } catch (IllegalArgumentException|ClassCastException e) { + return null; + } + } + + private int validateIpConfigFields(StaticIpConfiguration staticIpConfiguration) { if (mIpAddressView == null) return 0; String ipAddr = mIpAddressView.getText().toString(); if (TextUtils.isEmpty(ipAddr)) return R.string.wifi_ip_settings_invalid_ip_address; - InetAddress inetAddr = null; - try { - inetAddr = NetworkUtils.numericToInetAddress(ipAddr); - } catch (IllegalArgumentException e) { + Inet4Address inetAddr = getIPv4Address(ipAddr); + if (inetAddr == null) { return R.string.wifi_ip_settings_invalid_ip_address; } @@ -495,7 +556,7 @@ public class WifiConfigController implements TextWatcher, if (networkPrefixLength < 0 || networkPrefixLength > 32) { return R.string.wifi_ip_settings_invalid_network_prefix_length; } - linkProperties.addLinkAddress(new LinkAddress(inetAddr, networkPrefixLength)); + staticIpConfiguration.ipAddress = new LinkAddress(inetAddr, networkPrefixLength); } catch (NumberFormatException e) { // Set the hint as default after user types in ip address mNetworkPrefixLengthView.setText(mConfigUi.getContext().getString( @@ -514,13 +575,11 @@ public class WifiConfigController implements TextWatcher, } catch (java.net.UnknownHostException u) { } } else { - InetAddress gatewayAddr = null; - try { - gatewayAddr = NetworkUtils.numericToInetAddress(gateway); - } catch (IllegalArgumentException e) { + InetAddress gatewayAddr = getIPv4Address(gateway); + if (gatewayAddr == null) { return R.string.wifi_ip_settings_invalid_gateway; } - linkProperties.addRoute(new RouteInfo(gatewayAddr)); + staticIpConfiguration.gateway = gatewayAddr; } String dns = mDns1View.getText().toString(); @@ -530,22 +589,20 @@ public class WifiConfigController implements TextWatcher, //If everything else is valid, provide hint as a default option mDns1View.setText(mConfigUi.getContext().getString(R.string.wifi_dns1_hint)); } else { - try { - dnsAddr = NetworkUtils.numericToInetAddress(dns); - } catch (IllegalArgumentException e) { + dnsAddr = getIPv4Address(dns); + if (dnsAddr == null) { return R.string.wifi_ip_settings_invalid_dns; } - linkProperties.addDns(dnsAddr); + staticIpConfiguration.dnsServers.add(dnsAddr); } if (mDns2View.length() > 0) { dns = mDns2View.getText().toString(); - try { - dnsAddr = NetworkUtils.numericToInetAddress(dns); - } catch (IllegalArgumentException e) { + dnsAddr = getIPv4Address(dns); + if (dnsAddr == null) { return R.string.wifi_ip_settings_invalid_dns; } - linkProperties.addDns(dnsAddr); + staticIpConfiguration.dnsServers.add(dnsAddr); } return 0; } @@ -756,28 +813,26 @@ public class WifiConfigController implements TextWatcher, mDns2View.addTextChangedListener(this); } if (config != null) { - LinkProperties linkProperties = config.linkProperties; - Iterator<LinkAddress> iterator = linkProperties.getLinkAddresses().iterator(); - if (iterator.hasNext()) { - LinkAddress linkAddress = iterator.next(); - mIpAddressView.setText(linkAddress.getAddress().getHostAddress()); - mNetworkPrefixLengthView.setText(Integer.toString(linkAddress - .getNetworkPrefixLength())); - } + StaticIpConfiguration staticConfig = config.getStaticIpConfiguration(); + if (staticConfig != null) { + if (staticConfig.ipAddress != null) { + mIpAddressView.setText( + staticConfig.ipAddress.getAddress().getHostAddress()); + mNetworkPrefixLengthView.setText(Integer.toString(staticConfig.ipAddress + .getNetworkPrefixLength())); + } - for (RouteInfo route : linkProperties.getRoutes()) { - if (route.isDefaultRoute()) { - mGatewayView.setText(route.getGateway().getHostAddress()); - break; + if (staticConfig.gateway != null) { + mGatewayView.setText(staticConfig.gateway.getHostAddress()); } - } - Iterator<InetAddress> dnsIterator = linkProperties.getDnses().iterator(); - if (dnsIterator.hasNext()) { - mDns1View.setText(dnsIterator.next().getHostAddress()); - } - if (dnsIterator.hasNext()) { - mDns2View.setText(dnsIterator.next().getHostAddress()); + Iterator<InetAddress> dnsIterator = staticConfig.dnsServers.iterator(); + if (dnsIterator.hasNext()) { + mDns1View.setText(dnsIterator.next().getHostAddress()); + } + if (dnsIterator.hasNext()) { + mDns2View.setText(dnsIterator.next().getHostAddress()); + } } } } else { @@ -795,8 +850,9 @@ public class WifiConfigController implements TextWatcher, } if (mProxySettingsSpinner.getSelectedItemPosition() == PROXY_STATIC) { - mView.findViewById(R.id.proxy_warning_limited_support).setVisibility(View.VISIBLE); - mView.findViewById(R.id.proxy_fields).setVisibility(View.VISIBLE); + setVisibility(R.id.proxy_warning_limited_support, View.VISIBLE); + setVisibility(R.id.proxy_fields, View.VISIBLE); + setVisibility(R.id.proxy_pac_field, View.GONE); if (mProxyHostView == null) { mProxyHostView = (TextView) mView.findViewById(R.id.proxy_hostname); mProxyHostView.addTextChangedListener(this); @@ -806,20 +862,41 @@ public class WifiConfigController implements TextWatcher, mProxyExclusionListView.addTextChangedListener(this); } if (config != null) { - ProxyProperties proxyProperties = config.linkProperties.getHttpProxy(); + ProxyInfo proxyProperties = config.getHttpProxy(); if (proxyProperties != null) { mProxyHostView.setText(proxyProperties.getHost()); mProxyPortView.setText(Integer.toString(proxyProperties.getPort())); - mProxyExclusionListView.setText(proxyProperties.getExclusionList()); + mProxyExclusionListView.setText(proxyProperties.getExclusionListAsString()); + } + } + } else if (mProxySettingsSpinner.getSelectedItemPosition() == PROXY_PAC) { + setVisibility(R.id.proxy_warning_limited_support, View.GONE); + setVisibility(R.id.proxy_fields, View.GONE); + setVisibility(R.id.proxy_pac_field, View.VISIBLE); + + if (mProxyPacView == null) { + mProxyPacView = (TextView) mView.findViewById(R.id.proxy_pac); + mProxyPacView.addTextChangedListener(this); + } + if (config != null) { + ProxyInfo proxyInfo = config.getHttpProxy(); + if (proxyInfo != null) { + mProxyPacView.setText(proxyInfo.getPacFileUrl().toString()); } } } else { - mView.findViewById(R.id.proxy_warning_limited_support).setVisibility(View.GONE); - mView.findViewById(R.id.proxy_fields).setVisibility(View.GONE); + setVisibility(R.id.proxy_warning_limited_support, View.GONE); + setVisibility(R.id.proxy_fields, View.GONE); + setVisibility(R.id.proxy_pac_field, View.GONE); } } - + private void setVisibility(int id, int visibility) { + final View v = mView.findViewById(id); + if (v != null) { + v.setVisibility(visibility); + } + } private void loadCertificates(Spinner spinner, String prefix) { final Context context = mConfigUi.getContext(); diff --git a/src/com/android/settings/wifi/WifiDialog.java b/src/com/android/settings/wifi/WifiDialog.java index f1720c1..942c5dd 100644 --- a/src/com/android/settings/wifi/WifiDialog.java +++ b/src/com/android/settings/wifi/WifiDialog.java @@ -35,6 +35,13 @@ class WifiDialog extends AlertDialog implements WifiConfigUiBase { private View mView; private WifiConfigController mController; + private boolean mHideSubmitButton; + + public WifiDialog(Context context, DialogInterface.OnClickListener listener, + AccessPoint accessPoint, boolean edit, boolean hideSubmitButton) { + this(context, listener, accessPoint, edit); + mHideSubmitButton = hideSubmitButton; + } public WifiDialog(Context context, DialogInterface.OnClickListener listener, AccessPoint accessPoint, boolean edit) { @@ -42,6 +49,7 @@ class WifiDialog extends AlertDialog implements WifiConfigUiBase { mEdit = edit; mListener = listener; mAccessPoint = accessPoint; + mHideSubmitButton = false; } @Override @@ -56,9 +64,14 @@ class WifiDialog extends AlertDialog implements WifiConfigUiBase { setInverseBackgroundForced(true); mController = new WifiConfigController(this, mView, mAccessPoint, mEdit); super.onCreate(savedInstanceState); - /* During creation, the submit button can be unavailable to determine - * visibility. Right after creation, update button visibility */ - mController.enableSubmitIfAppropriate(); + + if (mHideSubmitButton) { + mController.hideSubmitButton(); + } else { + /* During creation, the submit button can be unavailable to determine + * visibility. Right after creation, update button visibility */ + mController.enableSubmitIfAppropriate(); + } } @Override diff --git a/src/com/android/settings/wifi/WifiEnabler.java b/src/com/android/settings/wifi/WifiEnabler.java index a6989f0..0952941 100644 --- a/src/com/android/settings/wifi/WifiEnabler.java +++ b/src/com/android/settings/wifi/WifiEnabler.java @@ -20,25 +20,27 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.database.ContentObserver; import android.net.NetworkInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.os.UserHandle; +import android.os.Handler; +import android.os.Message; 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; +import com.android.settings.search.Index; +import com.android.settings.widget.SwitchBar; import java.util.concurrent.atomic.AtomicBoolean; -public class WifiEnabler implements CompoundButton.OnCheckedChangeListener { - private final Context mContext; - private Switch mSwitch; +public class WifiEnabler implements SwitchBar.OnSwitchChangeListener { + private Context mContext; + private SwitchBar mSwitchBar; + private boolean mListeningToOnSwitchChange = false; private AtomicBoolean mConnected = new AtomicBoolean(false); private final WifiManager mWifiManager; @@ -65,98 +67,110 @@ public class WifiEnabler implements CompoundButton.OnCheckedChangeListener { } }; - public WifiEnabler(Context context, Switch switch_) { + private static final String EVENT_DATA_IS_WIFI_ON = "is_wifi_on"; + private static final int EVENT_UPDATE_INDEX = 0; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_UPDATE_INDEX: + final boolean isWiFiOn = msg.getData().getBoolean(EVENT_DATA_IS_WIFI_ON); + Index.getInstance(mContext).updateFromClassNameResource( + WifiSettings.class.getName(), true, isWiFiOn); + break; + } + } + }; + + public WifiEnabler(Context context, SwitchBar switchBar) { mContext = context; - mSwitch = switch_; + mSwitchBar = switchBar; mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mIntentFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); // The order matters! We really should not depend on this. :( mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); - } - public void resume() { - // Wi-Fi state is sticky, so just let the receiver update UI - mContext.registerReceiver(mReceiver, mIntentFilter); - mSwitch.setOnCheckedChangeListener(this); - } - - public void pause() { - mContext.unregisterReceiver(mReceiver); - mSwitch.setOnCheckedChangeListener(null); + setupSwitchBar(); } - 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 setupSwitchBar() { + final int state = mWifiManager.getWifiState(); + handleWifiStateChanged(state); + if (!mListeningToOnSwitchChange) { + mSwitchBar.addOnSwitchChangeListener(this); + mListeningToOnSwitchChange = true; + } + mSwitchBar.show(); } - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - //Do nothing if called as a result of a state machine event - if (mStateMachineEvent) { - return; - } - // Show toast message if Wi-Fi is not allowed in airplane mode - if (isChecked && !WirelessSettings.isRadioAllowed(mContext, Settings.Global.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); - return; + public void teardownSwitchBar() { + if (mListeningToOnSwitchChange) { + mSwitchBar.removeOnSwitchChangeListener(this); + mListeningToOnSwitchChange = false; } + mSwitchBar.hide(); + } - // Disable tethering if enabling Wifi - int wifiApState = mWifiManager.getWifiApState(); - if (isChecked && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || - (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { - mWifiManager.setWifiApEnabled(null, false); + public void resume(Context context) { + mContext = context; + // Wi-Fi state is sticky, so just let the receiver update UI + mContext.registerReceiver(mReceiver, mIntentFilter); + if (!mListeningToOnSwitchChange) { + mSwitchBar.addOnSwitchChangeListener(this); + mListeningToOnSwitchChange = true; } + } - mSwitch.setEnabled(false); - if (!mWifiManager.setWifiEnabled(isChecked)) { - // Error - mSwitch.setEnabled(true); - Toast.makeText(mContext, R.string.wifi_error, Toast.LENGTH_SHORT).show(); + public void pause() { + mContext.unregisterReceiver(mReceiver); + if (mListeningToOnSwitchChange) { + mSwitchBar.removeOnSwitchChangeListener(this); + mListeningToOnSwitchChange = false; } } private void handleWifiStateChanged(int state) { switch (state) { case WifiManager.WIFI_STATE_ENABLING: - mSwitch.setEnabled(false); + mSwitchBar.setEnabled(false); break; case WifiManager.WIFI_STATE_ENABLED: - setSwitchChecked(true); - mSwitch.setEnabled(true); + setSwitchBarChecked(true); + mSwitchBar.setEnabled(true); + updateSearchIndex(true); break; case WifiManager.WIFI_STATE_DISABLING: - mSwitch.setEnabled(false); + mSwitchBar.setEnabled(false); break; case WifiManager.WIFI_STATE_DISABLED: - setSwitchChecked(false); - mSwitch.setEnabled(true); + setSwitchBarChecked(false); + mSwitchBar.setEnabled(true); + updateSearchIndex(false); break; default: - setSwitchChecked(false); - mSwitch.setEnabled(true); - break; + setSwitchBarChecked(false); + mSwitchBar.setEnabled(true); + updateSearchIndex(false); } } - private void setSwitchChecked(boolean checked) { - if (checked != mSwitch.isChecked()) { - mStateMachineEvent = true; - mSwitch.setChecked(checked); - mStateMachineEvent = false; - } + private void updateSearchIndex(boolean isWiFiOn) { + mHandler.removeMessages(EVENT_UPDATE_INDEX); + + Message msg = new Message(); + msg.what = EVENT_UPDATE_INDEX; + msg.getData().putBoolean(EVENT_DATA_IS_WIFI_ON, isWiFiOn); + mHandler.sendMessage(msg); + } + + private void setSwitchBarChecked(boolean checked) { + mStateMachineEvent = true; + mSwitchBar.setChecked(checked); + mStateMachineEvent = false; } private void handleStateChanged(@SuppressWarnings("unused") NetworkInfo.DetailedState state) { @@ -174,4 +188,32 @@ public class WifiEnabler implements CompoundButton.OnCheckedChangeListener { } */ } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + //Do nothing if called as a result of a state machine event + if (mStateMachineEvent) { + return; + } + // Show toast message if Wi-Fi is not allowed in airplane mode + if (isChecked && !WirelessSettings.isRadioAllowed(mContext, Settings.Global.RADIO_WIFI)) { + Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show(); + // Reset switch to off. No infinite check/listenenr loop. + mSwitchBar.setChecked(false); + return; + } + + // Disable tethering if enabling Wifi + int wifiApState = mWifiManager.getWifiApState(); + if (isChecked && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || + (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { + mWifiManager.setWifiApEnabled(null, false); + } + + if (!mWifiManager.setWifiEnabled(isChecked)) { + // Error + mSwitchBar.setEnabled(true); + Toast.makeText(mContext, R.string.wifi_error, Toast.LENGTH_SHORT).show(); + } + } } diff --git a/src/com/android/settings/wifi/WifiPickerActivity.java b/src/com/android/settings/wifi/WifiPickerActivity.java index e1e7c51..5bdceb9 100644 --- a/src/com/android/settings/wifi/WifiPickerActivity.java +++ b/src/com/android/settings/wifi/WifiPickerActivity.java @@ -16,31 +16,24 @@ package com.android.settings.wifi; import com.android.settings.ButtonBarHandler; -import com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; +import com.android.settings.SettingsActivity; import com.android.settings.wifi.p2p.WifiP2pSettings; +import com.android.settings.R; -import android.app.Fragment; import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceActivity; -import android.widget.Button; +import android.preference.PreferenceFragment; -public class WifiPickerActivity extends PreferenceActivity implements ButtonBarHandler { +import java.lang.Class; - // Same as what are in PreferenceActivity as private. - private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; - private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; - private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; - private static final String EXTRA_WIFI_SHOW_ACTION_BAR = "wifi_show_action_bar"; - private static final String EXTRA_WIFI_SHOW_MENUS = "wifi_show_menus"; +public class WifiPickerActivity extends SettingsActivity implements ButtonBarHandler { @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); if (!modIntent.hasExtra(EXTRA_SHOW_FRAGMENT)) { - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, WifiSettings.class.getName()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getWifiSettingsClass().getName()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, R.string.wifi_select_network); } - modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } @@ -48,62 +41,12 @@ public class WifiPickerActivity extends PreferenceActivity implements ButtonBarH protected boolean isValidFragment(String fragmentName) { if (WifiSettings.class.getName().equals(fragmentName) || WifiP2pSettings.class.getName().equals(fragmentName) + || SavedAccessPointsWifiSettings.class.getName().equals(fragmentName) || AdvancedWifiSettings.class.getName().equals(fragmentName)) return true; return false; } - /** - * Almost dead copy of - * {@link PreferenceActivity#startWithFragment(String, Bundle, Fragment, int)}, except - * this has additional codes for button bar handling. - */ - @Override - public void startWithFragment(String fragmentName, Bundle args, - Fragment resultTo, int resultRequestCode) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(this, getClass()); - intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); - intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); - intent.putExtra(EXTRA_NO_HEADERS, true); - - final Intent orgIntent = getIntent(); - if (orgIntent.hasExtra(EXTRA_PREFS_SHOW_BUTTON_BAR)) { - intent.putExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, - orgIntent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)); - } - if (orgIntent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { - intent.putExtra(EXTRA_PREFS_SET_NEXT_TEXT, - orgIntent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT)); - } - if (orgIntent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { - intent.putExtra(EXTRA_PREFS_SET_BACK_TEXT, - orgIntent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT)); - } - if (orgIntent.hasExtra(EXTRA_WIFI_SHOW_ACTION_BAR)) { - intent.putExtra(EXTRA_WIFI_SHOW_ACTION_BAR, - orgIntent.getBooleanExtra(EXTRA_WIFI_SHOW_ACTION_BAR, true)); - } - if (orgIntent.hasExtra(EXTRA_WIFI_SHOW_MENUS)) { - intent.putExtra(EXTRA_WIFI_SHOW_MENUS, - orgIntent.getBooleanExtra(EXTRA_WIFI_SHOW_MENUS, true)); - } - - if (resultTo == null) { - startActivity(intent); - } else { - resultTo.startActivityForResult(intent, resultRequestCode); - } - } - - @Override - public boolean hasNextButton() { - // PreferenceActivity#hasNextButton() is protected, so we need to expose it here. - return super.hasNextButton(); - } - - @Override - public Button getNextButton() { - // PreferenceActivity#getNextButton() is protected, so we need to expose it here. - return super.getNextButton(); + /* package */ Class<? extends PreferenceFragment> getWifiSettingsClass() { + return WifiSettings.class; } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java index 1caf58b..6c58bc1 100644 --- a/src/com/android/settings/wifi/WifiSettings.java +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -19,61 +19,57 @@ package com.android.settings.wifi; import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID; import static android.os.UserManager.DISALLOW_CONFIG_WIFI; -import com.android.settings.R; -import com.android.settings.RestrictedSettingsFragment; -import com.android.settings.wifi.p2p.WifiP2pSettings; - -import android.app.ActionBar; import android.app.Activity; -import android.app.AlertDialog; +import android.app.ActivityManager; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; +import android.content.SharedPreferences; import android.content.res.Resources; import android.content.res.TypedArray; import android.location.LocationManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; +import android.net.NetworkScoreManager; +import android.net.NetworkScorerAppManager; +import android.net.NetworkScorerAppManager.NetworkScorerAppData; import android.net.wifi.ScanResult; -import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WpsInfo; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; -import android.provider.Settings; -import android.util.AttributeSet; 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.View.OnClickListener; -import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.Button; -import android.widget.ImageButton; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnMenuItemClickListener; -import android.widget.RelativeLayout; -import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; +import com.android.settings.R; +import com.android.settings.RestrictedSettingsFragment; +import com.android.settings.SettingsActivity; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -90,23 +86,29 @@ import java.util.concurrent.atomic.AtomicBoolean; * and menus. */ public class WifiSettings extends RestrictedSettingsFragment - implements DialogInterface.OnClickListener { + implements DialogInterface.OnClickListener, Indexable { + private static final String TAG = "WifiSettings"; - private static final int MENU_ID_WPS_PBC = Menu.FIRST; + + private static final int REQUEST_ENABLE_WIFI_ASSISTANT = 1; + + /* package */ static final int MENU_ID_WPS_PBC = Menu.FIRST; private static final int MENU_ID_WPS_PIN = Menu.FIRST + 1; - private static final int MENU_ID_P2P = Menu.FIRST + 2; - private static final int MENU_ID_ADD_NETWORK = Menu.FIRST + 3; + private static final int MENU_ID_SAVED_NETWORK = Menu.FIRST + 2; + /* package */ static final int MENU_ID_ADD_NETWORK = Menu.FIRST + 3; private static final int MENU_ID_ADVANCED = Menu.FIRST + 4; private static final int MENU_ID_SCAN = Menu.FIRST + 5; private static final int MENU_ID_CONNECT = Menu.FIRST + 6; private static final int MENU_ID_FORGET = Menu.FIRST + 7; private static final int MENU_ID_MODIFY = Menu.FIRST + 8; + private static final int MENU_ID_WRITE_NFC = Menu.FIRST + 9; - private static final int WIFI_DIALOG_ID = 1; - private static final int WPS_PBC_DIALOG_ID = 2; + private static final String KEY_ASSISTANT_DISMISS_PLATFORM = "assistant_dismiss_platform"; + + public static final int WIFI_DIALOG_ID = 1; + /* package */ static final int WPS_PBC_DIALOG_ID = 2; private static final int WPS_PIN_DIALOG_ID = 3; - private static final int WIFI_SKIPPED_DIALOG_ID = 4; - private static final int WIFI_AND_MOBILE_SKIPPED_DIALOG_ID = 5; + private static final int WRITE_NFC_DIALOG_ID = 6; // Combo scans can take 5-6s to complete - set to 10s. private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; @@ -115,18 +117,16 @@ public class WifiSettings extends RestrictedSettingsFragment private static final String SAVE_DIALOG_EDIT_MODE = "edit_mode"; private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; - // Activity result when pressing the Skip button - private static final int RESULT_SKIP = Activity.RESULT_FIRST_USER; + private static boolean savedNetworksExist; private final IntentFilter mFilter; private final BroadcastReceiver mReceiver; private final Scanner mScanner; - private WifiManager mWifiManager; + /* package */ WifiManager mWifiManager; private WifiManager.ActionListener mConnectListener; private WifiManager.ActionListener mSaveListener; private WifiManager.ActionListener mForgetListener; - private boolean mP2pSupported; private WifiEnabler mWifiEnabler; // An access point being editted is stored here. @@ -138,42 +138,89 @@ public class WifiSettings extends RestrictedSettingsFragment private final AtomicBoolean mConnected = new AtomicBoolean(false); private WifiDialog mDialog; + private WriteWifiConfigToNfcDialog mWifiToNfcDialog; private TextView mEmptyView; - /* Used in Wifi Setup context */ - - // this boolean extra specifies whether to disable the Next button when not connected + // this boolean extra specifies whether to disable the Next button when not connected. Used by + // account creation outside of setup wizard. private static final String EXTRA_ENABLE_NEXT_ON_CONNECT = "wifi_enable_next_on_connect"; - // this boolean extra specifies whether to auto finish when connection is established - private static final String EXTRA_AUTO_FINISH_ON_CONNECT = "wifi_auto_finish_on_connect"; - - // this boolean extra shows a custom button that we can control - protected static final String EXTRA_SHOW_CUSTOM_BUTTON = "wifi_show_custom_button"; - - // show a text regarding data charges when wifi connection is required during setup wizard - protected static final String EXTRA_SHOW_WIFI_REQUIRED_INFO = "wifi_show_wifi_required_info"; - - // this boolean extra is set if we are being invoked by the Setup Wizard - private static final String EXTRA_IS_FIRST_RUN = "firstRun"; - // should Next button only be enabled when we have a connection? private boolean mEnableNextOnConnection; - // should activity finish once we have a connection? - private boolean mAutoFinishOnConnection; - // Save the dialog details private boolean mDlgEdit; private AccessPoint mDlgAccessPoint; private Bundle mAccessPointSavedState; + private View mWifiAssistantCard; + private NetworkScorerAppData mWifiAssistantApp; - // the action bar uses a different set of controls for Setup Wizard - private boolean mSetupWizardMode; + /** verbose logging flag. this flag is set thru developer debugging options + * and used so as to assist with in-the-field WiFi connectivity debugging */ + public static int mVerboseLogging = 0; /* End of "used in Wifi Setup context" */ + /** A restricted multimap for use in constructAccessPoints */ + private static class Multimap<K,V> { + private final HashMap<K,List<V>> store = new HashMap<K,List<V>>(); + /** retrieve a non-null list of values with key K */ + List<V> getAll(K key) { + List<V> values = store.get(key); + return values != null ? values : Collections.<V>emptyList(); + } + + void put(K key, V val) { + List<V> curVals = store.get(key); + if (curVals == null) { + curVals = new ArrayList<V>(3); + store.put(key, curVals); + } + curVals.add(val); + } + } + + private static class Scanner extends Handler { + private int mRetry = 0; + private WifiSettings mWifiSettings = null; + + Scanner(WifiSettings wifiSettings) { + mWifiSettings = wifiSettings; + } + + void resume() { + if (!hasMessages(0)) { + sendEmptyMessage(0); + } + } + + void forceScan() { + removeMessages(0); + sendEmptyMessage(0); + } + + void pause() { + mRetry = 0; + removeMessages(0); + } + + @Override + public void handleMessage(Message message) { + if (mWifiSettings.mWifiManager.startScan()) { + mRetry = 0; + } else if (++mRetry >= 3) { + mRetry = 0; + Activity activity = mWifiSettings.getActivity(); + if (activity != null) { + Toast.makeText(activity, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show(); + } + return; + } + sendEmptyMessageDelayed(0, WIFI_RESCAN_INTERVAL_MS); + } + } + public WifiSettings() { super(DISALLOW_CONFIG_WIFI); mFilter = new IntentFilter(); @@ -189,105 +236,17 @@ public class WifiSettings extends RestrictedSettingsFragment mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - handleEvent(context, intent); + handleEvent(intent); } }; - mScanner = new Scanner(); - } - - @Override - public void onCreate(Bundle icicle) { - // Set this flag early, as it's needed by getHelpResource(), which is called by super - mSetupWizardMode = getActivity().getIntent().getBooleanExtra(EXTRA_IS_FIRST_RUN, false); - - super.onCreate(icicle); - } - - @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - if (mSetupWizardMode) { - View view = inflater.inflate(R.layout.setup_preference, container, false); - View other = view.findViewById(R.id.other_network); - other.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (mWifiManager.isWifiEnabled()) { - onAddNetworkPressed(); - } - } - }); - final ImageButton b = (ImageButton) view.findViewById(R.id.more); - if (b != null) { - b.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (mWifiManager.isWifiEnabled()) { - PopupMenu pm = new PopupMenu(inflater.getContext(), b); - pm.inflate(R.menu.wifi_setup); - pm.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (R.id.wifi_wps == item.getItemId()) { - showDialog(WPS_PBC_DIALOG_ID); - return true; - } - return false; - } - }); - pm.show(); - } - } - }); - } - - Intent intent = getActivity().getIntent(); - if (intent.getBooleanExtra(EXTRA_SHOW_CUSTOM_BUTTON, false)) { - view.findViewById(R.id.button_bar).setVisibility(View.VISIBLE); - view.findViewById(R.id.back_button).setVisibility(View.INVISIBLE); - view.findViewById(R.id.skip_button).setVisibility(View.INVISIBLE); - view.findViewById(R.id.next_button).setVisibility(View.INVISIBLE); - - Button customButton = (Button) view.findViewById(R.id.custom_button); - customButton.setVisibility(View.VISIBLE); - customButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - boolean isConnected = false; - Activity activity = getActivity(); - final ConnectivityManager connectivity = (ConnectivityManager) - activity.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity != null) { - final NetworkInfo info = connectivity.getActiveNetworkInfo(); - isConnected = (info != null) && info.isConnected(); - } - if (isConnected) { - // Warn of possible data charges - showDialog(WIFI_SKIPPED_DIALOG_ID); - } else { - // Warn of lack of updates - showDialog(WIFI_AND_MOBILE_SKIPPED_DIALOG_ID); - } - } - }); - } - - if (intent.getBooleanExtra(EXTRA_SHOW_WIFI_REQUIRED_INFO, false)) { - view.findViewById(R.id.wifi_required_info).setVisibility(View.VISIBLE); - } - - return view; - } else { - return super.onCreateView(inflater, container, savedInstanceState); - } + mScanner = new Scanner(this); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mP2pSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT); mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); mConnectListener = new WifiManager.ActionListener() { @@ -335,42 +294,23 @@ public class WifiSettings extends RestrictedSettingsFragment } }; - if (savedInstanceState != null - && savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { + if (savedInstanceState != null) { mDlgEdit = savedInstanceState.getBoolean(SAVE_DIALOG_EDIT_MODE); - mAccessPointSavedState = savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); - } - - final Activity activity = getActivity(); - final Intent intent = activity.getIntent(); - - // first if we're supposed to finish once we have a connection - mAutoFinishOnConnection = intent.getBooleanExtra(EXTRA_AUTO_FINISH_ON_CONNECT, false); - - if (mAutoFinishOnConnection) { - // Hide the next button - if (hasNextButton()) { - getNextButton().setVisibility(View.GONE); - } - - final ConnectivityManager connectivity = (ConnectivityManager) - activity.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity != null - && connectivity.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected()) { - activity.setResult(Activity.RESULT_OK); - activity.finish(); - return; + if (savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { + mAccessPointSavedState = + savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); } } // if we're supposed to enable/disable the Next button based on our current connection // state, start it off in the right state + Intent intent = getActivity().getIntent(); mEnableNextOnConnection = intent.getBooleanExtra(EXTRA_ENABLE_NEXT_ON_CONNECT, false); if (mEnableNextOnConnection) { if (hasNextButton()) { final ConnectivityManager connectivity = (ConnectivityManager) - activity.getSystemService(Context.CONNECTIVITY_SERVICE); + getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivity != null) { NetworkInfo info = connectivity.getNetworkInfo( ConnectivityManager.TYPE_WIFI); @@ -381,54 +321,60 @@ public class WifiSettings extends RestrictedSettingsFragment addPreferencesFromResource(R.xml.wifi_settings); - if (mSetupWizardMode) { - getView().setSystemUiVisibility( -// View.STATUS_BAR_DISABLE_BACK | - View.STATUS_BAR_DISABLE_HOME | - View.STATUS_BAR_DISABLE_RECENT | - View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS | - View.STATUS_BAR_DISABLE_CLOCK); - } + prepareWifiAssistantCard(); - // On/off switch is hidden for Setup Wizard - if (!mSetupWizardMode) { - 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.setPaddingRelative(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.END)); - } - } + mEmptyView = initEmptyView(); + registerForContextMenu(getListView()); + setHasOptionsMenu(true); + } - mWifiEnabler = new WifiEnabler(activity, actionBarSwitch); + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if (requestCode == REQUEST_ENABLE_WIFI_ASSISTANT) { + if (resultCode == Activity.RESULT_OK) { + disableWifiAssistantCardUntilPlatformUpgrade(); + getListView().removeHeaderView(mWifiAssistantCard); + mWifiAssistantApp = null; + } + } else { + super.onActivityResult(requestCode, resultCode, resultData); } + } - mEmptyView = (TextView) getView().findViewById(android.R.id.empty); - getListView().setEmptyView(mEmptyView); + @Override + public void onDestroyView() { + super.onDestroyView(); - if (!mSetupWizardMode) { - registerForContextMenu(getListView()); + if (mWifiEnabler != null) { + mWifiEnabler.teardownSwitchBar(); } - setHasOptionsMenu(true); + } + + @Override + public void onStart() { + super.onStart(); + + // On/off switch is hidden for Setup Wizard (returns null) + mWifiEnabler = createWifiEnabler(); + } + + /** + * @return new WifiEnabler or null (as overridden by WifiSettingsForSetupWizard) + */ + /* package */ WifiEnabler createWifiEnabler() { + final SettingsActivity activity = (SettingsActivity) getActivity(); + return new WifiEnabler(activity, activity.getSwitchBar()); } @Override public void onResume() { + final Activity activity = getActivity(); super.onResume(); if (mWifiEnabler != null) { - mWifiEnabler.resume(); + mWifiEnabler.resume(activity); } - getActivity().registerReceiver(mReceiver, mFilter); + activity.registerReceiver(mReceiver, mFilter); updateAccessPoints(); } @@ -438,6 +384,7 @@ public class WifiSettings extends RestrictedSettingsFragment if (mWifiEnabler != null) { mWifiEnabler.pause(); } + getActivity().unregisterReceiver(mReceiver); mScanner.pause(); } @@ -445,46 +392,35 @@ public class WifiSettings extends RestrictedSettingsFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // If the user is not allowed to configure wifi, do not show the menu. - if (isRestrictedAndNotPinProtected()) return; + if (isUiRestricted()) return; + addOptionsMenuItems(menu); + super.onCreateOptionsMenu(menu, inflater); + } + + /** + * @param menu + */ + void addOptionsMenuItems(Menu menu) { final boolean wifiIsEnabled = mWifiManager.isWifiEnabled(); TypedArray ta = getActivity().getTheme().obtainStyledAttributes( new int[] {R.attr.ic_menu_add, R.attr.ic_wps}); - if (mSetupWizardMode) { - menu.add(Menu.NONE, MENU_ID_WPS_PBC, 0, R.string.wifi_menu_wps_pbc) - .setIcon(ta.getDrawable(1)) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - } else { - menu.add(Menu.NONE, MENU_ID_WPS_PBC, 0, R.string.wifi_menu_wps_pbc) - .setIcon(ta.getDrawable(1)) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network) + menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network) + .setIcon(ta.getDrawable(0)) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + if (savedNetworksExist) { + menu.add(Menu.NONE, MENU_ID_SAVED_NETWORK, 0, R.string.wifi_saved_access_points_label) .setIcon(ta.getDrawable(0)) .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.add(Menu.NONE, MENU_ID_SCAN, 0, R.string.wifi_menu_scan) - //.setIcon(R.drawable.ic_menu_scan_network) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - menu.add(Menu.NONE, MENU_ID_WPS_PIN, 0, R.string.wifi_menu_wps_pin) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - if (mP2pSupported) { - menu.add(Menu.NONE, MENU_ID_P2P, 0, R.string.wifi_menu_p2p) - .setEnabled(wifiIsEnabled) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - } - menu.add(Menu.NONE, MENU_ID_ADVANCED, 0, R.string.wifi_menu_advanced) - //.setIcon(android.R.drawable.ic_menu_manage) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } + menu.add(Menu.NONE, MENU_ID_SCAN, 0, R.string.menu_stats_refresh) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(Menu.NONE, MENU_ID_ADVANCED, 0, R.string.wifi_menu_advanced) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); ta.recycle(); - super.onCreateOptionsMenu(menu, inflater); } @Override @@ -505,23 +441,26 @@ public class WifiSettings extends RestrictedSettingsFragment @Override public boolean onOptionsItemSelected(MenuItem item) { // If the user is not allowed to configure wifi, do not handle menu selections. - if (isRestrictedAndNotPinProtected()) return false; + if (isUiRestricted()) return false; switch (item.getItemId()) { case MENU_ID_WPS_PBC: showDialog(WPS_PBC_DIALOG_ID); return true; + /* case MENU_ID_P2P: - if (getActivity() instanceof PreferenceActivity) { - ((PreferenceActivity) getActivity()).startPreferencePanel( + if (getActivity() instanceof SettingsActivity) { + ((SettingsActivity) getActivity()).startPreferencePanel( WifiP2pSettings.class.getCanonicalName(), null, R.string.wifi_p2p_settings_title, null, this, 0); } else { - startFragment(this, WifiP2pSettings.class.getCanonicalName(), -1, null); + startFragment(this, WifiP2pSettings.class.getCanonicalName(), + R.string.wifi_p2p_settings_title, -1, null); } return true; + */ case MENU_ID_WPS_PIN: showDialog(WPS_PIN_DIALOG_ID); return true; @@ -535,15 +474,26 @@ public class WifiSettings extends RestrictedSettingsFragment onAddNetworkPressed(); } return true; + case MENU_ID_SAVED_NETWORK: + if (getActivity() instanceof SettingsActivity) { + ((SettingsActivity) getActivity()).startPreferencePanel( + SavedAccessPointsWifiSettings.class.getCanonicalName(), null, + R.string.wifi_saved_access_points_titlebar, null, this, 0); + } else { + startFragment(this, SavedAccessPointsWifiSettings.class.getCanonicalName(), + R.string.wifi_saved_access_points_titlebar, + -1 /* Do not request a result */, null); + } + return true; case MENU_ID_ADVANCED: - if (getActivity() instanceof PreferenceActivity) { - ((PreferenceActivity) getActivity()).startPreferencePanel( - AdvancedWifiSettings.class.getCanonicalName(), - null, - R.string.wifi_advanced_titlebar, null, - this, 0); + if (getActivity() instanceof SettingsActivity) { + ((SettingsActivity) getActivity()).startPreferencePanel( + AdvancedWifiSettings.class.getCanonicalName(), null, + R.string.wifi_advanced_titlebar, null, this, 0); } else { - startFragment(this, AdvancedWifiSettings.class.getCanonicalName(), -1, null); + startFragment(this, AdvancedWifiSettings.class.getCanonicalName(), + R.string.wifi_advanced_titlebar, -1 /* Do not request a results */, + null); } return true; } @@ -564,8 +514,15 @@ public class WifiSettings extends RestrictedSettingsFragment menu.add(Menu.NONE, MENU_ID_CONNECT, 0, R.string.wifi_menu_connect); } if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) { - menu.add(Menu.NONE, MENU_ID_FORGET, 0, R.string.wifi_menu_forget); + if (ActivityManager.getCurrentUser() == UserHandle.USER_OWNER) { + menu.add(Menu.NONE, MENU_ID_FORGET, 0, R.string.wifi_menu_forget); + } menu.add(Menu.NONE, MENU_ID_MODIFY, 0, R.string.wifi_menu_modify); + + if (mSelectedAccessPoint.security != AccessPoint.SECURITY_NONE) { + // Only allow writing of NFC tags for password-protected networks. + menu.add(Menu.NONE, MENU_ID_WRITE_NFC, 0, R.string.wifi_menu_write_to_nfc); + } } } } @@ -579,13 +536,11 @@ public class WifiSettings extends RestrictedSettingsFragment switch (item.getItemId()) { case MENU_ID_CONNECT: { if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) { - mWifiManager.connect(mSelectedAccessPoint.networkId, - mConnectListener); + connect(mSelectedAccessPoint.networkId); } else if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE) { /** Bypass dialog for unsecured networks */ mSelectedAccessPoint.generateOpenNetworkConfig(); - mWifiManager.connect(mSelectedAccessPoint.getConfig(), - mConnectListener); + connect(mSelectedAccessPoint.getConfig()); } else { showDialog(mSelectedAccessPoint, true); } @@ -599,6 +554,10 @@ public class WifiSettings extends RestrictedSettingsFragment showDialog(mSelectedAccessPoint, true); return true; } + case MENU_ID_WRITE_NFC: + showDialog(WRITE_NFC_DIALOG_ID); + return true; + } return super.onContextItemSelected(item); } @@ -611,7 +570,11 @@ public class WifiSettings extends RestrictedSettingsFragment if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE && mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) { mSelectedAccessPoint.generateOpenNetworkConfig(); - mWifiManager.connect(mSelectedAccessPoint.getConfig(), mConnectListener); + if (!savedNetworksExist) { + savedNetworksExist = true; + getActivity().invalidateOptionsMenu(); + } + connect(mSelectedAccessPoint.getConfig()); } else { showDialog(mSelectedAccessPoint, false); } @@ -648,7 +611,7 @@ public class WifiSettings extends RestrictedSettingsFragment mAccessPointSavedState = null; } } - // If it's still null, fine, it's for Add Network + // If it's null, fine, it's for Add Network mSelectedAccessPoint = ap; mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit); return mDialog; @@ -656,73 +619,54 @@ public class WifiSettings extends RestrictedSettingsFragment return new WpsDialog(getActivity(), WpsInfo.PBC); case WPS_PIN_DIALOG_ID: return new WpsDialog(getActivity(), WpsInfo.DISPLAY); - case WIFI_SKIPPED_DIALOG_ID: - return new AlertDialog.Builder(getActivity()) - .setMessage(R.string.wifi_skipped_message) - .setCancelable(false) - .setNegativeButton(R.string.wifi_skip_anyway, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - getActivity().setResult(RESULT_SKIP); - getActivity().finish(); - } - }) - .setPositiveButton(R.string.wifi_dont_skip, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - } - }) - .create(); - case WIFI_AND_MOBILE_SKIPPED_DIALOG_ID: - return new AlertDialog.Builder(getActivity()) - .setMessage(R.string.wifi_and_mobile_skipped_message) - .setCancelable(false) - .setNegativeButton(R.string.wifi_skip_anyway, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - getActivity().setResult(RESULT_SKIP); - getActivity().finish(); - } - }) - .setPositiveButton(R.string.wifi_dont_skip, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - } - }) - .create(); + case WRITE_NFC_DIALOG_ID: + if (mSelectedAccessPoint != null) { + mWifiToNfcDialog = new WriteWifiConfigToNfcDialog( + getActivity(), mSelectedAccessPoint, mWifiManager); + return mWifiToNfcDialog; + } } return super.onCreateDialog(dialogId); } /** - * Shows the latest access points available with supplimental information like + * Shows the latest access points available with supplemental information like * the strength of network and the security for it. */ private void updateAccessPoints() { // Safeguard from some delayed event handling if (getActivity() == null) return; - if (isRestrictedAndNotPinProtected()) { + if (isUiRestricted()) { addMessagePreference(R.string.wifi_empty_list_user_restricted); return; } final int wifiState = mWifiManager.getWifiState(); + //when we update the screen, check if verbose logging has been turned on or off + mVerboseLogging = mWifiManager.getVerboseLoggingLevel(); + switch (wifiState) { case WifiManager.WIFI_STATE_ENABLED: // AccessPoints are automatically sorted with TreeSet. - final Collection<AccessPoint> accessPoints = constructAccessPoints(); + final Collection<AccessPoint> accessPoints = + constructAccessPoints(getActivity(), mWifiManager, mLastInfo, mLastState); getPreferenceScreen().removeAll(); - if(accessPoints.size() == 0) { + if (accessPoints.size() == 0) { addMessagePreference(R.string.wifi_empty_list_wifi_on); } + + getListView().removeHeaderView(mWifiAssistantCard); + if (mWifiAssistantApp != null) { + getListView().addHeaderView(mWifiAssistantCard); + } + for (AccessPoint accessPoint : accessPoints) { - getPreferenceScreen().addPreference(accessPoint); + // Ignore access points that are out of range. + if (accessPoint.getLevel() != -1) { + getPreferenceScreen().addPreference(accessPoint); + } } break; @@ -740,15 +684,117 @@ public class WifiSettings extends RestrictedSettingsFragment } } + /** + * Returns the Network Scorer for the Wifi Assistant App. + */ + public static NetworkScorerAppData getWifiAssistantApp(Context context) { + Collection<NetworkScorerAppData> scorers = + NetworkScorerAppManager.getAllValidScorers(context); + + if (scorers.isEmpty()) { + return null; + } + + // TODO: b/13780935 - Implement proper scorer selection. Rather than pick the first + // scorer on the system, we should allow the user to select one. + return scorers.iterator().next(); + } + + private void prepareWifiAssistantCard() { + if (getActivity() instanceof WifiPickerActivity) { + return; + } + + if (NetworkScorerAppManager.getActiveScorer(getActivity()) != null) { + // A scorer is already enabled; don't show the card. + return; + } + + Collection<NetworkScorerAppData> scorers = + NetworkScorerAppManager.getAllValidScorers(getActivity()); + if (scorers.isEmpty()) { + // No scorers are available to enable; don't show the card. + return; + } + + SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); + int lastDismissPlatform = sharedPreferences.getInt(KEY_ASSISTANT_DISMISS_PLATFORM, 0); + + if (Build.VERSION.SDK_INT <= lastDismissPlatform) { + // User has dismissed the Wi-Fi assistant card on this SDK release. Suppress the card + // until the next major platform upgrade. + return; + } + + // TODO: b/13780935 - Implement proper scorer selection. Rather than pick the first + // scorer on the system, we should allow the user to select one. + mWifiAssistantApp = scorers.iterator().next(); + + if (mWifiAssistantCard == null) { + mWifiAssistantCard = LayoutInflater.from(getActivity()) + .inflate(R.layout.wifi_assistant_card, getListView(), false); + Button setup = (Button) mWifiAssistantCard.findViewById(R.id.setup); + Button noThanks = (Button) mWifiAssistantCard.findViewById(R.id.no_thanks_button); + TextView assistantText = + (TextView) mWifiAssistantCard.findViewById(R.id.wifi_assistant_text); + assistantText.setText(getResources().getString( + R.string.wifi_assistant_title_message, mWifiAssistantApp.mScorerName)); + + if (setup != null && noThanks != null) { + setup.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(); + if (mWifiAssistantApp.mConfigurationActivityClassName != null) { + // App has a custom configuration activity; launch that. + // This custom activity will be responsible for launching the system + // dialog. + intent.setClassName(mWifiAssistantApp.mPackageName, + mWifiAssistantApp.mConfigurationActivityClassName); + } else { + // Fall back on the system dialog. + intent.setAction(NetworkScoreManager.ACTION_CHANGE_ACTIVE); + intent.putExtra(NetworkScoreManager.EXTRA_PACKAGE_NAME, + mWifiAssistantApp.mPackageName); + } + startActivityForResult(intent, REQUEST_ENABLE_WIFI_ASSISTANT); + } + }); + + noThanks.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + disableWifiAssistantCardUntilPlatformUpgrade(); + getListView().removeHeaderView(mWifiAssistantCard); + mWifiAssistantApp = null; + } + }); + } + } + } + + private void disableWifiAssistantCardUntilPlatformUpgrade() { + SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(KEY_ASSISTANT_DISMISS_PLATFORM, Build.VERSION.SDK_INT); + editor.apply(); + } + + protected TextView initEmptyView() { + TextView emptyView = (TextView) getActivity().findViewById(android.R.id.empty); + getListView().setEmptyView(emptyView); + return emptyView; + } + private void setOffMessage() { if (mEmptyView != null) { mEmptyView.setText(R.string.wifi_empty_list_wifi_off); - if (Settings.Global.getInt(getActivity().getContentResolver(), - Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1) { + if (android.provider.Settings.Global.getInt(getActivity().getContentResolver(), + android.provider.Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1) { mEmptyView.append("\n\n"); int resId; - if (Settings.Secure.isLocationProviderEnabled(getActivity().getContentResolver(), - LocationManager.NETWORK_PROVIDER)) { + if (android.provider.Settings.Secure.isLocationProviderEnabled( + getActivity().getContentResolver(), LocationManager.NETWORK_PROVIDER)) { resId = R.string.wifi_scan_notify_text_location_on; } else { resId = R.string.wifi_scan_notify_text_location_off; @@ -766,23 +812,36 @@ public class WifiSettings extends RestrictedSettingsFragment } /** Returns sorted list of access points */ - private List<AccessPoint> constructAccessPoints() { + private static List<AccessPoint> constructAccessPoints(Context context, + WifiManager wifiManager, WifiInfo lastInfo, DetailedState lastState) { ArrayList<AccessPoint> accessPoints = new ArrayList<AccessPoint>(); /** Lookup table to more quickly update AccessPoints by only considering objects with the * correct SSID. Maps SSID -> List of AccessPoints with the given SSID. */ Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>(); - final List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks(); + final List<WifiConfiguration> configs = wifiManager.getConfiguredNetworks(); if (configs != null) { + // Update "Saved Networks" menu option. + if (savedNetworksExist != (configs.size() > 0)) { + savedNetworksExist = !savedNetworksExist; + if (context instanceof Activity) { + ((Activity) context).invalidateOptionsMenu(); + } + } for (WifiConfiguration config : configs) { - AccessPoint accessPoint = new AccessPoint(getActivity(), config); - accessPoint.update(mLastInfo, mLastState); + if (config.selfAdded && config.numAssociation == 0) { + continue; + } + AccessPoint accessPoint = new AccessPoint(context, config); + if (lastInfo != null && lastState != null) { + accessPoint.update(lastInfo, lastState); + } accessPoints.add(accessPoint); apMap.put(accessPoint.ssid, accessPoint); } } - final List<ScanResult> results = mWifiManager.getScanResults(); + final List<ScanResult> results = wifiManager.getScanResults(); if (results != null) { for (ScanResult result : results) { // Ignore hidden and ad-hoc networks. @@ -797,7 +856,7 @@ public class WifiSettings extends RestrictedSettingsFragment found = true; } if (!found) { - AccessPoint accessPoint = new AccessPoint(getActivity(), result); + AccessPoint accessPoint = new AccessPoint(context, result); accessPoints.add(accessPoint); apMap.put(accessPoint.ssid, accessPoint); } @@ -809,26 +868,7 @@ public class WifiSettings extends RestrictedSettingsFragment return accessPoints; } - /** A restricted multimap for use in constructAccessPoints */ - private class Multimap<K,V> { - private final HashMap<K,List<V>> store = new HashMap<K,List<V>>(); - /** retrieve a non-null list of values with key K */ - List<V> getAll(K key) { - List<V> values = store.get(key); - return values != null ? values : Collections.<V>emptyList(); - } - - void put(K key, V val) { - List<V> curVals = store.get(key); - if (curVals == null) { - curVals = new ArrayList<V>(3); - store.put(key, curVals); - } - curVals.add(val); - } - } - - private void handleEvent(Context context, Intent intent) { + private void handleEvent(Intent intent) { String action = intent.getAction(); if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, @@ -837,23 +877,6 @@ public class WifiSettings extends RestrictedSettingsFragment WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) || WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) { updateAccessPoints(); - } else if (WifiManager.SUPPLICANT_STATE_CHANGED_ACTION.equals(action)) { - //Ignore supplicant state changes when network is connected - //TODO: we should deprecate SUPPLICANT_STATE_CHANGED_ACTION and - //introduce a broadcast that combines the supplicant and network - //network state change events so the apps dont have to worry about - //ignoring supplicant state change when network is connected - //to get more fine grained information. - SupplicantState state = (SupplicantState) intent.getParcelableExtra( - WifiManager.EXTRA_NEW_STATE); - if (!mConnected.get() && SupplicantState.isHandshakeState(state)) { - updateConnectionState(WifiInfo.getDetailedStateOf(state)); - } else { - // During a connect, we may have the supplicant - // state change affect the detailed network state. - // Make sure a lost connection is updated as well. - updateConnectionState(null); - } } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { NetworkInfo info = (NetworkInfo) intent.getParcelableExtra( WifiManager.EXTRA_NETWORK_INFO); @@ -861,14 +884,6 @@ public class WifiSettings extends RestrictedSettingsFragment changeNextButtonState(info.isConnected()); updateAccessPoints(); updateConnectionState(info.getDetailedState()); - if (mAutoFinishOnConnection && info.isConnected()) { - Activity activity = getActivity(); - if (activity != null) { - activity.setResult(Activity.RESULT_OK); - activity.finish(); - } - return; - } } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) { updateConnectionState(null); } @@ -927,51 +942,15 @@ public class WifiSettings extends RestrictedSettingsFragment mScanner.pause(); } - private class Scanner extends Handler { - private int mRetry = 0; - - void resume() { - if (!hasMessages(0)) { - sendEmptyMessage(0); - } - } - - void forceScan() { - removeMessages(0); - sendEmptyMessage(0); - } - - void pause() { - mRetry = 0; - removeMessages(0); - } - - @Override - public void handleMessage(Message message) { - if (mWifiManager.startScan()) { - mRetry = 0; - } else if (++mRetry >= 3) { - mRetry = 0; - Activity activity = getActivity(); - if (activity != null) { - Toast.makeText(activity, R.string.wifi_fail_to_scan, - Toast.LENGTH_LONG).show(); - } - return; - } - sendEmptyMessageDelayed(0, WIFI_RESCAN_INTERVAL_MS); - } - } - /** * Renames/replaces "Next" button when appropriate. "Next" button usually exists in * Wifi setup screens, not in usual wifi settings screen. * - * @param connected true when the device is connected to a wifi network. + * @param enabled true when the device is connected to a wifi network. */ - private void changeNextButtonState(boolean connected) { + private void changeNextButtonState(boolean enabled) { if (mEnableNextOnConnection && hasNextButton()) { - getNextButton().setEnabled(connected); + getNextButton().setEnabled(enabled); } } @@ -993,8 +972,7 @@ public class WifiSettings extends RestrictedSettingsFragment if (config == null) { if (mSelectedAccessPoint != null && mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) { - mWifiManager.connect(mSelectedAccessPoint.networkId, - mConnectListener); + connect(mSelectedAccessPoint.networkId); } } else if (config.networkId != INVALID_NETWORK_ID) { if (mSelectedAccessPoint != null) { @@ -1004,7 +982,7 @@ public class WifiSettings extends RestrictedSettingsFragment if (configController.isEdit()) { mWifiManager.save(config, mSaveListener); } else { - mWifiManager.connect(config, mConnectListener); + connect(config); } } @@ -1016,7 +994,7 @@ public class WifiSettings extends RestrictedSettingsFragment /* package */ void forget() { if (mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) { - // Should not happen, but a monkey seems to triger it + // Should not happen, but a monkey seems to trigger it Log.e(TAG, "Failed to forget invalid network " + mSelectedAccessPoint.getConfig()); return; } @@ -1032,6 +1010,14 @@ public class WifiSettings extends RestrictedSettingsFragment changeNextButtonState(false); } + protected void connect(final WifiConfiguration config) { + mWifiManager.connect(config, mConnectListener); + } + + protected void connect(final int networkId) { + mWifiManager.connect(networkId, mConnectListener); + } + /** * Refreshes acccess points and ask Wifi module to scan networks again. */ @@ -1081,49 +1067,39 @@ public class WifiSettings extends RestrictedSettingsFragment @Override protected int getHelpResource() { - if (mSetupWizardMode) { - return 0; - } return R.string.help_url_wifi; } - /** - * Used as the outer frame of all setup wizard pages that need to adjust their margins based - * on the total size of the available display. (e.g. side margins set to 10% of total width.) - */ - public static class ProportionalOuterFrame extends RelativeLayout { - public ProportionalOuterFrame(Context context) { - super(context); - } - public ProportionalOuterFrame(Context context, AttributeSet attrs) { - super(context, attrs); - } - public ProportionalOuterFrame(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); + final Resources res = context.getResources(); + + // Add fragment title + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.wifi_settings); + data.screenTitle = res.getString(R.string.wifi_settings); + data.keywords = res.getString(R.string.keywords_wifi); + result.add(data); + + // Add available Wi-Fi access points + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + final Collection<AccessPoint> accessPoints = + constructAccessPoints(context, wifiManager, null, null); + for (AccessPoint accessPoint : accessPoints) { + // We are indexing only the saved Wi-Fi networks. + if (accessPoint.getConfig() == null) continue; + data = new SearchIndexableRaw(context); + data.title = accessPoint.getTitle().toString(); + data.screenTitle = res.getString(R.string.wifi_settings); + data.enabled = enabled; + result.add(data); + } - /** - * Set our margins and title area height proportionally to the available display size - */ - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int parentWidth = MeasureSpec.getSize(widthMeasureSpec); - int parentHeight = MeasureSpec.getSize(heightMeasureSpec); - final Resources resources = getContext().getResources(); - float titleHeight = resources.getFraction(R.dimen.setup_title_height, 1, 1); - float sideMargin = resources.getFraction(R.dimen.setup_border_width, 1, 1); - int bottom = resources.getDimensionPixelSize(R.dimen.setup_margin_bottom); - setPaddingRelative( - (int) (parentWidth * sideMargin), - 0, - (int) (parentWidth * sideMargin), - bottom); - View title = findViewById(R.id.title_area); - if (title != null) { - title.setMinimumHeight((int) (parentHeight * titleHeight)); + return result; } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - + }; } diff --git a/src/com/android/settings/wifi/WifiSettingsForSetupWizard.java b/src/com/android/settings/wifi/WifiSettingsForSetupWizard.java new file mode 100644 index 0000000..c4a5c96 --- /dev/null +++ b/src/com/android/settings/wifi/WifiSettingsForSetupWizard.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2014 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.Intent; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.net.wifi.WifiConfiguration; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.settings.R; + +/** + * This customized version of WifiSettings is shown to the user only during Setup Wizard. Menu + * selections are limited, clicking on an access point will auto-advance to the next screen (once + * connected), and, if the user opts to skip ahead without a wifi connection, a warning message + * alerts of possible carrier data charges or missing software updates. + */ +public class WifiSettingsForSetupWizard extends WifiSettings { + + private static final String TAG = "WifiSettingsForSetupWizard"; + + // show a text regarding data charges when wifi connection is required during setup wizard + protected static final String EXTRA_SHOW_WIFI_REQUIRED_INFO = "wifi_show_wifi_required_info"; + + private View mAddOtherNetworkItem; + private ListAdapter mAdapter; + private TextView mEmptyFooter; + private boolean mListLastEmpty = false; + + @Override + public View onCreateView(final LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + final View view = inflater.inflate(R.layout.setup_preference, container, false); + + final ListView list = (ListView) view.findViewById(android.R.id.list); + final View title = view.findViewById(R.id.title); + if (title == null) { + final View header = inflater.inflate(R.layout.setup_wizard_header, list, false); + list.addHeaderView(header, null, false); + } + + mAddOtherNetworkItem = inflater.inflate(R.layout.setup_wifi_add_network, list, false); + list.addFooterView(mAddOtherNetworkItem, null, true); + mAddOtherNetworkItem.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mWifiManager.isWifiEnabled()) { + onAddNetworkPressed(); + } + } + }); + + final Intent intent = getActivity().getIntent(); + if (intent.getBooleanExtra(EXTRA_SHOW_WIFI_REQUIRED_INFO, false)) { + view.findViewById(R.id.wifi_required_info).setVisibility(View.VISIBLE); + } + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getView().setSystemUiVisibility( + View.STATUS_BAR_DISABLE_HOME | + View.STATUS_BAR_DISABLE_RECENT | + View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS | + View.STATUS_BAR_DISABLE_CLOCK); + + if (hasNextButton()) { + getNextButton().setVisibility(View.GONE); + } + + mAdapter = getPreferenceScreen().getRootAdapter(); + mAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + super.onChanged(); + updateFooter(); + } + }); + } + + @Override + public void registerForContextMenu(View view) { + // Suppressed during setup wizard + } + + @Override + /* package */ WifiEnabler createWifiEnabler() { + // Not shown during setup wizard + return null; + } + + @Override + /* package */ void addOptionsMenuItems(Menu menu) { + final boolean wifiIsEnabled = mWifiManager.isWifiEnabled(); + final TypedArray ta = getActivity().getTheme() + .obtainStyledAttributes(new int[] {R.attr.ic_wps}); + menu.add(Menu.NONE, MENU_ID_WPS_PBC, 0, R.string.wifi_menu_wps_pbc) + .setIcon(ta.getDrawable(0)) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network) + .setEnabled(wifiIsEnabled) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + ta.recycle(); + } + + @Override + protected void connect(final WifiConfiguration config) { + WifiSetupActivity activity = (WifiSetupActivity) getActivity(); + activity.networkSelected(); + super.connect(config); + } + + @Override + protected void connect(final int networkId) { + WifiSetupActivity activity = (WifiSetupActivity) getActivity(); + activity.networkSelected(); + super.connect(networkId); + } + + @Override + protected TextView initEmptyView() { + mEmptyFooter = new TextView(getActivity()); + mEmptyFooter.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + mEmptyFooter.setGravity(Gravity.CENTER); + mEmptyFooter.setCompoundDrawablesWithIntrinsicBounds(0, + R.drawable.ic_wifi_emptystate, 0,0); + return mEmptyFooter; + } + + protected void updateFooter() { + final boolean isEmpty = mAdapter.isEmpty(); + if (isEmpty != mListLastEmpty) { + final ListView list = getListView(); + if (isEmpty) { + list.removeFooterView(mAddOtherNetworkItem); + list.addFooterView(mEmptyFooter, null, false); + } else { + list.removeFooterView(mEmptyFooter); + list.addFooterView(mAddOtherNetworkItem, null, true); + } + mListLastEmpty = isEmpty; + } + } +} diff --git a/src/com/android/settings/wifi/WifiSetupActivity.java b/src/com/android/settings/wifi/WifiSetupActivity.java index 1739750..a87c733 100644 --- a/src/com/android/settings/wifi/WifiSetupActivity.java +++ b/src/com/android/settings/wifi/WifiSetupActivity.java @@ -15,27 +15,293 @@ */ package com.android.settings.wifi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Color; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.util.Log; + import com.android.settings.ButtonBarHandler; +import com.android.settings.R; +import com.android.setupwizard.navigationbar.SetupWizardNavBar; +import com.android.setupwizard.navigationbar.SetupWizardNavBar.NavigationBarListener; -import android.content.res.Resources; +public class WifiSetupActivity extends WifiPickerActivity + implements ButtonBarHandler, NavigationBarListener { + private static final String TAG = "WifiSetupActivity"; + + private static final String EXTRA_ALLOW_SKIP = "allowSkip"; + private static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; + + // this boolean extra specifies whether to auto finish when connection is established + private static final String EXTRA_AUTO_FINISH_ON_CONNECT = "wifi_auto_finish_on_connect"; + + // Whether auto finish is suspended until user connects to an access point + private static final String EXTRA_REQUIRE_USER_NETWORK_SELECTION = + "wifi_require_user_network_selection"; -public class WifiSetupActivity extends WifiPickerActivity implements ButtonBarHandler { // Extra containing the resource name of the theme to be used private static final String EXTRA_THEME = "theme"; private static final String THEME_HOLO = "holo"; private static final String THEME_HOLO_LIGHT = "holo_light"; + private static final String THEME_MATERIAL = "material"; + private static final String THEME_MATERIAL_LIGHT = "material_light"; + + // Key for whether the user selected network in saved instance state bundle + private static final String PARAM_USER_SELECTED_NETWORK = "userSelectedNetwork"; + + // Activity result when pressing the Skip button + private static final int RESULT_SKIP = Activity.RESULT_FIRST_USER; + + // From WizardManager (must match constants maintained there) + private static final String ACTION_NEXT = "com.android.wizard.NEXT"; + private static final String EXTRA_SCRIPT_URI = "scriptUri"; + private static final String EXTRA_ACTION_ID = "actionId"; + private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode"; + private static final int NEXT_REQUEST = 10000; + + // Whether we allow skipping without a valid network connection + private boolean mAllowSkip = true; + // Whether to auto finish when the user selected a network and successfully connected + private boolean mAutoFinishOnConnection; + // Whether the user connected to a network. This excludes the auto-connecting by the system. + private boolean mUserSelectedNetwork; + // Whether the device is connected to WiFi + private boolean mWifiConnected; + + private SetupWizardNavBar mNavigationBar; + + private final IntentFilter mFilter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION); + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Refresh the connection state with the latest connection info. Use the connection info + // from ConnectivityManager instead of the one attached in the intent to make sure + // we have the most up-to-date connection state. b/17511772 + refreshConnectionState(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + + mAutoFinishOnConnection = intent.getBooleanExtra(EXTRA_AUTO_FINISH_ON_CONNECT, false); + mAllowSkip = intent.getBooleanExtra(EXTRA_ALLOW_SKIP, true); + // Behave like the user already selected a network if we do not require selection + mUserSelectedNetwork = !intent.getBooleanExtra(EXTRA_REQUIRE_USER_NETWORK_SELECTION, false); + } - // Style resources containing theme settings - private static final String RESOURCE_THEME_DARK = "SetupWizardWifiTheme"; - private static final String RESOURCE_THEME_LIGHT = "SetupWizardWifiTheme.Light"; + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(PARAM_USER_SELECTED_NETWORK, mUserSelectedNetwork); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mUserSelectedNetwork = savedInstanceState.getBoolean(PARAM_USER_SELECTED_NETWORK, true); + } + + private void refreshConnectionState() { + final ConnectivityManager connectivity = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + boolean connected = connectivity != null && + connectivity.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected(); + refreshConnectionState(connected); + } + + private void refreshConnectionState(boolean connected) { + mWifiConnected = connected; + if (connected) { + if (mAutoFinishOnConnection && mUserSelectedNetwork) { + Log.d(TAG, "Auto-finishing with connection"); + finishOrNext(Activity.RESULT_OK); + // Require a user selection before auto-finishing next time we are here. The user + // can either connect to a different network or press "next" to proceed. + mUserSelectedNetwork = false; + } + if (mNavigationBar != null) { + mNavigationBar.getNextButton().setText(R.string.setup_wizard_next_button_label); + mNavigationBar.getNextButton().setEnabled(true); + } + } else { + if (mNavigationBar != null) { + mNavigationBar.getNextButton().setText(R.string.skip_label); + mNavigationBar.getNextButton().setEnabled(mAllowSkip); + } + } + } + + /* package */ void networkSelected() { + Log.d(TAG, "Network selected by user"); + mUserSelectedNetwork = true; + } + + @Override + public void onResume() { + super.onResume(); + registerReceiver(mReceiver, mFilter); + refreshConnectionState(); + } + + @Override + public void onPause() { + unregisterReceiver(mReceiver); + super.onPause(); + } @Override protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { String themeName = getIntent().getStringExtra(EXTRA_THEME); - if (themeName != null && themeName.equalsIgnoreCase(THEME_HOLO_LIGHT)) { - resid = getResources().getIdentifier(RESOURCE_THEME_LIGHT, "style", - getPackageName()); + if (THEME_HOLO_LIGHT.equalsIgnoreCase(themeName) || + THEME_MATERIAL_LIGHT.equalsIgnoreCase(themeName)) { + resid = R.style.SetupWizardWifiTheme_Light; + } else if (THEME_HOLO.equalsIgnoreCase(themeName) || + THEME_MATERIAL.equalsIgnoreCase(themeName)) { + resid = R.style.SetupWizardWifiTheme; } super.onApplyThemeResource(theme, resid, first); } + + @Override + protected boolean isValidFragment(String fragmentName) { + return WifiSettingsForSetupWizard.class.getName().equals(fragmentName); + } + + @Override + /* package */ Class<? extends PreferenceFragment> getWifiSettingsClass() { + return WifiSettingsForSetupWizard.class; + } + + /** + * Complete this activity and return the results to the caller. If using WizardManager, this + * will invoke the next scripted action; otherwise, we simply finish. + */ + public void finishOrNext(int resultCode) { + Log.d(TAG, "finishOrNext resultCode=" + resultCode + + " isUsingWizardManager=" + isUsingWizardManager()); + if (isUsingWizardManager()) { + sendResultsToSetupWizard(resultCode); + } else { + setResult(resultCode); + finish(); + } + } + + private boolean isUsingWizardManager() { + return getIntent().hasExtra(EXTRA_SCRIPT_URI); + } + + /** + * Send the results of this activity to WizardManager, which will then send out the next + * scripted activity. WizardManager does not actually return an activity result, but if we + * invoke WizardManager without requesting a result, the framework will choose not to issue a + * call to onActivityResult with RESULT_CANCELED when navigating backward. + */ + private void sendResultsToSetupWizard(int resultCode) { + final Intent intent = getIntent(); + final Intent nextIntent = new Intent(ACTION_NEXT); + nextIntent.putExtra(EXTRA_SCRIPT_URI, intent.getStringExtra(EXTRA_SCRIPT_URI)); + nextIntent.putExtra(EXTRA_ACTION_ID, intent.getStringExtra(EXTRA_ACTION_ID)); + nextIntent.putExtra(EXTRA_THEME, intent.getStringExtra(EXTRA_THEME)); + nextIntent.putExtra(EXTRA_RESULT_CODE, resultCode); + startActivityForResult(nextIntent, NEXT_REQUEST); + } + + @Override + public void onNavigationBarCreated(final SetupWizardNavBar bar) { + mNavigationBar = bar; + final boolean useImmersiveMode = + getIntent().getBooleanExtra(EXTRA_USE_IMMERSIVE_MODE, false); + bar.setUseImmersiveMode(useImmersiveMode); + if (useImmersiveMode) { + getWindow().setNavigationBarColor(Color.TRANSPARENT); + getWindow().setStatusBarColor(Color.TRANSPARENT); + } + } + + @Override + public void onNavigateBack() { + onBackPressed(); + } + + @Override + public void onNavigateNext() { + if (mWifiConnected) { + finishOrNext(RESULT_OK); + } else { + // Warn of possible data charges if there is a network connection, or lack of updates + // if there is none. + final int message = isNetworkConnected() ? R.string.wifi_skipped_message : + R.string.wifi_and_mobile_skipped_message; + WifiSkipDialog.newInstance(message).show(getFragmentManager(), "dialog"); + } + } + + /** + * @return True if there is a valid network connection, whether it is via WiFi, mobile data or + * other means. + */ + private boolean isNetworkConnected() { + final ConnectivityManager connectivity = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) { + return false; + } + final NetworkInfo info = connectivity.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } + + public static class WifiSkipDialog extends DialogFragment { + public static WifiSkipDialog newInstance(int messageRes) { + final Bundle args = new Bundle(); + args.putInt("messageRes", messageRes); + final WifiSkipDialog dialog = new WifiSkipDialog(); + dialog.setArguments(args); + return dialog; + } + + public WifiSkipDialog() { + // no-arg constructor for fragment + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + int messageRes = getArguments().getInt("messageRes"); + return new AlertDialog.Builder(getActivity()) + .setMessage(messageRes) + .setCancelable(false) + .setNegativeButton(R.string.wifi_skip_anyway, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + WifiSetupActivity activity = (WifiSetupActivity) getActivity(); + activity.finishOrNext(RESULT_SKIP); + } + }) + .setPositiveButton(R.string.wifi_dont_skip, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + } + }) + .create(); + } + } } diff --git a/src/com/android/settings/wifi/WpsDialog.java b/src/com/android/settings/wifi/WpsDialog.java index 2a93884..d0b116b 100644 --- a/src/com/android/settings/wifi/WpsDialog.java +++ b/src/com/android/settings/wifi/WpsDialog.java @@ -27,7 +27,6 @@ import android.net.wifi.WifiManager; import android.net.wifi.WpsInfo; import android.os.Bundle; import android.os.Handler; -import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; @@ -45,6 +44,8 @@ import com.android.settings.R; public class WpsDialog extends AlertDialog { private final static String TAG = "WpsDialog"; + private static final String DIALOG_STATE = "android:dialogState"; + private static final String DIALOG_MSG_STRING = "android:dialogMsg"; private View mView; private TextView mTextView; @@ -56,7 +57,7 @@ public class WpsDialog extends AlertDialog { private static final int WPS_TIMEOUT_S = 120; private WifiManager mWifiManager; - private WifiManager.WpsListener mWpsListener; + private WifiManager.WpsCallback mWpsListener; private int mWpsSetup; private final IntentFilter mFilter; @@ -64,6 +65,7 @@ public class WpsDialog extends AlertDialog { private Context mContext; private Handler mHandler = new Handler(); + private String mMsgString = ""; private enum DialogState { WPS_INIT, @@ -79,8 +81,9 @@ public class WpsDialog extends AlertDialog { mContext = context; mWpsSetup = wpsSetup; - class WpsListener implements WifiManager.WpsListener { - public void onStartSuccess(String pin) { + class WpsListener extends WifiManager.WpsCallback { + + public void onStarted(String pin) { if (pin != null) { updateDialog(DialogState.WPS_START, String.format( mContext.getString(R.string.wifi_wps_onstart_pin), pin)); @@ -89,12 +92,13 @@ public class WpsDialog extends AlertDialog { R.string.wifi_wps_onstart_pbc)); } } - public void onCompletion() { + + public void onSucceeded() { updateDialog(DialogState.WPS_COMPLETE, mContext.getString(R.string.wifi_wps_complete)); } - public void onFailure(int reason) { + public void onFailed(int reason) { String msg; switch (reason) { case WifiManager.WPS_OVERLAP_ERROR: @@ -128,6 +132,25 @@ public class WpsDialog extends AlertDialog { handleEvent(context, intent); } }; + setCanceledOnTouchOutside(false); + } + + @Override + public Bundle onSaveInstanceState () { + Bundle bundle = super.onSaveInstanceState(); + bundle.putString(DIALOG_STATE, mDialogState.toString()); + bundle.putString(DIALOG_MSG_STRING, mMsgString.toString()); + return bundle; + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + super.onRestoreInstanceState(savedInstanceState); + DialogState dialogState = mDialogState.valueOf(savedInstanceState.getString(DIALOG_STATE)); + String msg = savedInstanceState.getString(DIALOG_MSG_STRING); + updateDialog(dialogState, msg); + } } @Override @@ -207,6 +230,7 @@ public class WpsDialog extends AlertDialog { return; } mDialogState = state; + mMsgString = msg; mHandler.post(new Runnable() { @Override diff --git a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java new file mode 100644 index 0000000..2667e0b --- /dev/null +++ b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2014 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.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.wifi.WifiManager; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import android.os.Bundle; +import android.os.Handler; +import android.os.PowerManager; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.android.settings.R; + +import java.io.IOException; + +class WriteWifiConfigToNfcDialog extends AlertDialog + implements TextWatcher, View.OnClickListener, CompoundButton.OnCheckedChangeListener { + + private static final String NFC_TOKEN_MIME_TYPE = "application/vnd.wfa.wsc"; + + private static final String TAG = WriteWifiConfigToNfcDialog.class.getName().toString(); + private static final String PASSWORD_FORMAT = "102700%s%s"; + private static final int HEX_RADIX = 16; + private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); + + private final PowerManager.WakeLock mWakeLock; + + private AccessPoint mAccessPoint; + private View mView; + private Button mSubmitButton; + private Button mCancelButton; + private Handler mOnTextChangedHandler; + private TextView mPasswordView; + private TextView mLabelView; + private CheckBox mPasswordCheckBox; + private ProgressBar mProgressBar; + private WifiManager mWifiManager; + private String mWpsNfcConfigurationToken; + private Context mContext; + + WriteWifiConfigToNfcDialog(Context context, AccessPoint accessPoint, + WifiManager wifiManager) { + super(context); + + mContext = context; + mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)) + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock"); + mAccessPoint = accessPoint; + mOnTextChangedHandler = new Handler(); + mWifiManager = wifiManager; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + mView = getLayoutInflater().inflate(R.layout.write_wifi_config_to_nfc, null); + + setView(mView); + setInverseBackgroundForced(true); + setTitle(R.string.setup_wifi_nfc_tag); + setCancelable(true); + setButton(DialogInterface.BUTTON_NEUTRAL, + mContext.getResources().getString(R.string.write_tag), (OnClickListener) null); + setButton(DialogInterface.BUTTON_NEGATIVE, + mContext.getResources().getString(com.android.internal.R.string.cancel), + (OnClickListener) null); + + mPasswordView = (TextView) mView.findViewById(R.id.password); + mLabelView = (TextView) mView.findViewById(R.id.password_label); + mPasswordView.addTextChangedListener(this); + mPasswordCheckBox = (CheckBox) mView.findViewById(R.id.show_password); + mPasswordCheckBox.setOnCheckedChangeListener(this); + mProgressBar = (ProgressBar) mView.findViewById(R.id.progress_bar); + + super.onCreate(savedInstanceState); + + mSubmitButton = getButton(DialogInterface.BUTTON_NEUTRAL); + mSubmitButton.setOnClickListener(this); + mSubmitButton.setEnabled(false); + + mCancelButton = getButton(DialogInterface.BUTTON_NEGATIVE); + } + + @Override + public void onClick(View v) { + mWakeLock.acquire(); + + String password = mPasswordView.getText().toString(); + String wpsNfcConfigurationToken + = mWifiManager.getWpsNfcConfigurationToken(mAccessPoint.networkId); + String passwordHex = byteArrayToHexString(password.getBytes()); + + String passwordLength = password.length() >= HEX_RADIX + ? Integer.toString(password.length(), HEX_RADIX) + : "0" + Character.forDigit(password.length(), HEX_RADIX); + + passwordHex = String.format(PASSWORD_FORMAT, passwordLength, passwordHex).toUpperCase(); + + if (wpsNfcConfigurationToken.contains(passwordHex)) { + mWpsNfcConfigurationToken = wpsNfcConfigurationToken; + + Activity activity = getOwnerActivity(); + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); + + nfcAdapter.enableReaderMode(activity, new NfcAdapter.ReaderCallback() { + @Override + public void onTagDiscovered(Tag tag) { + handleWriteNfcEvent(tag); + } + }, NfcAdapter.FLAG_READER_NFC_A | + NfcAdapter.FLAG_READER_NFC_B | + NfcAdapter.FLAG_READER_NFC_BARCODE | + NfcAdapter.FLAG_READER_NFC_F | + NfcAdapter.FLAG_READER_NFC_V, + null); + + mPasswordView.setVisibility(View.GONE); + mPasswordCheckBox.setVisibility(View.GONE); + mSubmitButton.setVisibility(View.GONE); + InputMethodManager imm = (InputMethodManager) + getOwnerActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mPasswordView.getWindowToken(), 0); + + mLabelView.setText(R.string.status_awaiting_tap); + + mView.findViewById(R.id.password_layout).setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + mProgressBar.setVisibility(View.VISIBLE); + } else { + mLabelView.setText(R.string.status_invalid_password); + } + } + + private void handleWriteNfcEvent(Tag tag) { + Ndef ndef = Ndef.get(tag); + + if (ndef != null) { + if (ndef.isWritable()) { + NdefRecord record = NdefRecord.createMime( + NFC_TOKEN_MIME_TYPE, + hexStringToByteArray(mWpsNfcConfigurationToken)); + try { + ndef.connect(); + ndef.writeNdefMessage(new NdefMessage(record)); + getOwnerActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mProgressBar.setVisibility(View.GONE); + } + }); + setViewText(mLabelView, R.string.status_write_success); + setViewText(mCancelButton, com.android.internal.R.string.done_label); + } catch (IOException e) { + setViewText(mLabelView, R.string.status_failed_to_write); + Log.e(TAG, "Unable to write Wi-Fi config to NFC tag.", e); + return; + } catch (FormatException e) { + setViewText(mLabelView, R.string.status_failed_to_write); + Log.e(TAG, "Unable to write Wi-Fi config to NFC tag.", e); + return; + } + } else { + setViewText(mLabelView, R.string.status_tag_not_writable); + Log.e(TAG, "Tag is not writable"); + } + } else { + setViewText(mLabelView, R.string.status_tag_not_writable); + Log.e(TAG, "Tag does not support NDEF"); + } + } + + @Override + public void dismiss() { + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + + super.dismiss(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mOnTextChangedHandler.post(new Runnable() { + @Override + public void run() { + enableSubmitIfAppropriate(); + } + }); + } + + private void enableSubmitIfAppropriate() { + + if (mPasswordView != null) { + if (mAccessPoint.security == AccessPoint.SECURITY_WEP) { + mSubmitButton.setEnabled(mPasswordView.length() > 0); + } else if (mAccessPoint.security == AccessPoint.SECURITY_PSK) { + mSubmitButton.setEnabled(mPasswordView.length() >= 8); + } + } else { + mSubmitButton.setEnabled(false); + } + + } + + private void setViewText(final TextView view, final int resid) { + getOwnerActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + view.setText(resid); + } + }); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mPasswordView.setInputType( + InputType.TYPE_CLASS_TEXT | + (isChecked + ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + : InputType.TYPE_TEXT_VARIATION_PASSWORD)); + } + + private static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), HEX_RADIX) << 4) + + Character.digit(s.charAt(i + 1), HEX_RADIX)); + } + + return data; + } + + private static String byteArrayToHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} +} diff --git a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java index 07d66b0..cd70796 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java @@ -16,7 +16,6 @@ package com.android.settings.wifi.p2p; -import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -34,40 +33,32 @@ import android.net.wifi.p2p.WifiP2pDeviceList; import android.net.wifi.p2p.WifiP2pGroup; import android.net.wifi.p2p.WifiP2pGroupList; import android.net.wifi.p2p.WifiP2pManager; -import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener; +import android.net.wifi.p2p.WifiP2pManager.PeerListListener; import android.net.wifi.p2p.WifiP2pManager.PersistentGroupInfoListener; import android.net.wifi.WpsInfo; import android.os.Bundle; -import android.os.Handler; import android.os.SystemProperties; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.text.InputFilter; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.EditText; -import android.widget.Switch; import android.widget.Toast; 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 - implements PersistentGroupInfoListener, GroupInfoListener { + implements PersistentGroupInfoListener, PeerListListener { private static final String TAG = "WifiP2pSettings"; private static final boolean DBG = false; @@ -89,7 +80,6 @@ public class WifiP2pSettings extends SettingsPreferenceFragment private boolean mWifiP2pEnabled; private boolean mWifiP2pSearching; private int mConnectedDevices; - private WifiP2pGroup mConnectedGroup; private boolean mLastGroupFormed = false; private PreferenceGroup mPeersGroup; @@ -129,9 +119,6 @@ public class WifiP2pSettings extends SettingsPreferenceFragment WifiP2pManager.EXTRA_NETWORK_INFO); WifiP2pInfo wifip2pinfo = (WifiP2pInfo) intent.getParcelableExtra( WifiP2pManager.EXTRA_WIFI_P2P_INFO); - if (mWifiP2pManager != null) { - mWifiP2pManager.requestGroupInfo(mChannel, WifiP2pSettings.this); - } if (networkInfo.isConnected()) { if (DBG) Log.d(TAG, "Connected"); } else if (mLastGroupFormed != true) { @@ -311,16 +298,20 @@ public class WifiP2pSettings extends SettingsPreferenceFragment final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.removeAll(); - preferenceScreen.setOrderingAsAdded(true); + mThisDevicePref = new Preference(getActivity()); + mThisDevicePref.setPersistent(false); + mThisDevicePref.setSelectable(false); preferenceScreen.addPreference(mThisDevicePref); mPeersGroup = new PreferenceCategory(getActivity()); mPeersGroup.setTitle(R.string.wifi_p2p_peer_devices); + preferenceScreen.addPreference(mPeersGroup); mPersistentGroup = new PreferenceCategory(getActivity()); mPersistentGroup.setTitle(R.string.wifi_p2p_remembered_groups); + preferenceScreen.addPreference(mPersistentGroup); super.onActivityCreated(savedInstanceState); } @@ -329,12 +320,17 @@ public class WifiP2pSettings extends SettingsPreferenceFragment public void onResume() { super.onResume(); getActivity().registerReceiver(mReceiver, mIntentFilter); + if (mWifiP2pManager != null) { + mWifiP2pManager.requestPeers(mChannel, WifiP2pSettings.this); + } } @Override public void onPause() { super.onPause(); - mWifiP2pManager.stopPeerDiscovery(mChannel, null); + if (mWifiP2pManager != null) { + mWifiP2pManager.stopPeerDiscovery(mChannel, null); + } getActivity().unregisterReceiver(mReceiver); } @@ -519,6 +515,7 @@ public class WifiP2pSettings extends SettingsPreferenceFragment if (DBG) Log.d(TAG, " mConnectedDevices " + mConnectedDevices); } + @Override public void onPersistentGroupInfoAvailable(WifiP2pGroupList groups) { mPersistentGroup.removeAll(); @@ -541,27 +538,18 @@ public class WifiP2pSettings extends SettingsPreferenceFragment } } - public void onGroupInfoAvailable(WifiP2pGroup group) { - if (DBG) Log.d(TAG, " group " + group); - mConnectedGroup = group; - updateDevicePref(); + @Override + public void onPeersAvailable(WifiP2pDeviceList peers) { + if (DBG) Log.d(TAG, "Requested peers are available"); + mPeers = peers; + handlePeersChanged(); } private void handleP2pStateChanged() { updateSearchMenu(false); - if (mWifiP2pEnabled) { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - preferenceScreen.removeAll(); - - preferenceScreen.setOrderingAsAdded(true); - preferenceScreen.addPreference(mThisDevicePref); - - mPeersGroup.setEnabled(true); - preferenceScreen.addPreference(mPeersGroup); - - mPersistentGroup.setEnabled(true); - preferenceScreen.addPreference(mPersistentGroup); - } + mThisDevicePref.setEnabled(mWifiP2pEnabled); + mPeersGroup.setEnabled(mWifiP2pEnabled); + mPersistentGroup.setEnabled(mWifiP2pEnabled); } private void updateSearchMenu(boolean searching) { @@ -589,10 +577,6 @@ public class WifiP2pSettings extends SettingsPreferenceFragment } else { mThisDevicePref.setTitle(mThisDevice.deviceName); } - - mThisDevicePref.setPersistent(false); - mThisDevicePref.setEnabled(true); - mThisDevicePref.setSelectable(false); } } } |