diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:34 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:32:34 -0800 |
commit | afc4ab2ffbb8327ddce9907961295a32cbf49d0f (patch) | |
tree | 64baf0ce0e3c9fb39cc086994dbe9805b74e5bca /src/com | |
parent | 4e14e5ccbf1ef27220419849133d482a546d5c04 (diff) | |
download | packages_apps_Settings-afc4ab2ffbb8327ddce9907961295a32cbf49d0f.tar.gz packages_apps_Settings-afc4ab2ffbb8327ddce9907961295a32cbf49d0f.tar.bz2 packages_apps_Settings-afc4ab2ffbb8327ddce9907961295a32cbf49d0f.zip |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'src/com')
78 files changed, 19917 insertions, 0 deletions
diff --git a/src/com/android/settings/ActivityPicker.java b/src/com/android/settings/ActivityPicker.java new file mode 100644 index 000000000..47e005fc3 --- /dev/null +++ b/src/com/android/settings/ActivityPicker.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings; + +import android.app.LauncherActivity; +import android.content.Intent; +import android.os.Parcelable; +import android.view.View; +import android.widget.ListView; + +/** + * Displays a list of all activities matching the incoming {@link Intent.EXTRA_INTENT} + * query, along with any applicable icons. + */ +public class ActivityPicker extends LauncherActivity { + + @Override + protected Intent getTargetIntent() { + Intent intent = this.getIntent(); + Intent targetIntent = new Intent(Intent.ACTION_MAIN, null); + targetIntent.addCategory(Intent.CATEGORY_DEFAULT); + + // Use a custom title for this dialog, if provided + if (intent.hasExtra(Intent.EXTRA_TITLE)) { + String title = intent.getStringExtra(Intent.EXTRA_TITLE); + setTitle(title); + } + + Parcelable parcel = intent.getParcelableExtra(Intent.EXTRA_INTENT); + if (parcel instanceof Intent) { + targetIntent = (Intent) parcel; + } + + return targetIntent; + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Intent intent = intentForPosition(position); + setResult(RESULT_OK, intent); + finish(); + } + +} diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java new file mode 100644 index 000000000..f10571226 --- /dev/null +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -0,0 +1,118 @@ +/* + * 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.internal.telephony.PhoneStateIntentReceiver; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.provider.Settings; +import android.telephony.ServiceState; + +public class AirplaneModeEnabler implements Preference.OnPreferenceChangeListener { + + private final Context mContext; + + private PhoneStateIntentReceiver mPhoneStateReceiver; + + private final CheckBoxPreference mCheckBoxPref; + + private static final int EVENT_SERVICE_STATE_CHANGED = 3; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_SERVICE_STATE_CHANGED: + onAirplaneModeChanged(); + break; + } + } + }; + + public AirplaneModeEnabler(Context context, CheckBoxPreference airplaneModeCheckBoxPreference) { + + mContext = context; + mCheckBoxPref = airplaneModeCheckBoxPreference; + + airplaneModeCheckBoxPreference.setPersistent(false); + + mPhoneStateReceiver = new PhoneStateIntentReceiver(mContext, mHandler); + mPhoneStateReceiver.notifyServiceState(EVENT_SERVICE_STATE_CHANGED); + } + + public void resume() { + + // This is the widget enabled state, not the preference toggled state + mCheckBoxPref.setEnabled(true); + mCheckBoxPref.setChecked(isAirplaneModeOn(mContext)); + + mPhoneStateReceiver.registerIntent(); + mCheckBoxPref.setOnPreferenceChangeListener(this); + } + + public void pause() { + mPhoneStateReceiver.unregisterIntent(); + mCheckBoxPref.setOnPreferenceChangeListener(null); + } + + static boolean isAirplaneModeOn(Context context) { + return Settings.System.getInt(context.getContentResolver(), + Settings.System.AIRPLANE_MODE_ON, 0) != 0; + } + + private void setAirplaneModeOn(boolean enabling) { + + mCheckBoxPref.setEnabled(false); + mCheckBoxPref.setSummary(enabling ? R.string.airplane_mode_turning_on + : R.string.airplane_mode_turning_off); + + // Change the system setting + Settings.System.putInt(mContext.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, + enabling ? 1 : 0); + + // Post the intent + Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", enabling); + mContext.sendBroadcast(intent); + } + + /** + * Called when we've received confirmation that the airplane mode was set. + */ + private void onAirplaneModeChanged() { + ServiceState serviceState = mPhoneStateReceiver.getServiceState(); + boolean airplaneModeEnabled = serviceState.getState() == ServiceState.STATE_POWER_OFF; + mCheckBoxPref.setChecked(airplaneModeEnabled); + mCheckBoxPref.setSummary(airplaneModeEnabled ? null : + mContext.getString(R.string.airplane_mode_summary)); + mCheckBoxPref.setEnabled(true); + } + + /** + * Called when someone clicks on the checkbox preference. + */ + public boolean onPreferenceChange(Preference preference, Object newValue) { + setAirplaneModeOn((Boolean) newValue); + return true; + } + +} diff --git a/src/com/android/settings/ApnEditor.java b/src/com/android/settings/ApnEditor.java new file mode 100644 index 000000000..f1fa2efd0 --- /dev/null +++ b/src/com/android/settings/ApnEditor.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings; + +import android.app.AlertDialog; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemProperties; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.provider.Telephony; +import com.android.internal.telephony.TelephonyProperties; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; + + +public class ApnEditor extends PreferenceActivity + implements SharedPreferences.OnSharedPreferenceChangeListener { + + + private final static String TAG = ApnEditor.class.getSimpleName(); + + private final static String SAVED_POS = "pos"; + + private static final int MENU_DELETE = Menu.FIRST; + private static final int MENU_SAVE = Menu.FIRST + 1; + private static final int MENU_CANCEL = Menu.FIRST + 2; + + private static String sNotSet; + private EditTextPreference mName; + private EditTextPreference mApn; + private EditTextPreference mProxy; + private EditTextPreference mPort; + private EditTextPreference mUser; + private EditTextPreference mServer; + private EditTextPreference mPassword; + private EditTextPreference mMmsc; + private EditTextPreference mMcc; + private EditTextPreference mMnc; + private EditTextPreference mMmsProxy; + private EditTextPreference mMmsPort; + private EditTextPreference mApnType; + private String mCurMnc; + private String mCurMcc; + + private Uri mUri; + private Cursor mCursor; + private boolean mNewApn; + private boolean mFirstTime; + private Resources mRes; + + /** + * Standard projection for the interesting columns of a normal note. + */ + private static final String[] sProjection = new String[] { + Telephony.Carriers._ID, // 0 + Telephony.Carriers.NAME, // 1 + Telephony.Carriers.APN, // 2 + Telephony.Carriers.PROXY, // 3 + Telephony.Carriers.PORT, // 4 + Telephony.Carriers.USER, // 5 + Telephony.Carriers.SERVER, // 6 + Telephony.Carriers.PASSWORD, // 7 + Telephony.Carriers.MMSC, // 8 + Telephony.Carriers.MCC, // 9 + Telephony.Carriers.MNC, // 10 + Telephony.Carriers.NUMERIC, // 11 + Telephony.Carriers.MMSPROXY,// 12 + Telephony.Carriers.MMSPORT, // 13 + Telephony.Carriers.TYPE, // 14 + }; + + private static final int ID_INDEX = 0; + private static final int NAME_INDEX = 1; + private static final int APN_INDEX = 2; + private static final int PROXY_INDEX = 3; + private static final int PORT_INDEX = 4; + private static final int USER_INDEX = 5; + private static final int SERVER_INDEX = 6; + private static final int PASSWORD_INDEX = 7; + private static final int MMSC_INDEX = 8; + private static final int MCC_INDEX = 9; + private static final int MNC_INDEX = 10; + private static final int MMSPROXY_INDEX = 12; + private static final int MMSPORT_INDEX = 13; + private static final int TYPE_INDEX = 14; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.apn_editor); + + sNotSet = getResources().getString(R.string.apn_not_set); + mName = (EditTextPreference) findPreference("apn_name"); + mApn = (EditTextPreference) findPreference("apn_apn"); + mProxy = (EditTextPreference) findPreference("apn_http_proxy"); + mPort = (EditTextPreference) findPreference("apn_http_port"); + mUser = (EditTextPreference) findPreference("apn_user"); + mServer = (EditTextPreference) findPreference("apn_server"); + mPassword = (EditTextPreference) findPreference("apn_password"); + mMmsProxy = (EditTextPreference) findPreference("apn_mms_proxy"); + mMmsPort = (EditTextPreference) findPreference("apn_mms_port"); + mMmsc = (EditTextPreference) findPreference("apn_mmsc"); + mMcc = (EditTextPreference) findPreference("apn_mcc"); + mMnc = (EditTextPreference) findPreference("apn_mnc"); + mApnType = (EditTextPreference) findPreference("apn_type"); + + mRes = getResources(); + + final Intent intent = getIntent(); + final String action = intent.getAction(); + + mFirstTime = icicle == null; + + if (action.equals(Intent.ACTION_EDIT)) { + mUri = intent.getData(); + } else if (action.equals(Intent.ACTION_INSERT)) { + if (mFirstTime) { + mUri = getContentResolver().insert(intent.getData(), new ContentValues()); + } else { + mUri = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, + icicle.getInt(SAVED_POS)); + } + mNewApn = true; + // If we were unable to create a new note, then just finish + // this activity. A RESULT_CANCELED will be sent back to the + // original activity if they requested a result. + if (mUri == null) { + Log.w(TAG, "Failed to insert new telephony provider into " + + getIntent().getData()); + finish(); + return; + } + + // The new entry was created, so assume all will end well and + // set the result to be returned. + setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); + + } else { + finish(); + return; + } + + mCursor = managedQuery(mUri, sProjection, null, null); + mCursor.moveToFirst(); + + fillUi(); + } + + @Override + public void onResume() { + super.onResume(); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + private void fillUi() { + if (mFirstTime) { + mFirstTime = false; + // Fill in all the values from the db in both text editor and summary + mName.setText(mCursor.getString(NAME_INDEX)); + mApn.setText(mCursor.getString(APN_INDEX)); + mProxy.setText(mCursor.getString(PROXY_INDEX)); + mPort.setText(mCursor.getString(PORT_INDEX)); + mUser.setText(mCursor.getString(USER_INDEX)); + mServer.setText(mCursor.getString(SERVER_INDEX)); + mPassword.setText(mCursor.getString(PASSWORD_INDEX)); + mMmsProxy.setText(mCursor.getString(MMSPROXY_INDEX)); + mMmsPort.setText(mCursor.getString(MMSPORT_INDEX)); + mMmsc.setText(mCursor.getString(MMSC_INDEX)); + mMcc.setText(mCursor.getString(MCC_INDEX)); + mMnc.setText(mCursor.getString(MNC_INDEX)); + mApnType.setText(mCursor.getString(TYPE_INDEX)); + if (mNewApn) { + String numeric = + SystemProperties.get(TelephonyProperties.PROPERTY_SIM_OPERATOR_NUMERIC); + // MCC is first 3 chars and then in 2 - 3 chars of MNC + if (numeric != null && numeric.length() > 4) { + // Country code + String mcc = numeric.substring(0, 3); + // Network code + String mnc = numeric.substring(3); + // Auto populate MNC and MCC for new entries, based on what SIM reports + mMcc.setText(mcc); + mMnc.setText(mnc); + mCurMnc = mnc; + mCurMcc = mcc; + } + } + } + + mName.setSummary(checkNull(mName.getText())); + mApn.setSummary(checkNull(mApn.getText())); + mProxy.setSummary(checkNull(mProxy.getText())); + mPort.setSummary(checkNull(mPort.getText())); + mUser.setSummary(checkNull(mUser.getText())); + mServer.setSummary(checkNull(mServer.getText())); + mPassword.setSummary(starify(mPassword.getText())); + mMmsProxy.setSummary(checkNull(mMmsProxy.getText())); + mMmsPort.setSummary(checkNull(mMmsPort.getText())); + mMmsc.setSummary(checkNull(mMmsc.getText())); + mMcc.setSummary(checkNull(mMcc.getText())); + mMnc.setSummary(checkNull(mMnc.getText())); + mApnType.setSummary(checkNull(mApnType.getText())); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + // 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(android.R.drawable.ic_menu_delete); + } + menu.add(0, MENU_SAVE, 0, R.string.menu_save) + .setIcon(android.R.drawable.ic_menu_save); + menu.add(0, MENU_CANCEL, 0, R.string.menu_cancel) + .setIcon(android.R.drawable.ic_menu_close_clear_cancel); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_DELETE: + deleteApn(); + return true; + case MENU_SAVE: + if (validateAndSave(false)) { + finish(); + } + return true; + case MENU_CANCEL: + if (mNewApn) { + getContentResolver().delete(mUri, null, null); + } + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: { + if (validateAndSave(false)) { + finish(); + } + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onSaveInstanceState(Bundle icicle) { + super.onSaveInstanceState(icicle); + validateAndSave(true); + icicle.putInt(SAVED_POS, mCursor.getInt(ID_INDEX)); + } + + /** + * Check the key fields' validity and save if valid. + * @param force save even if the fields are not valid, if the app is + * being suspended + * @return true if the data was saved + */ + private boolean validateAndSave(boolean force) { + String name = checkNotSet(mName.getText()); + String apn = checkNotSet(mApn.getText()); + String mcc = checkNotSet(mMcc.getText()); + String mnc = checkNotSet(mMnc.getText()); + + String errorMsg = null; + if (name.length() < 1) { + errorMsg = mRes.getString(R.string.error_name_empty); + } else if (apn.length() < 1) { + errorMsg = mRes.getString(R.string.error_apn_empty); + } else if (mcc.length() != 3) { + errorMsg = mRes.getString(R.string.error_mcc_not3); + } else if ((mnc.length() & 0xFFFE) != 2) { + errorMsg = mRes.getString(R.string.error_mnc_not23); + } + + if (errorMsg != null && !force) { + showErrorMessage(errorMsg); + return false; + } + + if (!mCursor.moveToFirst()) { + Log.w(TAG, + "Could not go to the first row in the Cursor when saving data."); + return false; + } + + ContentValues values = new ContentValues(); + + values.put(Telephony.Carriers.NAME, name); + values.put(Telephony.Carriers.APN, apn); + values.put(Telephony.Carriers.PROXY, checkNotSet(mProxy.getText())); + values.put(Telephony.Carriers.PORT, checkNotSet(mPort.getText())); + values.put(Telephony.Carriers.MMSPROXY, checkNotSet(mMmsProxy.getText())); + values.put(Telephony.Carriers.MMSPORT, checkNotSet(mMmsPort.getText())); + values.put(Telephony.Carriers.USER, checkNotSet(mUser.getText())); + values.put(Telephony.Carriers.SERVER, checkNotSet(mServer.getText())); + values.put(Telephony.Carriers.PASSWORD, checkNotSet(mPassword.getText())); + values.put(Telephony.Carriers.MMSC, checkNotSet(mMmsc.getText())); + values.put(Telephony.Carriers.TYPE, checkNotSet(mApnType.getText())); + + values.put(Telephony.Carriers.MCC, mcc); + values.put(Telephony.Carriers.MNC, mnc); + + values.put(Telephony.Carriers.NUMERIC, mcc + mnc); + + if (mCurMnc != null && mCurMcc != null) { + if (mCurMnc.equals(mnc) && mCurMcc.equals(mcc)) { + values.put(Telephony.Carriers.CURRENT, 1); + } + } + + getContentResolver().update(mUri, values, null, null); + + return true; + } + + private void showErrorMessage(String message) { + new AlertDialog.Builder(this) + .setTitle(R.string.error_title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + private void deleteApn() { + getContentResolver().delete(mUri, null, null); + finish(); + } + + private String starify(String value) { + if (value == null || value.length() == 0) { + return sNotSet; + } else { + char[] password = new char[value.length()]; + for (int i = 0; i < password.length; i++) { + password[i] = '*'; + } + return new String(password); + } + } + + private String checkNull(String value) { + if (value == null || value.length() == 0) { + return sNotSet; + } else { + return value; + } + } + + private String checkNotSet(String value) { + if (value == null || value.equals(sNotSet)) { + return ""; + } else { + return value; + } + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + Preference pref = findPreference(key); + if (pref != null) { + pref.setSummary(checkNull(sharedPreferences.getString(key, ""))); + } + } +} diff --git a/src/com/android/settings/ApnSettings.java b/src/com/android/settings/ApnSettings.java new file mode 100644 index 000000000..87e3412d2 --- /dev/null +++ b/src/com/android/settings/ApnSettings.java @@ -0,0 +1,229 @@ +/* + * 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 android.app.Dialog; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.provider.Telephony; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +public class ApnSettings extends PreferenceActivity { + + public static final String EXTRA_POSITION = "position"; + public static final String RESTORE_CARRIERS_URI = + "content://telephony/carriers/restore"; + + private static final int ID_INDEX = 0; + private static final int NAME_INDEX = 1; + private static final int APN_INDEX = 2; + + private static final int MENU_NEW = Menu.FIRST; + private static final int MENU_RESTORE = Menu.FIRST + 1; + + private static final int EVENT_RESTORE_DEFAULTAPN_START = 1; + private static final int EVENT_RESTORE_DEFAULTAPN_COMPLETE = 2; + + private static final int DIALOG_RESTORE_DEFAULTAPN = 1001; + + private static final Uri DEFAULTAPN_URI = Uri.parse(RESTORE_CARRIERS_URI); + + private static boolean mRestoreDefaultApnMode; + + private RestoreApnUiHandler mRestoreApnUiHandler; + private RestoreApnProcessHandler mRestoreApnProcessHandler; + + private Cursor mCursor; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.apn_settings); + } + + @Override + protected void onResume() { + super.onResume(); + + if (!mRestoreDefaultApnMode) { + fillList(); + } else { + showDialog(DIALOG_RESTORE_DEFAULTAPN); + } + } + + private void fillList() { + mCursor = managedQuery(Telephony.Carriers.CONTENT_URI, new String[] { + "_id", "name", "apn"}, null, Telephony.Carriers.DEFAULT_SORT_ORDER); + + PreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list"); + apnList.removeAll(); + + mCursor.moveToFirst(); + while (!mCursor.isAfterLast()) { + String name = mCursor.getString(NAME_INDEX); + String apn = mCursor.getString(APN_INDEX); + + if (name != null && apn != null && TextUtils.getTrimmedLength(name) > 0 + && TextUtils.getTrimmedLength(apn) > 0) { + Preference pref = new Preference((Context) this); + pref.setKey(mCursor.getString(ID_INDEX)); + pref.setTitle(name); + pref.setSummary(apn); + pref.setPersistent(false); + apnList.addPreference(pref); + } + + mCursor.moveToNext(); + } + } + + @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); + menu.add(0, MENU_RESTORE, 0, + getResources().getString(R.string.menu_restore)) + .setIcon(android.R.drawable.ic_menu_upload); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_NEW: + addNewApn(); + return true; + + case MENU_RESTORE: + restoreDefaultApn(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void addNewApn() { + startActivity(new Intent(Intent.ACTION_INSERT, Telephony.Carriers.CONTENT_URI)); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + int pos = Integer.parseInt(preference.getKey()); + Uri url = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, pos); + startActivity(new Intent(Intent.ACTION_EDIT, url)); + return true; + } + + private boolean restoreDefaultApn() { + showDialog(DIALOG_RESTORE_DEFAULTAPN); + mRestoreDefaultApnMode = true; + + if (mRestoreApnUiHandler == null) { + mRestoreApnUiHandler = new RestoreApnUiHandler(); + } + + if (mRestoreApnProcessHandler == null) { + HandlerThread restoreDefaultApnThread = new HandlerThread( + "Restore default APN Handler: Process Thread"); + restoreDefaultApnThread.start(); + mRestoreApnProcessHandler = new RestoreApnProcessHandler( + restoreDefaultApnThread.getLooper(), mRestoreApnUiHandler); + } + + mRestoreApnProcessHandler + .sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_START); + return true; + } + + private class RestoreApnUiHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_RESTORE_DEFAULTAPN_COMPLETE: + fillList(); + getPreferenceScreen().setEnabled(true); + mRestoreDefaultApnMode = false; + dismissDialog(DIALOG_RESTORE_DEFAULTAPN); + Toast.makeText( + ApnSettings.this, + getResources().getString( + R.string.restore_default_apn_completed), + Toast.LENGTH_LONG).show(); + break; + } + } + } + + private class RestoreApnProcessHandler extends Handler { + private Handler mRestoreApnUiHandler; + + public RestoreApnProcessHandler(Looper looper, Handler restoreApnUiHandler) { + super(looper); + this.mRestoreApnUiHandler = restoreApnUiHandler; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_RESTORE_DEFAULTAPN_START: + ContentResolver resolver = getContentResolver(); + resolver.delete(DEFAULTAPN_URI, null, null); + mRestoreApnUiHandler + .sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_COMPLETE); + break; + } + } + } + + @Override + protected Dialog onCreateDialog(int id) { + if (id == DIALOG_RESTORE_DEFAULTAPN) { + ProgressDialog dialog = new ProgressDialog(this); + 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/ApplicationSettings.java b/src/com/android/settings/ApplicationSettings.java new file mode 100644 index 000000000..85fe11fcb --- /dev/null +++ b/src/com/android/settings/ApplicationSettings.java @@ -0,0 +1,98 @@ +/* + * 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 android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.provider.Settings; + +public class ApplicationSettings extends PreferenceActivity implements + DialogInterface.OnClickListener { + + private static final String KEY_TOGGLE_INSTALL_APPLICATIONS = "toggle_install_applications"; + private static final String KEY_QUICK_LAUNCH = "quick_launch"; + + private CheckBoxPreference mToggleAppInstallation; + + private DialogInterface mWarnInstallApps; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.application_settings); + + mToggleAppInstallation = (CheckBoxPreference) findPreference(KEY_TOGGLE_INSTALL_APPLICATIONS); + mToggleAppInstallation.setChecked(isNonMarketAppsAllowed()); + + if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) { + // No hard keyboard, remove the setting for quick launch + Preference quickLaunchSetting = findPreference(KEY_QUICK_LAUNCH); + getPreferenceScreen().removePreference(quickLaunchSetting); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference == mToggleAppInstallation) { + if (mToggleAppInstallation.isChecked()) { + mToggleAppInstallation.setChecked(false); + warnAppInstallation(); + } else { + setNonMarketAppsAllowed(false); + } + } + + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + + public void onClick(DialogInterface dialog, int which) { + if (dialog == mWarnInstallApps && which == DialogInterface.BUTTON1) { + setNonMarketAppsAllowed(true); + mToggleAppInstallation.setChecked(true); + } + } + + private void setNonMarketAppsAllowed(boolean enabled) { + // Change the system setting + Settings.Secure.putInt(getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS, + enabled ? 1 : 0); + } + + private boolean isNonMarketAppsAllowed() { + return Settings.Secure.getInt(getContentResolver(), + Settings.Secure.INSTALL_NON_MARKET_APPS, 0) > 0; + } + + private void warnAppInstallation() { + mWarnInstallApps = new AlertDialog.Builder(this) + .setTitle(getString(R.string.error_title)) + .setIcon(com.android.internal.R.drawable.ic_dialog_alert) + .setMessage(getResources().getString(R.string.install_all_warning)) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + +} diff --git a/src/com/android/settings/BandMode.java b/src/com/android/settings/BandMode.java new file mode 100644 index 000000000..1297cad5d --- /dev/null +++ b/src/com/android/settings/BandMode.java @@ -0,0 +1,217 @@ +package com.android.settings; + +import android.app.Activity; +import android.app.AlertDialog; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneFactory; +import android.os.Bundle; +import android.os.Message; +import android.os.Handler; +import android.os.AsyncResult; +import android.util.Log; +import android.content.DialogInterface; +import android.view.View; +import android.view.WindowManager; +import android.view.Window; +import android.widget.ListView; +import android.widget.ArrayAdapter; +import android.widget.AdapterView; + + +/** + * Radio Band Mode Selection Class + * + * It will query baseband about all available band modes and display them + * in screen. It will display all six band modes if the query failed. + * + * After user select one band, it will send the selection to baseband. + * + * It will alter user the result of select operation and exit, no matter success + * or not. + * + */ +public class BandMode extends Activity { + private static final String LOG_TAG = "phone"; + private static final boolean DBG = false; + + private static final int EVENT_BAND_SCAN_COMPLETED = 100; + private static final int EVENT_BAND_SELECTION_DONE = 200; + + private static final String[] BAND_NAMES = new String[] { + "Automatic", + "EURO Band", + "USA Band", + "JAPAN Band", + "AUS Band", + "AUS2 Band" + }; + + private ListView mBandList; + private ArrayAdapter mBandListAdapter; + private BandListItem mTargetBand = null; + private DialogInterface mProgressPanel; + + private Phone mPhone = null; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.band_mode); + + setTitle(getString(R.string.band_mode_title)); + getWindow().setLayout(WindowManager.LayoutParams.FILL_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT); + + mPhone = PhoneFactory.getDefaultPhone(); + + mBandList = (ListView) findViewById(R.id.band); + mBandListAdapter = new ArrayAdapter<BandListItem>(this, + android.R.layout.simple_list_item_1); + mBandList.setAdapter(mBandListAdapter); + mBandList.setOnItemClickListener(mBandSelectionHandler); + + + + loadBandList(); + } + + private AdapterView.OnItemClickListener mBandSelectionHandler = + new AdapterView.OnItemClickListener () { + public void onItemClick(AdapterView parent, View v, + int position, long id) { + + getWindow().setFeatureInt( + Window.FEATURE_INDETERMINATE_PROGRESS, + Window.PROGRESS_VISIBILITY_ON); + + mTargetBand = (BandListItem) parent.getAdapter().getItem(position); + + if (DBG) log("Select band : " + mTargetBand.toString()); + + Message msg = + mHandler.obtainMessage(EVENT_BAND_SELECTION_DONE); + mPhone.setBandMode(mTargetBand.getBand(), msg); + } + }; + + private class BandListItem { + private int mBandMode = Phone.BM_UNSPECIFIED; + + public BandListItem(int bm) { + mBandMode = bm; + } + + public int getBand() { + return mBandMode; + } + + public String toString() { + return BAND_NAMES[mBandMode]; + } + } + + private void loadBandList() { + String str = getString(R.string.band_mode_loading); + + if (DBG) log(str); + + + //ProgressDialog.show(this, null, str, true, true, null); + mProgressPanel = new AlertDialog.Builder(this) + .setMessage(str) + .show(); + + Message msg = mHandler.obtainMessage(EVENT_BAND_SCAN_COMPLETED); + mPhone.queryAvailableBandMode(msg); + + } + + private void bandListLoaded(AsyncResult result) { + if (DBG) log("network list loaded"); + + if (mProgressPanel != null) mProgressPanel.dismiss(); + + clearList(); + + boolean addBandSuccess = false; + BandListItem item; + + if (result.result != null) { + int bands[] = (int[])result.result; + int size = bands[0]; + + if (size > 0) { + for (int i=1; i<size; i++) { + item = new BandListItem(bands[i]); + mBandListAdapter.add(item); + if (DBG) log("Add " + item.toString()); + } + addBandSuccess = true; + } + } + + if (addBandSuccess == false) { + if (DBG) log("Error in query, add default list"); + for (int i=0; i<Phone.BM_BOUNDARY; i++) { + item = new BandListItem(i); + mBandListAdapter.add(item); + if (DBG) log("Add default " + item.toString()); + } + } + mBandList.requestFocus(); + } + + private void displayBandSelectionResult(Throwable ex) { + String status = getString(R.string.band_mode_set) + +" [" + mTargetBand.toString() + "] "; + + if (ex != null) { + status = status + getString(R.string.band_mode_failed); + } else { + status = status + getString(R.string.band_mode_succeeded); + } + + mProgressPanel = new AlertDialog.Builder(this) + .setMessage(status) + .setPositiveButton(android.R.string.ok, null).show(); + } + + private void clearList() { + while(mBandListAdapter.getCount() > 0) { + mBandListAdapter.remove( + mBandListAdapter.getItem(0)); + } + } + + private void log(String msg) { + Log.d(LOG_TAG, "[BandsList] " + msg); + } + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + AsyncResult ar; + switch (msg.what) { + case EVENT_BAND_SCAN_COMPLETED: + ar = (AsyncResult) msg.obj; + + bandListLoaded(ar); + break; + + case EVENT_BAND_SELECTION_DONE: + ar = (AsyncResult) msg.obj; + + getWindow().setFeatureInt( + Window.FEATURE_INDETERMINATE_PROGRESS, + Window.PROGRESS_VISIBILITY_OFF); + + displayBandSelectionResult(ar.exception); + break; + } + } + }; + + +} diff --git a/src/com/android/settings/BatteryInfo.java b/src/com/android/settings/BatteryInfo.java new file mode 100644 index 000000000..f9962fa2f --- /dev/null +++ b/src/com/android/settings/BatteryInfo.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2006 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.text.format.DateUtils; +import android.widget.TextView; + +import com.android.internal.app.IBatteryStats; + +public class BatteryInfo extends Activity { + private TextView mStatus; + private TextView mLevel; + private TextView mScale; + private TextView mHealth; + private TextView mVoltage; + private TextView mTemperature; + private TextView mTechnology; + private TextView mUptime; + private TextView mAwakeBattery; + private TextView mAwakePlugged; + private TextView mScreenOn; + private IBatteryStats mBatteryStats; + private IPowerManager mScreenStats; + + private static final int EVENT_TICK = 1; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_TICK: + updateBatteryStats(); + sendEmptyMessageDelayed(EVENT_TICK, 1000); + break; + } + } + }; + + /** + * Format a number of tenths-units as a decimal string without using a + * conversion to float. E.g. 347 -> "34.7" + */ + private final String tenthsToFixedString(int x) { + int tens = x / 10; + return new String("" + tens + "." + (x - 10*tens)); + } + + /** + *Listens for intent broadcasts + */ + private IntentFilter mIntentFilter; + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { + int plugType = intent.getIntExtra("plugged", 0); + + mLevel.setText("" + intent.getIntExtra("level", 0)); + mScale.setText("" + intent.getIntExtra("scale", 0)); + mVoltage.setText("" + intent.getIntExtra("voltage", 0) + " " + + getString(R.string.battery_info_voltage_units)); + mTemperature.setText("" + tenthsToFixedString(intent.getIntExtra("temperature", 0)) + + getString(R.string.battery_info_temperature_units)); + mTechnology.setText("" + intent.getStringExtra("technology")); + + int status = intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN); + String statusString; + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + statusString = getString(R.string.battery_info_status_charging); + if (plugType > 0) { + statusString = statusString + " " + getString( + (plugType == BatteryManager.BATTERY_PLUGGED_AC) + ? R.string.battery_info_status_charging_ac + : R.string.battery_info_status_charging_usb); + } + } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { + statusString = getString(R.string.battery_info_status_discharging); + } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { + statusString = getString(R.string.battery_info_status_not_charging); + } else if (status == BatteryManager.BATTERY_STATUS_FULL) { + statusString = getString(R.string.battery_info_status_full); + } else { + statusString = getString(R.string.battery_info_status_unknown); + } + mStatus.setText(statusString); + + int health = intent.getIntExtra("health", BatteryManager.BATTERY_HEALTH_UNKNOWN); + String healthString; + if (health == BatteryManager.BATTERY_HEALTH_GOOD) { + healthString = getString(R.string.battery_info_health_good); + } else if (health == BatteryManager.BATTERY_HEALTH_OVERHEAT) { + healthString = getString(R.string.battery_info_health_overheat); + } else if (health == BatteryManager.BATTERY_HEALTH_DEAD) { + healthString = getString(R.string.battery_info_health_dead); + } else if (health == BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE) { + healthString = getString(R.string.battery_info_health_over_voltage); + } else if (health == BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE) { + healthString = getString(R.string.battery_info_health_unspecified_failure); + } else { + healthString = getString(R.string.battery_info_health_unknown); + } + mHealth.setText(healthString); + } + } + }; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.battery_info); + + // create the IntentFilter that will be used to listen + // to battery status broadcasts + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); + } + + @Override + public void onResume() { + super.onResume(); + + mStatus = (TextView)findViewById(R.id.status); + mLevel = (TextView)findViewById(R.id.level); + mScale = (TextView)findViewById(R.id.scale); + mHealth = (TextView)findViewById(R.id.health); + mTechnology = (TextView)findViewById(R.id.technology); + mVoltage = (TextView)findViewById(R.id.voltage); + mTemperature = (TextView)findViewById(R.id.temperature); + mUptime = (TextView) findViewById(R.id.uptime); + mAwakeBattery = (TextView) findViewById(R.id.awakeBattery); + mAwakePlugged = (TextView) findViewById(R.id.awakePlugged); + mScreenOn = (TextView) findViewById(R.id.screenOn); + + // Get awake time plugged in and on battery + mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); + mScreenStats = IPowerManager.Stub.asInterface(ServiceManager.getService(POWER_SERVICE)); + mHandler.sendEmptyMessageDelayed(EVENT_TICK, 1000); + + registerReceiver(mIntentReceiver, mIntentFilter); + } + + @Override + public void onPause() { + super.onPause(); + mHandler.removeMessages(EVENT_TICK); + + // we are no longer on the screen stop the observers + unregisterReceiver(mIntentReceiver); + } + + private void updateBatteryStats() { + long uptime = SystemClock.elapsedRealtime(); + mUptime.setText(DateUtils.formatElapsedTime(uptime / 1000)); + + if (mBatteryStats != null) { + try { + long awakeTimeBattery = mBatteryStats.getAwakeTimeBattery() / 1000; + long awakeTimePluggedIn = mBatteryStats.getAwakeTimePlugged() / 1000; + mAwakeBattery.setText(DateUtils.formatElapsedTime(awakeTimeBattery / 1000) + + " (" + (100 * awakeTimeBattery / uptime) + "%)"); + mAwakePlugged.setText(DateUtils.formatElapsedTime(awakeTimePluggedIn / 1000) + + " (" + (100 * awakeTimePluggedIn / uptime) + "%)"); + } catch (RemoteException re) { + mAwakeBattery.setText("Unknown"); + mAwakePlugged.setText("Unknown"); + } + } + if (mScreenStats != null) { + try { + long screenOnTime = mScreenStats.getScreenOnTime(); + mScreenOn.setText(DateUtils.formatElapsedTime(screenOnTime / 1000)); + } catch (RemoteException re) { + mScreenOn.setText("Unknown"); + } + } + } + +} diff --git a/src/com/android/settings/BrightnessPreference.java b/src/com/android/settings/BrightnessPreference.java new file mode 100644 index 000000000..a9851cc93 --- /dev/null +++ b/src/com/android/settings/BrightnessPreference.java @@ -0,0 +1,103 @@ +/* + * 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 android.content.Context; +import android.os.RemoteException; +import android.os.IHardwareService; +import android.os.ServiceManager; +import android.preference.SeekBarPreference; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.SeekBar; + +import java.util.Map; + +public class BrightnessPreference extends SeekBarPreference implements + SeekBar.OnSeekBarChangeListener { + + private SeekBar mSeekBar; + + private int mOldBrightness; + + // Backlight range is from 0 - 255. Need to make sure that user + // doesn't set the backlight to 0 and get stuck + private static final int MINIMUM_BACKLIGHT = android.os.Power.BRIGHTNESS_DIM + 10; + private static final int MAXIMUM_BACKLIGHT = android.os.Power.BRIGHTNESS_ON; + + public BrightnessPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + mSeekBar = getSeekBar(view); + mSeekBar.setOnSeekBarChangeListener(this); + mSeekBar.setMax(MAXIMUM_BACKLIGHT - MINIMUM_BACKLIGHT); + try { + mOldBrightness = Settings.System.getInt(getContext().getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS); + } catch (SettingNotFoundException snfe) { + mOldBrightness = MAXIMUM_BACKLIGHT; + } + mSeekBar.setProgress(mOldBrightness - MINIMUM_BACKLIGHT); + } + + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + setBrightness(progress + MINIMUM_BACKLIGHT); + } + + public void onStartTrackingTouch(SeekBar seekBar) { + // NA + } + + public void onStopTrackingTouch(SeekBar seekBar) { + // NA + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS, + mSeekBar.getProgress() + MINIMUM_BACKLIGHT); + } else { + setBrightness(mOldBrightness); + } + } + + private void setBrightness(int brightness) { + try { + IHardwareService hardware = IHardwareService.Stub.asInterface( + ServiceManager.getService("hardware")); + if (hardware != null) { + hardware.setScreenBacklight(brightness); + } + } catch (RemoteException doe) { + + } + } +} + diff --git a/src/com/android/settings/ChooseLockPattern.java b/src/com/android/settings/ChooseLockPattern.java new file mode 100644 index 000000000..47fc07f64 --- /dev/null +++ b/src/com/android/settings/ChooseLockPattern.java @@ -0,0 +1,497 @@ +/* + * 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.google.android.collect.Lists; + +import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import static com.android.internal.widget.LockPatternView.DisplayMode; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * If the user has a lock pattern set already, makes them confirm the existing one. + * + * Then, prompts the user to choose a lock pattern: + * - prompts for initial pattern + * - asks for confirmation / restart + * - saves chosen password when confirmed + */ +public class ChooseLockPattern extends Activity implements View.OnClickListener{ + + /** + * Used by the choose lock pattern wizard to indicate the wizard is + * finished, and each activity in the wizard should finish. + * <p> + * Previously, each activity in the wizard would finish itself after + * starting the next activity. However, this leads to broken 'Back' + * behavior. So, now an activity does not finish itself until it gets this + * result. + */ + static final int RESULT_FINISHED = RESULT_FIRST_USER; + + // how long after a confirmation message is shown before moving on + static final int INFORMATION_MSG_TIMEOUT_MS = 3000; + + // how long we wait to clear a wrong pattern + private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; + + private static final int ID_EMPTY_MESSAGE = -1; + + + protected TextView mHeaderText; + protected LockPatternView mLockPatternView; + protected TextView mFooterText; + private TextView mFooterLeftButton; + private TextView mFooterRightButton; + + protected List<LockPatternView.Cell> mChosenPattern = null; + + protected LockPatternUtils mLockPatternUtils; + + /** + * The patten used during the help screen to show how to draw a pattern. + */ + private final List<LockPatternView.Cell> mAnimatePattern = + Collections.unmodifiableList( + Lists.newArrayList( + LockPatternView.Cell.of(0, 0), + LockPatternView.Cell.of(0, 1), + LockPatternView.Cell.of(1, 1), + LockPatternView.Cell.of(2, 1) + )); + + + /** + * The pattern listener that responds according to a user choosing a new + * lock pattern. + */ + protected LockPatternView.OnPatternListener mChooseNewLockPatternListener = new LockPatternView.OnPatternListener() { + + public void onPatternStart() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + patternInProgress(); + } + + public void onPatternCleared() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + } + + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) { + if (mChosenPattern == null) throw new IllegalStateException("null chosen pattern in stage 'need to confirm"); + if (mChosenPattern.equals(pattern)) { + updateStage(Stage.ChoiceConfirmed); + } else { + updateStage(Stage.ConfirmWrong); + } + } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort){ + if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { + updateStage(Stage.ChoiceTooShort); + } else { + mChosenPattern = new ArrayList<LockPatternView.Cell>(pattern); + updateStage(Stage.FirstChoiceValid); + } + } else { + throw new IllegalStateException("Unexpected stage " + mUiStage + " when " + + "entering the pattern."); + } + } + + private void patternInProgress() { + mHeaderText.setText(R.string.lockpattern_recording_inprogress); + mFooterText.setText(""); + mFooterLeftButton.setEnabled(false); + mFooterRightButton.setEnabled(false); + } + }; + + + /** + * The states of the left footer button. + */ + enum LeftButtonMode { + Cancel(R.string.cancel, true), + CancelDisabled(R.string.cancel, false), + Retry(R.string.lockpattern_retry_button_text, true), + RetryDisabled(R.string.lockpattern_retry_button_text, false), + Gone(ID_EMPTY_MESSAGE, false); + + + /** + * @param text The displayed text for this mode. + * @param enabled Whether the button should be enabled. + */ + LeftButtonMode(int text, boolean enabled) { + this.text = text; + this.enabled = enabled; + } + + final int text; + final boolean enabled; + } + + /** + * The states of the right button. + */ + enum RightButtonMode { + Continue(R.string.lockpattern_continue_button_text, true), + ContinueDisabled(R.string.lockpattern_continue_button_text, false), + Confirm(R.string.lockpattern_confirm_button_text, true), + ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), + Ok(android.R.string.ok, true); + + /** + * @param text The displayed text for this mode. + * @param enabled Whether the button should be enabled. + */ + RightButtonMode(int text, boolean enabled) { + this.text = text; + this.enabled = enabled; + } + + final int text; + final boolean enabled; + } + + /** + * Keep track internally of where the user is in choosing a pattern. + */ + protected enum Stage { + + Introduction( + R.string.lockpattern_recording_intro_header, + LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, + R.string.lockpattern_recording_intro_footer, true), + HelpScreen( + R.string.lockpattern_settings_help_how_to_record, + LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), + ChoiceTooShort( + R.string.lockpattern_recording_incorrect_too_short, + LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, + ID_EMPTY_MESSAGE, true), + FirstChoiceValid( + R.string.lockpattern_pattern_entered_header, + LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), + NeedToConfirm( + R.string.lockpattern_need_to_confirm, + LeftButtonMode.CancelDisabled, RightButtonMode.ConfirmDisabled, + ID_EMPTY_MESSAGE, true), + ConfirmWrong( + R.string.lockpattern_need_to_unlock_wrong, + LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, + ID_EMPTY_MESSAGE, true), + ChoiceConfirmed( + R.string.lockpattern_pattern_confirmed_header, + LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false); + + + /** + * @param headerMessage The message displayed at the top. + * @param leftMode The mode of the left button. + * @param rightMode The mode of the right button. + * @param footerMessage The footer message. + * @param patternEnabled Whether the pattern widget is enabled. + */ + Stage(int headerMessage, + LeftButtonMode leftMode, + RightButtonMode rightMode, + int footerMessage, boolean patternEnabled) { + this.headerMessage = headerMessage; + this.leftMode = leftMode; + this.rightMode = rightMode; + this.footerMessage = footerMessage; + this.patternEnabled = patternEnabled; + } + + final int headerMessage; + final LeftButtonMode leftMode; + final RightButtonMode rightMode; + final int footerMessage; + final boolean patternEnabled; + } + + private Stage mUiStage = Stage.Introduction; + + private Runnable mClearPatternRunnable = new Runnable() { + public void run() { + mLockPatternView.clearPattern(); + } + }; + + private static final String KEY_UI_STAGE = "uiStage"; + private static final String KEY_PATTERN_CHOICE = "chosenPattern"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLockPatternUtils = new LockPatternUtils(getContentResolver()); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + + setupViews(); + + // make it so unhandled touch events within the unlock screen go to the + // lock pattern view. + final LinearLayoutWithDefaultTouchRecepient topLayout + = (LinearLayoutWithDefaultTouchRecepient) findViewById( + R.id.topLayout); + topLayout.setDefaultTouchRecepient(mLockPatternView); + + if (savedInstanceState == null) { + // first launch + updateStage(Stage.Introduction); + if (mLockPatternUtils.savedPatternExists()) { + confirmPattern(); + } + } else { + // restore from previous state + final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); + if (patternString != null) { + mChosenPattern = LockPatternUtils.stringToPattern(patternString); + } + updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); + } + } + + /** + * Keep all "find view" related stuff confined to this function since in + * case someone needs to subclass and customize. + */ + protected void setupViews() { + setContentView(R.layout.choose_lock_pattern); + + mHeaderText = (TextView) findViewById(R.id.headerText); + + mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); + mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); + mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); + + mFooterText = (TextView) findViewById(R.id.footerText); + + mFooterLeftButton = (TextView) findViewById(R.id.footerLeftButton); + mFooterRightButton = (TextView) findViewById(R.id.footerRightButton); + + mFooterLeftButton.setOnClickListener(this); + mFooterRightButton.setOnClickListener(this); + } + + public void onClick(View v) { + if (v == mFooterLeftButton) { + if (mUiStage.leftMode == LeftButtonMode.Retry) { + mChosenPattern = null; + mLockPatternView.clearPattern(); + updateStage(Stage.Introduction); + } else if (mUiStage.leftMode == LeftButtonMode.Cancel) { + // They are canceling the entire wizard + setResult(RESULT_FINISHED); + finish(); + } else { + throw new IllegalStateException("left footer button pressed, but stage of " + + mUiStage + " doesn't make sense"); + } + } else if (v == mFooterRightButton) { + + if (mUiStage.rightMode == RightButtonMode.Continue) { + if (mUiStage != Stage.FirstChoiceValid) { + throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid + + " when button is " + RightButtonMode.Continue); + } + updateStage(Stage.NeedToConfirm); + } else if (mUiStage.rightMode == RightButtonMode.Confirm) { + if (mUiStage != Stage.ChoiceConfirmed) { + throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed + + " when button is " + RightButtonMode.Confirm); + } + saveChosenPatternAndFinish(); + } else if (mUiStage.rightMode == RightButtonMode.Ok) { + if (mUiStage != Stage.HelpScreen) { + throw new IllegalStateException("Help screen is only mode with ok button, but " + + "stage is " + mUiStage); + } + mLockPatternView.clearPattern(); + mLockPatternView.setDisplayMode(DisplayMode.Correct); + updateStage(Stage.Introduction); + } + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { + if (mUiStage == Stage.HelpScreen) { + updateStage(Stage.Introduction); + return true; + } + } + if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) { + updateStage(Stage.HelpScreen); + return true; + } + + return super.onKeyDown(keyCode, event); + } + + /** + * Launch screen to confirm the existing lock pattern. + * @see #onActivityResult(int, int, android.content.Intent) + */ + protected void confirmPattern() { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", "com.android.settings.ConfirmLockPattern"); + startActivityForResult(intent, 55); + } + + /** + * @see #confirmPattern + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode != 55) { + return; + } + + if (resultCode != Activity.RESULT_OK) { + setResult(RESULT_FINISHED); + finish(); + } + updateStage(Stage.Introduction); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); + if (mChosenPattern != null) { + outState.putString(KEY_PATTERN_CHOICE, LockPatternUtils.patternToString(mChosenPattern)); + } + } + + + /** + * Updates the messages and buttons appropriate to what stage the user + * is at in choosing a view. This doesn't handle clearing out the pattern; + * the pattern is expected to be in the right state. + * @param stage + */ + protected void updateStage(Stage stage) { + + mUiStage = stage; + + // header text, footer text, visibility and + // enabled state all known from the stage + if (stage == Stage.ChoiceTooShort) { + mHeaderText.setText( + getResources().getString( + stage.headerMessage, + LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); + } else { + mHeaderText.setText(stage.headerMessage); + } + if (stage.footerMessage == ID_EMPTY_MESSAGE) { + mFooterText.setText(""); + } else { + mFooterText.setText(stage.footerMessage); + } + + if (stage.leftMode == LeftButtonMode.Gone) { + mFooterLeftButton.setVisibility(View.GONE); + } else { + mFooterLeftButton.setVisibility(View.VISIBLE); + mFooterLeftButton.setText(stage.leftMode.text); + mFooterLeftButton.setEnabled(stage.leftMode.enabled); + } + + mFooterRightButton.setText(stage.rightMode.text); + mFooterRightButton.setEnabled(stage.rightMode.enabled); + + // same for whether the patten is enabled + if (stage.patternEnabled) { + mLockPatternView.enableInput(); + } else { + mLockPatternView.disableInput(); + } + + // the rest of the stuff varies enough that it is easier just to handle + // on a case by case basis. + mLockPatternView.setDisplayMode(DisplayMode.Correct); + + switch (mUiStage) { + case Introduction: + mLockPatternView.clearPattern(); + break; + case HelpScreen: + mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); + break; + case ChoiceTooShort: + mLockPatternView.setDisplayMode(DisplayMode.Wrong); + postClearPatternRunnable(); + break; + case FirstChoiceValid: + break; + case NeedToConfirm: + mLockPatternView.clearPattern(); + break; + case ConfirmWrong: + mLockPatternView.setDisplayMode(DisplayMode.Wrong); + postClearPatternRunnable(); + break; + case ChoiceConfirmed: + break; + } + } + + + // clear the wrong pattern unless they have started a new one + // already + private void postClearPatternRunnable() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); + } + + private void saveChosenPatternAndFinish() { + boolean patternExistedBefore = mLockPatternUtils.savedPatternExists(); + mLockPatternUtils.saveLockPattern(mChosenPattern); + + // if setting pattern for first time, enable the lock gesture. otherwise, + // keep the user's setting. + if (!patternExistedBefore) { + mLockPatternUtils.setLockPatternEnabled(true); + mLockPatternUtils.setVisiblePatternEnabled(true); + } + + setResult(RESULT_FINISHED); + finish(); + } +} diff --git a/src/com/android/settings/ChooseLockPatternExample.java b/src/com/android/settings/ChooseLockPatternExample.java new file mode 100644 index 000000000..77517b9d3 --- /dev/null +++ b/src/com/android/settings/ChooseLockPatternExample.java @@ -0,0 +1,104 @@ +/* + * 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 android.app.Activity; +import android.content.Intent; +import android.graphics.drawable.AnimationDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.ImageView; + +public class ChooseLockPatternExample extends Activity implements View.OnClickListener { + private static final int REQUESTCODE_CHOOSE = 1; + private static final long START_DELAY = 1000; + protected static final String TAG = "Settings"; + private View mNextButton; + private View mSkipButton; + private View mImageView; + private AnimationDrawable mAnimation; + private Handler mHandler = new Handler(); + private Runnable mRunnable = new Runnable() { + public void run() { + startAnimation(mAnimation); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.choose_lock_pattern_example); + initViews(); + } + + @Override + protected void onResume() { + super.onResume(); + mHandler.postDelayed(mRunnable, START_DELAY); + } + + @Override + protected void onPause() { + super.onPause(); + stopAnimation(mAnimation); + } + + public void onClick(View v) { + if (v == mSkipButton) { + // Canceling, so finish all + setResult(ChooseLockPattern.RESULT_FINISHED); + finish(); + } else if (v == mNextButton) { + stopAnimation(mAnimation); + Intent intent = new Intent(this, ChooseLockPattern.class); + startActivityForResult(intent, REQUESTCODE_CHOOSE); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUESTCODE_CHOOSE && resultCode == ChooseLockPattern.RESULT_FINISHED) { + setResult(resultCode); + finish(); + } + } + + private void initViews() { + mNextButton = findViewById(R.id.next_button); + mNextButton.setOnClickListener(this); + + mSkipButton = findViewById(R.id.skip_button); + mSkipButton.setOnClickListener(this); + + mImageView = (ImageView) findViewById(R.id.lock_anim); + mImageView.setBackgroundResource(R.drawable.lock_anim); + mImageView.setOnClickListener(this); + mAnimation = (AnimationDrawable) mImageView.getBackground(); + } + + protected void startAnimation(final AnimationDrawable animation) { + if (animation != null && !animation.isRunning()) { + animation.run(); + } + } + + protected void stopAnimation(final AnimationDrawable animation) { + if (animation != null && animation.isRunning()) animation.stop(); + } +} + diff --git a/src/com/android/settings/ChooseLockPatternTutorial.java b/src/com/android/settings/ChooseLockPatternTutorial.java new file mode 100644 index 000000000..a0a878a05 --- /dev/null +++ b/src/com/android/settings/ChooseLockPatternTutorial.java @@ -0,0 +1,75 @@ +/* + * 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 com.android.internal.widget.LockPatternUtils; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +public class ChooseLockPatternTutorial extends Activity implements View.OnClickListener { + private static final int REQUESTCODE_EXAMPLE = 1; + + private View mNextButton; + private View mSkipButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Don't show the tutorial if the user has seen it before. + LockPatternUtils lockPatternUtils = new LockPatternUtils(getContentResolver()); + if (savedInstanceState == null && lockPatternUtils.savedPatternExists()) { + Intent intent = new Intent(); + intent.setClassName("com.android.settings", "com.android.settings.ChooseLockPattern"); + startActivity(intent); + finish(); + } else { + initViews(); + } + } + + private void initViews() { + setContentView(R.layout.choose_lock_pattern_tutorial); + mNextButton = findViewById(R.id.next_button); + mNextButton.setOnClickListener(this); + mSkipButton = findViewById(R.id.skip_button); + mSkipButton.setOnClickListener(this); + } + + public void onClick(View v) { + if (v == mSkipButton) { + // Canceling, so finish all + setResult(ChooseLockPattern.RESULT_FINISHED); + finish(); + } else if (v == mNextButton) { + startActivityForResult(new Intent(this, ChooseLockPatternExample.class), + REQUESTCODE_EXAMPLE); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUESTCODE_EXAMPLE && resultCode == ChooseLockPattern.RESULT_FINISHED) { + setResult(resultCode); + finish(); + } + } + +} + diff --git a/src/com/android/settings/ConfirmLockPattern.java b/src/com/android/settings/ConfirmLockPattern.java new file mode 100644 index 000000000..44baccc65 --- /dev/null +++ b/src/com/android/settings/ConfirmLockPattern.java @@ -0,0 +1,260 @@ +/* + * 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 com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; + +import android.app.Activity; +import android.content.Intent; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.os.Bundle; +import android.widget.TextView; +import android.view.Window; + +import java.util.List; + +/** + * Launch this when you want the user to confirm their lock pattern. + * + * Sets an activity result of {@link Activity#RESULT_OK} when the user + * successfully confirmed their pattern. + */ +public class ConfirmLockPattern extends Activity { + + /** + * Names of {@link CharSequence} fields within the originating {@link Intent} + * that are used to configure the keyguard confirmation view's labeling. + * The view will use the system-defined resource strings for any labels that + * the caller does not supply. + */ + public static final String HEADER_TEXT = "com.android.settings.ConfirmLockPattern.header"; + public static final String FOOTER_TEXT = "com.android.settings.ConfirmLockPattern.footer"; + public static final String HEADER_WRONG_TEXT = "com.android.settings.ConfirmLockPattern.header_wrong"; + public static final String FOOTER_WRONG_TEXT = "com.android.settings.ConfirmLockPattern.footer_wrong"; + + // how long we wait to clear a wrong pattern + private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; + + private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts"; + + private LockPatternView mLockPatternView; + private LockPatternUtils mLockPatternUtils; + private int mNumWrongConfirmAttempts; + private CountDownTimer mCountdownTimer; + + private TextView mHeaderTextView; + private TextView mFooterTextView; + + // caller-supplied text for various prompts + private CharSequence mHeaderText; + private CharSequence mFooterText; + private CharSequence mHeaderWrongText; + private CharSequence mFooterWrongText; + + + private enum Stage { + NeedToUnlock, + NeedToUnlockWrong, + LockedOut + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLockPatternUtils = new LockPatternUtils(getContentResolver()); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.confirm_lock_pattern); + + mHeaderTextView = (TextView) findViewById(R.id.headerText); + mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); + mFooterTextView = (TextView) findViewById(R.id.footerText); + + // make it so unhandled touch events within the unlock screen go to the + // lock pattern view. + final LinearLayoutWithDefaultTouchRecepient topLayout + = (LinearLayoutWithDefaultTouchRecepient) findViewById( + R.id.topLayout); + topLayout.setDefaultTouchRecepient(mLockPatternView); + + Intent intent = getIntent(); + if (intent != null) { + mHeaderText = intent.getCharSequenceExtra(HEADER_TEXT); + mFooterText = intent.getCharSequenceExtra(FOOTER_TEXT); + mHeaderWrongText = intent.getCharSequenceExtra(HEADER_WRONG_TEXT); + mFooterWrongText = intent.getCharSequenceExtra(FOOTER_WRONG_TEXT); + } + + mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); + mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener); + updateStage(Stage.NeedToUnlock); + + if (savedInstanceState != null) { + mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS); + } else { + // on first launch, if no lock pattern is set, then finish with + // success (don't want user to get stuck confirming something that + // doesn't exist). + if (!mLockPatternUtils.savedPatternExists()) { + setResult(RESULT_OK); + finish(); + } + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + // deliberately not calling super since we are managing this in full + outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); + if (deadline != 0) { + handleAttemptLockout(deadline); + } + } + + private void updateStage(Stage stage) { + + switch (stage) { + case NeedToUnlock: + if (mHeaderText != null) { + mHeaderTextView.setText(mHeaderText); + } else { + mHeaderTextView.setText(R.string.lockpattern_need_to_unlock); + } + if (mFooterText != null) { + mFooterTextView.setText(mFooterText); + } else { + mFooterTextView.setText(R.string.lockpattern_need_to_unlock_footer); + } + + mLockPatternView.setEnabled(true); + mLockPatternView.enableInput(); + break; + case NeedToUnlockWrong: + if (mHeaderWrongText != null) { + mHeaderTextView.setText(mHeaderWrongText); + } else { + mHeaderTextView.setText(R.string.lockpattern_need_to_unlock_wrong); + } + if (mFooterWrongText != null) { + mFooterTextView.setText(mFooterWrongText); + } else { + mFooterTextView.setText(R.string.lockpattern_need_to_unlock_wrong_footer); + } + + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); + mLockPatternView.setEnabled(true); + mLockPatternView.enableInput(); + break; + case LockedOut: + mLockPatternView.clearPattern(); + // enabled = false means: disable input, and have the + // appearance of being disabled. + mLockPatternView.setEnabled(false); // appearance of being disabled + break; + } + } + + private Runnable mClearPatternRunnable = new Runnable() { + public void run() { + mLockPatternView.clearPattern(); + } + }; + + // clear the wrong pattern unless they have started a new one + // already + private void postClearPatternRunnable() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); + } + + /** + * The pattern listener that responds according to a user confirming + * an existing lock pattern. + */ + private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener = new LockPatternView.OnPatternListener() { + + public void onPatternStart() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + } + + public void onPatternCleared() { + mLockPatternView.removeCallbacks(mClearPatternRunnable); + } + + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + if (mLockPatternUtils.checkPattern(pattern)) { + setResult(RESULT_OK); + finish(); + } else { + if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL && + ++mNumWrongConfirmAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); + handleAttemptLockout(deadline); + } else { + updateStage(Stage.NeedToUnlockWrong); + postClearPatternRunnable(); + } + } + } + }; + + + private void handleAttemptLockout(long elapsedRealtimeDeadline) { + updateStage(Stage.LockedOut); + long elapsedRealtime = SystemClock.elapsedRealtime(); + mCountdownTimer = new CountDownTimer( + elapsedRealtimeDeadline - elapsedRealtime, + LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) { + + @Override + public void onTick(long millisUntilFinished) { + mHeaderTextView.setText(R.string.lockpattern_too_many_failed_confirmation_attempts_header); + final int secondsCountdown = (int) (millisUntilFinished / 1000); + mFooterTextView.setText(getString( + R.string.lockpattern_too_many_failed_confirmation_attempts_footer, + secondsCountdown)); + } + + @Override + public void onFinish() { + mNumWrongConfirmAttempts = 0; + updateStage(Stage.NeedToUnlock); + } + }.start(); + } +} diff --git a/src/com/android/settings/DateTimeSettings.java b/src/com/android/settings/DateTimeSettings.java new file mode 100644 index 000000000..e78215af5 --- /dev/null +++ b/src/com/android/settings/DateTimeSettings.java @@ -0,0 +1,366 @@ +/* + * 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 android.app.Dialog; +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +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; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.format.DateFormat; +import android.widget.DatePicker; +import android.widget.TimePicker; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class DateTimeSettings + extends PreferenceActivity + implements OnSharedPreferenceChangeListener, + TimePickerDialog.OnTimeSetListener , DatePickerDialog.OnDateSetListener { + + private static final String HOURS_12 = "12"; + private static final String HOURS_24 = "24"; + + private Calendar mDummyDate; + private static final String KEY_DATE_FORMAT = "date_format"; + private static final String KEY_AUTO_TIME = "auto_time"; + + private static final int DIALOG_DATEPICKER = 0; + private static final int DIALOG_TIMEPICKER = 1; + + private CheckBoxPreference mAutoPref; + private Preference mTimePref; + private Preference mTime24Pref; + private Preference mTimeZone; + private Preference mDatePref; + private ListPreference mDateFormat; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.date_time_prefs); + + initUI(); + } + + private void initUI() { + boolean autoEnabled = getAutoState(); + + mDummyDate = Calendar.getInstance(); + mDummyDate.set(mDummyDate.get(Calendar.YEAR), 11, 31, 13, 0, 0); + + mAutoPref = (CheckBoxPreference) findPreference(KEY_AUTO_TIME); + mAutoPref.setChecked(autoEnabled); + mTimePref = findPreference("time"); + mTime24Pref = findPreference("24 hour"); + mTimeZone = findPreference("timezone"); + mDatePref = findPreference("date"); + mDateFormat = (ListPreference) findPreference(KEY_DATE_FORMAT); + + int currentFormatIndex = -1; + String [] dateFormats = getResources().getStringArray(R.array.date_format_values); + String [] formattedDates = new String[dateFormats.length]; + String currentFormat = getDateFormat(); + // Initialize if DATE_FORMAT is not set in the system settings + // This can happen after a factory reset (or data wipe) + if (currentFormat == null) { + currentFormat = getResources().getString(R.string.default_date_format); + setDateFormat(currentFormat); + } + for (int i = 0; i < formattedDates.length; i++) { + formattedDates[i] = DateFormat.format(dateFormats[i], mDummyDate).toString(); + if (currentFormat.equals(dateFormats[i])) currentFormatIndex = i; + } + + mDateFormat.setEntries(formattedDates); + mDateFormat.setEntryValues(R.array.date_format_values); + mDateFormat.setValue(currentFormat); + + mTimePref.setEnabled(!autoEnabled); + mDatePref.setEnabled(!autoEnabled); + mTimeZone.setEnabled(!autoEnabled); + + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + + @Override + protected void onResume() { + super.onResume(); + + ((CheckBoxPreference)mTime24Pref).setChecked(is24Hour()); + + // Register for time ticks and other reasons for time change + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + registerReceiver(mIntentReceiver, filter, null, null); + + updateTimeAndDateDisplay(); + } + + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(mIntentReceiver); + } + + private void updateTimeAndDateDisplay() { + java.text.DateFormat shortDateFormat = DateFormat.getDateFormat(this); + Date now = Calendar.getInstance().getTime(); + Date dummyDate = mDummyDate.getTime(); + mTimePref.setSummary(DateFormat.getTimeFormat(this).format(now)); + mTimeZone.setSummary(getTimeZoneText()); + mDatePref.setSummary(shortDateFormat.format(now)); + mDateFormat.setSummary(shortDateFormat.format(dummyDate)); + } + + public void onDateSet(DatePicker view, int year, int month, int day) { + Calendar c = Calendar.getInstance(); + + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month); + c.set(Calendar.DAY_OF_MONTH, day); + long when = c.getTimeInMillis(); + + if (when / 1000 < Integer.MAX_VALUE) { + SystemClock.setCurrentTimeMillis(when); + } + updateTimeAndDateDisplay(); + } + + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + Calendar c = Calendar.getInstance(); + + c.set(Calendar.HOUR_OF_DAY, hourOfDay); + c.set(Calendar.MINUTE, minute); + long when = c.getTimeInMillis(); + + if (when / 1000 < Integer.MAX_VALUE) { + SystemClock.setCurrentTimeMillis(when); + } + updateTimeAndDateDisplay(); + timeUpdated(); + } + + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key.equals(KEY_DATE_FORMAT)) { + String format = preferences.getString(key, + getResources().getString(R.string.default_date_format)); + Settings.System.putString(getContentResolver(), + Settings.System.DATE_FORMAT, format); + updateTimeAndDateDisplay(); + } else if (key.equals(KEY_AUTO_TIME)) { + boolean autoEnabled = preferences.getBoolean(key, true); + Settings.System.putInt(getContentResolver(), + Settings.System.AUTO_TIME, + autoEnabled ? 1 : 0); + mTimePref.setEnabled(!autoEnabled); + mDatePref.setEnabled(!autoEnabled); + mTimeZone.setEnabled(!autoEnabled); + } + } + + @Override + public Dialog onCreateDialog(int id) { + Dialog d; + + switch (id) { + case DIALOG_DATEPICKER: { + final Calendar calendar = Calendar.getInstance(); + d = new DatePickerDialog( + this, + this, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)); + d.setTitle(getResources().getString(R.string.date_time_changeDate_text)); + break; + } + case DIALOG_TIMEPICKER: { + final Calendar calendar = Calendar.getInstance(); + d = new TimePickerDialog( + this, + this, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + DateFormat.is24HourFormat(this)); + d.setTitle(getResources().getString(R.string.date_time_changeTime_text)); + break; + } + default: + d = null; + break; + } + + return d; + } + + @Override + public void onPrepareDialog(int id, Dialog d) { + switch (id) { + case DIALOG_DATEPICKER: { + DatePickerDialog datePicker = (DatePickerDialog)d; + final Calendar calendar = Calendar.getInstance(); + datePicker.updateDate( + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)); + break; + } + case DIALOG_TIMEPICKER: { + TimePickerDialog timePicker = (TimePickerDialog)d; + final Calendar calendar = Calendar.getInstance(); + timePicker.updateTime( + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE)); + break; + } + default: + break; + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference == mDatePref) { + showDialog(DIALOG_DATEPICKER); + } else if (preference == mTimePref) { + // The 24-hour mode may have changed, so recreate the dialog + removeDialog(DIALOG_TIMEPICKER); + showDialog(DIALOG_TIMEPICKER); + } else if (preference == mTime24Pref) { + set24Hour(((CheckBoxPreference)mTime24Pref).isChecked()); + updateTimeAndDateDisplay(); + timeUpdated(); + } else if (preference == mTimeZone) { + Intent intent = new Intent(); + intent.setClass(this, ZoneList.class); + startActivityForResult(intent, 0); + } + return false; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + updateTimeAndDateDisplay(); + } + + private void timeUpdated() { + Intent timeChanged = new Intent(Intent.ACTION_TIME_CHANGED); + sendBroadcast(timeChanged); + } + + /* Get & Set values from the system settings */ + + private boolean is24Hour() { + return DateFormat.is24HourFormat(this); + } + + private void set24Hour(boolean is24Hour) { + Settings.System.putString(getContentResolver(), + Settings.System.TIME_12_24, + is24Hour? HOURS_24 : HOURS_12); + } + + private String getDateFormat() { + return Settings.System.getString(getContentResolver(), + Settings.System.DATE_FORMAT); + } + + private boolean getAutoState() { + try { + return Settings.System.getInt(getContentResolver(), + Settings.System.AUTO_TIME) > 0; + } catch (SettingNotFoundException snfe) { + return true; + } + } + + private void setDateFormat(String format) { + Settings.System.putString(getContentResolver(), Settings.System.DATE_FORMAT, format); + } + + /* Helper routines to format timezone */ + + private String getTimeZoneText() { + TimeZone tz = java.util.Calendar.getInstance().getTimeZone(); + boolean daylight = tz.inDaylightTime(new Date()); + StringBuilder sb = new StringBuilder(); + + sb.append(formatOffset(tz.getRawOffset() + + (daylight ? tz.getDSTSavings() : 0))). + append(", "). + append(tz.getDisplayName(daylight, TimeZone.LONG)); + + return sb.toString(); + } + + private char[] formatOffset(int off) { + off = off / 1000 / 60; + + char[] buf = new char[9]; + buf[0] = 'G'; + buf[1] = 'M'; + buf[2] = 'T'; + + if (off < 0) { + buf[3] = '-'; + off = -off; + } else { + buf[3] = '+'; + } + + int hours = off / 60; + int minutes = off % 60; + + buf[4] = (char) ('0' + hours / 10); + buf[5] = (char) ('0' + hours % 10); + + buf[6] = ':'; + + buf[7] = (char) ('0' + minutes / 10); + buf[8] = (char) ('0' + minutes % 10); + + return buf; + } + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateTimeAndDateDisplay(); + } + }; +} diff --git a/src/com/android/settings/DateTimeSettingsSetupWizard.java b/src/com/android/settings/DateTimeSettingsSetupWizard.java new file mode 100644 index 000000000..8dd970b0e --- /dev/null +++ b/src/com/android/settings/DateTimeSettingsSetupWizard.java @@ -0,0 +1,41 @@ +/* + * 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 android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.widget.LinearLayout; + +public class DateTimeSettingsSetupWizard extends DateTimeSettings implements OnClickListener { + private View mNextButton; + + @Override + protected void onCreate(Bundle icicle) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + super.onCreate(icicle); + setContentView(R.layout.date_time_settings_setupwizard); + mNextButton = findViewById(R.id.next_button); + mNextButton.setOnClickListener(this); + } + + public void onClick(View v) { + setResult(RESULT_OK); + finish(); + } +} diff --git a/src/com/android/settings/DebugIntentSender.java b/src/com/android/settings/DebugIntentSender.java new file mode 100644 index 000000000..9fed94749 --- /dev/null +++ b/src/com/android/settings/DebugIntentSender.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2006 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.widget.EditText; +import android.widget.Button; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.text.TextUtils; +import android.text.Spannable; +import android.text.Selection; +import android.net.Uri; + +/** + * A simple activity that provides a UI for sending intents + */ +public class DebugIntentSender extends Activity { + private EditText mIntentField; + private EditText mDataField; + private EditText mAccountField; + private EditText mResourceField; + private Button mSendBroadcastButton; + private Button mStartActivityButton; + private View.OnClickListener mClicked = new View.OnClickListener() { + public void onClick(View v) { + if ((v == mSendBroadcastButton) || + (v == mStartActivityButton)) { + String intentAction = mIntentField.getText().toString(); + String intentData = mDataField.getText().toString(); + String account = mAccountField.getText().toString(); + String resource = mResourceField.getText().toString(); + + Intent intent = new Intent(intentAction); + if (!TextUtils.isEmpty(intentData)) { + intent.setData(Uri.parse(intentData)); + } + intent.putExtra("account", account); + intent.putExtra("resource", resource); + if (v == mSendBroadcastButton) { + sendBroadcast(intent); + } else { + startActivity(intent); + } + + setResult(RESULT_OK); + finish(); + } + } + }; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.intent_sender); + + mIntentField = (EditText) findViewById(R.id.intent); + mIntentField.setText(Intent.ACTION_SYNC); + Selection.selectAll((Spannable) mIntentField.getText()); + + mDataField = (EditText) findViewById(R.id.data); + mDataField.setBackgroundResource(android.R.drawable.editbox_background); + + mAccountField = (EditText) findViewById(R.id.account); + mResourceField = (EditText) findViewById(R.id.resource); + + mSendBroadcastButton = (Button) findViewById(R.id.sendbroadcast); + mSendBroadcastButton.setOnClickListener(mClicked); + + mStartActivityButton = (Button) findViewById(R.id.startactivity); + mStartActivityButton.setOnClickListener(mClicked); + } +} diff --git a/src/com/android/settings/DefaultRingtonePreference.java b/src/com/android/settings/DefaultRingtonePreference.java new file mode 100644 index 000000000..8eed56350 --- /dev/null +++ b/src/com/android/settings/DefaultRingtonePreference.java @@ -0,0 +1,62 @@ +/* + * 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 android.content.Context; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.preference.RingtonePreference; +import android.util.AttributeSet; +import android.util.Config; +import android.util.Log; + +public class DefaultRingtonePreference extends RingtonePreference { + private static final String TAG = "DefaultRingtonePreference"; + + public DefaultRingtonePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) { + super.onPrepareRingtonePickerIntent(ringtonePickerIntent); + + /* + * Since this preference is for choosing the default ringtone, it + * doesn't make sense to show a 'Default' item. + */ + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); + + /* + * Similarly, 'Silent' shouldn't be shown here. + */ + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); + } + + @Override + protected void onSaveRingtone(Uri ringtoneUri) { + RingtoneManager.setActualDefaultRingtoneUri(getContext(), getRingtoneType(), ringtoneUri); + } + + @Override + protected Uri onRestoreRingtone() { + return RingtoneManager.getActualDefaultRingtoneUri(getContext(), getRingtoneType()); + } + +} diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java new file mode 100644 index 000000000..155f085f5 --- /dev/null +++ b/src/com/android/settings/DevelopmentSettings.java @@ -0,0 +1,88 @@ +/* + * 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 android.os.BatteryManager; +import android.os.Bundle; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.preference.CheckBoxPreference; +import android.provider.Settings; +import android.text.TextUtils; + +/* + * Displays preferences for application developers. + */ +public class DevelopmentSettings extends PreferenceActivity { + + private static final String ENABLE_ADB = "enable_adb"; + private static final String KEEP_SCREEN_ON = "keep_screen_on"; + private static final String ALLOW_MOCK_LOCATION = "allow_mock_location"; + + private CheckBoxPreference mEnableAdb; + private CheckBoxPreference mKeepScreenOn; + private CheckBoxPreference mAllowMockLocation; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.development_prefs); + + mEnableAdb = (CheckBoxPreference) findPreference(ENABLE_ADB); + mKeepScreenOn = (CheckBoxPreference) findPreference(KEEP_SCREEN_ON); + mAllowMockLocation = (CheckBoxPreference) findPreference(ALLOW_MOCK_LOCATION); + } + + @Override + protected void onResume() { + super.onResume(); + + mEnableAdb.setChecked(Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ADB_ENABLED, 0) != 0); + mKeepScreenOn.setChecked(Settings.System.getInt(getContentResolver(), + Settings.System.STAY_ON_WHILE_PLUGGED_IN, 0) != 0); + mAllowMockLocation.setChecked(Settings.Secure.getInt(getContentResolver(), + Settings.Secure.ALLOW_MOCK_LOCATION, 0) != 0); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + + // Those monkeys kept committing suicide, so we add this property + // to disable this functionality + if (!TextUtils.isEmpty(SystemProperties.get("ro.monkey"))) { + return false; + } + + if (preference == mEnableAdb) { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.ADB_ENABLED, + mEnableAdb.isChecked() ? 1 : 0); + } else if (preference == mKeepScreenOn) { + Settings.System.putInt(getContentResolver(), Settings.System.STAY_ON_WHILE_PLUGGED_IN, + mKeepScreenOn.isChecked() ? + (BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB) : 0); + } else if (preference == mAllowMockLocation) { + Settings.Secure.putInt(getContentResolver(), Settings.Secure.ALLOW_MOCK_LOCATION, + mAllowMockLocation.isChecked() ? 1 : 0); + } + + return false; + } +} diff --git a/src/com/android/settings/DeviceInfoSettings.java b/src/com/android/settings/DeviceInfoSettings.java new file mode 100644 index 000000000..be01f7dc5 --- /dev/null +++ b/src/com/android/settings/DeviceInfoSettings.java @@ -0,0 +1,153 @@ +/* + * 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 android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.util.Config; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DeviceInfoSettings extends PreferenceActivity { + + private static final String TAG = "DeviceInfoSettings"; + private static final boolean LOGD = false || Config.LOGD; + + 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_TERMS = "terms"; + private static final String KEY_LICENSE = "license"; + private static final String KEY_COPYRIGHT = "copyright"; + private static final String KEY_SYSTEM_UPDATE_SETTINGS = "system_update_settings"; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.device_info_settings); + + setStringSummary("firmware_version", Build.VERSION.RELEASE); + setValueSummary("baseband_version", "gsm.version.baseband"); + setStringSummary("device_model", Build.MODEL); + setStringSummary("build_number", Build.DISPLAY); + findPreference("kernel_version").setSummary(getFormattedKernelVersion()); + + /* + * Settings is a generic app and should not contain any device-specific + * info. + */ + + // These are contained in the "container" preference group + PreferenceGroup parentPreference = (PreferenceGroup) findPreference(KEY_CONTAINER); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, KEY_TERMS, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, KEY_LICENSE, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, KEY_COPYRIGHT, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, KEY_TEAM, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + + // These are contained by the root preference screen + parentPreference = getPreferenceScreen(); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, + KEY_SYSTEM_UPDATE_SETTINGS, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parentPreference, KEY_CONTRIBUTORS, + Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); + } + + private void setStringSummary(String preference, String value) { + try { + findPreference(preference).setSummary(value); + } catch (RuntimeException e) { + findPreference(preference).setSummary( + getResources().getString(R.string.device_info_default)); + } + } + + private void setValueSummary(String preference, String property) { + try { + findPreference(preference).setSummary( + SystemProperties.get(property, + getResources().getString(R.string.device_info_default))); + } catch (RuntimeException e) { + + } + } + + private String getFormattedKernelVersion() { + String procVersionStr; + + try { + BufferedReader reader = new BufferedReader(new FileReader("/proc/version"), 256); + try { + procVersionStr = reader.readLine(); + } finally { + reader.close(); + } + + final String PROC_VERSION_REGEX = + "\\w+\\s+" + /* ignore: Linux */ + "\\w+\\s+" + /* ignore: version */ + "([^\\s]+)\\s+" + /* group 1: 2.6.22-omap1 */ + "\\(([^\\s@]+(?:@[^\\s.]+)?)[^)]*\\)\\s+" + /* group 2: (xxxxxx@xxxxx.constant) */ + "\\([^)]+\\)\\s+" + /* ignore: (gcc ..) */ + "([^\\s]+)\\s+" + /* group 3: #26 */ + "(?:PREEMPT\\s+)?" + /* ignore: PREEMPT (optional) */ + "(.+)"; /* group 4: date */ + + Pattern p = Pattern.compile(PROC_VERSION_REGEX); + Matcher m = p.matcher(procVersionStr); + + if (!m.matches()) { + Log.e(TAG, "Regex did not match on /proc/version: " + procVersionStr); + return "Unavailable"; + } else if (m.groupCount() < 4) { + Log.e(TAG, "Regex match on /proc/version only returned " + m.groupCount() + + " groups"); + return "Unavailable"; + } else { + return (new StringBuilder(m.group(1)).append("\n").append( + m.group(2)).append(" ").append(m.group(3)).append("\n") + .append(m.group(4))).toString(); + } + } catch (IOException e) { + Log.e(TAG, + "IO Exception when getting kernel version for Device Info screen", + e); + + return "Unavailable"; + } + } + +} diff --git a/src/com/android/settings/Display.java b/src/com/android/settings/Display.java new file mode 100644 index 000000000..f90e0f054 --- /dev/null +++ b/src/com/android/settings/Display.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2006 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.ActivityManagerNative; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.TextView; + + +public class Display extends Activity implements View.OnClickListener { + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.display); + + mFontSize = (Spinner) findViewById(R.id.fontSize); + mFontSize.setOnItemSelectedListener(mFontSizeChanged); + String[] states = new String[3]; + Resources r = getResources(); + states[0] = r.getString(R.string.small_font); + states[1] = r.getString(R.string.medium_font); + states[2] = r.getString(R.string.large_font); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + android.R.layout.simple_spinner_item, states); + adapter.setDropDownViewResource( + android.R.layout.simple_spinner_dropdown_item); + mFontSize.setAdapter(adapter); + + mPreview = (TextView) findViewById(R.id.preview); + mPreview.setText(r.getText(R.string.font_size_preview_text)); + + Button save = (Button) findViewById(R.id.save); + save.setText(r.getText(R.string.font_size_save)); + save.setOnClickListener(this); + + mTextSizeTyped = new TypedValue(); + TypedArray styledAttributes = + obtainStyledAttributes(android.R.styleable.TextView); + styledAttributes.getValue(android.R.styleable.TextView_textSize, + mTextSizeTyped); + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + mDisplayMetrics = new DisplayMetrics(); + mDisplayMetrics.density = metrics.density; + mDisplayMetrics.heightPixels = metrics.heightPixels; + mDisplayMetrics.scaledDensity = metrics.scaledDensity; + mDisplayMetrics.widthPixels = metrics.widthPixels; + mDisplayMetrics.xdpi = metrics.xdpi; + mDisplayMetrics.ydpi = metrics.ydpi; + + styledAttributes.recycle(); + } + + @Override + public void onResume() { + super.onResume(); + try { + mCurConfig.updateFrom( + ActivityManagerNative.getDefault().getConfiguration()); + } catch (RemoteException e) { + } + if (mCurConfig.fontScale < 1) { + mFontSize.setSelection(0); + } else if (mCurConfig.fontScale > 1) { + mFontSize.setSelection(2); + } else { + mFontSize.setSelection(1); + } + updateFontScale(); + } + + private void updateFontScale() { + mDisplayMetrics.scaledDensity = mDisplayMetrics.density * + mCurConfig.fontScale; + + float size = mTextSizeTyped.getDimension(mDisplayMetrics); + mPreview.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); + } + + public void onClick(View v) { + try { + ActivityManagerNative.getDefault().updateConfiguration(mCurConfig); + } catch (RemoteException e) { + } + finish(); + } + + private Spinner.OnItemSelectedListener mFontSizeChanged + = new Spinner.OnItemSelectedListener() { + public void onItemSelected(android.widget.AdapterView av, View v, + int position, long id) { + if (position == 0) { + mCurConfig.fontScale = .75f; + } else if (position == 2) { + mCurConfig.fontScale = 1.25f; + } else { + mCurConfig.fontScale = 1.0f; + } + + updateFontScale(); + } + + public void onNothingSelected(android.widget.AdapterView av) { + } + }; + + private Spinner mFontSize; + private TextView mPreview; + private TypedValue mTextSizeTyped; + private DisplayMetrics mDisplayMetrics; + private Configuration mCurConfig = new Configuration(); +} diff --git a/src/com/android/settings/EditPinPreference.java b/src/com/android/settings/EditPinPreference.java new file mode 100644 index 000000000..ee3143c39 --- /dev/null +++ b/src/com/android/settings/EditPinPreference.java @@ -0,0 +1,84 @@ +/* + * 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 android.content.Context; +import android.preference.EditTextPreference; +import android.text.method.DigitsKeyListener; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; + +import java.util.Map; + +/** + * TODO: Add a soft dialpad for PIN entry. + */ +class EditPinPreference extends EditTextPreference { + + private boolean mDialogOpen; + + interface OnPinEnteredListener { + void onPinEntered(EditPinPreference preference, boolean positiveResult); + } + + private OnPinEnteredListener mPinListener; + + public EditPinPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditPinPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setOnPinEnteredListener(OnPinEnteredListener listener) { + mPinListener = listener; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + + if (editText != null) { + editText.setSingleLine(true); + editText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + editText.setKeyListener(DigitsKeyListener.getInstance()); + } + } + + public boolean isDialogOpen() { + return mDialogOpen; + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + mDialogOpen = false; + if (mPinListener != null) { + mPinListener.onPinEntered(this, positiveResult); + } + } + + public void showPinDialog() { + mDialogOpen = true; + showDialog(null); + } +} diff --git a/src/com/android/settings/GadgetPickActivity.java b/src/com/android/settings/GadgetPickActivity.java new file mode 100644 index 000000000..840a6a55f --- /dev/null +++ b/src/com/android/settings/GadgetPickActivity.java @@ -0,0 +1,131 @@ +/* + * 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 android.app.LauncherActivity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.gadget.GadgetProviderInfo; +import android.gadget.GadgetManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.View; +import android.widget.ListView; +import android.util.Log; + +import java.text.Collator; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class GadgetPickActivity extends LauncherActivity +{ + private static final String TAG = "GadgetPickActivity"; + + GadgetManager mGadgetManager; + int mGadgetId; + + public GadgetPickActivity() { + mGadgetManager = GadgetManager.getInstance(this); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + Bundle extras = getIntent().getExtras(); + mGadgetId = extras.getInt(GadgetManager.EXTRA_GADGET_ID); + + setResultData(RESULT_CANCELED); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) + { + Intent intent = intentForPosition(position); + int result; + try { + mGadgetManager.bindGadgetId(mGadgetId, intent.getComponent()); + 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 gadget will simply not + // appear. + result = RESULT_CANCELED; + } + setResultData(result); + finish(); + } + + @Override + public List<ListItem> makeListItems() { + List<GadgetProviderInfo> installed = mGadgetManager.getInstalledProviders(); + PackageManager pm = getPackageManager(); + + Drawable defaultIcon = null; + IconResizer resizer = new IconResizer(); + + ArrayList<ListItem> result = new ArrayList(); + final int N = installed.size(); + for (int i=0; i<N; i++) { + GadgetProviderInfo info = installed.get(i); + + LauncherActivity.ListItem item = new LauncherActivity.ListItem(); + item.packageName = info.provider.getPackageName(); + item.className = info.provider.getClassName(); + + item.label = info.label; + if (info.icon != 0) { + Drawable d = pm.getDrawable( item.packageName, info.icon, null); + if (d != null) { + item.icon = resizer.createIconThumbnail(d); + } else { + Log.w(TAG, "Can't load icon drawable 0x" + Integer.toHexString(info.icon) + + " for package: " + item.packageName); + } + } + if (item.icon == null) { + // (including error case above) + if (defaultIcon == null) { + // TODO: Load standard icon. + } + item.icon = defaultIcon; + } + + result.add(item); + } + + Collections.sort(result, new Comparator<ListItem>() { + Collator mCollator = Collator.getInstance(); + public int compare(ListItem lhs, ListItem rhs) { + return mCollator.compare(lhs.label, rhs.label); + } + }); + return result; + } + + void setResultData(int code) { + Intent result = new Intent(); + result.putExtra(GadgetManager.EXTRA_GADGET_ID, mGadgetId); + setResult(code, result); + } +} + diff --git a/src/com/android/settings/InputMethodsSettings.java b/src/com/android/settings/InputMethodsSettings.java new file mode 100644 index 000000000..51b770d09 --- /dev/null +++ b/src/com/android/settings/InputMethodsSettings.java @@ -0,0 +1,191 @@ +/* + * 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 java.util.HashSet; +import java.util.List; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.preference.CheckBoxPreference; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; + +/* + * Displays preferences for input methods. + */ +public class InputMethodsSettings extends PreferenceActivity { + private List<InputMethodInfo> mInputMethodProperties; + + final TextUtils.SimpleStringSplitter mStringColonSplitter + = new TextUtils.SimpleStringSplitter(':'); + + private String mLastInputMethodId; + private String mLastTickedInputMethodId; + + static public String getInputMethodIdFromKey(String key) { + return key; + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.input_methods_prefs); + + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + mInputMethodProperties = imm.getInputMethodList(); + + mLastInputMethodId = Settings.Secure.getString(getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD); + + int N = (mInputMethodProperties == null ? 0 : mInputMethodProperties + .size()); + for (int i = 0; i < N; ++i) { + InputMethodInfo property = mInputMethodProperties.get(i); + String prefKey = property.getId(); + + CharSequence label = property.loadLabel(getPackageManager()); + + // Add a check box. + CheckBoxPreference chkbxPref = new CheckBoxPreference(this); + chkbxPref.setKey(prefKey); + chkbxPref.setTitle(label); + getPreferenceScreen().addPreference(chkbxPref); + + // If setting activity is available, add a setting screen entry. + if (null != property.getSettingsActivity()) { + PreferenceScreen prefScreen = new PreferenceScreen(this, null); + prefScreen.setKey(property.getSettingsActivity()); + prefScreen.setTitle(getResources().getString( + R.string.input_methods_settings_label_format, label)); + getPreferenceScreen().addPreference(prefScreen); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + + final HashSet<String> enabled = new HashSet<String>(); + String enabledStr = Settings.Secure.getString(getContentResolver(), + Settings.Secure.ENABLED_INPUT_METHODS); + if (enabledStr != null) { + final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(enabledStr); + while (splitter.hasNext()) { + enabled.add(splitter.next()); + } + } + + // Update the statuses of the Check Boxes. + int N = mInputMethodProperties.size(); + for (int i = 0; i < N; ++i) { + final String id = mInputMethodProperties.get(i).getId(); + CheckBoxPreference pref = (CheckBoxPreference) findPreference(mInputMethodProperties + .get(i).getId()); + pref.setChecked(enabled.contains(id)); + } + mLastTickedInputMethodId = null; + } + + @Override + protected void onPause() { + super.onPause(); + + StringBuilder builder = new StringBuilder(256); + + boolean haveLastInputMethod = false; + + int firstEnabled = -1; + int N = mInputMethodProperties.size(); + for (int i = 0; i < N; ++i) { + final String id = mInputMethodProperties.get(i).getId(); + CheckBoxPreference pref = (CheckBoxPreference) findPreference(id); + boolean hasIt = id.equals(mLastInputMethodId); + if (pref.isChecked()) { + if (builder.length() > 0) builder.append(':'); + builder.append(id); + if (firstEnabled < 0) { + firstEnabled = i; + } + if (hasIt) haveLastInputMethod = true; + } else if (hasIt) { + mLastInputMethodId = mLastTickedInputMethodId; + } + } + + // If the last input method is unset, set it as the first enabled one. + if (null == mLastInputMethodId || "".equals(mLastInputMethodId)) { + if (firstEnabled >= 0) { + mLastInputMethodId = mInputMethodProperties.get(firstEnabled).getId(); + } else { + mLastInputMethodId = null; + } + } + + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_INPUT_METHODS, builder.toString()); + Settings.Secure.putString(getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD, mLastInputMethodId); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + + // Those monkeys kept committing suicide, so we add this property + // to disable this functionality + if (!TextUtils.isEmpty(SystemProperties.get("ro.monkey"))) { + return false; + } + + if (preference instanceof CheckBoxPreference) { + CheckBoxPreference chkPref = (CheckBoxPreference) preference; + String id = getInputMethodIdFromKey(chkPref.getKey()); + if (chkPref.isChecked()) { + mLastTickedInputMethodId = id; + } else if (id.equals(mLastTickedInputMethodId)) { + mLastTickedInputMethodId = null; + } + } else if (preference instanceof PreferenceScreen) { + if (preference.getIntent() == null) { + PreferenceScreen pref = (PreferenceScreen) preference; + String activityName = pref.getKey(); + String packageName = activityName.substring(0, activityName + .lastIndexOf(".")); + if (activityName.length() > 0) { + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClassName(packageName, activityName); + startActivity(i); + } + } + } + + return false; + } +} diff --git a/src/com/android/settings/InstalledAppDetails.java b/src/com/android/settings/InstalledAppDetails.java new file mode 100644 index 000000000..327874b64 --- /dev/null +++ b/src/com/android/settings/InstalledAppDetails.java @@ -0,0 +1,478 @@ + + +/** + * 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.R; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageStatsObserver; +import android.content.pm.PackageManager; +import android.content.pm.PackageStats; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.format.Formatter; +import android.util.Config; +import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import android.content.ComponentName; +import android.view.View; +import android.widget.AppSecurityPermissions; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * Activity to display application information from Settings. This activity presents + * extended information associated with a package like code, data, total size, permissions + * used by the application and also the set of default launchable activities. + * For system applications, an option to clear user data is displayed only if data size is > 0. + * System applications that do not want clear user data do not have this option. + * For non-system applications, there is no option to clear data. Instead there is an option to + * uninstall the application. + */ +public class InstalledAppDetails extends Activity implements View.OnClickListener, DialogInterface.OnClickListener { + private static final String TAG="InstalledAppDetails"; + private static final int _UNKNOWN_APP=R.string.unknown; + private ApplicationInfo mAppInfo; + private Button mAppButton; + private Button mActivitiesButton; + private boolean mCanUninstall; + private boolean localLOGV=Config.LOGV || false; + private TextView mAppSnippetSize; + private TextView mTotalSize; + private TextView mAppSize; + private TextView mDataSize; + private PkgSizeObserver mSizeObserver; + private ClearUserDataObserver mClearDataObserver; + // Views related to cache info + private View mCachePanel; + private TextView mCacheSize; + private Button mClearCacheButton; + private ClearCacheObserver mClearCacheObserver; + private Button mForceStopButton; + + PackageStats mSizeInfo; + private Button mManageSpaceButton; + private PackageManager mPm; + + //internal constants used in Handler + private static final int OP_SUCCESSFUL = 1; + private static final int OP_FAILED = 2; + private static final int CLEAR_USER_DATA = 1; + private static final int GET_PKG_SIZE = 2; + private static final int CLEAR_CACHE = 3; + private static final String ATTR_PACKAGE_STATS="PackageStats"; + + // invalid size value used initially and also when size retrieval through PackageManager + // fails for whatever reason + private static final int SIZE_INVALID = -1; + + // Resource strings + private CharSequence mInvalidSizeStr; + private CharSequence mComputingStr; + private CharSequence mAppButtonText; + + // Possible btn states + private enum AppButtonStates { + CLEAR_DATA, + UNINSTALL, + NONE + } + private AppButtonStates mAppButtonState; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case CLEAR_USER_DATA: + processClearMsg(msg); + break; + case GET_PKG_SIZE: + refreshSizeInfo(msg); + break; + case CLEAR_CACHE: + // Refresh size info + mPm.getPackageSizeInfo(mAppInfo.packageName, mSizeObserver); + break; + default: + break; + } + } + }; + + private boolean isUninstallable() { + if (((mAppInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0) && + ((mAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0)) { + return false; + } + return true; + } + + class ClearUserDataObserver extends IPackageDataObserver.Stub { + public void onRemoveCompleted(final String packageName, final boolean succeeded) { + final Message msg = mHandler.obtainMessage(CLEAR_USER_DATA); + msg.arg1 = succeeded?OP_SUCCESSFUL:OP_FAILED; + mHandler.sendMessage(msg); + } + } + + class PkgSizeObserver extends IPackageStatsObserver.Stub { + public int idx; + public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) { + Message msg = mHandler.obtainMessage(GET_PKG_SIZE); + Bundle data = new Bundle(); + data.putParcelable(ATTR_PACKAGE_STATS, pStats); + msg.setData(data); + mHandler.sendMessage(msg); + + } + } + + class ClearCacheObserver extends IPackageDataObserver.Stub { + public void onRemoveCompleted(final String packageName, final boolean succeeded) { + final Message msg = mHandler.obtainMessage(CLEAR_CACHE); + msg.arg1 = succeeded?OP_SUCCESSFUL:OP_FAILED; + mHandler.sendMessage(msg); + } + } + + private String getSizeStr(long size) { + if (size == SIZE_INVALID) { + return mInvalidSizeStr.toString(); + } + return Formatter.formatFileSize(this, size); + } + + private void setAppBtnState() { + boolean visible = false; + if(mCanUninstall) { + //app can clear user data + if((mAppInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) + == ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) { + mAppButtonText = getText(R.string.clear_user_data_text); + mAppButtonState = AppButtonStates.CLEAR_DATA; + visible = true; + } else { + //hide button if diableClearUserData is set + visible = false; + mAppButtonState = AppButtonStates.NONE; + } + } else { + visible = true; + mAppButtonState = AppButtonStates.UNINSTALL; + mAppButtonText = getText(R.string.uninstall_text); + } + if(visible) { + mAppButton.setText(mAppButtonText); + mAppButton.setVisibility(View.VISIBLE); + } else { + mAppButton.setVisibility(View.GONE); + } + } + + /** Called when the activity is first created. */ + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + //get package manager + mPm = getPackageManager(); + //get application's name from intent + Intent intent = getIntent(); + final String packageName = intent.getStringExtra(ManageApplications.APP_PKG_NAME); + mComputingStr = getText(R.string.computing_size); + // Try retrieving package stats again + CharSequence totalSizeStr, appSizeStr, dataSizeStr; + totalSizeStr = appSizeStr = dataSizeStr = mComputingStr; + if(localLOGV) Log.i(TAG, "Have to compute package sizes"); + mSizeObserver = new PkgSizeObserver(); + mPm.getPackageSizeInfo(packageName, mSizeObserver); + + try { + mAppInfo = mPm.getApplicationInfo(packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + Log.e(TAG, "Exception when retrieving package:"+packageName, e); + displayErrorDialog(R.string.app_not_found_dlg_text, true, true); + } + setContentView(R.layout.installed_app_details); + ((ImageView)findViewById(R.id.app_icon)).setImageDrawable(mAppInfo.loadIcon(mPm)); + //set application name TODO version + CharSequence appName = mAppInfo.loadLabel(mPm); + if(appName == null) { + appName = getString(_UNKNOWN_APP); + } + ((TextView)findViewById(R.id.app_name)).setText(appName); + mAppSnippetSize = ((TextView)findViewById(R.id.app_size)); + mAppSnippetSize.setText(totalSizeStr); + //TODO download str and download url + //set values on views + mTotalSize = (TextView)findViewById(R.id.total_size_text); + mTotalSize.setText(totalSizeStr); + mAppSize = (TextView)findViewById(R.id.application_size_text); + mAppSize.setText(appSizeStr); + mDataSize = (TextView)findViewById(R.id.data_size_text); + mDataSize.setText(dataSizeStr); + + mAppButton = ((Button)findViewById(R.id.uninstall_button)); + //determine if app is a system app + mCanUninstall = !isUninstallable(); + if(localLOGV) Log.i(TAG, "Is systemPackage "+mCanUninstall); + setAppBtnState(); + mManageSpaceButton = (Button)findViewById(R.id.manage_space_button); + if(mAppInfo.manageSpaceActivityName != null) { + mManageSpaceButton.setVisibility(View.VISIBLE); + mManageSpaceButton.setOnClickListener(this); + } + + // Cache section + mCachePanel = findViewById(R.id.cache_panel); + mCacheSize = (TextView) findViewById(R.id.cache_size_text); + mCacheSize.setText(mComputingStr); + mClearCacheButton = (Button) findViewById(R.id.clear_cache_button); + mForceStopButton = (Button) findViewById(R.id.force_stop_button); + mForceStopButton.setOnClickListener(this); + + //clear activities + mActivitiesButton = (Button)findViewById(R.id.clear_activities_button); + List<ComponentName> prefActList = new ArrayList<ComponentName>(); + //intent list cannot be null. so pass empty list + List<IntentFilter> intentList = new ArrayList<IntentFilter>(); + mPm.getPreferredActivities(intentList, prefActList, packageName); + if(localLOGV) Log.i(TAG, "Have "+prefActList.size()+" number of activities in prefered list"); + TextView autoLaunchView = (TextView)findViewById(R.id.auto_launch); + if(prefActList.size() <= 0) { + //disable clear activities button + autoLaunchView.setText(R.string.auto_launch_disable_text); + mActivitiesButton.setEnabled(false); + } else { + autoLaunchView.setText(R.string.auto_launch_enable_text); + mActivitiesButton.setOnClickListener(this); + } + + // security permissions section + LinearLayout permsView = (LinearLayout) findViewById(R.id.permissions_section); + AppSecurityPermissions asp = new AppSecurityPermissions(this, packageName); + if(asp.getPermissionCount() > 0) { + permsView.setVisibility(View.VISIBLE); + // Make the security sections header visible + LinearLayout securityList = (LinearLayout) permsView.findViewById( + R.id.security_settings_list); + securityList.addView(asp.getPermissionsView()); + } else { + permsView.setVisibility(View.GONE); + } + } + + private void displayErrorDialog(int msgId, final boolean finish, final boolean changed) { + //display confirmation dialog + new AlertDialog.Builder(this) + .setTitle(getString(R.string.app_not_found_dlg_title)) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(getString(msgId)) + .setNeutralButton(getString(R.string.dlg_ok), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + //force to recompute changed value + setIntentAndFinish(finish, changed); + } + } + ) + .show(); + } + + private void setIntentAndFinish(boolean finish, boolean appChanged) { + if(localLOGV) Log.i(TAG, "appChanged="+appChanged); + Intent intent = new Intent(); + intent.putExtra(ManageApplications.APP_CHG, appChanged); + setResult(ManageApplications.RESULT_OK, intent); + mAppButton.setEnabled(false); + if(finish) { + finish(); + } + } + + /* + * Private method to handle get size info notification from observer when + * the async operation from PackageManager is complete. The current user data + * info has to be refreshed in the manage applications screen as well as the current screen. + */ + private void refreshSizeInfo(Message msg) { + boolean changed = false; + PackageStats newPs = msg.getData().getParcelable(ATTR_PACKAGE_STATS); + long newTot = newPs.cacheSize+newPs.codeSize+newPs.dataSize; + if(mSizeInfo == null) { + mSizeInfo = newPs; + String str = getSizeStr(newTot); + mTotalSize.setText(str); + mAppSnippetSize.setText(str); + mAppSize.setText(getSizeStr(newPs.codeSize)); + mDataSize.setText(getSizeStr(newPs.dataSize)); + mCacheSize.setText(getSizeStr(newPs.cacheSize)); + } else { + long oldTot = mSizeInfo.cacheSize+mSizeInfo.codeSize+mSizeInfo.dataSize; + if(newTot != oldTot) { + String str = getSizeStr(newTot); + mTotalSize.setText(str); + mAppSnippetSize.setText(str); + changed = true; + } + if(newPs.codeSize != mSizeInfo.codeSize) { + mAppSize.setText(getSizeStr(newPs.codeSize)); + changed = true; + } + if(newPs.dataSize != mSizeInfo.dataSize) { + mDataSize.setText(getSizeStr(newPs.dataSize)); + changed = true; + } + if(newPs.cacheSize != mSizeInfo.cacheSize) { + mCacheSize.setText(getSizeStr(newPs.cacheSize)); + changed = true; + } + if(changed) { + mSizeInfo = newPs; + } + } + + long data = mSizeInfo.dataSize; + // Disable button if data is 0 + if(mAppButtonState != AppButtonStates.NONE){ + mAppButton.setText(mAppButtonText); + if((mAppButtonState == AppButtonStates.CLEAR_DATA) && (data == 0)) { + mAppButton.setEnabled(false); + } else { + mAppButton.setEnabled(true); + mAppButton.setOnClickListener(this); + } + } + refreshCacheInfo(newPs.cacheSize); + } + + private void refreshCacheInfo(long cacheSize) { + // Set cache info + mCacheSize.setText(getSizeStr(cacheSize)); + if (cacheSize <= 0) { + mClearCacheButton.setEnabled(false); + } else { + mClearCacheButton.setOnClickListener(this); + } + } + + /* + * Private method to handle clear message notification from observer when + * the async operation from PackageManager is complete + */ + private void processClearMsg(Message msg) { + int result = msg.arg1; + String packageName = mAppInfo.packageName; + if(result == OP_SUCCESSFUL) { + Log.i(TAG, "Cleared user data for system package:"+packageName); + mPm.getPackageSizeInfo(packageName, mSizeObserver); + } else { + mAppButton.setText(R.string.clear_user_data_text); + mAppButton.setEnabled(true); + } + } + + /* + * Private method to initiate clearing user data when the user clicks the clear data + * button for a system package + */ + private void initiateClearUserDataForSysPkg() { + mAppButton.setEnabled(false); + //invoke uninstall or clear user data based on sysPackage + String packageName = mAppInfo.packageName; + Log.i(TAG, "Clearing user data for system package"); + if(mClearDataObserver == null) { + mClearDataObserver = new ClearUserDataObserver(); + } + ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + boolean res = am.clearApplicationUserData(packageName, mClearDataObserver); + if(!res) { + //doesnt initiate clear. some error. should not happen but just log error for now + Log.i(TAG, "Couldnt clear application user data for package:"+packageName); + displayErrorDialog(R.string.clear_data_failed, false, false); + } else { + mAppButton.setText(R.string.recompute_size); + } + } + + /* + * Method implementing functionality of buttons clicked + * @see android.view.View.OnClickListener#onClick(android.view.View) + */ + public void onClick(View v) { + String packageName = mAppInfo.packageName; + if(v == mAppButton) { + if(mCanUninstall) { + //display confirmation dialog + new AlertDialog.Builder(this) + .setTitle(getString(R.string.clear_data_dlg_title)) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(getString(R.string.clear_data_dlg_text)) + .setPositiveButton(R.string.dlg_ok, this) + .setNegativeButton(R.string.dlg_cancel, this) + .show(); + } else { + //create new intent to launch Uninstaller activity + Uri packageURI = Uri.parse("package:"+packageName); + Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI); + startActivity(uninstallIntent); + setIntentAndFinish(true, true); + } + } else if(v == mActivitiesButton) { + mPm.clearPackagePreferredActivities(packageName); + mActivitiesButton.setEnabled(false); + } else if(v == mManageSpaceButton) { + Intent intent = new Intent(Intent.ACTION_DEFAULT); + intent.setClassName(mAppInfo.packageName, mAppInfo.manageSpaceActivityName); + startActivityForResult(intent, -1); + } else if (v == mClearCacheButton) { + // Lazy initialization of observer + if (mClearCacheObserver == null) { + mClearCacheObserver = new ClearCacheObserver(); + } + mPm.deleteApplicationCacheFiles(packageName, mClearCacheObserver); + } else if (v == mForceStopButton) { + ActivityManager am = (ActivityManager)getSystemService( + Context.ACTIVITY_SERVICE); + am.restartPackage(packageName); + } + } + + public void onClick(DialogInterface dialog, int which) { + if(which == AlertDialog.BUTTON_POSITIVE) { + //invoke uninstall or clear user data based on sysPackage + initiateClearUserDataForSysPkg(); + } else { + //cancel do nothing just retain existing screen + } + } +} + diff --git a/src/com/android/settings/LanguageSettings.java b/src/com/android/settings/LanguageSettings.java new file mode 100644 index 000000000..8463d26fc --- /dev/null +++ b/src/com/android/settings/LanguageSettings.java @@ -0,0 +1,247 @@ +/* + * 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 android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.SystemProperties; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.provider.Settings.System; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; + +import java.util.HashSet; +import java.util.List; + +public class LanguageSettings extends PreferenceActivity { + + private final String[] mSettingsUiKey = { + "auto_caps", + "auto_replace", + "auto_punctuate", + }; + + // Note: Order of this array should correspond to the order of the above array + private final String[] mSettingsSystemId = { + System.TEXT_AUTO_CAPS, + System.TEXT_AUTO_REPLACE, + System.TEXT_AUTO_PUNCTUATE, + }; + + // Note: Order of this array should correspond to the order of the above array + private final int[] mSettingsDefault = { + 1, + 1, + 1, + }; + + private List<InputMethodInfo> mInputMethodProperties; + + final TextUtils.SimpleStringSplitter mStringColonSplitter + = new TextUtils.SimpleStringSplitter(':'); + + private String mLastInputMethodId; + private String mLastTickedInputMethodId; + + static public String getInputMethodIdFromKey(String key) { + return key; + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.language_settings); + + if (getAssets().getLocales().length == 1) { + getPreferenceScreen(). + removePreference(findPreference("language_category")); + } + + Configuration config = getResources().getConfiguration(); + if (config.keyboard != Configuration.KEYBOARD_QWERTY) { + getPreferenceScreen().removePreference( + getPreferenceScreen().findPreference("hardkeyboard_category")); + } else { + ContentResolver resolver = getContentResolver(); + for (int i = 0; i < mSettingsUiKey.length; i++) { + CheckBoxPreference pref = (CheckBoxPreference) findPreference(mSettingsUiKey[i]); + pref.setChecked(System.getInt(resolver, mSettingsSystemId[i], + mSettingsDefault[i]) > 0); + } + } + + onCreateIMM(); + } + + private void onCreateIMM() { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + mInputMethodProperties = imm.getInputMethodList(); + + mLastInputMethodId = Settings.Secure.getString(getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD); + + PreferenceGroup textCategory = (PreferenceGroup) findPreference("text_category"); + + int N = (mInputMethodProperties == null ? 0 : mInputMethodProperties + .size()); + for (int i = 0; i < N; ++i) { + InputMethodInfo property = mInputMethodProperties.get(i); + String prefKey = property.getId(); + + CharSequence label = property.loadLabel(getPackageManager()); + + // Add a check box. + CheckBoxPreference chkbxPref = new CheckBoxPreference(this); + chkbxPref.setKey(prefKey); + chkbxPref.setTitle(label); + textCategory.addPreference(chkbxPref); + + // If setting activity is available, add a setting screen entry. + if (null != property.getSettingsActivity()) { + PreferenceScreen prefScreen = new PreferenceScreen(this, null); + prefScreen.setKey(property.getSettingsActivity()); + CharSequence settingsLabel = getResources().getString( + R.string.input_methods_settings_label_format, label); + prefScreen.setTitle(settingsLabel); + textCategory.addPreference(prefScreen); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + + final HashSet<String> enabled = new HashSet<String>(); + String enabledStr = Settings.Secure.getString(getContentResolver(), + Settings.Secure.ENABLED_INPUT_METHODS); + if (enabledStr != null) { + final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(enabledStr); + while (splitter.hasNext()) { + enabled.add(splitter.next()); + } + } + + // Update the statuses of the Check Boxes. + int N = mInputMethodProperties.size(); + for (int i = 0; i < N; ++i) { + final String id = mInputMethodProperties.get(i).getId(); + CheckBoxPreference pref = (CheckBoxPreference) findPreference(mInputMethodProperties + .get(i).getId()); + pref.setChecked(enabled.contains(id)); + } + mLastTickedInputMethodId = null; + } + + @Override + protected void onPause() { + super.onPause(); + + StringBuilder builder = new StringBuilder(256); + + boolean haveLastInputMethod = false; + + int firstEnabled = -1; + int N = mInputMethodProperties.size(); + for (int i = 0; i < N; ++i) { + final String id = mInputMethodProperties.get(i).getId(); + CheckBoxPreference pref = (CheckBoxPreference) findPreference(id); + boolean hasIt = id.equals(mLastInputMethodId); + if (pref.isChecked()) { + if (builder.length() > 0) builder.append(':'); + builder.append(id); + if (firstEnabled < 0) { + firstEnabled = i; + } + if (hasIt) haveLastInputMethod = true; + } else if (hasIt) { + mLastInputMethodId = mLastTickedInputMethodId; + } + } + + // If the last input method is unset, set it as the first enabled one. + if (null == mLastInputMethodId || "".equals(mLastInputMethodId)) { + if (firstEnabled >= 0) { + mLastInputMethodId = mInputMethodProperties.get(firstEnabled).getId(); + } else { + mLastInputMethodId = null; + } + } + + Settings.Secure.putString(getContentResolver(), + Settings.Secure.ENABLED_INPUT_METHODS, builder.toString()); + Settings.Secure.putString(getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD, mLastInputMethodId); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + + // Physical keyboard stuff + for (int i = 0; i < mSettingsUiKey.length; i++) { + if (mSettingsUiKey[i].equals(preference.getKey())) { + System.putInt(getContentResolver(), mSettingsSystemId[i], + ((CheckBoxPreference)preference).isChecked()? 1 : 0); + return true; + } + } + + // Input Method stuff + // Those monkeys kept committing suicide, so we add this property + // to disable this functionality + if (!TextUtils.isEmpty(SystemProperties.get("ro.monkey"))) { + return false; + } + + if (preference instanceof CheckBoxPreference) { + CheckBoxPreference chkPref = (CheckBoxPreference) preference; + String id = getInputMethodIdFromKey(chkPref.getKey()); + if (chkPref.isChecked()) { + mLastTickedInputMethodId = id; + } else if (id.equals(mLastTickedInputMethodId)) { + mLastTickedInputMethodId = null; + } + } else if (preference instanceof PreferenceScreen) { + if (preference.getIntent() == null) { + PreferenceScreen pref = (PreferenceScreen) preference; + String activityName = pref.getKey(); + String packageName = activityName.substring(0, activityName + .lastIndexOf(".")); + if (activityName.length() > 0) { + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClassName(packageName, activityName); + startActivity(i); + } + } + } + + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + +} diff --git a/src/com/android/settings/LauncherGadgetBinder.java b/src/com/android/settings/LauncherGadgetBinder.java new file mode 100644 index 000000000..f7b5a61bb --- /dev/null +++ b/src/com/android/settings/LauncherGadgetBinder.java @@ -0,0 +1,225 @@ +/* + * 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 android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.ComponentName; +import android.database.Cursor; +import android.database.SQLException; +import android.gadget.GadgetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.util.Log; + +import java.util.ArrayList; + +public class LauncherGadgetBinder extends Activity { + private static final String TAG = "LauncherGadgetBinder"; + private static final boolean LOGD = true; + + static final String AUTHORITY = "com.android.launcher.settings"; + static final String TABLE_FAVORITES = "favorites"; + + static final String EXTRA_BIND_SOURCES = "com.android.launcher.settings.bindsources"; + static final String EXTRA_BIND_TARGETS = "com.android.launcher.settings.bindtargets"; + + static final String EXTRA_GADGET_BITMAPS = "com.android.camera.gadgetbitmaps"; + + /** + * {@link ContentProvider} constants pulled over from Launcher + */ + static final class LauncherProvider implements BaseColumns { + static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_FAVORITES); + + static final String ITEM_TYPE = "itemType"; + static final String GADGET_ID = "gadgetId"; + static final String ICON = "icon"; + + static final int ITEM_TYPE_GADGET = 4; + static final int ITEM_TYPE_WIDGET_CLOCK = 1000; + static final int ITEM_TYPE_WIDGET_SEARCH = 1001; + static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; + } + + static final String[] BIND_PROJECTION = new String[] { + LauncherProvider._ID, + LauncherProvider.ITEM_TYPE, + LauncherProvider.GADGET_ID, + LauncherProvider.ICON, + }; + + static final int INDEX_ID = 0; + static final int INDEX_ITEM_TYPE = 1; + static final int INDEX_GADGET_ID = 2; + static final int INDEX_ICON = 3; + + static final ComponentName BIND_PHOTO_GADGET = new ComponentName("com.android.camera", + "com.android.camera.PhotoGadgetBind"); + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + finish(); + + // This helper reaches into the Launcher database and binds any unlinked + // gadgets. If will remove any items that can't be bound successfully. + // We protect this binder at the manifest level by asserting the caller + // has the Launcher WRITE_SETTINGS permission. + + final Intent intent = getIntent(); + final Bundle extras = intent.getExtras(); + + int[] bindSources = null; + ArrayList<ComponentName> bindTargets = null; + Exception exception = null; + + try { + bindSources = extras.getIntArray(EXTRA_BIND_SOURCES); + bindTargets = intent.getParcelableArrayListExtra(EXTRA_BIND_TARGETS); + } catch (ClassCastException ex) { + exception = ex; + } + + if (exception != null || bindSources == null || bindTargets == null || + bindSources.length != bindTargets.size()) { + Log.w(TAG, "Problem reading incoming bind request, or invalid request", exception); + return; + } + + final String selectWhere = buildOrWhereString(LauncherProvider.ITEM_TYPE, bindSources); + + final ContentResolver resolver = getContentResolver(); + final GadgetManager gadgetManager = GadgetManager.getInstance(this); + + boolean foundPhotoGadgets = false; + final ArrayList<Integer> photoGadgetIds = new ArrayList<Integer>(); + final ArrayList<Bitmap> photoBitmaps = new ArrayList<Bitmap>(); + + Cursor c = null; + + try { + c = resolver.query(LauncherProvider.CONTENT_URI, + BIND_PROJECTION, selectWhere, null, null); + + if (LOGD) Log.d(TAG, "found bind cursor count="+c.getCount()); + + final ContentValues values = new ContentValues(); + while (c != null && c.moveToNext()) { + long favoriteId = c.getLong(INDEX_ID); + int itemType = c.getInt(INDEX_ITEM_TYPE); + int gadgetId = c.getInt(INDEX_GADGET_ID); + byte[] iconData = c.getBlob(INDEX_ICON); + + // Find the binding target for this type + ComponentName targetGadget = null; + for (int i = 0; i < bindSources.length; i++) { + if (bindSources[i] == itemType) { + targetGadget = bindTargets.get(i); + break; + } + } + + if (LOGD) Log.d(TAG, "found matching targetGadget="+targetGadget.toString()+" for favoriteId="+favoriteId); + + boolean bindSuccess = false; + try { + gadgetManager.bindGadgetId(gadgetId, targetGadget); + bindSuccess = true; + } catch (RuntimeException ex) { + Log.w(TAG, "Problem binding gadget", ex); + } + + // Handle special case of photo gadget by loading bitmap and + // preparing for later binding + if (bindSuccess && iconData != null && + itemType == LauncherProvider.ITEM_TYPE_WIDGET_PHOTO_FRAME) { + Bitmap bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length); + + photoGadgetIds.add(gadgetId); + photoBitmaps.add(bitmap); + foundPhotoGadgets = true; + } + + if (LOGD) Log.d(TAG, "after finished, success="+bindSuccess); + + // Depending on success, update launcher or remove item + Uri favoritesUri = ContentUris.withAppendedId(LauncherProvider.CONTENT_URI, favoriteId); + if (bindSuccess) { + values.clear(); + values.put(LauncherProvider.ITEM_TYPE, LauncherProvider.ITEM_TYPE_GADGET); + values.putNull(LauncherProvider.ICON); + resolver.update(favoritesUri, values, null, null); + } else { + resolver.delete(favoritesUri, null, null); + } + + } + } catch (SQLException ex) { + Log.w(TAG, "Problem while binding gadgetIds for Launcher", ex); + } finally { + if (c != null) { + c.close(); + } + } + + if (foundPhotoGadgets) { + // Convert gadgetIds into int[] + final int N = photoGadgetIds.size(); + final int[] photoGadgetIdsArray = new int[N]; + for (int i = 0; i < N; i++) { + photoGadgetIdsArray[i] = photoGadgetIds.get(i); + } + + // Launch intent over to handle bitmap binding, but we don't need to + // wait around for the result. + final Intent bindIntent = new Intent(); + bindIntent.setComponent(BIND_PHOTO_GADGET); + + final Bundle bindExtras = new Bundle(); + bindExtras.putIntArray(GadgetManager.EXTRA_GADGET_IDS, photoGadgetIdsArray); + bindExtras.putParcelableArrayList(EXTRA_GADGET_BITMAPS, photoBitmaps); + bindIntent.putExtras(bindExtras); + + startActivity(bindIntent); + } + + if (LOGD) Log.d(TAG, "completely finished with binding for Launcher"); + } + + /** + * Build a query string that will match any row where the column matches + * anything in the values list. + */ + static String buildOrWhereString(String column, int[] values) { + StringBuilder selectWhere = new StringBuilder(); + for (int i = values.length - 1; i >= 0; i--) { + selectWhere.append(column).append("=").append(values[i]); + if (i > 0) { + selectWhere.append(" OR "); + } + } + return selectWhere.toString(); + } + +} diff --git a/src/com/android/settings/LocalePicker.java b/src/com/android/settings/LocalePicker.java new file mode 100644 index 000000000..386d7e0f4 --- /dev/null +++ b/src/com/android/settings/LocalePicker.java @@ -0,0 +1,153 @@ +/* + * 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 android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.ListActivity; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Locale; + +public class LocalePicker extends ListActivity { + private static final String TAG = "LocalePicker"; + + Loc[] mLocales; + + private static class Loc { + String label; + Locale locale; + + public Loc(String label, Locale locale) { + this.label = label; + this.locale = locale; + } + + @Override + public String toString() { + return this.label; + } + } + + int getContentView() { + return R.layout.locale_picker; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(getContentView()); + + String[] locales = getAssets().getLocales(); + Arrays.sort(locales); + + final int origSize = locales.length; + Loc[] preprocess = new Loc[origSize]; + int finalSize = 0; + for (int i = 0 ; i < origSize; i++ ) { + String s = locales[i]; + int len = s.length(); + if (len == 2) { + Locale l = new Locale(s); + preprocess[finalSize++] = new Loc(toTitleCase(l.getDisplayLanguage()), l); + } else if (len == 5) { + String language = s.substring(0, 2); + String country = s.substring(3, 5); + Locale l = new Locale(language, country); + + if (finalSize == 0) { + preprocess[finalSize++] = new Loc(toTitleCase(l.getDisplayLanguage()), l); + } else { + // check previous entry: + // same lang and no country -> overwrite it with a lang-only name + // same lang and a country -> upgrade to full name and + // insert ours with full name + // diff lang -> insert ours with lang-only name + if (preprocess[finalSize-1].locale.getLanguage().equals(language)) { + String prevCountry = preprocess[finalSize-1].locale.getCountry(); + if (prevCountry.length() == 0) { + preprocess[finalSize-1].locale = l; + preprocess[finalSize-1].label = toTitleCase(l.getDisplayLanguage()); + } else { + preprocess[finalSize-1].label = toTitleCase(preprocess[finalSize-1].locale.getDisplayName()); + preprocess[finalSize++] = new Loc(toTitleCase(l.getDisplayName()), l); + } + } else { + String displayName; + if (s.equals("zz_ZZ")) { + displayName = "Pseudo..."; + } else { + displayName = toTitleCase(l.getDisplayLanguage()); + } + preprocess[finalSize++] = new Loc(displayName, l); + } + } + } + } + mLocales = new Loc[finalSize]; + for (int i = 0; i < finalSize ; i++) { + mLocales[i] = preprocess[i]; + } + int layoutId = R.layout.locale_picker_item; + int fieldId = R.id.locale; + ArrayAdapter<Loc> adapter = new ArrayAdapter<Loc>(this, layoutId, fieldId, mLocales); + getListView().setAdapter(adapter); + } + + private static String toTitleCase(String s) { + if (s.length() == 0) { + return s; + } + + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + + @Override + public void onResume() { + super.onResume(); + getListView().requestFocus(); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + try { + IActivityManager am = ActivityManagerNative.getDefault(); + Configuration config = am.getConfiguration(); + + Loc loc = mLocales[position]; + config.locale = loc.locale; + + // indicate this isn't some passing default - the user wants this remembered + config.userSetLocale = true; + + am.updateConfiguration(config); + } catch (RemoteException e) { + // Intentionally left blank + } + finish(); + } +} diff --git a/src/com/android/settings/LocalePickerInSetupWizard.java b/src/com/android/settings/LocalePickerInSetupWizard.java new file mode 100644 index 000000000..b160e899b --- /dev/null +++ b/src/com/android/settings/LocalePickerInSetupWizard.java @@ -0,0 +1,43 @@ +/* + * 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 android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.ListActivity; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Locale; + +public class LocalePickerInSetupWizard extends LocalePicker { + + @Override + int getContentView() { + return R.layout.locale_picker_in_setupwizard; + } + +} diff --git a/src/com/android/settings/ManageApplications.java b/src/com/android/settings/ManageApplications.java new file mode 100644 index 000000000..74957ed43 --- /dev/null +++ b/src/com/android/settings/ManageApplications.java @@ -0,0 +1,1307 @@ +/* + * Copyright (C) 2006 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.R; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageStatsObserver; +import android.content.pm.PackageManager; +import android.content.pm.PackageStats; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.format.Formatter; +import android.util.Config; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Activity to pick an application that will be used to display installation information and + * options to uninstall/delete user data for system applications. This activity + * can be launched through Settings or via the ACTION_MANAGE_PACKAGE_STORAGE + * intent. + * Initially a compute in progress message is displayed while the application retrieves + * the list of application information from the PackageManager. The size information + * for each package is refreshed to the screen. The resource(app description and + * icon) information for each package is not available yet, so some default values for size + * icon and descriptions are used initially. Later the resource information for each + * application is retrieved and dynamically updated on the screen. + * A Broadcast receiver registers for package additions or deletions when the activity is + * in focus. If the user installs or deletes packages when the activity has focus, the receiver + * gets notified and proceeds to add/delete these packages from the list on the screen. + * This is an unlikely scenario but could happen. The entire list gets created every time + * the activity's onStart gets invoked. This is to avoid having the receiver for the entire + * life cycle of the application. + * The applications can be sorted either alphabetically or + * based on size(descending). If this activity gets launched under low memory + * situations(A low memory notification dispatches intent + * ACTION_MANAGE_PACKAGE_STORAGE) the list is sorted per size. + * If the user selects an application, extended info(like size, uninstall/clear data options, + * permissions info etc.,) is displayed via the InstalledAppDetails activity. + */ +public class ManageApplications extends ListActivity implements + OnItemClickListener, DialogInterface.OnCancelListener, + DialogInterface.OnClickListener { + // TAG for this activity + private static final String TAG = "ManageApplications"; + + // Log information boolean + private boolean localLOGV = Config.LOGV || false; + + // attributes used as keys when passing values to InstalledAppDetails activity + public static final String APP_PKG_PREFIX = "com.android.settings."; + public static final String APP_PKG_NAME = APP_PKG_PREFIX+"ApplicationPkgName"; + public static final String APP_CHG = APP_PKG_PREFIX+"changed"; + + // attribute name used in receiver for tagging names of added/deleted packages + private static final String ATTR_PKG_NAME="PackageName"; + private static final String ATTR_APP_PKG_STATS="ApplicationPackageStats"; + + // constant value that can be used to check return code from sub activity. + private static final int INSTALLED_APP_DETAILS = 1; + + // sort order that can be changed through the menu can be sorted alphabetically + // or size(descending) + private static final int MENU_OPTIONS_BASE = 0; + public static final int SORT_ORDER_ALPHA = MENU_OPTIONS_BASE + 0; + public static final int SORT_ORDER_SIZE = MENU_OPTIONS_BASE + 1; + // Filter options used for displayed list of applications + public static final int FILTER_APPS_ALL = MENU_OPTIONS_BASE + 2; + public static final int FILTER_APPS_THIRD_PARTY = MENU_OPTIONS_BASE + 3; + public static final int FILTER_APPS_RUNNING = MENU_OPTIONS_BASE + 4; + public static final int FILTER_OPTIONS = MENU_OPTIONS_BASE + 5; + // Alert Dialog presented to user to find out the filter option + AlertDialog mAlertDlg; + // sort order + private int mSortOrder = SORT_ORDER_ALPHA; + // Filter value + int mFilterApps = FILTER_APPS_ALL; + + // Custom Adapter used for managing items in the list + private AppInfoAdapter mAppInfoAdapter; + + // messages posted to the handler + private static final int HANDLER_MESSAGE_BASE = 0; + private static final int INIT_PKG_INFO = HANDLER_MESSAGE_BASE+1; + private static final int COMPUTE_PKG_SIZE_DONE = HANDLER_MESSAGE_BASE+2; + private static final int REMOVE_PKG = HANDLER_MESSAGE_BASE+3; + private static final int REORDER_LIST = HANDLER_MESSAGE_BASE+4; + private static final int ADD_PKG_START = HANDLER_MESSAGE_BASE+5; + private static final int ADD_PKG_DONE = HANDLER_MESSAGE_BASE+6; + private static final int REFRESH_ICONS = HANDLER_MESSAGE_BASE+7; + private static final int NEXT_LOAD_STEP = HANDLER_MESSAGE_BASE+8; + + // observer object used for computing pkg sizes + private PkgSizeObserver mObserver; + // local handle to PackageManager + private PackageManager mPm; + // Broadcast Receiver object that receives notifications for added/deleted + // packages + private PackageIntentReceiver mReceiver; + // atomic variable used to track if computing pkg sizes is in progress. should be volatile? + + private boolean mComputeSizes = false; + // default icon thats used when displaying applications initially before resource info is + // retrieved + private Drawable mDefaultAppIcon; + + // temporary dialog displayed while the application info loads + private static final int DLG_BASE = 0; + private static final int DLG_LOADING = DLG_BASE + 1; + + // compute index used to track the application size computations + private int mComputeIndex; + + // Size resource used for packages whose size computation failed for some reason + private CharSequence mInvalidSizeStr; + private CharSequence mComputingSizeStr; + + // map used to store list of added and removed packages. Immutable Boolean + // variables indicate if a package has been added or removed. If a package is + // added or deleted multiple times a single entry with the latest operation will + // be recorded in the map. + private Map<String, Boolean> mAddRemoveMap; + + // layout inflater object used to inflate views + private LayoutInflater mInflater; + + // invalid size value used initially and also when size retrieval through PackageManager + // fails for whatever reason + private static final int SIZE_INVALID = -1; + + // debug boolean variable to test delays from PackageManager API's + private boolean DEBUG_PKG_DELAY = false; + + // Thread to load resources + ResourceLoaderThread mResourceThread; + + String mCurrentPkgName; + + //TODO implement a cache system + private Map<String, AppInfo> mAppPropCache; + + // empty message displayed when list is empty + private TextView mEmptyView; + + // Boolean variables indicating state + private boolean mLoadLabels = false; + private boolean mSizesFirst = false; + // ListView used to display list + private ListView mListView; + // State variables used to figure out menu options and also + // initiate the first computation and loading of resources + private boolean mJustCreated = true; + private boolean mFirst = false; + + /* + * Handler class to handle messages for various operations + * Most of the operations that effect Application related data + * are posted as messages to the handler to avoid synchronization + * when accessing these structures. + * When the size retrieval gets kicked off for the first time, a COMPUTE_PKG_SIZE_START + * message is posted to the handler which invokes the getSizeInfo for the pkg at index 0 + * When the PackageManager's asynchronous call back through + * PkgSizeObserver.onGetStatsCompleted gets invoked, the application resources like + * label, description, icon etc., is loaded in the same thread and these values are + * set on the observer. The observer then posts a COMPUTE_PKG_SIZE_DONE message + * to the handler. This information is updated on the AppInfoAdapter associated with + * the list view of this activity and size info retrieval is initiated for the next package as + * indicated by mComputeIndex + * When a package gets added while the activity has focus, the PkgSizeObserver posts + * ADD_PKG_START message to the handler. If the computation is not in progress, the size + * is retrieved for the newly added package through the observer object and the newly + * installed app info is updated on the screen. If the computation is still in progress + * the package is added to an internal structure and action deferred till the computation + * is done for all the packages. + * When a package gets deleted, REMOVE_PKG is posted to the handler + * if computation is not in progress(as indicated by + * mDoneIniting), the package is deleted from the displayed list of apps. If computation is + * still in progress the package is added to an internal structure and action deferred till + * the computation is done for all packages. + * When the sizes of all packages is computed, the newly + * added or removed packages are processed in order. + * If the user changes the order in which these applications are viewed by hitting the + * menu key, REORDER_LIST message is posted to the handler. this sorts the list + * of items based on the sort order. + */ + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + PackageStats ps; + ApplicationInfo info; + Bundle data; + String pkgName = null; + AppInfo appInfo; + data = msg.getData(); + if(data != null) { + pkgName = data.getString(ATTR_PKG_NAME); + } + switch (msg.what) { + case INIT_PKG_INFO: + if(localLOGV) Log.i(TAG, "Message INIT_PKG_INFO"); + // Retrieve the package list and init some structures + initAppList(mFilterApps); + mHandler.sendEmptyMessage(NEXT_LOAD_STEP); + break; + case COMPUTE_PKG_SIZE_DONE: + if(localLOGV) Log.i(TAG, "Message COMPUTE_PKG_SIZE_DONE"); + if(pkgName == null) { + Log.w(TAG, "Ignoring message"); + break; + } + ps = data.getParcelable(ATTR_APP_PKG_STATS); + if(ps == null) { + Log.i(TAG, "Invalid package stats for package:"+pkgName); + } else { + int pkgId = mAppInfoAdapter.getIndex(pkgName); + if(mComputeIndex != pkgId) { + //spurious call from stale observer + Log.w(TAG, "Stale call back from PkgSizeObserver"); + break; + } + mAppInfoAdapter.updateAppSize(pkgName, ps); + } + mComputeIndex++; + if (mComputeIndex < mAppInfoAdapter.getCount()) { + // initiate compute package size for next pkg in list + mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo( + mComputeIndex), + COMPUTE_PKG_SIZE_DONE); + } else { + // check for added/removed packages + Set<String> keys = mAddRemoveMap.keySet(); + Iterator<String> iter = keys.iterator(); + List<String> removeList = new ArrayList<String>(); + boolean added = false; + boolean removed = false; + while (iter.hasNext()) { + String key = iter.next(); + if (mAddRemoveMap.get(key) == Boolean.TRUE) { + // add + try { + info = mPm.getApplicationInfo(key, 0); + mAppInfoAdapter.addApplicationInfo(info); + added = true; + } catch (NameNotFoundException e) { + Log.w(TAG, "Invalid added package:"+key+" Ignoring entry"); + } + } else { + // remove + removeList.add(key); + removed = true; + } + } + // remove uninstalled packages from list + if (removed) { + mAppInfoAdapter.removeFromList(removeList); + } + // handle newly installed packages + if (added) { + mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo( + mComputeIndex), + COMPUTE_PKG_SIZE_DONE); + } else { + // end computation here + mComputeSizes = true; + mFirst = true; + mAppInfoAdapter.sortList(mSortOrder); + mHandler.sendEmptyMessage(NEXT_LOAD_STEP); + } + } + break; + case REMOVE_PKG: + if(localLOGV) Log.i(TAG, "Message REMOVE_PKG"); + if(pkgName == null) { + Log.w(TAG, "Ignoring message:REMOVE_PKG for null pkgName"); + break; + } + if (!mComputeSizes) { + Boolean currB = mAddRemoveMap.get(pkgName); + if (currB == null || (currB.equals(Boolean.TRUE))) { + mAddRemoveMap.put(pkgName, Boolean.FALSE); + } + break; + } + List<String> pkgList = new ArrayList<String>(); + pkgList.add(pkgName); + mAppInfoAdapter.removeFromList(pkgList); + break; + case REORDER_LIST: + if(localLOGV) Log.i(TAG, "Message REORDER_LIST"); + int menuOption = msg.arg1; + if((menuOption == SORT_ORDER_ALPHA) || + (menuOption == SORT_ORDER_SIZE)) { + // Option to sort list + if (menuOption != mSortOrder) { + mSortOrder = menuOption; + if (localLOGV) Log.i(TAG, "Changing sort order to "+mSortOrder); + mAppInfoAdapter.sortList(mSortOrder); + } + } else if(menuOption != mFilterApps) { + // Option to filter list + mFilterApps = menuOption; + boolean ret = mAppInfoAdapter.resetAppList(mFilterApps, + getInstalledApps(mFilterApps)); + if(!ret) { + // Reset cache + mAppPropCache = null; + mFilterApps = FILTER_APPS_ALL; + mHandler.sendEmptyMessage(INIT_PKG_INFO); + sendMessageToHandler(REORDER_LIST, menuOption); + } + } + break; + case ADD_PKG_START: + if(localLOGV) Log.i(TAG, "Message ADD_PKG_START"); + if(pkgName == null) { + Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName"); + break; + } + if (!mComputeSizes) { + Boolean currB = mAddRemoveMap.get(pkgName); + if (currB == null || (currB.equals(Boolean.FALSE))) { + mAddRemoveMap.put(pkgName, Boolean.TRUE); + } + break; + } + try { + info = mPm.getApplicationInfo(pkgName, 0); + } catch (NameNotFoundException e) { + Log.w(TAG, "Couldnt find application info for:"+pkgName); + break; + } + mObserver.invokeGetSizeInfo(info, ADD_PKG_DONE); + break; + case ADD_PKG_DONE: + if(localLOGV) Log.i(TAG, "Message COMPUTE_PKG_SIZE_DONE"); + if(pkgName == null) { + Log.w(TAG, "Ignoring message:ADD_PKG_START for null pkgName"); + break; + } + ps = data.getParcelable(ATTR_APP_PKG_STATS); + mAppInfoAdapter.addToList(pkgName, ps); + break; + case REFRESH_ICONS: + Map<String, AppInfo> iconMap = (Map<String, AppInfo>) msg.obj; + if(iconMap == null) { + Log.w(TAG, "Error loading icons for applications"); + } else { + mAppInfoAdapter.updateAppsResourceInfo(iconMap); + } + mLoadLabels = true; + mHandler.sendEmptyMessage(NEXT_LOAD_STEP); + break; + case NEXT_LOAD_STEP: + if (mComputeSizes && mLoadLabels) { + doneLoadingData(); + } else if (!mComputeSizes && !mLoadLabels) { + // Either load the package labels or initiate get size info + if (mSizesFirst) { + initComputeSizes(); + } else { + initResourceThread(); + } + } else { + // Create list view from the adapter here. Wait till the sort order + // of list is defined. its either by label or by size. so atleast one of the + // first steps should be complete before filling the list + if (mJustCreated) { + // Set the adapter here. + mJustCreated = false; + mListView.setAdapter(mAppInfoAdapter); + dismissLoadingMsg(); + } + if (!mComputeSizes) { + initComputeSizes(); + } else if (!mLoadLabels) { + initResourceThread(); + } + } + break; + default: + break; + } + } + }; + + + + private void doneLoadingData() { + setProgressBarIndeterminateVisibility(false); + } + + List<ApplicationInfo> getInstalledApps(int filterOption) { + List<ApplicationInfo> installedAppList = mPm.getInstalledApplications( + PackageManager.GET_UNINSTALLED_PACKAGES); + if (installedAppList == null) { + return new ArrayList<ApplicationInfo> (); + } + if (filterOption == FILTER_APPS_THIRD_PARTY) { + List<ApplicationInfo> appList =new ArrayList<ApplicationInfo> (); + for (ApplicationInfo appInfo : installedAppList) { + boolean flag = false; + if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + // Updated system app + flag = true; + } else if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + // Non-system app + flag = true; + } + if (flag) { + appList.add(appInfo); + } + } + return appList; + } else if (filterOption == FILTER_APPS_RUNNING) { + List<ApplicationInfo> appList =new ArrayList<ApplicationInfo> (); + List<ActivityManager.RunningAppProcessInfo> procList = getRunningAppProcessesList(); + if ((procList == null) || (procList.size() == 0)) { + return appList; + } + // Retrieve running processes from ActivityManager + for (ActivityManager.RunningAppProcessInfo appProcInfo : procList) { + if ((appProcInfo != null) && (appProcInfo.pkgList != null)){ + int size = appProcInfo.pkgList.length; + for (int i = 0; i < size; i++) { + ApplicationInfo appInfo = null; + try { + appInfo = mPm.getApplicationInfo(appProcInfo.pkgList[i], + PackageManager.GET_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + Log.w(TAG, "Error retrieving ApplicationInfo for pkg:"+appProcInfo.pkgList[i]); + continue; + } + if(appInfo != null) { + appList.add(appInfo); + } + } + } + } + return appList; + } else { + return installedAppList; + } + } + + private List<ActivityManager.RunningAppProcessInfo> getRunningAppProcessesList() { + ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); + return am.getRunningAppProcesses(); + } + + // some initialization code used when kicking off the size computation + private void initAppList(int filterOption) { + setProgressBarIndeterminateVisibility(true); + mComputeIndex = 0; + mComputeSizes = false; + mLoadLabels = false; + // Initialize lists + List<ApplicationInfo> appList = getInstalledApps(filterOption); + mAddRemoveMap = new TreeMap<String, Boolean>(); + mAppInfoAdapter.resetAppList(filterOption, appList); + } + + // Utility method to start a thread to read application labels and icons + private void initResourceThread() { + //load resources now + if(mResourceThread.isAlive()) { + mResourceThread.interrupt(); + } + mResourceThread.loadAllResources(mAppInfoAdapter.getAppList()); + } + + private void initComputeSizes() { + // initiate compute pkg sizes + if (localLOGV) Log.i(TAG, "Initiating compute sizes for first time"); + if (mAppInfoAdapter.getCount() > 0) { + mObserver.invokeGetSizeInfo(mAppInfoAdapter.getApplicationInfo(0), + COMPUTE_PKG_SIZE_DONE); + } else { + mComputeSizes = true; + } + } + + private void showEmptyViewIfListEmpty() { + if (localLOGV) Log.i(TAG, "Checking for empty view"); + if (mAppInfoAdapter.getCount() > 0) { + mListView.setVisibility(View.VISIBLE); + mEmptyView.setVisibility(View.GONE); + } else { + mListView.setVisibility(View.GONE); + mEmptyView.setVisibility(View.VISIBLE); + } + } + + // internal structure used to track added and deleted packages when + // the activity has focus + class AddRemoveInfo { + String pkgName; + boolean add; + public AddRemoveInfo(String pPkgName, boolean pAdd) { + pkgName = pPkgName; + add = pAdd; + } + } + + class ResourceLoaderThread extends Thread { + List<ApplicationInfo> mAppList; + + void loadAllResources(List<ApplicationInfo> appList) { + mAppList = appList; + start(); + } + + public void run() { + Map<String, AppInfo> iconMap = new HashMap<String, AppInfo>(); + if(mAppList == null || mAppList.size() <= 0) { + Log.w(TAG, "Empty or null application list"); + } else { + for (ApplicationInfo appInfo : mAppList) { + CharSequence appName = appInfo.loadLabel(mPm); + Drawable appIcon = appInfo.loadIcon(mPm); + iconMap.put(appInfo.packageName, + new AppInfo(appInfo.packageName, appName, appIcon)); + } + } + Message msg = mHandler.obtainMessage(REFRESH_ICONS); + msg.obj = iconMap; + mHandler.sendMessage(msg); + } + } + + /* Internal class representing an application or packages displayable attributes + * + */ + class AppInfo { + public String pkgName; + int index; + public CharSequence appName; + public Drawable appIcon; + public CharSequence appSize; + public PackageStats appStats; + + public void refreshIcon(AppInfo pInfo) { + appName = pInfo.appName; + appIcon = pInfo.appIcon; + } + + public AppInfo(String pName, CharSequence aName, Drawable aIcon) { + index = -1; + pkgName = pName; + appName = aName; + appIcon = aIcon; + appStats = null; + appSize = mComputingSizeStr; + } + + public AppInfo(String pName, int pIndex, CharSequence aName, Drawable aIcon, + PackageStats ps) { + index = pIndex; + pkgName = pName; + appName = aName; + appIcon = aIcon; + if(ps == null) { + appSize = mComputingSizeStr; + } else { + appStats = ps; + appSize = getSizeStr(); + } + } + public void setSize(PackageStats ps) { + appStats = ps; + if (ps != null) { + appSize = getSizeStr(); + } + } + public long getTotalSize() { + PackageStats ps = appStats; + if (ps != null) { + return ps.cacheSize+ps.codeSize+ps.dataSize; + } + return SIZE_INVALID; + } + + private String getSizeStr() { + PackageStats ps = appStats; + String retStr = ""; + // insert total size information into map to display in view + // at this point its guaranteed that ps is not null. but checking anyway + if (ps != null) { + long size = getTotalSize(); + if (size == SIZE_INVALID) { + return mInvalidSizeStr.toString(); + } + return Formatter.formatFileSize(ManageApplications.this, size); + } + return retStr; + } + } + + // View Holder used when displaying views + static class AppViewHolder { + TextView appName; + ImageView appIcon; + TextView appSize; + } + + /* Custom adapter implementation for the ListView + * This adapter maintains a map for each displayed application and its properties + * An index value on each AppInfo object indicates the correct position or index + * in the list. If the list gets updated dynamically when the user is viewing the list of + * applications, we need to return the correct index of position. This is done by mapping + * the getId methods via the package name into the internal maps and indices. + * The order of applications in the list is mirrored in mAppLocalList + */ + class AppInfoAdapter extends BaseAdapter { + private Map<String, AppInfo> mAppPropMap; + private List<ApplicationInfo> mAppLocalList; + ApplicationInfo.DisplayNameComparator mAlphaComparator; + AppInfoComparator mSizeComparator; + + private AppInfo getFromCache(String packageName) { + if(mAppPropCache == null) { + return null; + } + return mAppPropCache.get(packageName); + } + + public AppInfoAdapter(Context c, List<ApplicationInfo> appList) { + mAppLocalList = appList; + boolean useCache = false; + int sortOrder = SORT_ORDER_ALPHA; + int imax = mAppLocalList.size(); + if(mAppPropCache != null) { + useCache = true; + // Activity has been resumed. can use the cache to populate values initially + mAppPropMap = mAppPropCache; + sortOrder = mSortOrder; + } + sortAppList(sortOrder); + // Recreate property map + mAppPropMap = new TreeMap<String, AppInfo>(); + for (int i = 0; i < imax; i++) { + ApplicationInfo info = mAppLocalList.get(i); + AppInfo aInfo = getFromCache(info.packageName); + if(aInfo == null){ + aInfo = new AppInfo(info.packageName, i, + info.packageName, mDefaultAppIcon, null); + } else { + aInfo.index = i; + } + mAppPropMap.put(info.packageName, aInfo); + } + } + + public int getCount() { + return mAppLocalList.size(); + } + + public Object getItem(int position) { + return mAppLocalList.get(position); + } + + /* + * This method returns the index of the package position in the application list + */ + public int getIndex(String pkgName) { + if(pkgName == null) { + Log.w(TAG, "Getting index of null package in List Adapter"); + } + int imax = mAppLocalList.size(); + ApplicationInfo appInfo; + for(int i = 0; i < imax; i++) { + appInfo = mAppLocalList.get(i); + if(appInfo.packageName.equalsIgnoreCase(pkgName)) { + return i; + } + } + return -1; + } + + public ApplicationInfo getApplicationInfo(int position) { + int imax = mAppLocalList.size(); + if( (position < 0) || (position >= imax)) { + Log.w(TAG, "Position out of bounds in List Adapter"); + return null; + } + return mAppLocalList.get(position); + } + + public void addApplicationInfo(ApplicationInfo info) { + if(info == null) { + Log.w(TAG, "Ignoring null add in List Adapter"); + return; + } + mAppLocalList.add(info); + } + + public long getItemId(int position) { + int imax = mAppLocalList.size(); + if( (position < 0) || (position >= imax)) { + Log.w(TAG, "Position out of bounds in List Adapter"); + return -1; + } + return mAppPropMap.get(mAppLocalList.get(position).packageName).index; + } + + public List<ApplicationInfo> getAppList() { + return mAppLocalList; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (position >= mAppLocalList.size()) { + Log.w(TAG, "Invalid view position:"+position+", actual size is:"+mAppLocalList.size()); + return null; + } + // A ViewHolder keeps references to children views to avoid unneccessary calls + // to findViewById() on each row. + AppViewHolder holder; + + // When convertView is not null, we can reuse it directly, there is no need + // to reinflate it. We only inflate a new View when the convertView supplied + // by ListView is null. + if (convertView == null) { + convertView = mInflater.inflate(R.layout.manage_applications_item, null); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + holder = new AppViewHolder(); + holder.appName = (TextView) convertView.findViewById(R.id.app_name); + holder.appIcon = (ImageView) convertView.findViewById(R.id.app_icon); + holder.appSize = (TextView) convertView.findViewById(R.id.app_size); + convertView.setTag(holder); + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + holder = (AppViewHolder) convertView.getTag(); + } + + // Bind the data efficiently with the holder + ApplicationInfo appInfo = mAppLocalList.get(position); + AppInfo mInfo = mAppPropMap.get(appInfo.packageName); + if(mInfo != null) { + if(mInfo.appName != null) { + holder.appName.setText(mInfo.appName); + } + if(mInfo.appIcon != null) { + holder.appIcon.setImageDrawable(mInfo.appIcon); + } + if (mInfo.appSize != null) { + holder.appSize.setText(mInfo.appSize); + } + } else { + Log.w(TAG, "No info for package:"+appInfo.packageName+" in property map"); + } + return convertView; + } + + private void adjustIndex() { + int imax = mAppLocalList.size(); + ApplicationInfo info; + for (int i = 0; i < imax; i++) { + info = mAppLocalList.get(i); + mAppPropMap.get(info.packageName).index = i; + } + } + + public void sortAppList(int sortOrder) { + Collections.sort(mAppLocalList, getAppComparator(sortOrder)); + } + + public void sortList(int sortOrder) { + sortAppList(sortOrder); + adjustIndex(); + notifyDataSetChanged(); + } + + public boolean resetAppList(int filterOption, List<ApplicationInfo> appList) { + // Create application list based on the filter value + mAppLocalList = appList; + // Check for all properties in map before sorting. Populate values from cache + for(ApplicationInfo applicationInfo : mAppLocalList) { + AppInfo appInfo = mAppPropMap.get(applicationInfo.packageName); + if(appInfo == null) { + AppInfo rInfo = getFromCache(applicationInfo.packageName); + if(rInfo == null) { + // Need to load resources again. Inconsistency somewhere + return false; + } + mAppPropMap.put(applicationInfo.packageName, rInfo); + } + } + if (mAppLocalList.size() > 0) { + sortList(mSortOrder); + } else { + notifyDataSetChanged(); + } + showEmptyViewIfListEmpty(); + return true; + } + + private Comparator<ApplicationInfo> getAppComparator(int sortOrder) { + if (sortOrder == SORT_ORDER_ALPHA) { + // Lazy initialization + if (mAlphaComparator == null) { + mAlphaComparator = new ApplicationInfo.DisplayNameComparator(mPm); + } + return mAlphaComparator; + } + // Lazy initialization + if(mSizeComparator == null) { + mSizeComparator = new AppInfoComparator(mAppPropMap); + } + return mSizeComparator; + } + + public void updateAppsResourceInfo(Map<String, AppInfo> iconMap) { + if(iconMap == null) { + Log.w(TAG, "Null iconMap when refreshing icon in List Adapter"); + return; + } + boolean changed = false; + for (ApplicationInfo info : mAppLocalList) { + AppInfo pInfo = iconMap.get(info.packageName); + if(pInfo != null) { + AppInfo aInfo = mAppPropMap.get(info.packageName); + if (aInfo != null) { + aInfo.refreshIcon(pInfo); + } else { + mAppPropMap.put(info.packageName, pInfo); + } + changed = true; + } + } + if(changed) { + notifyDataSetChanged(); + } + } + + public void addToList(String pkgName, PackageStats ps) { + if(pkgName == null) { + Log.w(TAG, "Adding null pkg to List Adapter"); + return; + } + ApplicationInfo info; + try { + info = mPm.getApplicationInfo(pkgName, 0); + } catch (NameNotFoundException e) { + Log.w(TAG, "Ignoring non-existent package:"+pkgName); + return; + } + if(info == null) { + // Nothing to do log error message and return + Log.i(TAG, "Null ApplicationInfo for package:"+pkgName); + return; + } + // Binary search returns a negative index (ie --index) of the position where + // this might be inserted. + int newIdx = Collections.binarySearch(mAppLocalList, info, + getAppComparator(mSortOrder)); + if(newIdx >= 0) { + Log.i(TAG, "Strange. Package:"+pkgName+" is not new"); + return; + } + // New entry + newIdx = -newIdx-1; + mAppLocalList.add(newIdx, info); + mAppPropMap.put(info.packageName, new AppInfo(pkgName, newIdx, + info.loadLabel(mPm), info.loadIcon(mPm), ps)); + adjustIndex(); + notifyDataSetChanged(); + } + + public void removeFromList(List<String> pkgNames) { + if(pkgNames == null) { + Log.w(TAG, "Removing null pkg list from List Adapter"); + return; + } + int imax = mAppLocalList.size(); + boolean found = false; + ApplicationInfo info; + int i, k; + String pkgName; + int kmax = pkgNames.size(); + if(kmax <= 0) { + Log.w(TAG, "Removing empty pkg list from List Adapter"); + return; + } + int idxArr[] = new int[kmax]; + for (k = 0; k < kmax; k++) { + idxArr[k] = -1; + } + for (i = 0; i < imax; i++) { + info = mAppLocalList.get(i); + for (k = 0; k < kmax; k++) { + pkgName = pkgNames.get(k); + if (info.packageName.equalsIgnoreCase(pkgName)) { + idxArr[k] = i; + found = true; + break; + } + } + } + // Sort idxArr + Arrays.sort(idxArr); + // remove the packages based on decending indices + for (k = kmax-1; k >= 0; k--) { + // Check if package has been found in the list of existing apps first + if(idxArr[k] == -1) { + break; + } + info = mAppLocalList.get(idxArr[k]); + mAppLocalList.remove(idxArr[k]); + mAppPropMap.remove(info.packageName); + if (localLOGV) Log.i(TAG, "Removed pkg:"+info.packageName+ " list"); + } + if (found) { + adjustIndex(); + notifyDataSetChanged(); + } + } + + public void updateAppSize(String pkgName, PackageStats ps) { + if(pkgName == null) { + return; + } + AppInfo entry = mAppPropMap.get(pkgName); + if (entry == null) { + Log.w(TAG, "Entry for package:"+pkgName+"doesnt exist in map"); + return; + } + // Copy the index into the newly updated entry + entry.setSize(ps); + notifyDataSetChanged(); + } + + public PackageStats getAppStats(String pkgName) { + if(pkgName == null) { + return null; + } + AppInfo entry = mAppPropMap.get(pkgName); + if (entry == null) { + return null; + } + return entry.appStats; + } + } + + /* + * Utility method to clear messages to Handler + * We need'nt synchronize on the Handler since posting messages is guaranteed + * to be thread safe. Even if the other thread that retrieves package sizes + * posts a message, we do a cursory check of validity on mAppInfoAdapter's applist + */ + private void clearMessagesInHandler() { + mHandler.removeMessages(INIT_PKG_INFO); + mHandler.removeMessages(COMPUTE_PKG_SIZE_DONE); + mHandler.removeMessages(REMOVE_PKG); + mHandler.removeMessages(REORDER_LIST); + mHandler.removeMessages(ADD_PKG_START); + mHandler.removeMessages(ADD_PKG_DONE); + } + + private void sendMessageToHandler(int msgId, int arg1) { + Message msg = mHandler.obtainMessage(msgId); + msg.arg1 = arg1; + mHandler.sendMessage(msg); + } + + private void sendMessageToHandler(int msgId, Bundle data) { + Message msg = mHandler.obtainMessage(msgId); + msg.setData(data); + mHandler.sendMessage(msg); + } + + private void sendMessageToHandler(int msgId) { + mHandler.sendEmptyMessage(msgId); + } + + /* + * Stats Observer class used to compute package sizes and retrieve size information + * PkgSizeOberver is the call back thats used when invoking getPackageSizeInfo on + * PackageManager. The values in call back onGetStatsCompleted are validated + * and the specified message is passed to mHandler. The package name + * and the AppInfo object corresponding to the package name are set on the message + */ + class PkgSizeObserver extends IPackageStatsObserver.Stub { + private ApplicationInfo mAppInfo; + private int mMsgId; + public void onGetStatsCompleted(PackageStats pStats, boolean pSucceeded) { + if(DEBUG_PKG_DELAY) { + try { + Thread.sleep(10*1000); + } catch (InterruptedException e) { + } + } + AppInfo appInfo = null; + Bundle data = new Bundle(); + data.putString(ATTR_PKG_NAME, mAppInfo.packageName); + if(pSucceeded && pStats != null) { + if (localLOGV) Log.i(TAG, "onGetStatsCompleted::"+pStats.packageName+", ("+ + pStats.cacheSize+","+ + pStats.codeSize+", "+pStats.dataSize); + data.putParcelable(ATTR_APP_PKG_STATS, pStats); + } else { + Log.w(TAG, "Invalid package stats from PackageManager"); + } + //post message to Handler + Message msg = mHandler.obtainMessage(mMsgId, data); + msg.setData(data); + mHandler.sendMessage(msg); + } + + public void invokeGetSizeInfo(ApplicationInfo pAppInfo, int msgId) { + if(pAppInfo == null || pAppInfo.packageName == null) { + return; + } + if(localLOGV) Log.i(TAG, "Invoking getPackageSizeInfo for package:"+ + pAppInfo.packageName); + mMsgId = msgId; + mAppInfo = pAppInfo; + mPm.getPackageSizeInfo(pAppInfo.packageName, this); + } + } + + /** + * Receives notifications when applications are added/removed. + */ + private class PackageIntentReceiver extends BroadcastReceiver { + void registerReceiver() { + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + ManageApplications.this.registerReceiver(this, filter); + } + @Override + public void onReceive(Context context, Intent intent) { + String actionStr = intent.getAction(); + Uri data = intent.getData(); + String pkgName = data.getEncodedSchemeSpecificPart(); + if (localLOGV) Log.i(TAG, "action:"+actionStr+", for package:"+pkgName); + updatePackageList(actionStr, pkgName); + } + } + + private void updatePackageList(String actionStr, String pkgName) { + // technically we dont have to invoke handler since onReceive is invoked on + // the main thread but doing it here for better clarity + if (Intent.ACTION_PACKAGE_ADDED.equalsIgnoreCase(actionStr)) { + Bundle data = new Bundle(); + data.putString(ATTR_PKG_NAME, pkgName); + sendMessageToHandler(ADD_PKG_START, data); + } else if (Intent.ACTION_PACKAGE_REMOVED.equalsIgnoreCase(actionStr)) { + Bundle data = new Bundle(); + data.putString(ATTR_PKG_NAME, pkgName); + sendMessageToHandler(REMOVE_PKG, data); + } + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent lIntent = getIntent(); + String action = lIntent.getAction(); + if (action.equals(Intent.ACTION_MANAGE_PACKAGE_STORAGE)) { + mSortOrder = SORT_ORDER_SIZE; + mSizesFirst = true; + } + mPm = getPackageManager(); + // initialize some window features + requestWindowFeature(Window.FEATURE_RIGHT_ICON); + requestWindowFeature(Window.FEATURE_PROGRESS); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.compute_sizes); + mDefaultAppIcon =Resources.getSystem().getDrawable( + com.android.internal.R.drawable.sym_def_app_icon); + mInvalidSizeStr = getText(R.string.invalid_size_value); + mComputingSizeStr = getText(R.string.computing_size); + // initialize the inflater + mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mReceiver = new PackageIntentReceiver(); + mEmptyView = (TextView) findViewById(R.id.empty_view); + mObserver = new PkgSizeObserver(); + // Create adapter and list view here + List<ApplicationInfo> appList = getInstalledApps(mSortOrder); + mAppInfoAdapter = new AppInfoAdapter(this, appList); + ListView lv= (ListView) findViewById(android.R.id.list); + //lv.setAdapter(mAppInfoAdapter); + lv.setOnItemClickListener(this); + lv.setSaveEnabled(true); + lv.setItemsCanFocus(true); + lv.setOnItemClickListener(this); + mListView = lv; + showLoadingMsg(); + } + + @Override + public Dialog onCreateDialog(int id) { + if (id == DLG_LOADING) { + ProgressDialog dlg = new ProgressDialog(this); + dlg.setProgressStyle(ProgressDialog.STYLE_SPINNER); + dlg.setMessage(getText(R.string.loading)); + dlg.setIndeterminate(true); + dlg.setOnCancelListener(this); + return dlg; + } + return null; + } + + + private void showLoadingMsg() { + showDialog(DLG_LOADING); + if(localLOGV) Log.i(TAG, "Displaying Loading message"); + } + + private void dismissLoadingMsg() { + if(localLOGV) Log.i(TAG, "Dismissing Loading message"); + dismissDialog(DLG_LOADING); + } + + @Override + public void onStart() { + super.onStart(); + // Create a thread to load resources + mResourceThread = new ResourceLoaderThread(); + sendMessageToHandler(INIT_PKG_INFO); + // register receiver + mReceiver.registerReceiver(); + } + + @Override + public void onStop() { + super.onStop(); + // clear all messages related to application list + clearMessagesInHandler(); + // register receiver here + unregisterReceiver(mReceiver); + mAppPropCache = mAppInfoAdapter.mAppPropMap; + } + + // Avoid the restart and pause when orientation changes + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + /* + * comparator class used to sort AppInfo objects based on size + */ + public static class AppInfoComparator implements Comparator<ApplicationInfo> { + public AppInfoComparator(Map<String, AppInfo> pAppPropMap) { + mAppPropMap= pAppPropMap; + } + + public final int compare(ApplicationInfo a, ApplicationInfo b) { + AppInfo ainfo = mAppPropMap.get(a.packageName); + AppInfo binfo = mAppPropMap.get(b.packageName); + long atotal = ainfo.getTotalSize(); + long btotal = binfo.getTotalSize(); + long ret = atotal - btotal; + // negate result to sort in descending order + if (ret < 0) { + return 1; + } + if (ret == 0) { + return 0; + } + return -1; + } + private Map<String, AppInfo> mAppPropMap; + } + + // utility method used to start sub activity + private void startApplicationDetailsActivity() { + // Create intent to start new activity + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setClass(this, InstalledAppDetails.class); + intent.putExtra(APP_PKG_NAME, mCurrentPkgName); + // start new activity to display extended information + startActivityForResult(intent, INSTALLED_APP_DETAILS); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, SORT_ORDER_ALPHA, 1, R.string.sort_order_alpha) + .setIcon(android.R.drawable.ic_menu_sort_alphabetically); + menu.add(0, SORT_ORDER_SIZE, 2, R.string.sort_order_size) + .setIcon(android.R.drawable.ic_menu_sort_by_size); + menu.add(0, FILTER_OPTIONS, 3, R.string.filter) + .setIcon(R.drawable.ic_menu_filter_settings); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (mFirst) { + menu.findItem(SORT_ORDER_ALPHA).setVisible(mSortOrder != SORT_ORDER_ALPHA); + menu.findItem(SORT_ORDER_SIZE).setVisible(mSortOrder != SORT_ORDER_SIZE); + menu.findItem(FILTER_OPTIONS).setVisible(true); + return true; + } + return false; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int menuId = item.getItemId(); + if ((menuId == SORT_ORDER_ALPHA) || (menuId == SORT_ORDER_SIZE)) { + sendMessageToHandler(REORDER_LIST, menuId); + } else if (menuId == FILTER_OPTIONS) { + if (mAlertDlg == null) { + mAlertDlg = new AlertDialog.Builder(this). + setTitle(R.string.filter_dlg_title). + setNeutralButton(R.string.cancel, this). + setSingleChoiceItems(new CharSequence[] {getText(R.string.filter_apps_all), + getText(R.string.filter_apps_running), + getText(R.string.filter_apps_third_party)}, + -1, this). + create(); + } + mAlertDlg.show(); + } + return true; + } + + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + ApplicationInfo info = (ApplicationInfo)mAppInfoAdapter.getItem(position); + mCurrentPkgName = info.packageName; + startApplicationDetailsActivity(); + } + + // Finish the activity if the user presses the back button to cancel the activity + public void onCancel(DialogInterface dialog) { + finish(); + } + + public void onClick(DialogInterface dialog, int which) { + int newOption; + switch (which) { + // Make sure that values of 0, 1, 2 match options all, running, third_party when + // created via the AlertDialog.Builder + case 0: + newOption = FILTER_APPS_ALL; + break; + case 1: + newOption = FILTER_APPS_RUNNING; + break; + case 2: + newOption = FILTER_APPS_THIRD_PARTY; + break; + default: + return; + } + mAlertDlg.dismiss(); + sendMessageToHandler(REORDER_LIST, newOption); + } +} diff --git a/src/com/android/settings/MasterClear.java b/src/com/android/settings/MasterClear.java new file mode 100644 index 000000000..38ad60818 --- /dev/null +++ b/src/com/android/settings/MasterClear.java @@ -0,0 +1,205 @@ +/* + * 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 com.android.internal.widget.LockPatternUtils; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ICheckinService; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; + +/** + * Confirm and execute a reset of the device to a clean "just out of the box" + * state. Multiple confirmations are required: first, a general "are you sure + * you want to do this?" prompt, followed by a keyguard pattern trace if the user + * has defined one, followed by a final strongly-worded "THIS WILL ERASE EVERYTHING + * ON THE PHONE" prompt. If at any time the phone is allowed to go to sleep, is + * locked, et cetera, then the confirmation sequence is abandoned. + */ +public class MasterClear extends Activity { + + private static final int KEYGUARD_REQUEST = 55; + + private LayoutInflater mInflater; + private LockPatternUtils mLockUtils; + + private View mInitialView; + private Button mInitiateButton; + + private View mFinalView; + private Button mFinalButton; + + /** + * The user has gone through the multiple confirmation, so now we go ahead + * and invoke the Checkin Service to reset the device to its factory-default + * state (rebooting in the process). + */ + private Button.OnClickListener mFinalClickListener = new Button.OnClickListener() { + public void onClick(View v) { + + // Those monkeys kept committing suicide, so we add this property + // to disable going through with the master clear + if (!TextUtils.isEmpty(SystemProperties.get("ro.monkey"))) { + return; + } + + ICheckinService service = + ICheckinService.Stub.asInterface(ServiceManager.getService("checkin")); + if (service != null) { + try { + // This RPC should never return + service.masterClear(); + } catch (android.os.RemoteException e) { + // Intentionally blank - there's nothing we can do here + Log.w("MasterClear", "Unable to invoke ICheckinService.masterClear()"); + } + } else { + Log.w("MasterClear", "Unable to locate ICheckinService"); + } + + /* If we reach this point, the master clear didn't happen -- the + * service might have been unregistered with the ServiceManager, + * the RPC might have thrown an exception, or for some reason + * the implementation of masterClear() may have returned instead + * of resetting the device. + */ + new AlertDialog.Builder(MasterClear.this) + .setMessage(getText(R.string.master_clear_failed)) + .setPositiveButton(getText(android.R.string.ok), null) + .show(); + } + }; + + /** + * Keyguard validation is run using the standard {@link ConfirmLockPattern} + * component as a subactivity + */ + private void runKeyguardConfirmation() { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.ConfirmLockPattern"); + // supply header and footer text in the intent + intent.putExtra(ConfirmLockPattern.HEADER_TEXT, + getText(R.string.master_clear_gesture_prompt)); + intent.putExtra(ConfirmLockPattern.FOOTER_TEXT, + getText(R.string.master_clear_gesture_explanation)); + startActivityForResult(intent, KEYGUARD_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode != KEYGUARD_REQUEST) { + return; + } + + // 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) { + establishFinalConfirmationState(); + } else { + establishInitialState(); + } + } + + /** + * If the user clicks to begin the reset sequence, we next require a + * keyguard confirmation if the user has currently enabled one. If there + * is no keyguard available, we simply go to the final confirmation prompt. + */ + private Button.OnClickListener mInitiateListener = new Button.OnClickListener() { + public void onClick(View v) { + if (mLockUtils.isLockPatternEnabled()) { + runKeyguardConfirmation(); + } else { + establishFinalConfirmationState(); + } + } + }; + + /** + * Configure the UI for the final confirmation interaction + */ + private void establishFinalConfirmationState() { + if (mFinalView == null) { + mFinalView = mInflater.inflate(R.layout.master_clear_final, null); + mFinalButton = + (Button) mFinalView.findViewById(R.id.execute_master_clear); + mFinalButton.setOnClickListener(mFinalClickListener); + } + + setContentView(mFinalView); + } + + /** + * In its initial state, the activity presents a button for the user to + * click in order to initiate a confirmation sequence. This method is + * called from various other points in the code to reset the activity to + * this base state. + * + * <p>Reinflating views from resources is expensive and prevents us from + * caching widget pointers, so we use a single-inflate pattern: we lazy- + * inflate each view, caching all of the widget pointers we'll need at the + * time, then simply reuse the inflated views directly whenever we need + * to change contents. + */ + private void establishInitialState() { + if (mInitialView == null) { + mInitialView = mInflater.inflate(R.layout.master_clear_primary, null); + mInitiateButton = + (Button) mInitialView.findViewById(R.id.initiate_master_clear); + mInitiateButton.setOnClickListener(mInitiateListener); + } + + setContentView(mInitialView); + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + mInitialView = null; + mFinalView = null; + mInflater = LayoutInflater.from(this); + mLockUtils = new LockPatternUtils(getContentResolver()); + + establishInitialState(); + } + + /** Abandon all progress through the confirmation sequence by returning + * to the initial view any time the activity is interrupted (e.g. by + * idle timeout). + */ + @Override + public void onPause() { + super.onPause(); + + establishInitialState(); + } + +} diff --git a/src/com/android/settings/MediaFormat.java b/src/com/android/settings/MediaFormat.java new file mode 100644 index 000000000..3594572a9 --- /dev/null +++ b/src/com/android/settings/MediaFormat.java @@ -0,0 +1,193 @@ +/* + * 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 com.android.internal.widget.LockPatternUtils; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IMountService; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; + +/** + * Confirm and execute a format of the sdcard. + * Multiple confirmations are required: first, a general "are you sure + * you want to do this?" prompt, followed by a keyguard pattern trace if the user + * has defined one, followed by a final strongly-worded "THIS WILL ERASE EVERYTHING + * ON THE SD CARD" prompt. If at any time the phone is allowed to go to sleep, is + * locked, et cetera, then the confirmation sequence is abandoned. + */ +public class MediaFormat extends Activity { + + private static final int KEYGUARD_REQUEST = 55; + + private LayoutInflater mInflater; + private LockPatternUtils mLockUtils; + + private View mInitialView; + private Button mInitiateButton; + + private View mFinalView; + private Button mFinalButton; + + /** + * The user has gone through the multiple confirmation, so now we go ahead + * and invoke the Mount Service to format the SD card. + */ + private Button.OnClickListener mFinalClickListener = new Button.OnClickListener() { + public void onClick(View v) { + + // Those monkeys kept committing suicide, so we add this property + // to disable going through with the format + if (!TextUtils.isEmpty(SystemProperties.get("ro.monkey"))) { + return; + } + IMountService service = + IMountService.Stub.asInterface(ServiceManager.getService("mount")); + if (service != null) { + try { + service.formatMedia(Environment.getExternalStorageDirectory().toString()); + } catch (android.os.RemoteException e) { + // Intentionally blank - there's nothing we can do here + Log.w("MediaFormat", "Unable to invoke IMountService.formatMedia()"); + } + } else { + Log.w("MediaFormat", "Unable to locate IMountService"); + } + finish(); + } + }; + + /** + * Keyguard validation is run using the standard {@link ConfirmLockPattern} + * component as a subactivity + */ + private void runKeyguardConfirmation() { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.ConfirmLockPattern"); + // supply header and footer text in the intent + intent.putExtra(ConfirmLockPattern.HEADER_TEXT, + getText(R.string.media_format_gesture_prompt)); + intent.putExtra(ConfirmLockPattern.FOOTER_TEXT, + getText(R.string.media_format_gesture_explanation)); + startActivityForResult(intent, KEYGUARD_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode != KEYGUARD_REQUEST) { + return; + } + + // 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) { + establishFinalConfirmationState(); + } else { + establishInitialState(); + } + } + + /** + * If the user clicks to begin the reset sequence, we next require a + * keyguard confirmation if the user has currently enabled one. If there + * is no keyguard available, we simply go to the final confirmation prompt. + */ + private Button.OnClickListener mInitiateListener = new Button.OnClickListener() { + public void onClick(View v) { + if (mLockUtils.isLockPatternEnabled()) { + runKeyguardConfirmation(); + } else { + establishFinalConfirmationState(); + } + } + }; + + /** + * Configure the UI for the final confirmation interaction + */ + private void establishFinalConfirmationState() { + if (mFinalView == null) { + mFinalView = mInflater.inflate(R.layout.media_format_final, null); + mFinalButton = + (Button) mFinalView.findViewById(R.id.execute_media_format); + mFinalButton.setOnClickListener(mFinalClickListener); + } + + setContentView(mFinalView); + } + + /** + * In its initial state, the activity presents a button for the user to + * click in order to initiate a confirmation sequence. This method is + * called from various other points in the code to reset the activity to + * this base state. + * + * <p>Reinflating views from resources is expensive and prevents us from + * caching widget pointers, so we use a single-inflate pattern: we lazy- + * inflate each view, caching all of the widget pointers we'll need at the + * time, then simply reuse the inflated views directly whenever we need + * to change contents. + */ + private void establishInitialState() { + if (mInitialView == null) { + mInitialView = mInflater.inflate(R.layout.media_format_primary, null); + mInitiateButton = + (Button) mInitialView.findViewById(R.id.initiate_media_format); + mInitiateButton.setOnClickListener(mInitiateListener); + } + + setContentView(mInitialView); + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + mInitialView = null; + mFinalView = null; + mInflater = LayoutInflater.from(this); + mLockUtils = new LockPatternUtils(getContentResolver()); + + establishInitialState(); + } + + /** Abandon all progress through the confirmation sequence by returning + * to the initial view any time the activity is interrupted (e.g. by + * idle timeout). + */ + @Override + public void onPause() { + super.onPause(); + + establishInitialState(); + } + +} diff --git a/src/com/android/settings/ProgressCategory.java b/src/com/android/settings/ProgressCategory.java new file mode 100644 index 000000000..15810b338 --- /dev/null +++ b/src/com/android/settings/ProgressCategory.java @@ -0,0 +1,55 @@ +/* + * 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 android.content.Context; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Map; + +public class ProgressCategory extends PreferenceCategory { + + private boolean mProgress = false; + + public ProgressCategory(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutResource(R.layout.preference_progress_category); + } + + @Override + public void onBindView(View view) { + super.onBindView(view); + View textView = view.findViewById(R.id.scanning_text); + View progressBar = view.findViewById(R.id.scanning_progress); + + int visibility = mProgress ? View.VISIBLE : View.INVISIBLE; + textView.setVisibility(visibility); + progressBar.setVisibility(visibility); + } + + /** + * Turn on/off the progress indicator and text on the right. + * @param progressOn whether or not the progress should be displayed + */ + public void setProgress(boolean progressOn) { + mProgress = progressOn; + notifyChanged(); + } +} + diff --git a/src/com/android/settings/ProxySelector.java b/src/com/android/settings/ProxySelector.java new file mode 100644 index 000000000..80fe3c90c --- /dev/null +++ b/src/com/android/settings/ProxySelector.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2006 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; +import android.content.ContentResolver; +import android.content.Intent; +import android.net.Proxy; +import android.os.Bundle; +import android.provider.Settings; +import android.text.Selection; +import android.text.Spannable; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * To start the Proxy Selector activity, create the following intent. + * + * <code> + * Intent intent = new Intent(); + * intent.setClassName("com.android.browser.ProxySelector"); + * startActivity(intent); + * </code> + * + * you can add extra options to the intent by using + * + * <code> + * intent.putExtra(key, value); + * </code> + * + * the extra options are: + * + * button-label: a string label to display for the okay button + * title: the title of the window + * error-text: If not null, will be used as the label of the error message. + */ +public class ProxySelector extends Activity +{ + private final static String LOGTAG = "Settings"; + + EditText mHostnameField; + EditText mPortField; + Button mOKButton; + + // Matches blank input, ips, and domain names + private static final String HOSTNAME_REGEXP = "^$|^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*$"; + private static final Pattern HOSTNAME_PATTERN; + static { + HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP); + } + + + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (android.util.Config.LOGV) Log.v(LOGTAG, "[ProxySelector] onStart"); + + setContentView(R.layout.proxy); + initView(); + populateFields(false); + } + + protected void showError(int error) { + + new AlertDialog.Builder(this) + .setTitle(R.string.proxy_error) + .setMessage(error) + .setPositiveButton(R.string.proxy_error_dismiss, null) + .show(); + } + + void initView() { + + mHostnameField = (EditText)findViewById(R.id.hostname); + mHostnameField.setOnFocusChangeListener(mOnFocusChangeHandler); + + mPortField = (EditText)findViewById(R.id.port); + mPortField.setOnClickListener(mOKHandler); + mPortField.setOnFocusChangeListener(mOnFocusChangeHandler); + + mOKButton = (Button)findViewById(R.id.action); + mOKButton.setOnClickListener(mOKHandler); + + Button b = (Button)findViewById(R.id.clear); + b.setOnClickListener(mClearHandler); + + b = (Button)findViewById(R.id.defaultView); + b.setOnClickListener(mDefaultHandler); + } + + void populateFields(boolean useDefault) { + String hostname = null; + int port = -1; + if (useDefault) { + // Use the default proxy settings provided by the carrier + hostname = Proxy.getDefaultHost(); + port = Proxy.getDefaultPort(); + } else { + // Use the last setting given by the user + hostname = Proxy.getHost(this); + port = Proxy.getPort(this); + } + + if (hostname == null) { + hostname = ""; + } + + mHostnameField.setText(hostname); + + String portStr = port == -1 ? "" : Integer.toString(port); + mPortField.setText(portStr); + + Intent intent = getIntent(); + + String buttonLabel = intent.getStringExtra("button-label"); + if (!TextUtils.isEmpty(buttonLabel)) { + mOKButton.setText(buttonLabel); + } + + String title = intent.getStringExtra("title"); + if (!TextUtils.isEmpty(title)) { + setTitle(title); + } + } + + /** + * validate syntax of hostname and port entries + * @return 0 on success, string resource ID on failure + */ + int validate(String hostname, String port) { + Matcher match = HOSTNAME_PATTERN.matcher(hostname); + + if (!match.matches()) return R.string.proxy_error_invalid_host; + + if (hostname.length() > 0 && port.length() == 0) { + return R.string.proxy_error_empty_port; + } + + if (port.length() > 0) { + if (hostname.length() == 0) { + return R.string.proxy_error_empty_host_set_port; + } + int portVal = -1; + try { + portVal = Integer.parseInt(port); + } catch (NumberFormatException ex) { + return R.string.proxy_error_invalid_port; + } + if (portVal <= 0 || portVal > 0xFFFF) { + return R.string.proxy_error_invalid_port; + } + } + return 0; + } + + /** + * returns true on success, false if the user must correct something + */ + boolean saveToDb() { + + String hostname = mHostnameField.getText().toString().trim(); + String portStr = mPortField.getText().toString().trim(); + int port = -1; + + int result = validate(hostname, portStr); + if (result > 0) { + showError(result); + return false; + } + + if (portStr.length() > 0) { + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException ex) { + return false; + } + } + + // 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 + // carrier. http:/b/issue?id=756480 + // FIXME: This currently will not work if the default host is blank and + // the user has cleared the input boxes in order to not use a proxy. + // This is a UI problem and can be solved with some better form + // controls. + // FIXME: If the user types in a proxy that matches the default, should + // we keep that setting? Can be fixed with a new UI. + ContentResolver res = getContentResolver(); + if (hostname.equals(Proxy.getDefaultHost()) + && port == Proxy.getDefaultPort()) { + // If the user hit the default button and didn't change any of + // the input boxes, treat it as if the user has not specified a + // proxy. + hostname = null; + } + + if (!TextUtils.isEmpty(hostname)) { + hostname += ':' + portStr; + } + Settings.Secure.putString(res, Settings.Secure.HTTP_PROXY, hostname); + sendBroadcast(new Intent(Proxy.PROXY_CHANGE_ACTION)); + + return true; + } + + OnClickListener mOKHandler = new OnClickListener() { + public void onClick(View v) { + if (saveToDb()) { + finish(); + } + } + }; + + OnClickListener mClearHandler = new OnClickListener() { + public void onClick(View v) { + mHostnameField.setText(""); + mPortField.setText(""); + } + }; + + OnClickListener mDefaultHandler = new OnClickListener() { + public void onClick(View v) { + populateFields(true); + } + }; + + OnFocusChangeListener mOnFocusChangeHandler = new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + TextView textView = (TextView) v; + Selection.selectAll((Spannable) textView.getText()); + } + } + }; +} diff --git a/src/com/android/settings/RadioInfo.java b/src/com/android/settings/RadioInfo.java new file mode 100644 index 000000000..dbad45dca --- /dev/null +++ b/src/com/android/settings/RadioInfo.java @@ -0,0 +1,1184 @@ +/* + * Copyright (C) 2006 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; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Bundle; +import android.os.Handler; +import android.os.INetStatService; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.telephony.CellLocation; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.telephony.NeighboringCellInfo; +import android.telephony.gsm.GsmCellLocation; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.EditText; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneFactory; +import com.android.internal.telephony.PhoneStateIntentReceiver; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.gsm.PdpConnection; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +public class RadioInfo extends Activity { + private final String TAG = "phone"; + + private static final int EVENT_PHONE_STATE_CHANGED = 100; + private static final int EVENT_SIGNAL_STRENGTH_CHANGED = 200; + private static final int EVENT_SERVICE_STATE_CHANGED = 300; + private static final int EVENT_CFI_CHANGED = 302; + + private static final int EVENT_QUERY_PREFERRED_TYPE_DONE = 1000; + private static final int EVENT_SET_PREFERRED_TYPE_DONE = 1001; + private static final int EVENT_QUERY_NEIGHBORING_CIDS_DONE = 1002; + private static final int EVENT_SET_QXDMLOG_DONE = 1003; + private static final int EVENT_SET_CIPHER_DONE = 1004; + private static final int EVENT_QUERY_SMSC_DONE = 1005; + private static final int EVENT_UPDATE_SMSC_DONE = 1006; + + private static final int MENU_ITEM_SELECT_BAND = 0; + private static final int MENU_ITEM_VIEW_ADN = 1; + private static final int MENU_ITEM_VIEW_FDN = 2; + private static final int MENU_ITEM_VIEW_SDN = 3; + private static final int MENU_ITEM_GET_PDP_LIST = 4; + private static final int MENU_ITEM_TOGGLE_DATA = 5; + private static final int MENU_ITEM_TOGGLE_DATA_ON_BOOT = 6; + + private TextView mImei; + private TextView number; + private TextView callState; + private TextView operatorName; + private TextView roamingState; + private TextView gsmState; + private TextView gprsState; + private TextView network; + private TextView dBm; + private TextView mMwi; + private TextView mCfi; + private TextView mLocation; + private TextView mNeighboringCids; + private TextView resets; + private TextView attempts; + private TextView successes; + private TextView disconnects; + private TextView sentSinceReceived; + private TextView sent; + private TextView received; + private TextView mPingIpAddr; + private TextView mPingHostname; + private TextView mHttpClientTest; + private TextView cipherState; + private TextView dnsCheckState; + private EditText smsc; + private Button radioPowerButton; + private Button qxdmLogButton; + private Button cipherToggleButton; + private Button dnsCheckToggleButton; + private Button pingTestButton; + private Button updateSmscButton; + private Button refreshSmscButton; + private Spinner preferredNetworkType; + + private TelephonyManager mTelephonyManager; + private Phone phone = null; + private PhoneStateIntentReceiver mPhoneStateReceiver; + private INetStatService netstat; + + private OemCommands mOem = null; + private boolean mQxdmLogEnabled; + // The requested cipher state + private boolean mCipherOn; + + private String mPingIpAddrResult; + private String mPingHostnameResult; + private String mHttpClientTestResult; + private boolean mMwiValue = false; + private boolean mCfiValue = false; + + private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onDataConnectionStateChanged(int state) { + updateDataState(); + updateDataStats(); + updatePdpList(); + updateNetworkType(); + } + + @Override + public void onDataActivity(int direction) { + updateDataStats2(); + } + + @Override + public void onCellLocationChanged(CellLocation location) { + updateLocation(location); + } + + @Override + public void onMessageWaitingIndicatorChanged(boolean mwi) { + mMwiValue = mwi; + updateMessageWaiting(); + } + + @Override + public void onCallForwardingIndicatorChanged(boolean cfi) { + mCfiValue = cfi; + updateCallRedirect(); + } + }; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + AsyncResult ar; + switch (msg.what) { + case EVENT_PHONE_STATE_CHANGED: + updatePhoneState(); + break; + + case EVENT_SIGNAL_STRENGTH_CHANGED: + updateSignalStrength(); + break; + + case EVENT_SERVICE_STATE_CHANGED: + updateServiceState(); + updatePowerState(); + break; + + case EVENT_QUERY_PREFERRED_TYPE_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception == null) { + int type = ((int[])ar.result)[0]; + preferredNetworkType.setSelection(type, true); + } else { + preferredNetworkType.setSelection(3, true); + } + break; + case EVENT_SET_PREFERRED_TYPE_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception != null) { + phone.getPreferredNetworkType( + obtainMessage(EVENT_QUERY_PREFERRED_TYPE_DONE)); + } + break; + case EVENT_QUERY_NEIGHBORING_CIDS_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception == null) { + updateNeighboringCids((ArrayList<NeighboringCellInfo>)ar.result); + } else { + mNeighboringCids.setText("unknown"); + } + break; + case EVENT_SET_QXDMLOG_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception == null) { + mQxdmLogEnabled = !mQxdmLogEnabled; + + updateQxdmState(mQxdmLogEnabled); + displayQxdmEnableResult(); + } + break; + case EVENT_SET_CIPHER_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception == null) { + setCiphPref(mCipherOn); + } + updateCiphState(); + break; + case EVENT_QUERY_SMSC_DONE: + ar= (AsyncResult) msg.obj; + if (ar.exception != null) { + smsc.setText("refresh error"); + } else { + byte[] buf = (byte[]) ar.result; + smsc.setText(new String(buf)); + } + break; + case EVENT_UPDATE_SMSC_DONE: + updateSmscButton.setEnabled(true); + ar= (AsyncResult) msg.obj; + if (ar.exception != null) { + smsc.setText("update error"); + } + break; + default: + break; + + } + } + }; + + private class OemCommands { + + public final int OEM_QXDM_SDLOG_DEFAULT_FILE_SIZE = 32; + public final int OEM_QXDM_SDLOG_DEFAULT_MASK = 0; + public final int OEM_QXDM_SDLOG_DEFAULT_MAX_INDEX = 8; + + final int SIZE_OF_INT = 4; + final int OEM_FEATURE_ENABLE = 1; + final int OEM_FEATURE_DISABLE = 0; + final int OEM_SIMPE_FEAUTURE_LEN = 1; + + final int OEM_QXDM_SDLOG_FUNCTAG = 0x00010000; + final int OEM_QXDM_SDLOG_LEN = 4; + final int OEM_PS_AUTO_ATTACH_FUNCTAG = 0x00020000; + final int OEM_CIPHERING_FUNCTAG = 0x00020001; + final int OEM_SMSC_UPDATE_FUNCTAG = 0x00020002; + final int OEM_SMSC_QUERY_FUNCTAG = 0x00020003; + final int OEM_SMSC_QUERY_LEN = 0; + + /** + * The OEM interface to store QXDM to SD. + * + * To start/stop logging QXDM logs to SD card, use tag + * OEM_RIL_HOOK_QXDM_SD_LOG_SETUP 0x00010000 + * + * "data" is a const oem_ril_hook_qxdm_sdlog_setup_data_st * + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->head.func_tag + * should be OEM_RIL_HOOK_QXDM_SD_LOG_SETUP + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->head.len + * should be "sizeof(unsigned int) * 4" + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->mode + * could be 0 for 'stop logging', or 1 for 'start logging' + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->log_file_size + * will assign the size of each log file, and it could be a value between + * 1 and 512 (in megabytes, default value is recommended to set as 32). + * This value will be ignored when mode == 0. + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->log_mask will + * assign the rule to filter logs, and it is a bitmask (bit0 is for MsgAll, + * bit1 is for LogAll, and bit2 is for EventAll) recommended to be set as 0 + * by default. This value will be ignored when mode == 0. + * ((const oem_ril_hook_qxdm_sdlog_setup_data_st *)data)->log_max_fileindex + * set the how many logfiles will storted before roll over. This value will + * be ignored when mode == 0. + * + * "response" is NULL + * + * typedef struct _oem_ril_hook_raw_head_st { + * unsigned int func_tag; + * unsigned int len; + * } oem_ril_hook_raw_head_st; + * + * typedef struct _oem_ril_hook_qxdm_sdlog_setup_data_st { + * oem_ril_hook_raw_head_st head; + * unsigned int mode; + * unsigned int log_file_size; + * unsigned int log_mask; + * unsigned int log_max_fileindex; + * } oem_ril_hook_qxdm_sdlog_setup_data_st; + * + * @param enable set true to start logging QXDM in SD card + * @param fileSize is the log file size in MB + * @param mask is the log mask to filter + * @param maxIndex is the maximum roll-over file number + * @return byteArray to use in RIL RAW command + */ + byte[] getQxdmSdlogData(boolean enable, int fileSize, int mask, int maxIndex) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + try { + writeIntLittleEndian(dos, OEM_QXDM_SDLOG_FUNCTAG); + writeIntLittleEndian(dos, OEM_QXDM_SDLOG_LEN * SIZE_OF_INT); + writeIntLittleEndian(dos, enable ? + OEM_FEATURE_ENABLE : OEM_FEATURE_DISABLE); + writeIntLittleEndian(dos, fileSize); + writeIntLittleEndian(dos, mask); + writeIntLittleEndian(dos, maxIndex); + } catch (IOException e) { + return null; + } + return bos.toByteArray(); + } + + byte[] getSmscQueryData() { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + try { + writeIntLittleEndian(dos, OEM_SMSC_QUERY_FUNCTAG); + writeIntLittleEndian(dos, OEM_SMSC_QUERY_LEN * SIZE_OF_INT); + } catch (IOException e) { + return null; + } + return bos.toByteArray(); + } + + byte[] getSmscUpdateData(String smsc) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + try { + byte[] smsc_bytes = smsc.getBytes(); + writeIntLittleEndian(dos, OEM_SMSC_UPDATE_FUNCTAG); + writeIntLittleEndian(dos, smsc_bytes.length); + dos.write(smsc_bytes); + } catch (IOException e) { + return null; + } + return bos.toByteArray(); + } + + byte[] getPsAutoAttachData(boolean enable) { + return getSimpleFeatureData(OEM_PS_AUTO_ATTACH_FUNCTAG, enable); + } + + byte[] getCipheringData(boolean enable) { + return getSimpleFeatureData(OEM_CIPHERING_FUNCTAG, enable); + } + + private byte[] getSimpleFeatureData(int tag, boolean enable) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + try { + writeIntLittleEndian(dos, tag); + writeIntLittleEndian(dos, OEM_SIMPE_FEAUTURE_LEN * SIZE_OF_INT); + writeIntLittleEndian(dos, enable ? + OEM_FEATURE_ENABLE : OEM_FEATURE_DISABLE); + } catch (IOException e) { + return null; + } + return bos.toByteArray(); + } + + private void writeIntLittleEndian(DataOutputStream dos, int val) + throws IOException { + dos.writeByte(val); + dos.writeByte(val >> 8); + dos.writeByte(val >> 16); + dos.writeByte(val >> 24); + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.radio_info); + + mTelephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); + phone = PhoneFactory.getDefaultPhone(); + + mImei = (TextView) findViewById(R.id.imei); + number = (TextView) findViewById(R.id.number); + callState = (TextView) findViewById(R.id.call); + operatorName = (TextView) findViewById(R.id.operator); + roamingState = (TextView) findViewById(R.id.roaming); + gsmState = (TextView) findViewById(R.id.gsm); + gprsState = (TextView) findViewById(R.id.gprs); + network = (TextView) findViewById(R.id.network); + dBm = (TextView) findViewById(R.id.dbm); + mMwi = (TextView) findViewById(R.id.mwi); + mCfi = (TextView) findViewById(R.id.cfi); + mLocation = (TextView) findViewById(R.id.location); + mNeighboringCids = (TextView) findViewById(R.id.neighboring); + + resets = (TextView) findViewById(R.id.resets); + attempts = (TextView) findViewById(R.id.attempts); + successes = (TextView) findViewById(R.id.successes); + disconnects = (TextView) findViewById(R.id.disconnects); + sentSinceReceived = (TextView) findViewById(R.id.sentSinceReceived); + sent = (TextView) findViewById(R.id.sent); + received = (TextView) findViewById(R.id.received); + cipherState = (TextView) findViewById(R.id.ciphState); + smsc = (EditText) findViewById(R.id.smsc); + dnsCheckState = (TextView) findViewById(R.id.dnsCheckState); + + mPingIpAddr = (TextView) findViewById(R.id.pingIpAddr); + mPingHostname = (TextView) findViewById(R.id.pingHostname); + mHttpClientTest = (TextView) findViewById(R.id.httpClientTest); + + preferredNetworkType = (Spinner) findViewById(R.id.preferredNetworkType); + ArrayAdapter<String> adapter = new ArrayAdapter<String> (this, + android.R.layout.simple_spinner_item, mPreferredNetworkLabels); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + preferredNetworkType.setAdapter(adapter); + preferredNetworkType.setOnItemSelectedListener(mPreferredNetworkHandler); + + radioPowerButton = (Button) findViewById(R.id.radio_power); + radioPowerButton.setOnClickListener(mPowerButtonHandler); + + qxdmLogButton = (Button) findViewById(R.id.qxdm_log); + qxdmLogButton.setOnClickListener(mQxdmButtonHandler); + + cipherToggleButton = (Button) findViewById(R.id.ciph_toggle); + cipherToggleButton.setOnClickListener(mCipherButtonHandler); + pingTestButton = (Button) findViewById(R.id.ping_test); + pingTestButton.setOnClickListener(mPingButtonHandler); + updateSmscButton = (Button) findViewById(R.id.update_smsc); + updateSmscButton.setOnClickListener(mUpdateSmscButtonHandler); + refreshSmscButton = (Button) findViewById(R.id.refresh_smsc); + refreshSmscButton.setOnClickListener(mRefreshSmscButtonHandler); + dnsCheckToggleButton = (Button) findViewById(R.id.dns_check_toggle); + dnsCheckToggleButton.setOnClickListener(mDnsCheckButtonHandler); + + mPhoneStateReceiver = new PhoneStateIntentReceiver(this, mHandler); + mPhoneStateReceiver.notifySignalStrength(EVENT_SIGNAL_STRENGTH_CHANGED); + mPhoneStateReceiver.notifyServiceState(EVENT_SERVICE_STATE_CHANGED); + mPhoneStateReceiver.notifyPhoneCallState(EVENT_PHONE_STATE_CHANGED); + + updateQxdmState(null); + mOem = new OemCommands(); + + phone.getPreferredNetworkType( + mHandler.obtainMessage(EVENT_QUERY_PREFERRED_TYPE_DONE)); + phone.getNeighboringCids( + mHandler.obtainMessage(EVENT_QUERY_NEIGHBORING_CIDS_DONE)); + + netstat = INetStatService.Stub.asInterface(ServiceManager.getService("netstat")); + + CellLocation.requestLocationUpdate(); + } + + @Override + protected void onResume() { + super.onResume(); + + updatePhoneState(); + updateSignalStrength(); + updateMessageWaiting(); + updateCallRedirect(); + updateServiceState(); + updateLocation(mTelephonyManager.getCellLocation()); + updateDataState(); + updateDataStats(); + updateDataStats2(); + updatePowerState(); + updateQxdmState(null); + updateProperties(); + updateCiphState(); + updateDnsCheckState(); + + Log.i(TAG, "[RadioInfo] onResume: register phone & data intents"); + + mPhoneStateReceiver.registerIntent(); + mTelephonyManager.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_DATA_CONNECTION_STATE + | PhoneStateListener.LISTEN_DATA_ACTIVITY + | PhoneStateListener.LISTEN_CELL_LOCATION + | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR + | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR); + } + + @Override + public void onPause() { + super.onPause(); + + Log.i(TAG, "[RadioInfo] onPause: unregister phone & data intents"); + + mPhoneStateReceiver.unregisterIntent(); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_ITEM_SELECT_BAND, 0, R.string.radio_info_band_mode_label).setOnMenuItemClickListener(mSelectBandCallback) + .setAlphabeticShortcut('b'); + menu.add(1, MENU_ITEM_VIEW_ADN, 0, + R.string.radioInfo_menu_viewADN).setOnMenuItemClickListener(mViewADNCallback); + menu.add(1, MENU_ITEM_VIEW_FDN, 0, + R.string.radioInfo_menu_viewFDN).setOnMenuItemClickListener(mViewFDNCallback); + menu.add(1, MENU_ITEM_VIEW_SDN, 0, + R.string.radioInfo_menu_viewSDN).setOnMenuItemClickListener(mViewSDNCallback); + menu.add(1, MENU_ITEM_GET_PDP_LIST, + 0, R.string.radioInfo_menu_getPDP).setOnMenuItemClickListener(mGetPdpList); + menu.add(1, MENU_ITEM_TOGGLE_DATA, + 0, R.string.radioInfo_menu_disableData).setOnMenuItemClickListener(mToggleData); + menu.add(1, MENU_ITEM_TOGGLE_DATA_ON_BOOT, + 0, R.string.radioInfo_menu_disableDataOnBoot).setOnMenuItemClickListener(mToggleDataOnBoot); + return true; + } + + + @Override + public boolean onPrepareOptionsMenu(Menu menu) + { + // Get the TOGGLE DATA menu item in the right state. + MenuItem item = menu.findItem(MENU_ITEM_TOGGLE_DATA); + int state = mTelephonyManager.getDataState(); + boolean visible = true; + + switch (state) { + case TelephonyManager.DATA_CONNECTED: + case TelephonyManager.DATA_SUSPENDED: + item.setTitle(R.string.radioInfo_menu_disableData); + break; + case TelephonyManager.DATA_DISCONNECTED: + item.setTitle(R.string.radioInfo_menu_enableData); + break; + default: + visible = false; + break; + } + item.setVisible(visible); + + // Get the toggle-data-on-boot menu item in the right state. + item = menu.findItem(MENU_ITEM_TOGGLE_DATA_ON_BOOT); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this.getApplication()); + boolean value = sp.getBoolean(GSMPhone.DATA_DISABLED_ON_BOOT_KEY, false); + if (value) { + item.setTitle(R.string.radioInfo_menu_enableDataOnBoot); + } else { + item.setTitle(R.string.radioInfo_menu_disableDataOnBoot); + } + return true; + } + + private boolean isRadioOn() { + return phone.getServiceState().getState() != ServiceState.STATE_POWER_OFF; + } + + private void updatePowerState() { + //log("updatePowerState"); + String buttonText = isRadioOn() ? + getString(R.string.turn_off_radio) : + getString(R.string.turn_on_radio); + radioPowerButton.setText(buttonText); + } + + private void updateQxdmState(Boolean newQxdmStatus) { + SharedPreferences sp = + PreferenceManager.getDefaultSharedPreferences(this.getApplication()); + mQxdmLogEnabled = sp.getBoolean("qxdmstatus", false); + // This is called from onCreate, onResume, and the handler when the status + // is updated. + if (newQxdmStatus != null) { + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean("qxdmstatus", newQxdmStatus); + editor.commit(); + mQxdmLogEnabled = newQxdmStatus; + } + + String buttonText = mQxdmLogEnabled ? + getString(R.string.turn_off_qxdm) : + getString(R.string.turn_on_qxdm); + qxdmLogButton.setText(buttonText); + } + + private void setCiphPref(boolean value) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this.getApplication()); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(GSMPhone.CIPHERING_KEY, value); + editor.commit(); + } + + private boolean getCiphPref() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this.getApplication()); + boolean ret = sp.getBoolean(GSMPhone.CIPHERING_KEY, true); + return ret; + } + + private void updateCiphState() { + cipherState.setText(getCiphPref() ? "Ciphering ON" : "Ciphering OFF"); + } + + private void updateDnsCheckState() { + GSMPhone gsmPhone = (GSMPhone) phone; + dnsCheckState.setText(gsmPhone.isDnsCheckDisabled() ? + "0.0.0.0 allowed" :"0.0.0.0 not allowed"); + } + + private final void + updateSignalStrength() { + int state = + mPhoneStateReceiver.getServiceState().getState(); + Resources r = getResources(); + + if ((ServiceState.STATE_OUT_OF_SERVICE == state) || + (ServiceState.STATE_POWER_OFF == state)) { + dBm.setText("0"); + } + + int signalDbm = mPhoneStateReceiver.getSignalStrengthDbm(); + + if (-1 == signalDbm) signalDbm = 0; + + int signalAsu = mPhoneStateReceiver.getSignalStrength(); + + if (-1 == signalAsu) signalAsu = 0; + + dBm.setText(String.valueOf(signalDbm) + " " + + r.getString(R.string.radioInfo_display_dbm) + " " + + String.valueOf(signalAsu) + " " + + r.getString(R.string.radioInfo_display_asu)); + } + + private final void updateLocation(CellLocation location) { + GsmCellLocation loc = (GsmCellLocation)location; + Resources r = getResources(); + + int lac = loc.getLac(); + int cid = loc.getCid(); + + mLocation.setText(r.getString(R.string.radioInfo_lac) + " = " + + ((lac == -1) ? "unknown" : Integer.toHexString(lac)) + + " " + + r.getString(R.string.radioInfo_cid) + " = " + + ((cid == -1) ? "unknown" : Integer.toHexString(cid))); + } + + private final void updateNeighboringCids(ArrayList<NeighboringCellInfo> cids) { + String neighborings = ""; + if (cids != null) { + if ( cids.isEmpty() ) { + neighborings = "no neighboring cells"; + } else { + for (NeighboringCellInfo cell : cids) { + neighborings += "{" + Integer.toHexString(cell.getCid()) + + "@" + cell.getRssi() + "} "; + } + } + } else { + neighborings = "unknown"; + } + mNeighboringCids.setText(neighborings); + } + + private final void + updateMessageWaiting() { + mMwi.setText(String.valueOf(mMwiValue)); + } + + private final void + updateCallRedirect() { + mCfi.setText(String.valueOf(mCfiValue)); + } + + + private final void + updateServiceState() { + ServiceState serviceState = mPhoneStateReceiver.getServiceState(); + int state = serviceState.getState(); + Resources r = getResources(); + String display = r.getString(R.string.radioInfo_unknown); + + switch (state) { + case ServiceState.STATE_IN_SERVICE: + display = r.getString(R.string.radioInfo_service_in); + break; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + display = r.getString(R.string.radioInfo_service_emergency); + break; + case ServiceState.STATE_POWER_OFF: + display = r.getString(R.string.radioInfo_service_off); + break; + } + + gsmState.setText(display); + + if (serviceState.getRoaming()) { + roamingState.setText(R.string.radioInfo_roaming_in); + } else { + roamingState.setText(R.string.radioInfo_roaming_not); + } + + operatorName.setText(serviceState.getOperatorAlphaLong()); + } + + private final void + updatePhoneState() { + Phone.State state = mPhoneStateReceiver.getPhoneState(); + Resources r = getResources(); + String display = r.getString(R.string.radioInfo_unknown); + + switch (state) { + case IDLE: + display = r.getString(R.string.radioInfo_phone_idle); + break; + case RINGING: + display = r.getString(R.string.radioInfo_phone_ringing); + break; + case OFFHOOK: + display = r.getString(R.string.radioInfo_phone_offhook); + break; + } + + callState.setText(display); + } + + private final void + updateDataState() { + int state = mTelephonyManager.getDataState(); + Resources r = getResources(); + String display = r.getString(R.string.radioInfo_unknown); + + switch (state) { + case TelephonyManager.DATA_CONNECTED: + display = r.getString(R.string.radioInfo_data_connected); + break; + case TelephonyManager.DATA_CONNECTING: + display = r.getString(R.string.radioInfo_data_connecting); + break; + case TelephonyManager.DATA_DISCONNECTED: + display = r.getString(R.string.radioInfo_data_disconnected); + break; + case TelephonyManager.DATA_SUSPENDED: + display = r.getString(R.string.radioInfo_data_suspended); + break; + } + + gprsState.setText(display); + } + + private final void updateNetworkType() { + Resources r = getResources(); + String display = SystemProperties.get(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, + r.getString(R.string.radioInfo_unknown)); + + network.setText(display); + } + + private final void + updateProperties() { + String s; + Resources r = getResources(); + + s = phone.getDeviceId(); + if (s == null) s = r.getString(R.string.radioInfo_unknown); + mImei.setText(s); + + s = phone.getLine1Number(); + if (s == null) s = r.getString(R.string.radioInfo_unknown); + number.setText(s); + } + + private final void updateDataStats() { + String s; + + s = SystemProperties.get("net.gsm.radio-reset", "0"); + resets.setText(s); + + s = SystemProperties.get("net.gsm.attempt-gprs", "0"); + attempts.setText(s); + + s = SystemProperties.get("net.gsm.succeed-gprs", "0"); + successes.setText(s); + + //s = SystemProperties.get("net.gsm.disconnect", "0"); + //disconnects.setText(s); + + s = SystemProperties.get("net.ppp.reset-by-timeout", "0"); + sentSinceReceived.setText(s); + } + + private final void updateDataStats2() { + Resources r = getResources(); + + try { + long txPackets = netstat.getMobileTxPackets(); + long rxPackets = netstat.getMobileRxPackets(); + long txBytes = netstat.getMobileTxBytes(); + long rxBytes = netstat.getMobileRxBytes(); + + String packets = r.getString(R.string.radioInfo_display_packets); + String bytes = r.getString(R.string.radioInfo_display_bytes); + + sent.setText(txPackets + " " + packets + ", " + txBytes + " " + bytes); + received.setText(rxPackets + " " + packets + ", " + rxBytes + " " + bytes); + } catch (RemoteException e) { + } + } + + /** + * Ping a IP address. + */ + private final void pingIpAddr() { + try { + // This is hardcoded IP addr. This is for testing purposes. + // We would need to get rid of this before release. + String ipAddress = "74.125.47.104"; + Process p = Runtime.getRuntime().exec("ping -c 1 " + ipAddress); + int status = p.waitFor(); + if (status == 0) { + mPingIpAddrResult = "Pass"; + } else { + mPingIpAddrResult = "Fail: IP addr not reachable"; + } + } catch (IOException e) { + mPingIpAddrResult = "Fail: IOException"; + } catch (InterruptedException e) { + mPingIpAddrResult = "Fail: InterruptedException"; + } + } + + /** + * Ping a host name + */ + private final void pingHostname() { + try { + Process p = Runtime.getRuntime().exec("ping -c 1 www.google.com"); + int status = p.waitFor(); + if (status == 0) { + mPingHostnameResult = "Pass"; + } else { + mPingHostnameResult = "Fail: Host unreachable"; + } + } catch (UnknownHostException e) { + mPingHostnameResult = "Fail: Unknown Host"; + } catch (IOException e) { + mPingHostnameResult= "Fail: IOException"; + } catch (InterruptedException e) { + mPingHostnameResult = "Fail: InterruptedException"; + } + } + + /** + * This function checks for basic functionality of HTTP Client. + */ + private void httpClientTest() { + HttpClient client = new DefaultHttpClient(); + try { + HttpGet request = new HttpGet("http://www.google.com"); + HttpResponse response = client.execute(request); + if (response.getStatusLine().getStatusCode() == 200) { + mHttpClientTestResult = "Pass"; + } else { + mHttpClientTestResult = "Fail: Code: " + String.valueOf(response); + } + request.abort(); + } catch (IOException e) { + mHttpClientTestResult = "Fail: IOException"; + } + } + + private void refreshSmsc() { + byte[] data = mOem.getSmscQueryData(); + if (data == null) return; + phone.invokeOemRilRequestRaw(data, + mHandler.obtainMessage(EVENT_QUERY_SMSC_DONE)); + } + + private final void updatePingState() { + final Handler handler = new Handler(); + // Set all to unknown since the threads will take a few secs to update. + mPingIpAddrResult = getResources().getString(R.string.radioInfo_unknown); + mPingHostnameResult = getResources().getString(R.string.radioInfo_unknown); + mHttpClientTestResult = getResources().getString(R.string.radioInfo_unknown); + + mPingIpAddr.setText(mPingIpAddrResult); + mPingHostname.setText(mPingHostnameResult); + mHttpClientTest.setText(mHttpClientTestResult); + + final Runnable updatePingResults = new Runnable() { + public void run() { + mPingIpAddr.setText(mPingIpAddrResult); + mPingHostname.setText(mPingHostnameResult); + mHttpClientTest.setText(mHttpClientTestResult); + } + }; + Thread ipAddr = new Thread() { + @Override + public void run() { + pingIpAddr(); + handler.post(updatePingResults); + } + }; + ipAddr.start(); + + Thread hostname = new Thread() { + @Override + public void run() { + pingHostname(); + handler.post(updatePingResults); + } + }; + hostname.start(); + + Thread httpClient = new Thread() { + @Override + public void run() { + httpClientTest(); + handler.post(updatePingResults); + } + }; + httpClient.start(); + } + + private final void updatePdpList() { + StringBuilder sb = new StringBuilder("========DATA=======\n"); + + List<PdpConnection> pdps = phone.getCurrentPdpList(); + + for (PdpConnection pdp : pdps) { + sb.append(" State: ").append(pdp.getState().toString()).append("\n"); + if (pdp.getState().isActive()) { + long timeElapsed = + (System.currentTimeMillis() - pdp.getConnectionTime())/1000; + sb.append(" connected at ") + .append(DateUtils.timeString(pdp.getConnectionTime())) + .append(" and elapsed ") + .append(DateUtils.formatElapsedTime(timeElapsed)) + .append("\n to ") + .append(pdp.getApn().toString()) + .append("\ninterface: ") + .append(phone.getInterfaceName(phone.getActiveApnTypes()[0])) + .append("\naddress: ") + .append(phone.getIpAddress(phone.getActiveApnTypes()[0])) + .append("\ngateway: ") + .append(phone.getGateway(phone.getActiveApnTypes()[0])); + String[] dns = phone.getDnsServers(phone.getActiveApnTypes()[0]); + if (dns != null) { + sb.append("\ndns: ").append(dns[0]).append(", ").append(dns[1]); + } + } else if (pdp.getState().isInactive()) { + sb.append(" disconnected with last try at ") + .append(DateUtils.timeString(pdp.getLastFailTime())) + .append("\n fail because ") + .append(pdp.getLastFailCause().toString()); + } else { + sb.append(" is connecting to ") + .append(pdp.getApn().toString()); + } + sb.append("\n==================="); + } + + + disconnects.setText(sb.toString()); + } + + private void displayQxdmEnableResult() { + String status = mQxdmLogEnabled ? "Start QXDM Log" : "Stop QXDM Log"; + + DialogInterface mProgressPanel = new AlertDialog. + Builder(this).setMessage(status).show(); + + mHandler.postDelayed( + new Runnable() { + public void run() { + finish(); + } + }, 2000); + } + + private MenuItem.OnMenuItemClickListener mViewADNCallback = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(Intent.ACTION_VIEW); + // XXX We need to specify the component here because if we don't + // the activity manager will try to resolve the type by calling + // the content provider, which causes it to be loaded in a process + // other than the Dialer process, which causes a lot of stuff to + // break. + intent.setClassName("com.android.phone", + "com.android.phone.SimContacts"); + startActivity(intent); + return true; + } + }; + + private MenuItem.OnMenuItemClickListener mViewFDNCallback = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(Intent.ACTION_VIEW); + // XXX We need to specify the component here because if we don't + // the activity manager will try to resolve the type by calling + // the content provider, which causes it to be loaded in a process + // other than the Dialer process, which causes a lot of stuff to + // break. + intent.setClassName("com.android.phone", + "com.android.phone.FdnList"); + startActivity(intent); + return true; + } + }; + + private MenuItem.OnMenuItemClickListener mViewSDNCallback = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent( + Intent.ACTION_VIEW, Uri.parse("content://sim/sdn")); + // XXX We need to specify the component here because if we don't + // the activity manager will try to resolve the type by calling + // the content provider, which causes it to be loaded in a process + // other than the Dialer process, which causes a lot of stuff to + // break. + intent.setClassName("com.android.phone", + "com.android.phone.ADNList"); + startActivity(intent); + return true; + } + }; + + private void toggleDataDisabledOnBoot() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this.getApplication()); + SharedPreferences.Editor editor = sp.edit(); + boolean value = sp.getBoolean(GSMPhone.DATA_DISABLED_ON_BOOT_KEY, false); + editor.putBoolean(GSMPhone.DATA_DISABLED_ON_BOOT_KEY, !value); + byte[] data = mOem.getPsAutoAttachData(value); + if (data == null) { + // don't commit + return; + } + + editor.commit(); + phone.invokeOemRilRequestRaw(data, null); + } + + private MenuItem.OnMenuItemClickListener mToggleDataOnBoot = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + toggleDataDisabledOnBoot(); + return true; + } + }; + + private MenuItem.OnMenuItemClickListener mToggleData = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + int state = mTelephonyManager.getDataState(); + switch (state) { + case TelephonyManager.DATA_CONNECTED: + phone.disableDataConnectivity(); + break; + case TelephonyManager.DATA_DISCONNECTED: + phone.enableDataConnectivity(); + break; + default: + // do nothing + break; + } + return true; + } + }; + + private MenuItem.OnMenuItemClickListener mGetPdpList = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + phone.getPdpContextList(null); + return true; + } + }; + + private MenuItem.OnMenuItemClickListener mSelectBandCallback = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(); + intent.setClass(RadioInfo.this, BandMode.class); + startActivity(intent); + return true; + } + }; + + OnClickListener mPowerButtonHandler = new OnClickListener() { + public void onClick(View v) { + //log("toggle radio power: currently " + (isRadioOn()?"on":"off")); + phone.setRadioPower(!isRadioOn()); + } + }; + + OnClickListener mCipherButtonHandler = new OnClickListener() { + public void onClick(View v) { + mCipherOn = !getCiphPref(); + byte[] data = mOem.getCipheringData(mCipherOn); + + if (data == null) + return; + + cipherState.setText("Setting..."); + phone.invokeOemRilRequestRaw(data, + mHandler.obtainMessage(EVENT_SET_CIPHER_DONE)); + } + }; + + OnClickListener mDnsCheckButtonHandler = new OnClickListener() { + public void onClick(View v) { + GSMPhone gsmPhone = (GSMPhone) phone; + gsmPhone.disableDnsCheck(!gsmPhone.isDnsCheckDisabled()); + updateDnsCheckState(); + } + }; + + OnClickListener mPingButtonHandler = new OnClickListener() { + public void onClick(View v) { + updatePingState(); + } + }; + + OnClickListener mUpdateSmscButtonHandler = new OnClickListener() { + public void onClick(View v) { + updateSmscButton.setEnabled(false); + byte[] data = mOem.getSmscUpdateData(smsc.getText().toString()); + if (data == null) return; + phone.invokeOemRilRequestRaw(data, + mHandler.obtainMessage(EVENT_UPDATE_SMSC_DONE)); + } + }; + + OnClickListener mRefreshSmscButtonHandler = new OnClickListener() { + public void onClick(View v) { + refreshSmsc(); + } + }; + + OnClickListener mQxdmButtonHandler = new OnClickListener() { + public void onClick(View v) { + byte[] data = mOem.getQxdmSdlogData( + !mQxdmLogEnabled, + mOem.OEM_QXDM_SDLOG_DEFAULT_FILE_SIZE, + mOem.OEM_QXDM_SDLOG_DEFAULT_MASK, + mOem.OEM_QXDM_SDLOG_DEFAULT_MAX_INDEX); + + if (data == null) + return; + + phone.invokeOemRilRequestRaw(data, + mHandler.obtainMessage(EVENT_SET_QXDMLOG_DONE)); + } + }; + + AdapterView.OnItemSelectedListener + mPreferredNetworkHandler = new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View v, int pos, long id) { + Message msg = mHandler.obtainMessage(EVENT_SET_PREFERRED_TYPE_DONE); + if (pos>=0 && pos<=2) { + phone.setPreferredNetworkType(pos, msg); + } + } + + public void onNothingSelected(AdapterView parent) { + } + }; + + private String[] mPreferredNetworkLabels = { + "WCDMA preferred", "GSM only", "WCDMA only", "Unknown"}; +} diff --git a/src/com/android/settings/RingerVolumePreference.java b/src/com/android/settings/RingerVolumePreference.java new file mode 100644 index 000000000..2d21ec6e9 --- /dev/null +++ b/src/com/android/settings/RingerVolumePreference.java @@ -0,0 +1,129 @@ +/* + * 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 android.content.Context; +import android.media.AudioManager; +import android.preference.VolumePreference; +import android.preference.VolumePreference.SeekBarVolumizer; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +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 implements + CheckBox.OnCheckedChangeListener { + private static final String TAG = "RingerVolumePreference"; + + private CheckBox mNotificationsUseRingVolumeCheckbox; + 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); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + mNotificationsUseRingVolumeCheckbox = + (CheckBox) view.findViewById(R.id.same_notification_volume); + mNotificationsUseRingVolumeCheckbox.setOnCheckedChangeListener(this); + mNotificationsUseRingVolumeCheckbox.setChecked(Settings.System.getInt( + getContext().getContentResolver(), + Settings.System.NOTIFICATIONS_USE_RING_VOLUME, 1) == 1); + + final SeekBar seekBar = (SeekBar) view.findViewById(R.id.notification_volume_seekbar); + mNotificationSeekBarVolumizer = new SeekBarVolumizer(getContext(), seekBar, + AudioManager.STREAM_NOTIFICATION); + + mNotificationVolumeTitle = (TextView) view.findViewById(R.id.notification_volume_title); + + setNotificationVolumeVisibility(!mNotificationsUseRingVolumeCheckbox.isChecked()); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (!positiveResult && mNotificationSeekBarVolumizer != null) { + mNotificationSeekBarVolumizer.revertVolume(); + } + + cleanup(); + } + + @Override + public void onActivityStop() { + super.onActivityStop(); + cleanup(); + } + + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setNotificationVolumeVisibility(!isChecked); + + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATIONS_USE_RING_VOLUME, isChecked ? 1 : 0); + + if (isChecked) { + // The user wants the notification to be same as ring, so do a + // one-time sync right now + AudioManager audioManager = (AudioManager) getContext() + .getSystemService(Context.AUDIO_SERVICE); + audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, + audioManager.getStreamVolume(AudioManager.STREAM_RING), 0); + } + } + + @Override + protected void onSampleStarting(SeekBarVolumizer volumizer) { + super.onSampleStarting(volumizer); + + if (mNotificationSeekBarVolumizer != null && volumizer != mNotificationSeekBarVolumizer) { + mNotificationSeekBarVolumizer.stopSample(); + } + } + + private void setNotificationVolumeVisibility(boolean visible) { + if (mNotificationSeekBarVolumizer != null) { + mNotificationSeekBarVolumizer.getSeekBar().setVisibility( + visible ? View.VISIBLE : View.GONE); + mNotificationVolumeTitle.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + private void cleanup() { + if (mNotificationSeekBarVolumizer != null) { + mNotificationSeekBarVolumizer.stop(); + mNotificationSeekBarVolumizer = null; + } + } + +} diff --git a/src/com/android/settings/SdCardIntentReceiver.java b/src/com/android/settings/SdCardIntentReceiver.java new file mode 100644 index 000000000..9648ec113 --- /dev/null +++ b/src/com/android/settings/SdCardIntentReceiver.java @@ -0,0 +1,70 @@ +/* + * 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 android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.BroadcastReceiver; +import android.util.Config; +import android.util.Log; + +/** + * + */ +public class SdCardIntentReceiver extends BroadcastReceiver { + + private static final int SDCARD_STATUS = 1; + private static final String TAG = "SdCardIntentReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + NotificationManager nm = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + String action = intent.getAction(); + if (Config.LOGD) Log.d(TAG, "onReceiveIntent " + action); + + if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { + nm.cancel(SDCARD_STATUS); + + Intent statusIntent = new Intent(Intent.ACTION_MAIN, null); + statusIntent.setClass(context, SdCardSettings.class); + nm.notify(SDCARD_STATUS, new Notification(context, + android.R.drawable.stat_notify_sdcard, + null, + System.currentTimeMillis(), + context.getText(R.string.sdcard_setting), + null, + statusIntent)); + } else if (action.equals(Intent.ACTION_MEDIA_REMOVED)) { + nm.cancel(SDCARD_STATUS); + } else if (action.equals(Intent.ACTION_MEDIA_SHARED)) { + nm.cancel(SDCARD_STATUS); + + Intent statusIntent = new Intent(Intent.ACTION_MAIN, null); + statusIntent.setClass(context, SdCardSettings.class); + nm.notify(SDCARD_STATUS, new Notification(context, + android.R.drawable.stat_notify_sdcard_usb, + null, + System.currentTimeMillis(), + "SD Card", + null, + statusIntent)); + } + } +} diff --git a/src/com/android/settings/SdCardSettings.java b/src/com/android/settings/SdCardSettings.java new file mode 100644 index 000000000..b6935a2fe --- /dev/null +++ b/src/com/android/settings/SdCardSettings.java @@ -0,0 +1,232 @@ +/* + * 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 android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.Environment; +import android.os.IMountService; +import android.os.ServiceManager; +import android.os.StatFs; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; + +import java.io.File; + + +public class SdCardSettings extends Activity +{ + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.sdcard_settings_screen); + + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + + mRemovedLayout = findViewById(R.id.removed); + mMountedLayout = findViewById(R.id.mounted); + mUnmountedLayout = findViewById(R.id.unmounted); + mScanningLayout = findViewById(R.id.scanning); + mSharedLayout = findViewById(R.id.shared); + mBadRemovalLayout = findViewById(R.id.bad_removal); + mReadOnlyStatus = findViewById(R.id.read_only); + + mMassStorage = (CheckBox)findViewById(R.id.mass_storage); + mMassStorage.setOnClickListener(mMassStorageListener); + + Button unmountButton = (Button)findViewById(R.id.sdcard_unmount); + unmountButton.setOnClickListener(mUnmountButtonHandler); + + Button formatButton = (Button)findViewById(R.id.sdcard_format); + formatButton.setOnClickListener(mFormatButtonHandler); + + mTotalSize = (TextView)findViewById(R.id.total); + mUsedSize = (TextView)findViewById(R.id.used); + mAvailableSize = (TextView)findViewById(R.id.available); + + // install an intent filter to receive SD card related events. + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_REMOVED); + intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_SHARED); + intentFilter.addAction(Intent.ACTION_MEDIA_CHECKING); + intentFilter.addAction(Intent.ACTION_MEDIA_NOFS); + intentFilter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); + intentFilter.addDataScheme("file"); + registerReceiver(mReceiver, intentFilter); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + public void onResume() { + super.onResume(); + update(); + } + + private void setLayout(View layout) { + mRemovedLayout.setVisibility(layout == mRemovedLayout ? View.VISIBLE : View.GONE); + mMountedLayout.setVisibility(layout == mMountedLayout ? View.VISIBLE : View.GONE); + mUnmountedLayout.setVisibility(layout == mUnmountedLayout ? View.VISIBLE : View.GONE); + mScanningLayout.setVisibility(layout == mScanningLayout ? View.VISIBLE : View.GONE); + mSharedLayout.setVisibility(layout == mSharedLayout ? View.VISIBLE : View.GONE); + mBadRemovalLayout.setVisibility(layout == mBadRemovalLayout ? View.VISIBLE : View.GONE); + } + + private void update() { + try { + mMassStorage.setChecked(mMountService.getMassStorageEnabled()); + } catch (RemoteException ex) { + } + + String scanVolume = null; // this no longer exists: SystemProperties.get(MediaScanner.CURRENT_VOLUME_PROPERTY, ""); + boolean scanning = "external".equals(scanVolume); + + if (scanning) { + setLayout(mScanningLayout); + } else { + String status = Environment.getExternalStorageState(); + boolean readOnly = false; + + if (status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { + status = Environment.MEDIA_MOUNTED; + readOnly = true; + } + + if (status.equals(Environment.MEDIA_MOUNTED)) { + try { + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long totalBlocks = stat.getBlockCount(); + long availableBlocks = stat.getAvailableBlocks(); + + mTotalSize.setText(formatSize(totalBlocks * blockSize)); + mUsedSize.setText(formatSize((totalBlocks - availableBlocks) * blockSize)); + mAvailableSize.setText(formatSize(availableBlocks * blockSize)); + } catch (IllegalArgumentException e) { + // this can occur if the SD card is removed, but we haven't received the + // ACTION_MEDIA_REMOVED Intent yet. + status = Environment.MEDIA_REMOVED; + } + + mReadOnlyStatus.setVisibility(readOnly ? View.VISIBLE : View.GONE); + setLayout(mMountedLayout); + } else if (status.equals(Environment.MEDIA_UNMOUNTED)) { + setLayout(mUnmountedLayout); + } else if (status.equals(Environment.MEDIA_REMOVED)) { + setLayout(mRemovedLayout); + } else if (status.equals(Environment.MEDIA_SHARED)) { + setLayout(mSharedLayout); + } else if (status.equals(Environment.MEDIA_BAD_REMOVAL)) { + setLayout(mBadRemovalLayout); + } + } + } + + private String formatSize(long size) { + String suffix = null; + + // add K or M suffix if size is greater than 1K or 1M + if (size >= 1024) { + suffix = "K"; + size /= 1024; + if (size >= 1024) { + suffix = "M"; + size /= 1024; + } + } + + StringBuilder resultBuffer = new StringBuilder(Long.toString(size)); + + int commaOffset = resultBuffer.length() - 3; + while (commaOffset > 0) { + resultBuffer.insert(commaOffset, ','); + commaOffset -= 3; + } + + if (suffix != null) + resultBuffer.append(suffix); + return resultBuffer.toString(); + } + + OnClickListener mMassStorageListener = new OnClickListener() { + public void onClick(View v) { + try { + mMountService.setMassStorageEnabled(mMassStorage.isChecked()); + } catch (RemoteException ex) { + } + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + update(); + } + }; + + OnClickListener mUnmountButtonHandler = new OnClickListener() { + public void onClick(View v) { + try { + mMountService.unmountMedia(Environment.getExternalStorageDirectory().toString()); + } catch (RemoteException ex) { + } + } + }; + + OnClickListener mFormatButtonHandler = new OnClickListener() { + public void onClick(View v) { + try { + mMountService.formatMedia(Environment.getExternalStorageDirectory().toString()); + } catch (RemoteException ex) { + } + } + }; + + + private int mStatus; + private IMountService mMountService; + + private CheckBox mMassStorage; + + private TextView mTotalSize; + private TextView mUsedSize; + private TextView mAvailableSize; + + private View mRemovedLayout; + private View mMountedLayout; + private View mUnmountedLayout; + private View mScanningLayout; + private View mSharedLayout; + private View mBadRemovalLayout; + private View mReadOnlyStatus; +} diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java new file mode 100644 index 000000000..cd26492a9 --- /dev/null +++ b/src/com/android/settings/SecuritySettings.java @@ -0,0 +1,304 @@ +/* + * 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 android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.location.LocationManager; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.provider.Settings; +import android.util.Config; +import android.util.Log; + +import com.android.internal.widget.LockPatternUtils; + +/** + * Gesture lock pattern settings. + */ +public class SecuritySettings extends PreferenceActivity + implements SharedPreferences.OnSharedPreferenceChangeListener { + + // Lock Settings + + private static final String KEY_LOCK_ENABLED = "lockenabled"; + private static final String KEY_VISIBLE_PATTERN = "visiblepattern"; + private static final String KEY_TACTILE_FEEDBACK_ENABLED = "tactilefeedback"; + private static final int CONFIRM_PATTERN_THEN_DISABLE_REQUEST_CODE = 55; + private static final int CONFIRM_PATTERN_THEN_ENABLE_REQUEST_CODE = 56; + + private LockPatternUtils mLockPatternUtils; + private CheckBoxPreference mLockEnabled; + private CheckBoxPreference mVisiblePattern; + private CheckBoxPreference mTactileFeedback; + private Preference mChoosePattern; + + private CheckBoxPreference mShowPassword; + + // Location Settings + + private static final String LOCATION_NETWORK = "location_network"; + private static final String LOCATION_GPS = "location_gps"; + + private CheckBoxPreference mNetwork; + private CheckBoxPreference mGps; + private LocationManager mLocationManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.security_settings); + + mLockPatternUtils = new LockPatternUtils(getContentResolver()); + + createPreferenceHierarchy(); + + // Get the available location providers + mLocationManager = (LocationManager) + getSystemService(Context.LOCATION_SERVICE); + + mNetwork = (CheckBoxPreference) getPreferenceScreen().findPreference(LOCATION_NETWORK); + mGps = (CheckBoxPreference) getPreferenceScreen().findPreference(LOCATION_GPS); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + updateToggles(); + } + + private PreferenceScreen createPreferenceHierarchy() { + // Root + PreferenceScreen root = this.getPreferenceScreen(); + + // Inline preferences + PreferenceCategory inlinePrefCat = new PreferenceCategory(this); + inlinePrefCat.setTitle(R.string.lock_settings_title); + root.addPreference(inlinePrefCat); + + // autolock toggle + mLockEnabled = new LockEnabledPref(this); + mLockEnabled.setTitle(R.string.lockpattern_settings_enable_title); + mLockEnabled.setSummary(R.string.lockpattern_settings_enable_summary); + mLockEnabled.setKey(KEY_LOCK_ENABLED); + inlinePrefCat.addPreference(mLockEnabled); + + // visible pattern + mVisiblePattern = new CheckBoxPreference(this); + mVisiblePattern.setKey(KEY_VISIBLE_PATTERN); + mVisiblePattern.setTitle(R.string.lockpattern_settings_enable_visible_pattern_title); + inlinePrefCat.addPreference(mVisiblePattern); + + // tactile feedback + mTactileFeedback = new CheckBoxPreference(this); + mTactileFeedback.setKey(KEY_TACTILE_FEEDBACK_ENABLED); + mTactileFeedback.setTitle(R.string.lockpattern_settings_enable_tactile_feedback_title); + inlinePrefCat.addPreference(mTactileFeedback); + + // change pattern lock + Intent intent = new Intent(); + intent.setClassName("com.android.settings", + "com.android.settings.ChooseLockPatternTutorial"); + mChoosePattern = getPreferenceManager().createPreferenceScreen(this); + mChoosePattern.setIntent(intent); + inlinePrefCat.addPreference(mChoosePattern); + + PreferenceScreen simLockPreferences = getPreferenceManager() + .createPreferenceScreen(this); + simLockPreferences.setTitle(R.string.sim_lock_settings_category); + // Intent to launch SIM lock settings + intent = new Intent(); + intent.setClassName("com.android.settings", "com.android.settings.SimLockSettings"); + simLockPreferences.setIntent(intent); + + PreferenceCategory simLockCat = new PreferenceCategory(this); + simLockCat.setTitle(R.string.sim_lock_settings_title); + root.addPreference(simLockCat); + simLockCat.addPreference(simLockPreferences); + + // Passwords + PreferenceCategory passwordsCat = new PreferenceCategory(this); + passwordsCat.setTitle(R.string.security_passwords_title); + root.addPreference(passwordsCat); + + CheckBoxPreference showPassword = mShowPassword = new CheckBoxPreference(this); + showPassword.setKey("show_password"); + showPassword.setTitle(R.string.show_password); + showPassword.setSummary(R.string.show_password_summary); + showPassword.setPersistent(false); + passwordsCat.addPreference(showPassword); + + return root; + } + + @Override + protected void onResume() { + super.onResume(); + + boolean patternExists = mLockPatternUtils.savedPatternExists(); + mLockEnabled.setEnabled(patternExists); + mVisiblePattern.setEnabled(patternExists); + mTactileFeedback.setEnabled(patternExists); + + mLockEnabled.setChecked(mLockPatternUtils.isLockPatternEnabled()); + mVisiblePattern.setChecked(mLockPatternUtils.isVisiblePatternEnabled()); + mTactileFeedback.setChecked(mLockPatternUtils.isTactileFeedbackEnabled()); + + int chooseStringRes = mLockPatternUtils.savedPatternExists() ? + R.string.lockpattern_settings_change_lock_pattern : + R.string.lockpattern_settings_choose_lock_pattern; + mChoosePattern.setTitle(chooseStringRes); + + mShowPassword + .setChecked(Settings.System.getInt(getContentResolver(), + Settings.System.TEXT_SHOW_PASSWORD, 1) != 0); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + final String key = preference.getKey(); + + if (KEY_LOCK_ENABLED.equals(key)) { + mLockPatternUtils.setLockPatternEnabled(isToggled(preference)); + } else if (KEY_VISIBLE_PATTERN.equals(key)) { + mLockPatternUtils.setVisiblePatternEnabled(isToggled(preference)); + } else if (KEY_TACTILE_FEEDBACK_ENABLED.equals(key)) { + mLockPatternUtils.setTactileFeedbackEnabled(isToggled(preference)); + } else if (preference == mShowPassword) { + Settings.System.putInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, + mShowPassword.isChecked() ? 1 : 0); + } + + return false; + } + + /* + * Creates toggles for each available location provider + */ + private void updateToggles() { + String providers = getAllowedProviders(); + mNetwork.setChecked(providers.contains(LocationManager.NETWORK_PROVIDER)); + mGps.setChecked(providers.contains(LocationManager.GPS_PROVIDER)); + } + + private void updateProviders() { + String preferredProviders = ""; + if (mNetwork.isChecked()) { + preferredProviders += LocationManager.NETWORK_PROVIDER; + } + if (mGps.isChecked()) { + preferredProviders += "," + LocationManager.GPS_PROVIDER; + } + setProviders(preferredProviders); + } + + private void setProviders(String providers) { + // Update the secure setting LOCATION_PROVIDERS_ALLOWED + Settings.Secure.putString(getContentResolver(), + Settings.Secure.LOCATION_PROVIDERS_ALLOWED, providers); + if (Config.LOGV) { + Log.v("Location Accuracy", "Setting LOCATION_PROVIDERS_ALLOWED = " + providers); + } + // Inform the location manager about the changes + mLocationManager.updateProviders(); + } + + /** + * @return string containing a list of providers that have been enabled for use + */ + private String getAllowedProviders() { + String allowedProviders = + Settings.Secure.getString(getContentResolver(), + Settings.Secure.LOCATION_PROVIDERS_ALLOWED); + if (allowedProviders == null) { + allowedProviders = ""; + } + return allowedProviders; + } + + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (LOCATION_NETWORK.equals(key) || LOCATION_GPS.equals(key)) { + updateProviders(); + } + } + + private boolean isToggled(Preference pref) { + return ((CheckBoxPreference) pref).isChecked(); + } + + + /** + * For the user to disable keyguard, we first make them verify their + * existing pattern. + */ + private class LockEnabledPref extends CheckBoxPreference { + + public LockEnabledPref(Context context) { + super(context); + } + + @Override + protected void onClick() { + if (mLockPatternUtils.savedPatternExists()) { + if (isChecked()) { + confirmPatternThenDisable(); + } else { + confirmPatternThenEnable(); + } + } else { + super.onClick(); + } + } + } + + private void confirmPatternThenEnable() { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", "com.android.settings.ConfirmLockPattern"); + startActivityForResult(intent, CONFIRM_PATTERN_THEN_ENABLE_REQUEST_CODE); + } + + /** + * Launch screen to confirm the existing lock pattern. + * @see #onActivityResult(int, int, android.content.Intent) + */ + private void confirmPatternThenDisable() { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", "com.android.settings.ConfirmLockPattern"); + startActivityForResult(intent, CONFIRM_PATTERN_THEN_DISABLE_REQUEST_CODE); + } + + /** + * @see #confirmPatternThenDisable + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + final boolean resultOk = resultCode == Activity.RESULT_OK; + + if ((requestCode == CONFIRM_PATTERN_THEN_DISABLE_REQUEST_CODE) && resultOk) { + mLockPatternUtils.setLockPatternEnabled(false); + } else if ((requestCode == CONFIRM_PATTERN_THEN_ENABLE_REQUEST_CODE) && resultOk) { + mLockPatternUtils.setLockPatternEnabled(true); + } + } +} diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java new file mode 100644 index 000000000..0c4545eef --- /dev/null +++ b/src/com/android/settings/Settings.java @@ -0,0 +1,46 @@ +/* + * 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 android.os.Bundle; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.provider.Settings.System; + +public class Settings extends PreferenceActivity { + + private static final String KEY_PARENT = "parent"; + private static final String KEY_CALL_SETTINGS = "call_settings"; + private static final String KEY_SYNC_SETTINGS = "sync_settings"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.settings); + + PreferenceGroup parent = (PreferenceGroup) findPreference(KEY_PARENT); + Utils.updatePreferenceToSpecificActivityOrRemove(this, parent, KEY_SYNC_SETTINGS, 0); + } + + @Override + protected void onResume() { + super.onResume(); + findPreference(KEY_CALL_SETTINGS).setEnabled(!AirplaneModeEnabler.isAirplaneModeOn(this)); + } + +} diff --git a/src/com/android/settings/SettingsLicenseActivity.java b/src/com/android/settings/SettingsLicenseActivity.java new file mode 100644 index 000000000..0b809e19a --- /dev/null +++ b/src/com/android/settings/SettingsLicenseActivity.java @@ -0,0 +1,123 @@ +/* + * 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 android.os.Bundle; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Toast; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.zip.GZIPInputStream; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; + +/** + * The "dialog" that shows from "License" in the Settings app. + */ +public class SettingsLicenseActivity extends AlertActivity { + + private static final String TAG = "SettingsLicenseActivity"; + private static final boolean LOGV = false || Config.LOGV; + + private static final String DEFAULT_LICENSE_PATH = "/system/etc/NOTICE.html.gz"; + private static final String PROPERTY_LICENSE_PATH = "ro.config.license_path"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String fileName = SystemProperties.get(PROPERTY_LICENSE_PATH, DEFAULT_LICENSE_PATH); + if (TextUtils.isEmpty(fileName)) { + Log.e(TAG, "The system property for the license file is empty."); + showErrorAndFinish(); + return; + } + + InputStreamReader inputReader = null; + StringBuilder data = null; + try { + data = new StringBuilder(2048); + char tmp[] = new char[2048]; + int numRead; + if (fileName.endsWith(".gz")) { + inputReader = new InputStreamReader( + new GZIPInputStream(new FileInputStream(fileName))); + } else { + inputReader = new FileReader(fileName); + } + while ((numRead = inputReader.read(tmp)) >= 0) { + data.append(tmp, 0, numRead); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "License HTML file not found at " + fileName, e); + showErrorAndFinish(); + return; + } catch (IOException e) { + Log.e(TAG, "Error reading license HTML file at " + fileName, e); + showErrorAndFinish(); + return; + } finally { + try { + if (inputReader != null) { + inputReader.close(); + } + } catch (IOException e) { + } + } + + if (TextUtils.isEmpty(data)) { + Log.e(TAG, "License HTML is empty (from " + fileName + ")"); + showErrorAndFinish(); + return; + } + + WebView webView = new WebView(this); + + // Begin the loading. This will be done in a separate thread in WebView. + webView.loadDataWithBaseURL(null, data.toString(), "text/html", "utf-8", null); + webView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + // Change from 'Loading...' to the real title + mAlert.setTitle(getString(R.string.settings_license_activity_title)); + } + }); + + final AlertController.AlertParams p = mAlertParams; + p.mTitle = getString(R.string.settings_license_activity_loading); + p.mView = webView; + p.mForceInverseBackground = true; + setupAlert(); + } + + private void showErrorAndFinish() { + Toast.makeText(this, R.string.settings_license_activity_unavailable, Toast.LENGTH_LONG) + .show(); + finish(); + } + +} diff --git a/src/com/android/settings/SimLockSettings.java b/src/com/android/settings/SimLockSettings.java new file mode 100644 index 000000000..286e3d6f1 --- /dev/null +++ b/src/com/android/settings/SimLockSettings.java @@ -0,0 +1,321 @@ +/* + * 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 android.content.Context; +import android.content.res.Resources; +import android.os.AsyncResult; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.CheckBoxPreference; +import android.preference.PreferenceScreen; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneFactory; +import android.widget.Toast; + +/** + * Implements the preference screen to enable/disable SIM lock and + * also the dialogs to change the SIM PIN. In the former case, enabling/disabling + * the SIM lock will prompt the user for the current PIN. + * In the Change PIN case, it prompts the user for old pin, new pin and new pin + * again before attempting to change it. Calls the SimCard interface to execute + * these operations. + * + */ +public class SimLockSettings extends PreferenceActivity + implements EditPinPreference.OnPinEnteredListener { + + private static final int OFF_MODE = 0; + // State when enabling/disabling SIM lock + private static final int SIM_LOCK_MODE = 1; + // State when entering the old pin + private static final int SIM_OLD_MODE = 2; + // State when entering the new pin - first time + private static final int SIM_NEW_MODE = 3; + // State when entering the new pin - second time + private static final int SIM_REENTER_MODE = 4; + + // Keys in xml file + private static final String PIN_DIALOG = "sim_pin"; + private static final String PIN_TOGGLE = "sim_toggle"; + // Keys in icicle + private static final String DIALOG_STATE = "dialogState"; + private static final String DIALOG_PIN = "dialogPin"; + private static final String DIALOG_ERROR = "dialogError"; + private static final String ENABLE_TO_STATE = "enableState"; + + private static final int MIN_PIN_LENGTH = 4; + private static final int MAX_PIN_LENGTH = 8; + // Which dialog to show next when popped up + private int mDialogState = OFF_MODE; + + private String mPin; + private String mOldPin; + private String mNewPin; + private String mError; + // Are we trying to enable or disable SIM lock? + private boolean mToState; + + private Phone mPhone; + + private EditPinPreference mPinDialog; + private CheckBoxPreference mPinToggle; + + private Resources mRes; + + // For async handler to identify request type + private static final int ENABLE_SIM_PIN_COMPLETE = 100; + private static final int CHANGE_SIM_PIN_COMPLETE = 101; + + // For replies from SimCard interface + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + AsyncResult ar = (AsyncResult) msg.obj; + switch (msg.what) { + case ENABLE_SIM_PIN_COMPLETE: + simLockChanged(ar.exception == null); + break; + case CHANGE_SIM_PIN_COMPLETE: + simPinChanged(ar.exception == null); + break; + } + + return; + } + }; + + // For top-level settings screen to query + static boolean isSimLockEnabled() { + return PhoneFactory.getDefaultPhone().getSimCard().getSimLockEnabled(); + } + + static String getSummary(Context context) { + Resources res = context.getResources(); + String summary = isSimLockEnabled() + ? res.getString(R.string.sim_lock_on) + : res.getString(R.string.sim_lock_off); + return summary; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.sim_lock_settings); + + mPinDialog = (EditPinPreference) findPreference(PIN_DIALOG); + mPinToggle = (CheckBoxPreference) findPreference(PIN_TOGGLE); + if (savedInstanceState != null && savedInstanceState.containsKey(DIALOG_STATE)) { + mDialogState = savedInstanceState.getInt(DIALOG_STATE); + mPin = savedInstanceState.getString(DIALOG_PIN); + mError = savedInstanceState.getString(DIALOG_ERROR); + mToState = savedInstanceState.getBoolean(ENABLE_TO_STATE); + } + + mPinDialog.setOnPinEnteredListener(this); + + // Don't need any changes to be remembered + getPreferenceScreen().setPersistent(false); + + mPhone = PhoneFactory.getDefaultPhone(); + mRes = getResources(); + } + + @Override + protected void onResume() { + super.onResume(); + + mPinToggle.setChecked(mPhone.getSimCard().getSimLockEnabled()); + + if (mDialogState != OFF_MODE) { + showPinDialog(); + } else { + // Prep for standard click on "Change PIN" + resetDialogState(); + } + } + + @Override + protected void onSaveInstanceState(Bundle out) { + // Need to store this state for slider open/close + // There is one case where the dialog is popped up by the preference + // framework. In that case, let the preference framework store the + // dialog state. In other cases, where this activity manually launches + // the dialog, store the state of the dialog. + if (mPinDialog.isDialogOpen()) { + out.putInt(DIALOG_STATE, mDialogState); + out.putString(DIALOG_PIN, mPinDialog.getEditText().getText().toString()); + out.putString(DIALOG_ERROR, mError); + out.putBoolean(ENABLE_TO_STATE, mToState); + } else { + super.onSaveInstanceState(out); + } + } + + private void showPinDialog() { + if (mDialogState == OFF_MODE) { + return; + } + setDialogValues(); + + mPinDialog.showPinDialog(); + } + + private void setDialogValues() { + mPinDialog.setText(mPin); + String message = ""; + switch (mDialogState) { + case SIM_LOCK_MODE: + message = mRes.getString(R.string.sim_enter_pin); + mPinDialog.setDialogTitle(mToState + ? mRes.getString(R.string.sim_enable_sim_lock) + : mRes.getString(R.string.sim_disable_sim_lock)); + break; + case SIM_OLD_MODE: + message = mRes.getString(R.string.sim_enter_old); + mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); + break; + case SIM_NEW_MODE: + message = mRes.getString(R.string.sim_enter_new); + mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); + break; + case SIM_REENTER_MODE: + message = mRes.getString(R.string.sim_reenter_new); + mPinDialog.setDialogTitle(mRes.getString(R.string.sim_change_pin)); + break; + } + if (mError != null) { + message = mError + "\n" + message; + mError = null; + } + mPinDialog.setDialogMessage(message); + } + + public void onPinEntered(EditPinPreference preference, boolean positiveResult) { + if (!positiveResult) { + resetDialogState(); + return; + } + + mPin = preference.getText(); + if (!reasonablePin(mPin)) { + // inject error message and display dialog again + mError = mRes.getString(R.string.sim_bad_pin); + showPinDialog(); + return; + } + switch (mDialogState) { + case SIM_LOCK_MODE: + tryChangeSimLockState(); + break; + case SIM_OLD_MODE: + mOldPin = mPin; + mDialogState = SIM_NEW_MODE; + mError = null; + mPin = null; + showPinDialog(); + break; + case SIM_NEW_MODE: + mNewPin = mPin; + mDialogState = SIM_REENTER_MODE; + mPin = null; + showPinDialog(); + break; + case SIM_REENTER_MODE: + if (!mPin.equals(mNewPin)) { + mError = mRes.getString(R.string.sim_pins_dont_match); + mDialogState = SIM_NEW_MODE; + mPin = null; + showPinDialog(); + } else { + mError = null; + tryChangePin(); + } + break; + } + } + + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference == mPinToggle) { + // Get the new, preferred state + mToState = mPinToggle.isChecked(); + // Flip it back and pop up pin dialog + mPinToggle.setChecked(!mToState); + mDialogState = SIM_LOCK_MODE; + showPinDialog(); + } + return true; + } + + private void tryChangeSimLockState() { + // Try to change sim lock. If it succeeds, toggle the lock state and + // reset dialog state. Else inject error message and show dialog again. + Message callback = Message.obtain(mHandler, ENABLE_SIM_PIN_COMPLETE); + mPhone.getSimCard().setSimLockEnabled(mToState, mPin, callback); + + } + + private void simLockChanged(boolean success) { + if (success) { + mPinToggle.setChecked(mToState); + } else { + // TODO: I18N + Toast.makeText(this, mRes.getString(R.string.sim_lock_failed), Toast.LENGTH_SHORT) + .show(); + } + resetDialogState(); + } + + private void simPinChanged(boolean success) { + if (!success) { + // TODO: I18N + Toast.makeText(this, mRes.getString(R.string.sim_change_failed), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(this, mRes.getString(R.string.sim_change_succeeded), + Toast.LENGTH_SHORT) + .show(); + + } + resetDialogState(); + } + + private void tryChangePin() { + Message callback = Message.obtain(mHandler, CHANGE_SIM_PIN_COMPLETE); + mPhone.getSimCard().changeSimLockPassword(mOldPin, + mNewPin, callback); + } + + private boolean reasonablePin(String pin) { + if (pin == null || pin.length() < MIN_PIN_LENGTH || pin.length() > MAX_PIN_LENGTH) { + return false; + } else { + return true; + } + } + + private void resetDialogState() { + mError = null; + mDialogState = SIM_OLD_MODE; // Default for when Change PIN is clicked + mPin = ""; + setDialogValues(); + } +} diff --git a/src/com/android/settings/SoundAndDisplaySettings.java b/src/com/android/settings/SoundAndDisplaySettings.java new file mode 100644 index 000000000..53912e348 --- /dev/null +++ b/src/com/android/settings/SoundAndDisplaySettings.java @@ -0,0 +1,245 @@ +/* + * 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 static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; + +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.os.Bundle; +import android.os.IMountService; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.preference.CheckBoxPreference; +import android.provider.Settings; +import android.util.Log; +import android.view.IWindowManager; + +public class SoundAndDisplaySettings extends PreferenceActivity implements + Preference.OnPreferenceChangeListener { + private static final String TAG = "SoundAndDisplaysSettings"; + + /** If there is no setting in the provider, use this. */ + private static final int FALLBACK_SCREEN_TIMEOUT_VALUE = 30000; + + private static final String KEY_SILENT = "silent"; + private static final String KEY_VIBRATE = "vibrate"; + private static final String KEY_SCREEN_TIMEOUT = "screen_timeout"; + private static final String KEY_DTMF_TONE = "dtmf_tone"; + private static final String KEY_SOUND_EFFECTS = "sound_effects"; + private static final String KEY_ANIMATIONS = "animations"; + private static final String KEY_PLAY_MEDIA_NOTIFICATION_SOUNDS = "play_media_notification_sounds"; + + private CheckBoxPreference mSilent; + + private CheckBoxPreference mPlayMediaNotificationSounds; + + private IMountService mMountService = null; + + /* + * If we are currently in one of the silent modes (the ringer mode is set to either + * "silent mode" or "vibrate mode"), then toggling the "Phone vibrate" + * preference will switch between "silent mode" and "vibrate mode". + * Otherwise, it will adjust the normal ringer mode's ring or ring+vibrate + * setting. + */ + private CheckBoxPreference mVibrate; + private CheckBoxPreference mDtmfTone; + private CheckBoxPreference mSoundEffects; + private CheckBoxPreference mAnimations; + private float[] mAnimationScales; + + private AudioManager mAudioManager; + + private IWindowManager mWindowManager; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateState(false); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ContentResolver resolver = getContentResolver(); + + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mWindowManager = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + + addPreferencesFromResource(R.xml.sound_and_display_settings); + + mSilent = (CheckBoxPreference) findPreference(KEY_SILENT); + mPlayMediaNotificationSounds = (CheckBoxPreference) findPreference(KEY_PLAY_MEDIA_NOTIFICATION_SOUNDS); + + mVibrate = (CheckBoxPreference) findPreference(KEY_VIBRATE); + 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, 0) != 0); + mAnimations = (CheckBoxPreference) findPreference(KEY_ANIMATIONS); + mAnimations.setPersistent(false); + + ListPreference screenTimeoutPreference = + (ListPreference) findPreference(KEY_SCREEN_TIMEOUT); + screenTimeoutPreference.setValue(String.valueOf(Settings.System.getInt( + resolver, SCREEN_OFF_TIMEOUT, FALLBACK_SCREEN_TIMEOUT_VALUE))); + screenTimeoutPreference.setOnPreferenceChangeListener(this); + } + + @Override + protected void onResume() { + super.onResume(); + + updateState(true); + + IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); + registerReceiver(mReceiver, filter); + } + + @Override + protected void onPause() { + super.onPause(); + + unregisterReceiver(mReceiver); + } + + private void updateState(boolean force) { + final int ringerMode = mAudioManager.getRingerMode(); + final boolean silentOrVibrateMode = + ringerMode != AudioManager.RINGER_MODE_NORMAL; + + if (silentOrVibrateMode != mSilent.isChecked() || force) { + mSilent.setChecked(silentOrVibrateMode); + } + + try { + mPlayMediaNotificationSounds.setChecked(mMountService.getPlayNotificationSounds()); + } catch (RemoteException e) { + } + + boolean vibrateSetting; + if (silentOrVibrateMode) { + vibrateSetting = ringerMode == AudioManager.RINGER_MODE_VIBRATE; + } else { + vibrateSetting = mAudioManager.getVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER) + == AudioManager.VIBRATE_SETTING_ON; + } + if (vibrateSetting != mVibrate.isChecked() || force) { + mVibrate.setChecked(vibrateSetting); + } + + boolean animations = true; + try { + mAnimationScales = mWindowManager.getAnimationScales(); + } catch (RemoteException e) { + } + if (mAnimationScales != null) { + for (int i=0; i<mAnimationScales.length; i++) { + if (mAnimationScales[i] == 0) { + animations = false; + break; + } + } + } + if (animations != mAnimations.isChecked() || force) { + mAnimations.setChecked(animations); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + + if (preference == mSilent) { + final boolean silent = mSilent.isChecked(); + mAudioManager.setRingerMode(silent ? AudioManager.RINGER_MODE_SILENT + : AudioManager.RINGER_MODE_NORMAL); + updateState(false); + + } else if (preference == mPlayMediaNotificationSounds) { + try { + mMountService.setPlayNotificationSounds(mPlayMediaNotificationSounds.isChecked()); + } catch (RemoteException e) { + } + } else if (preference == mVibrate) { + final boolean vibrate = mVibrate.isChecked(); + final boolean silent = mSilent.isChecked(); + + if (silent) { + mAudioManager.setRingerMode(vibrate ? AudioManager.RINGER_MODE_VIBRATE : + AudioManager.RINGER_MODE_SILENT); + } else { + mAudioManager.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER, + vibrate ? AudioManager.VIBRATE_SETTING_ON + : AudioManager.VIBRATE_SETTING_OFF); + } + + } 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 == mAnimations) { + for (int i=0; i<mAnimationScales.length; i++) { + mAnimationScales[i] = mAnimations.isChecked() ? 1 : 0; + } + try { + mWindowManager.setAnimationScales(mAnimationScales); + } catch (RemoteException e) { + } + } + return true; + } + + public boolean onPreferenceChange(Preference preference, Object objValue) { + if (KEY_SCREEN_TIMEOUT.equals(preference.getKey())) { + int value = Integer.parseInt((String) objValue); + try { + Settings.System.putInt(getContentResolver(), + SCREEN_OFF_TIMEOUT, value); + } catch (NumberFormatException e) { + Log.e(TAG, "could not persist screen timeout setting", e); + } + } + + return true; + } + +} diff --git a/src/com/android/settings/TestingSettings.java b/src/com/android/settings/TestingSettings.java new file mode 100644 index 000000000..39945603a --- /dev/null +++ b/src/com/android/settings/TestingSettings.java @@ -0,0 +1,31 @@ +/* + * 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 android.os.Bundle; +import android.preference.PreferenceActivity; + +public class TestingSettings extends PreferenceActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.testing_settings); + } + +} diff --git a/src/com/android/settings/TestingSettingsBroadcastReceiver.java b/src/com/android/settings/TestingSettingsBroadcastReceiver.java new file mode 100644 index 000000000..c6cd7e1ac --- /dev/null +++ b/src/com/android/settings/TestingSettingsBroadcastReceiver.java @@ -0,0 +1,28 @@ +package com.android.settings; + +import android.provider.Telephony; +import static android.provider.Telephony.Intents.SECRET_CODE_ACTION; + +import android.content.Context; +import android.content.Intent; +import android.content.BroadcastReceiver; +import android.util.Config; +import android.util.Log; +import android.view.KeyEvent; + + +public class TestingSettingsBroadcastReceiver extends BroadcastReceiver { + + public TestingSettingsBroadcastReceiver() { + } + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(SECRET_CODE_ACTION)) { + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClass(context, TestingSettings.class); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + } + } +} diff --git a/src/com/android/settings/UsageStats.java b/src/com/android/settings/UsageStats.java new file mode 100755 index 000000000..89caa5485 --- /dev/null +++ b/src/com/android/settings/UsageStats.java @@ -0,0 +1,249 @@ + + +/** + * 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.internal.app.IUsageStats; +import com.android.settings.R; +import android.app.Activity; +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.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.AdapterView.OnItemSelectedListener; + +/** + * Activity to display package usage statistics. + */ +public class UsageStats extends Activity implements OnItemSelectedListener { + private static final String TAG="UsageStatsActivity"; + private static final boolean localLOGV = true; + private Spinner mTypeSpinner; + private ListView mListView; + private IUsageStats mUsageStatsService; + 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) { + mAppLabelList = appList; + } + public final int compare(PkgUsageStats a, PkgUsageStats b) { + String alabel = mAppLabelList.get(a.packageName).toString(); + String blabel = mAppLabelList.get(b.packageName).toString(); + return alabel.compareTo(blabel); + } + } + + public static class LaunchCountComparator implements Comparator<PkgUsageStats> { + public final int compare(PkgUsageStats a, PkgUsageStats b) { + // return by descending order + return b.launchCount - a.launchCount; + } + } + + 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; + } + } + + // View Holder used when displaying views + static class AppViewHolder { + TextView pkgName; + TextView launchCount; + 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_APP_NAME = 2; + + private int mDisplayOrder = _DISPLAY_ORDER_USAGE_TIME; + private List<PkgUsageStats> mUsageStats; + private LaunchCountComparator mLaunchCountComparator; + private UsageTimeComparator mUsageTimeComparator; + private AppNameComparator mAppLabelComparator; + private HashMap<String, CharSequence> mAppLabelMap; + + 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"); + 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); + } catch (NameNotFoundException e) { + label = ps.packageName; + } + mAppLabelMap.put(ps.packageName, label); + } + // Sort list + mLaunchCountComparator = new LaunchCountComparator(); + mUsageTimeComparator = new UsageTimeComparator(); + mAppLabelComparator = new AppNameComparator(mAppLabelMap); + sortList(); + } + public int getCount() { + return mUsageStats.size(); + } + + public Object getItem(int position) { + return mUsageStats.get(position); + } + + public long getItemId(int position) { + return position; + } + + 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. + AppViewHolder holder; + + // When convertView is not null, we can reuse it directly, there is no need + // to reinflate it. We only inflate a new View when the convertView supplied + // by ListView is null. + if (convertView == null) { + convertView = mInflater.inflate(R.layout.usage_stats_item, null); + + // Creates a ViewHolder and store references to the two children views + // 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.usageTime = (TextView) convertView.findViewById(R.id.usage_time); + convertView.setTag(holder); + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + holder = (AppViewHolder) convertView.getTag(); + } + + // Bind the data efficiently with the holder + PkgUsageStats pkgStats = mUsageStats.get(position); + if (pkgStats != null) { + CharSequence label = mAppLabelMap.get(pkgStats.packageName); + holder.pkgName.setText(label); + holder.launchCount.setText(String.valueOf(pkgStats.launchCount)); + holder.usageTime.setText(String.valueOf(pkgStats.usageTime)+" ms"); + } else { + Log.w(TAG, "No usage stats info for package:"+pkgStats.packageName); + } + return convertView; + } + + void sortList(int sortOrder) { + if (mDisplayOrder == sortOrder) { + // do nothing + return; + } + mDisplayOrder= sortOrder; + sortList(); + } + 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); + } else if (mDisplayOrder == _DISPLAY_ORDER_APP_NAME) { + if (localLOGV) Log.i(TAG, "Sorting by application name"); + Collections.sort(mUsageStats, mAppLabelComparator); + } + notifyDataSetChanged(); + } + } + + /** Called when the activity is first created. */ + 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; + } + 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 + + mAdapter = new UsageStatsAdapter(); + mListView.setAdapter(mAdapter); + } + + public void onItemSelected(AdapterView<?> parent, View view, int position, + long id) { + mAdapter.sortList(position); + } + + public void onNothingSelected(AdapterView<?> parent) { + // do nothing + } +} + diff --git a/src/com/android/settings/UserDictionarySettings.java b/src/com/android/settings/UserDictionarySettings.java new file mode 100644 index 000000000..aeddcf7a9 --- /dev/null +++ b/src/com/android/settings/UserDictionarySettings.java @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.android.settings; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.UserDictionary; +import android.text.InputType; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AlphabetIndexer; +import android.widget.EditText; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.SectionIndexer; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import android.widget.AdapterView.AdapterContextMenuInfo; + +import java.util.Locale; + +public class UserDictionarySettings extends ListActivity { + + private static final String INSTANCE_KEY_DIALOG_EDITING_WORD = "DIALOG_EDITING_WORD"; + private static final String INSTANCE_KEY_ADDED_WORD = "DIALOG_ADDED_WORD"; + + private static final String[] QUERY_PROJECTION = { + UserDictionary.Words._ID, UserDictionary.Words.WORD + }; + + // Either the locale is empty (means the word is applicable to all locales) + // or the word equals our current locale + private static final String QUERY_SELECTION = UserDictionary.Words.LOCALE + "=? OR " + + UserDictionary.Words.LOCALE + " is null"; + + private static final String DELETE_SELECTION = UserDictionary.Words.WORD + "=?"; + + private static final String EXTRA_WORD = "word"; + + private static final int CONTEXT_MENU_EDIT = Menu.FIRST; + private static final int CONTEXT_MENU_DELETE = Menu.FIRST + 1; + + private static final int OPTIONS_MENU_ADD = Menu.FIRST; + + private static final int DIALOG_ADD_OR_EDIT = 0; + + /** The word being edited in the dialog (null means the user is adding a word). */ + private String mDialogEditingWord; + + private Cursor mCursor; + + private boolean mAddedWordAlready; + private boolean mAutoReturn; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.list_content_with_empty_view); + + mCursor = createCursor(); + setListAdapter(createAdapter()); + + TextView emptyView = (TextView) findViewById(R.id.empty); + emptyView.setText(R.string.user_dict_settings_empty_text); + + ListView listView = getListView(); + listView.setFastScrollEnabled(true); + listView.setEmptyView(emptyView); + + registerForContextMenu(listView); + } + + @Override + protected void onResume() { + super.onResume(); + if (!mAddedWordAlready + && getIntent().getAction().equals("com.android.settings.USER_DICTIONARY_INSERT")) { + String word = getIntent().getStringExtra(EXTRA_WORD); + mAutoReturn = true; + if (word != null) { + showAddOrEditDialog(word); + } + } + } + @Override + protected void onRestoreInstanceState(Bundle state) { + super.onRestoreInstanceState(state); + mDialogEditingWord = state.getString(INSTANCE_KEY_DIALOG_EDITING_WORD); + mAddedWordAlready = state.getBoolean(INSTANCE_KEY_ADDED_WORD, false); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(INSTANCE_KEY_DIALOG_EDITING_WORD, mDialogEditingWord); + outState.putBoolean(INSTANCE_KEY_ADDED_WORD, mAddedWordAlready); + } + + private Cursor createCursor() { + String currentLocale = Locale.getDefault().toString(); + // Case-insensitive sort + return managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, + QUERY_SELECTION, new String[] { currentLocale }, + "UPPER(" + UserDictionary.Words.WORD + ")"); + } + + private ListAdapter createAdapter() { + return new MyAdapter(this, + android.R.layout.simple_list_item_1, mCursor, + new String[] { UserDictionary.Words.WORD }, + new int[] { android.R.id.text1 }); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + showAddOrEditDialog(getWord(position)); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (!(menuInfo instanceof AdapterContextMenuInfo)) return; + + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + menu.setHeaderTitle(getWord(adapterMenuInfo.position)); + menu.add(0, CONTEXT_MENU_EDIT, 0, R.string.user_dict_settings_context_menu_edit_title); + menu.add(0, CONTEXT_MENU_DELETE, 0, R.string.user_dict_settings_context_menu_delete_title); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + ContextMenuInfo menuInfo = item.getMenuInfo(); + if (!(menuInfo instanceof AdapterContextMenuInfo)) return false; + + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + String word = getWord(adapterMenuInfo.position); + + switch (item.getItemId()) { + case CONTEXT_MENU_DELETE: + deleteWord(word); + return true; + + case CONTEXT_MENU_EDIT: + showAddOrEditDialog(word); + return true; + } + + return false; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) + .setIcon(R.drawable.ic_menu_add); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + showAddOrEditDialog(null); + return true; + } + + private void showAddOrEditDialog(String editingWord) { + mDialogEditingWord = editingWord; + showDialog(DIALOG_ADD_OR_EDIT); + } + + private String getWord(int position) { + mCursor.moveToPosition(position); + return mCursor.getString( + mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD)); + } + + @Override + protected Dialog onCreateDialog(int id) { + View content = getLayoutInflater().inflate(R.layout.dialog_edittext, null); + final EditText editText = (EditText) content.findViewById(R.id.edittext); + // No prediction in soft keyboard mode. TODO: Create a better way to disable prediction + editText.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); + + return new AlertDialog.Builder(this) + .setTitle(R.string.user_dict_settings_add_dialog_title) + .setView(content) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + onAddOrEditFinished(editText.getText().toString()); + if (mAutoReturn) finish(); + }}) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (mAutoReturn) finish(); + }}) + .create(); + } + + @Override + protected void onPrepareDialog(int id, Dialog d) { + AlertDialog dialog = (AlertDialog) d; + EditText editText = (EditText) dialog.findViewById(R.id.edittext); + editText.setText(mDialogEditingWord); + } + + private void onAddOrEditFinished(String word) { + if (mDialogEditingWord != null) { + // The user was editing a word, so do a delete/add + deleteWord(mDialogEditingWord); + } + + // Disallow duplicates + deleteWord(word); + + // TODO: present UI for picking whether to add word to all locales, or current. + UserDictionary.Words.addWord(this, word.toString(), + 250, UserDictionary.Words.LOCALE_TYPE_ALL); + mCursor.requery(); + mAddedWordAlready = true; + } + + private void deleteWord(String word) { + getContentResolver().delete(UserDictionary.Words.CONTENT_URI, DELETE_SELECTION, + new String[] { word }); + } + + private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer { + private AlphabetIndexer mIndexer; + + public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { + super(context, layout, c, from, to); + + int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); + String alphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet); + mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); + } + + public int getPositionForSection(int section) { + return mIndexer.getPositionForSection(section); + } + + public int getSectionForPosition(int position) { + return mIndexer.getSectionForPosition(position); + } + + public Object[] getSections() { + return mIndexer.getSections(); + } + } +} diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java new file mode 100644 index 000000000..a23272b81 --- /dev/null +++ b/src/com/android/settings/Utils.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.android.settings; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.preference.Preference; +import android.preference.PreferenceGroup; + +import java.util.List; + +public class Utils { + + /** + * Set the preference's title to the matching activity's label. + */ + public static final int UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY = 1; + + /** + * Finds a matching activity for a preference's intent. If a matching + * activity is not found, it will remove the preference. + * + * @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. + * @param flags 0 or one or more of + * {@link #UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY} + * . + * @return Whether an activity was found. If false, the preference was + * removed. + */ + public static boolean updatePreferenceToSpecificActivityOrRemove(Context context, + PreferenceGroup parentPreferenceGroup, String preferenceKey, int flags) { + + Preference preference = parentPreferenceGroup.findPreference(preferenceKey); + if (preference == null) { + return false; + } + + 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, 0); + 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) { + + // Replace the intent with this specific activity + preference.setIntent(new Intent().setClassName( + resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name)); + + if ((flags & UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY) != 0) { + // Set the preference title to the activity's label + preference.setTitle(resolveInfo.loadLabel(pm)); + } + + return true; + } + } + } + + // Did not find a matching activity, so remove the preference + parentPreferenceGroup.removePreference(preference); + + return true; + } + +} diff --git a/src/com/android/settings/WirelessSettings.java b/src/com/android/settings/WirelessSettings.java new file mode 100644 index 000000000..d1129150b --- /dev/null +++ b/src/com/android/settings/WirelessSettings.java @@ -0,0 +1,80 @@ +/* + * 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.BluetoothEnabler; +import com.android.settings.wifi.WifiEnabler; + +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.preference.CheckBoxPreference; + +public class WirelessSettings extends PreferenceActivity { + + private static final String KEY_TOGGLE_AIRPLANE = "toggle_airplane"; + private static final String KEY_TOGGLE_BLUETOOTH = "toggle_bluetooth"; + private static final String KEY_TOGGLE_WIFI = "toggle_wifi"; + + private WifiEnabler mWifiEnabler; + private AirplaneModeEnabler mAirplaneModeEnabler; + private BluetoothEnabler mBtEnabler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.wireless_settings); + + initToggles(); + } + + @Override + protected void onResume() { + super.onResume(); + + mWifiEnabler.resume(); + mAirplaneModeEnabler.resume(); + mBtEnabler.resume(); + } + + @Override + protected void onPause() { + super.onPause(); + + mWifiEnabler.pause(); + mAirplaneModeEnabler.pause(); + mBtEnabler.pause(); + } + + private void initToggles() { + + mWifiEnabler = new WifiEnabler( + this, + (WifiManager) getSystemService(WIFI_SERVICE), + (CheckBoxPreference) findPreference(KEY_TOGGLE_WIFI)); + + mAirplaneModeEnabler = new AirplaneModeEnabler( + this, + (CheckBoxPreference) findPreference(KEY_TOGGLE_AIRPLANE)); + + mBtEnabler = new BluetoothEnabler( + this, + (CheckBoxPreference) findPreference(KEY_TOGGLE_BLUETOOTH)); + } + +} diff --git a/src/com/android/settings/ZoneList.java b/src/com/android/settings/ZoneList.java new file mode 100644 index 000000000..2877f0039 --- /dev/null +++ b/src/com/android/settings/ZoneList.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2006 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.AlarmManager; +import android.app.ListActivity; +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.SimpleAdapter; + +import org.xmlpull.v1.XmlPullParserException; + +import java.util.ArrayList; +import java.util.Arrays; +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 java.util.TimeZone; + +/** + * This activity displays a list of time zones that match a filter string + * such as "Africa", "Europe", etc. Choosing an item from the list will set + * the time zone. Pressing Back without choosing from the list will not + * result in a change in the time zone setting. + */ +public class ZoneList extends ListActivity { + + private static final String TAG = "ZoneList"; + private static final String KEY_ID = "id"; + private static final String KEY_DISPLAYNAME = "name"; + private static final String KEY_GMT = "gmt"; + private static final String KEY_OFFSET = "offset"; + private static final String XMLTAG_TIMEZONE = "timezone"; + + private static final int HOURS_1 = 60 * 60000; + private static final int HOURS_24 = 24 * HOURS_1; + private static final int HOURS_HALF = HOURS_1 / 2; + + private static final int MENU_TIMEZONE = Menu.FIRST+1; + private static final int MENU_ALPHABETICAL = Menu.FIRST; + + // Initial focus position + private int mDefault; + + private boolean mSortedByTimezone; + + private SimpleAdapter mTimezoneSortedAdapter; + private SimpleAdapter mAlphabeticalAdapter; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + String[] from = new String[] {KEY_DISPLAYNAME, KEY_GMT}; + int[] to = new int[] {android.R.id.text1, android.R.id.text2}; + + MyComparator comparator = new MyComparator(KEY_OFFSET); + + List<HashMap> timezoneSortedList = getZones(); + Collections.sort(timezoneSortedList, comparator); + mTimezoneSortedAdapter = new SimpleAdapter(this, + (List) timezoneSortedList, + android.R.layout.simple_list_item_2, + from, + to); + + List<HashMap> alphabeticalList = new ArrayList<HashMap>(timezoneSortedList); + comparator.setSortingKey(KEY_DISPLAYNAME); + Collections.sort(alphabeticalList, comparator); + mAlphabeticalAdapter = new SimpleAdapter(this, + (List) alphabeticalList, + android.R.layout.simple_list_item_2, + from, + to); + + // Sets the adapter + setSorting(true); + + // If current timezone is in this list, move focus to it + setSelection(mDefault); + + // Assume user may press Back + setResult(RESULT_CANCELED); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_ALPHABETICAL, 0, R.string.zone_list_menu_sort_alphabetically) + .setIcon(android.R.drawable.ic_menu_sort_alphabetically); + menu.add(0, MENU_TIMEZONE, 0, R.string.zone_list_menu_sort_by_timezone) + .setIcon(R.drawable.ic_menu_3d_globe); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + + if (mSortedByTimezone) { + menu.findItem(MENU_TIMEZONE).setVisible(false); + menu.findItem(MENU_ALPHABETICAL).setVisible(true); + } else { + menu.findItem(MENU_TIMEZONE).setVisible(true); + menu.findItem(MENU_ALPHABETICAL).setVisible(false); + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case MENU_TIMEZONE: + setSorting(true); + return true; + + case MENU_ALPHABETICAL: + setSorting(false); + return true; + + default: + return false; + } + } + + private void setSorting(boolean timezone) { + setListAdapter(timezone ? mTimezoneSortedAdapter : mAlphabeticalAdapter); + mSortedByTimezone = timezone; + } + + private List<HashMap> getZones() { + List<HashMap> myData = new ArrayList<HashMap>(); + long date = Calendar.getInstance().getTimeInMillis(); + try { + XmlResourceParser xrp = getResources().getXml(R.xml.timezones); + while (xrp.next() != XmlResourceParser.START_TAG) + ; + xrp.next(); + while (xrp.getEventType() != XmlResourceParser.END_TAG) { + while (xrp.getEventType() != XmlResourceParser.START_TAG) { + if (xrp.getEventType() == XmlResourceParser.END_DOCUMENT) { + return myData; + } + xrp.next(); + } + if (xrp.getName().equals(XMLTAG_TIMEZONE)) { + String id = xrp.getAttributeValue(0); + String displayName = xrp.nextText(); + addItem(myData, id, displayName, date); + } + while (xrp.getEventType() != XmlResourceParser.END_TAG) { + xrp.next(); + } + xrp.next(); + } + xrp.close(); + } catch (XmlPullParserException xppe) { + Log.e(TAG, "Ill-formatted timezones.xml file"); + } catch (java.io.IOException ioe) { + Log.e(TAG, "Unable to read timezones.xml file"); + } + + return myData; + } + + protected void addItem(List<HashMap> myData, String id, String displayName, + long date) { + HashMap map = new HashMap(); + map.put(KEY_ID, id); + map.put(KEY_DISPLAYNAME, displayName); + TimeZone tz = TimeZone.getTimeZone(id); + int offset = tz.getOffset(date); + int p = Math.abs(offset); + StringBuilder name = new StringBuilder(); + name.append("GMT"); + + if (offset < 0) { + name.append('-'); + } else { + name.append('+'); + } + + name.append(p / (HOURS_1)); + name.append(':'); + + int min = p / 60000; + min %= 60; + + if (min < 10) { + name.append('0'); + } + name.append(min); + + map.put(KEY_GMT, name.toString()); + map.put(KEY_OFFSET, offset); + + if (id.equals(TimeZone.getDefault().getID())) { + mDefault = myData.size(); + } + + myData.add(map); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Map map = (Map) l.getItemAtPosition(position); + // Update the system timezone value + AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone((String) map.get(KEY_ID)); + setResult(RESULT_OK); + finish(); + } + + private static class MyComparator implements Comparator<HashMap> { + private String mSortingKey; + + public MyComparator(String sortingKey) { + mSortingKey = sortingKey; + } + + public void setSortingKey(String sortingKey) { + mSortingKey = sortingKey; + } + + public int compare(HashMap map1, HashMap map2) { + Object value1 = map1.get(mSortingKey); + Object value2 = map2.get(mSortingKey); + + /* + * This should never happen, but just in-case, put non-comparable + * items at the end. + */ + if (!isComparable(value1)) { + return isComparable(value2) ? 1 : 0; + } else if (!isComparable(value2)) { + return -1; + } + + return ((Comparable) value1).compareTo(value2); + } + + private boolean isComparable(Object value) { + return (value != null) && (value instanceof Comparable); + } + } + +} diff --git a/src/com/android/settings/ZonePicker.java b/src/com/android/settings/ZonePicker.java new file mode 100644 index 000000000..def5036ae --- /dev/null +++ b/src/com/android/settings/ZonePicker.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 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.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ZonePicker extends ListActivity { + + private ArrayAdapter<CharSequence> mFilterAdapter; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mFilterAdapter = ArrayAdapter.createFromResource(this, + R.array.timezone_filters, android.R.layout.simple_list_item_1); + setListAdapter(mFilterAdapter); + } + + protected void addItem(List<Map> data, String name, String zone) { + HashMap temp = new HashMap(); + temp.put("title", name); + temp.put("zone", zone); + data.add(temp); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + String filter = (String) mFilterAdapter.getItem(position); + // If All is chosen, reset the filter + if (filter.equals("All")) { + filter = null; + } + Intent zoneList = new Intent(); + zoneList.setClass(this, ZoneList.class); + zoneList.putExtra("filter", filter); + + startActivityForResult(zoneList, 0); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // If subactivity has resulted in a timezone selection, close this act. + if (resultCode == RESULT_OK) { + finish(); + } + } +} diff --git a/src/com/android/settings/battery_history/BatteryHistory.java b/src/com/android/settings/battery_history/BatteryHistory.java new file mode 100644 index 000000000..dcf6cbf78 --- /dev/null +++ b/src/com/android/settings/battery_history/BatteryHistory.java @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2006 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.battery_history; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import com.android.internal.app.IBatteryStats; +import com.android.settings.R; + +import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.BatteryStats; +import android.os.Bundle; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.BatteryStats.Timer; +import android.os.BatteryStats.Uid; +import android.util.Log; +import android.util.LogPrinter; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.AdapterView.OnItemSelectedListener; + +public class BatteryHistory extends Activity implements OnClickListener, OnItemSelectedListener { + private static final String TAG = "BatteryHistory"; + + 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; + + // Must be in sync with the values in res/values/array.xml (id battery_history_type_spinner) + private static final int CPU_USAGE = 0; + private static final int NETWORK_USAGE = 1; + private static final int GPS_USAGE = 2; + private static final int SENSOR_USAGE = 3; + private static final int WAKELOCK_USAGE = 4; + private static final int MISC_USAGE = 5; + + // Must be in sync with the values in res/values/array.xml (id battery_history_which_spinner) + private static final int UNPLUGGED = 0; + private static final int CURRENT = 1; + private static final int TOTAL = 2; + + private BatteryStats mStats; + private int mWhich = BatteryStats.STATS_UNPLUGGED; + private int mType = CPU_USAGE; + + private GraphableButton[] mButtons; + IBatteryStats mBatteryInfo; + + private List<CpuUsage> mCpuUsage = new ArrayList<CpuUsage>(); + private List<NetworkUsage> mNetworkUsage = new ArrayList<NetworkUsage>(); + private List<SensorUsage> mSensorUsage = new ArrayList<SensorUsage>(); + private List<SensorUsage> mGpsUsage = new ArrayList<SensorUsage>(); + private List<WakelockUsage> mWakelockUsage = new ArrayList<WakelockUsage>(); + private List<MiscUsage> mMiscUsage = new ArrayList<MiscUsage>(); + + private boolean mHaveCpuUsage, mHaveNetworkUsage, mHaveSensorUsage, + mHaveWakelockUsage, mHaveMiscUsage; + + private LinearLayout mGraphLayout; + private LinearLayout mTextLayout; + private TextView mMessageText; + private TextView mDetailsText; + private Button mDetailsBackButton; + private Spinner mTypeSpinner; + private Spinner mWhichSpinner; + + private boolean mDetailsShown = false; + + private static String getLabel(String packageName, PackageManager pm) { + try { + ApplicationInfo ai = pm.getApplicationInfo(packageName, 0); + CharSequence label = ai.loadLabel(pm); + if (label != null) { + return label.toString(); + } + } catch (NameNotFoundException e) { + return packageName; + } + + return ""; + } + + void formatTime(double millis, StringBuilder sb) { + int seconds = (int) Math.floor(millis / 1000); + + 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 (days > 0) { + sb.append(getString(R.string.battery_history_days, days, hours, minutes, seconds)); + } else if (hours > 0) { + sb.append(getString(R.string.battery_history_hours, hours, minutes, seconds)); + } else if (minutes > 0) { + sb.append(getString(R.string.battery_history_minutes, minutes, seconds)); + } else { + sb.append(getString(R.string.battery_history_seconds, seconds)); + } + } + + abstract class Graphable implements Comparable<Graphable> { + protected String mName; + protected String mNamePackage; + protected boolean mUniqueName; + protected String[] mPackages; + protected String[] mPackageNames; + + public abstract String getLabel(); + public abstract double getSortValue(); + public abstract double[] getValues(); + public abstract void getInfo(StringBuilder info); + + public double getMaxValue() { + return -Double.MAX_VALUE; + } + + public int compareTo(Graphable o) { + double t = getSortValue(); + double ot = o.getSortValue(); + if (t < ot) { + // Largest first + return 1; + } else if (t > ot) { + return -1; + } else { + return 0; + } + } + + // Side effects: sets mName and mUniqueName + void getNameForUid(int uid) { + PackageManager pm = getPackageManager(); + mPackages = pm.getPackagesForUid(uid); + if (mPackages == null) { + mName = Integer.toString(uid); + mNamePackage = null; + return; + } + + mPackageNames = new String[mPackages.length]; + System.arraycopy(mPackages, 0, mPackageNames, 0, mPackages.length); + + // Convert package names to user-facing labels where possible + for (int i = 0; i < mPackageNames.length; i++) { + mPackageNames[i] = BatteryHistory.getLabel(mPackageNames[i], pm); + } + + if (mPackageNames.length == 1) { + mNamePackage = mPackages[0]; + mName = mPackageNames[0]; + mUniqueName = true; + } else { + mName = getString(R.string.battery_history_uid, uid); // Default name + // Look for an official name for this UID. + for (String name : mPackages) { + try { + PackageInfo pi = pm.getPackageInfo(name, 0); + if (pi.sharedUserLabel != 0) { + CharSequence nm = pm.getText(name, + pi.sharedUserLabel, pi.applicationInfo); + if (nm != null) { + mName = nm.toString(); + break; + } + } + } catch (PackageManager.NameNotFoundException e) { + } + } + } + } + } + + class CpuUsage extends Graphable { + String mProcess; + double[] mUsage; + double mTotalRuntime; + long mStarts; + + public CpuUsage(int uid, String process, long userTime, long systemTime, + long starts, long totalRuntime) { + getNameForUid(uid); + mProcess = process; + PackageManager pm = BatteryHistory.this.getPackageManager(); + mName = BatteryHistory.getLabel(process, pm); + mUsage = new double[2]; + + mUsage[0] = userTime; + mUsage[1] = userTime + systemTime; + mTotalRuntime = totalRuntime; + mStarts = starts; + } + + public String getLabel() { + return mName; + } + + public double getSortValue() { + return mUsage[1]; + } + + public double[] getValues() { + return mUsage; + } + + public double getMaxValue() { + return mTotalRuntime; + } + + public void getInfo(StringBuilder info) { + info.append(getString(R.string.battery_history_cpu_usage, mProcess)); + info.append("\n\n"); + info.append(getString(R.string.battery_history_user_time)); + formatTime(mUsage[0] * 10, info); + info.append('\n'); + info.append(getString(R.string.battery_history_system_time)); + formatTime((mUsage[1] - mUsage[0]) * 10, info); + info.append('\n'); + info.append(getString(R.string.battery_history_total_time)); + formatTime((mUsage[1]) * 10, info); + info.append('\n'); + info.append(getString(R.string.battery_history_starts, mStarts)); + } + } + + class NetworkUsage extends Graphable { + double[] mUsage; + + public NetworkUsage(int uid, long received, long sent) { + getNameForUid(uid); + + mUsage = new double[2]; + mUsage[0] = received; + mUsage[1] = received + sent; + } + + public String getLabel() { + return mName; + } + + public double getSortValue() { + return mUsage[1]; + } + + public double[] getValues() { + return mUsage; + } + + public void getInfo(StringBuilder info) { + info.append(getString(R.string.battery_history_network_usage, mName)); + info.append("\n\n"); + info.append(getString(R.string.battery_history_bytes_received, (long) mUsage[0])); + info.append('\n'); + info.append(getString(R.string.battery_history_bytes_sent, + (long) mUsage[1] - (long) mUsage[0])); + info.append('\n'); + info.append(getString(R.string.battery_history_bytes_total, (long) mUsage[1])); + + if (!mUniqueName) { + info.append("\n\n"); + info.append(getString(R.string.battery_history_packages_sharing_this_uid)); + info.append('\n'); + + PackageManager pm = BatteryHistory.this.getPackageManager(); + List<String> names = new ArrayList<String>(); + for (String name : mPackageNames) { + names.add(BatteryHistory.getLabel(name, pm)); + } + Collections.sort(names); + for (String name : names) { + info.append(" "); + info.append(name); + info.append('\n'); + } + } + } + } + + class SensorUsage extends Graphable { + double[] mUsage; + double mTotalRealtime; + int mCount; + + public SensorUsage(int uid, long time, int count, long totalRealtime) { + getNameForUid(uid); + + mUsage = new double[1]; + mUsage[0] = time; + mTotalRealtime = totalRealtime; + + mCount = count; + } + + public String getLabel() { + return mName; + } + + public double getSortValue() { + return mUsage[0]; + } + + public double[] getValues() { + return mUsage; + } + + public double getMaxValue() { + return mTotalRealtime; + } + + public void getInfo(StringBuilder info) { + info.append(getString(R.string.battery_history_sensor)); + info.append(mName); + info.append("\n\n"); + info.append(getString(R.string.battery_history_total_time)); + formatTime(mUsage[0], info); + info.append("\n\n"); + } + } + + + class WakelockUsage extends Graphable { + double[] mUsage; + double mTotalRealtime; + int mCount; + + public WakelockUsage(int uid, long time, int count, long totalRealtime) { + getNameForUid(uid); + + mUsage = new double[1]; + mUsage[0] = time; + mTotalRealtime = totalRealtime; + + mCount = count; + } + + public String getLabel() { + return mName; + } + + public double getSortValue() { + return mUsage[0]; + } + + public double[] getValues() { + return mUsage; + } + + public double getMaxValue() { + return mTotalRealtime; + } + + public void getInfo(StringBuilder info) { + info.append(getString(R.string.battery_history_wakelock)); + info.append(mName); + info.append("\n\n"); + info.append(getString(R.string.battery_history_total_time)); + formatTime(mUsage[0], info); + info.append("\n\n"); + } + } + + class MiscUsage extends Graphable { + int mInfoLabelRes; + double[] mUsage; + double mTotalRealtime; + + public MiscUsage(String name, int infoLabelRes, long value, + long totalRealtime) { + mName = name; + + mInfoLabelRes = infoLabelRes; + + mUsage = new double[2]; + mUsage[0] = value; + mTotalRealtime = totalRealtime; + } + + public String getLabel() { + return mName; + } + + public double getSortValue() { + return mUsage[1]; + } + + public double[] getValues() { + return mUsage; + } + + public double getMaxValue() { + return mTotalRealtime; + } + + public void getInfo(StringBuilder info) { + info.append(getString(mInfoLabelRes)); + info.append(' '); + formatTime(mUsage[0], info); + info.append(" ("); + info.append((mUsage[0]*100)/mTotalRealtime); + info.append("%)"); + } + } + + private List<? extends Graphable> getGraphRecords() { + switch (mType) { + case CPU_USAGE: return mCpuUsage; + case NETWORK_USAGE : return mNetworkUsage; + case SENSOR_USAGE: return mSensorUsage; + case GPS_USAGE: return mGpsUsage; + case WAKELOCK_USAGE: return mWakelockUsage; + case MISC_USAGE: return mMiscUsage; + default: + return (List<? extends Graphable>) null; // TODO + } + } + + private void displayGraph() { + Log.i(TAG, "displayGraph"); + + collectStatistics(); + + // Hide the UI and selectively enable it below + mMessageText.setVisibility(View.GONE); + for (int i = 0; i < mButtons.length; i++) { + mButtons[i].setVisibility(View.INVISIBLE); + } + + double maxValue = -Double.MAX_VALUE; + + List<? extends Graphable> records = getGraphRecords(); + for (Graphable g : records) { + double[] values = g.getValues(); + maxValue = Math.max(maxValue, values[values.length - 1]); + maxValue = Math.max(maxValue, g.getMaxValue()); + } + + int[] colors = new int[2]; + colors[0] = 0xff0000ff; + colors[1] = 0xffff0000; + + for (int i = 0; i < mButtons.length; i++) { + mButtons[i].setVisibility(View.INVISIBLE); + } + + int numRecords = Math.min(records.size(), mButtons.length); + if (numRecords == 0) { + mMessageText.setVisibility(View.VISIBLE); + mMessageText.setText(R.string.battery_history_no_data); + } else { + for (int i = 0; i < numRecords; i++) { + Graphable r = records.get(i); + + mButtons[i].setText(r.getLabel()); + mButtons[i].setValues(r.getValues(), maxValue); + mButtons[i].setVisibility(View.VISIBLE); + } + } + } + + private void hideDetails() { + mTextLayout.setVisibility(View.GONE); + mGraphLayout.setVisibility(View.VISIBLE); + mDetailsShown = false; + } + + private void showDetails(int id) { + mGraphLayout.setVisibility(View.GONE); + mTextLayout.setVisibility(View.VISIBLE); + + StringBuilder info = new StringBuilder(); + List<? extends Graphable> records = getGraphRecords(); + if (id < records.size()) { + Graphable record = records.get(id); + record.getInfo(info); + } else { + info.append(getString(R.string.battery_history_details_for, id)); + } + mDetailsText.setText(info.toString()); + mDetailsShown = true; + } + + private void processCpuUsage() { + mCpuUsage.clear(); + + long uSecTime = SystemClock.uptimeMillis() * 1000; + final long uSecNow = mStats.computeBatteryUptime(uSecTime, mWhich) / 1000; + + SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + + Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); + if (processStats.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent + : processStats.entrySet()) { + + Uid.Proc ps = ent.getValue(); + long userTime = ps.getUserTime(mWhich); + long systemTime = ps.getSystemTime(mWhich); + long starts = ps.getStarts(mWhich); + + if (userTime != 0 || systemTime != 0) { + mCpuUsage.add(new CpuUsage(u.getUid(), ent.getKey(), + userTime, systemTime, starts, uSecNow)); + } + } + } + } + Collections.sort(mCpuUsage); + } + + private void processNetworkUsage() { + mNetworkUsage.clear(); + + SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + + long received = u.getTcpBytesReceived(mWhich); + long sent = u.getTcpBytesSent(mWhich); + if (received + sent > 0) { + mNetworkUsage.add(new NetworkUsage(u.getUid(), received, sent)); + } + } + Collections.sort(mNetworkUsage); + } + + private void processSensorUsage() { + mGpsUsage.clear(); + mSensorUsage.clear(); + + long uSecTime = SystemClock.elapsedRealtime() * 1000; + final long uSecNow = mStats.computeBatteryRealtime(uSecTime, mWhich) / 1000; + + SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + int uid = u.getUid(); + + Map<Integer, ? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); + long timeGps = 0; + int countGps = 0; + long timeOther = 0; + int countOther = 0; + if (sensorStats.size() > 0) { + for (Map.Entry<Integer, ? extends BatteryStats.Uid.Sensor> ent + : sensorStats.entrySet()) { + + Uid.Sensor se = ent.getValue(); + int handle = se.getHandle(); + Timer timer = se.getSensorTime(); + if (timer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTime(uSecNow, mWhich) + 500) / 1000; + int count = timer.getCount(mWhich); + if (handle == BatteryStats.Uid.Sensor.GPS) { + timeGps += totalTime; + countGps += count; + } else { + timeOther += totalTime; + countOther += count; + } + } + } + } + + if (timeGps > 0) { + mGpsUsage.add(new SensorUsage(uid, timeGps, countGps, uSecNow)); + } + if (timeOther > 0) { + mSensorUsage.add(new SensorUsage(uid, timeOther, countOther, uSecNow)); + } + } + + Collections.sort(mGpsUsage); + Collections.sort(mSensorUsage); + } + + private void processWakelockUsage() { + mWakelockUsage.clear(); + + long uSecTime = SystemClock.elapsedRealtime() * 1000; + final long uSecNow = mStats.computeBatteryRealtime(uSecTime, mWhich) / 1000; + + SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + int uid = u.getUid(); + + Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = u.getWakelockStats(); + long time = 0; + int count = 0; + if (wakelockStats.size() > 0) { + for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> ent + : wakelockStats.entrySet()) { + + Uid.Wakelock wl = ent.getValue(); + Timer timer = wl.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); + if (timer != null) { + // Convert from microseconds to milliseconds with rounding + time += (timer.getTotalTime(uSecNow, mWhich) + 500) / 1000; + count += timer.getCount(mWhich); + } + } + } + + if (time > 0) { + mWakelockUsage.add(new WakelockUsage(uid, time, count, uSecNow)); + } + } + + Collections.sort(mWakelockUsage); + } + + private void processMiscUsage() { + mMiscUsage.clear(); + + long rawRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryRealtime = mStats.getBatteryRealtime(rawRealtime); + final long whichRealtime = mStats.computeBatteryRealtime(rawRealtime, mWhich) / 1000; + + long time = mStats.computeBatteryUptime(SystemClock.uptimeMillis() * 1000, mWhich) / 1000; + if (time > 0) { + mMiscUsage.add(new MiscUsage(getString( + R.string.battery_history_awake_label), + R.string.battery_history_awake, + time, whichRealtime)); + } + + time = mStats.getScreenOnTime(batteryRealtime, mWhich) / 1000; + if (time > 0) { + mMiscUsage.add(new MiscUsage(getString( + R.string.battery_history_screen_on_label), + R.string.battery_history_screen_on, + time, whichRealtime)); + } + + time = mStats.getPhoneOnTime(batteryRealtime, mWhich) / 1000; + if (time > 0) { + mMiscUsage.add(new MiscUsage(getString( + R.string.battery_history_phone_on_label), + R.string.battery_history_phone_on, + time, whichRealtime)); + } + + Collections.sort(mMiscUsage); + } + + private void collectStatistics() { + if (mType == CPU_USAGE) { + if (!mHaveCpuUsage) { + mHaveCpuUsage = true; + processCpuUsage(); + } + } + if (mType == NETWORK_USAGE) { + if (!mHaveNetworkUsage) { + mHaveNetworkUsage = true; + processNetworkUsage(); + } + } + if (mType == GPS_USAGE || mType == SENSOR_USAGE) { + if (!mHaveSensorUsage) { + mHaveSensorUsage = true; + processSensorUsage(); + } + } + if (mType == WAKELOCK_USAGE) { + if (!mHaveWakelockUsage) { + mHaveWakelockUsage = true; + processWakelockUsage(); + } + } + if (mType == MISC_USAGE) { + if (!mHaveMiscUsage) { + mHaveMiscUsage = true; + processMiscUsage(); + } + } + } + + private void load() { + try { + byte[] data = mBatteryInfo.getStatistics(); + Parcel parcel = Parcel.obtain(); + //Log.i(TAG, "Got data: " + data.length + " bytes"); + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + mStats = com.android.internal.os.BatteryStatsImpl.CREATOR + .createFromParcel(parcel); + //Log.i(TAG, "RECEIVED BATTERY INFO:"); + //mStats.dumpLocked(new LogPrinter(Log.INFO, TAG)); + + mHaveCpuUsage = mHaveNetworkUsage = mHaveSensorUsage + = mHaveWakelockUsage = mHaveMiscUsage = false; + } catch (RemoteException e) { + Log.e(TAG, "RemoteException:", e); + } + } + + public void onClick(View v) { + if (v == mDetailsBackButton) { + hideDetails(); + return; + } + + int id = ((Integer) v.getTag()).intValue(); + showDetails(id); + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && mDetailsShown) { + hideDetails(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + int oldWhich = mWhich; + + if (parent.equals(mTypeSpinner)) { + mType = position; + } else if (parent.equals(mWhichSpinner)) { + switch (position) { + case UNPLUGGED: + mWhich = BatteryStats.STATS_UNPLUGGED; + break; + case CURRENT: + mWhich = BatteryStats.STATS_CURRENT; + break; + case TOTAL: + mWhich = BatteryStats.STATS_TOTAL; + break; + } + } + + if (oldWhich != mWhich) { + mHaveCpuUsage = mHaveNetworkUsage = mHaveSensorUsage + = mHaveWakelockUsage = mHaveMiscUsage = false; + } + + displayGraph(); + } + + public void onNothingSelected(AdapterView<?> parent) { + // Do nothing + } + + @Override + public Object onRetainNonConfigurationInstance() { + BatteryStats stats = mStats; + mStats = null; + return stats; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mStats != null) { + outState.putParcelable("stats", mStats); + } + outState.putInt("type", mType); + outState.putInt("which", mWhich); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + Log.i(TAG, "onCreate"); + + setContentView(R.layout.battery_history); + + mGraphLayout = (LinearLayout) findViewById(R.id.graphLayout); + mTextLayout = (LinearLayout) findViewById(R.id.textLayout); + mDetailsText = (TextView) findViewById(R.id.detailsText); + mMessageText = (TextView) findViewById(R.id.messageText); + + mTypeSpinner = (Spinner) findViewById(R.id.typeSpinner); + mTypeSpinner.setOnItemSelectedListener(this); + + mWhichSpinner = (Spinner) findViewById(R.id.whichSpinner); + mWhichSpinner.setOnItemSelectedListener(this); + mWhichSpinner.setEnabled(true); + + mButtons = new GraphableButton[8]; + mButtons[0] = (GraphableButton) findViewById(R.id.button0); + mButtons[1] = (GraphableButton) findViewById(R.id.button1); + mButtons[2] = (GraphableButton) findViewById(R.id.button2); + mButtons[3] = (GraphableButton) findViewById(R.id.button3); + mButtons[4] = (GraphableButton) findViewById(R.id.button4); + mButtons[5] = (GraphableButton) findViewById(R.id.button5); + mButtons[6] = (GraphableButton) findViewById(R.id.button6); + mButtons[7] = (GraphableButton) findViewById(R.id.button7); + + for (int i = 0; i < mButtons.length; i++) { + mButtons[i].setTag(i); + mButtons[i].setOnClickListener(this); + } + + mBatteryInfo = IBatteryStats.Stub.asInterface( + ServiceManager.getService("batteryinfo")); + + mStats = (BatteryStats)getLastNonConfigurationInstance(); + if (icicle != null) { + if (mStats == null) { + mStats = (BatteryStats)icicle.getParcelable("stats"); + } + mType = icicle.getInt("type"); + mWhich = icicle.getInt("which"); + } + if (mStats == null) { + load(); + } + displayGraph(); + } +} diff --git a/src/com/android/settings/battery_history/GraphableButton.java b/src/com/android/settings/battery_history/GraphableButton.java new file mode 100644 index 000000000..39028d036 --- /dev/null +++ b/src/com/android/settings/battery_history/GraphableButton.java @@ -0,0 +1,55 @@ +package com.android.settings.battery_history; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.Button; + +public class GraphableButton extends Button { + private static final String TAG = "GraphableButton"; + + static Paint[] sPaint = new Paint[2]; + static { + sPaint[0] = new Paint(); + sPaint[0].setStyle(Paint.Style.FILL); + sPaint[0].setColor(Color.BLUE); + + sPaint[1] = new Paint(); + sPaint[1].setStyle(Paint.Style.FILL); + sPaint[1].setColor(Color.RED); + } + + double[] mValues; + + public GraphableButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setValues(double[] values, double maxValue) { + mValues = values.clone(); + for (int i = 0; i < values.length; i++) { + mValues[i] /= maxValue; + } + } + + @Override + public void onDraw(Canvas canvas) { + Log.i(TAG, "onDraw: w = " + getWidth() + ", h = " + getHeight()); + + int xmin = getPaddingLeft(); + int xmax = getWidth() - getPaddingRight(); + int ymin = getPaddingTop(); + int ymax = getHeight() - getPaddingBottom(); + + int startx = xmin; + for (int i = 0; i < mValues.length; i++) { + int endx = xmin + (int) (mValues[i] * (xmax - xmin)); + canvas.drawRect(startx, ymin, endx, ymax, sPaint[i]); + startx = endx; + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java new file mode 100644 index 000000000..f0a818909 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; + +import android.content.Context; +import android.preference.Preference; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; + +/** + * BluetoothDevicePreference is the preference type used to display each remote + * Bluetooth device in the Bluetooth Settings screen. + */ +public class BluetoothDevicePreference extends Preference implements LocalBluetoothDevice.Callback { + private static final String TAG = "BluetoothDevicePreference"; + + private static int sDimAlpha = Integer.MIN_VALUE; + + private LocalBluetoothDevice mLocalDevice; + + /** + * Cached local copy of whether the device is busy. This is only updated + * from {@link #onDeviceAttributesChanged(LocalBluetoothDevice)}. + */ + private boolean mIsBusy; + + public BluetoothDevicePreference(Context context, LocalBluetoothDevice localDevice) { + super(context); + + if (sDimAlpha == Integer.MIN_VALUE) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); + sDimAlpha = (int) (outValue.getFloat() * 255); + } + + mLocalDevice = localDevice; + + setLayoutResource(R.layout.preference_bluetooth); + + localDevice.registerCallback(this); + + onDeviceAttributesChanged(localDevice); + } + + public LocalBluetoothDevice getDevice() { + return mLocalDevice; + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + mLocalDevice.unregisterCallback(this); + } + + public void onDeviceAttributesChanged(LocalBluetoothDevice device) { + + /* + * The preference framework takes care of making sure the value has + * changed before proceeding. + */ + + setTitle(mLocalDevice.getName()); + + /* + * TODO: Showed "Paired" even though it was "Connected". This may be + * related to BluetoothHeadset not bound to the actual + * BluetoothHeadsetService when we got here. + */ + setSummary(mLocalDevice.getSummary()); + + // Used to gray out the item + mIsBusy = mLocalDevice.isBusy(); + + // Data has changed + notifyChanged(); + + // This could affect ordering, so notify that also + notifyHierarchyChanged(); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && !mIsBusy; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + ImageView btClass = (ImageView) view.findViewById(R.id.btClass); + btClass.setImageResource(mLocalDevice.getBtClassDrawable()); + btClass.setAlpha(isEnabled() ? 255 : sDimAlpha); + } + + @Override + public int compareTo(Preference another) { + if (!(another instanceof BluetoothDevicePreference)) { + // Put other preference types above us + return 1; + } + + return mLocalDevice.compareTo(((BluetoothDevicePreference) another).mLocalDevice); + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java new file mode 100644 index 000000000..a51f9b5ea --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.CheckBoxPreference; +import android.util.Log; + +/** + * BluetoothDiscoverableEnabler is a helper to manage the "Discoverable" + * checkbox. It sets/unsets discoverability and keeps track of how much time + * until the the discoverability is automatically turned off. + */ +public class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChangeListener { + private static final String TAG = "BluetoothDiscoverableEnabler"; + private static final boolean V = LocalBluetoothManager.V; + + private static final String SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT = + "debug.bt.discoverable_time"; + private static final int DISCOVERABLE_TIMEOUT = 120; + + private static final String SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP = + "discoverable_end_timestamp"; + + private final Context mContext; + private final Handler mUiHandler; + private final CheckBoxPreference mCheckBoxPreference; + + private final LocalBluetoothManager mLocalManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothIntent.SCAN_MODE_CHANGED_ACTION.equals(intent.getAction())) { + int mode = intent.getIntExtra(BluetoothIntent.SCAN_MODE, BluetoothError.ERROR); + if (mode != BluetoothError.ERROR) { + handleModeChanged(mode); + } + } + } + }; + + private final Runnable mUpdateCountdownSummaryRunnable = new Runnable() { + public void run() { + updateCountdownSummary(); + } + }; + + public BluetoothDiscoverableEnabler(Context context, CheckBoxPreference checkBoxPreference) { + mContext = context; + mUiHandler = new Handler(); + mCheckBoxPreference = checkBoxPreference; + + checkBoxPreference.setPersistent(false); + + mLocalManager = LocalBluetoothManager.getInstance(context); + if (mLocalManager == null) { + // Bluetooth not supported + checkBoxPreference.setEnabled(false); + } + } + + public void resume() { + if (mLocalManager == null) { + return; + } + + IntentFilter filter = new IntentFilter(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); + filter.addAction(BluetoothIntent.DISABLED_ACTION); + mContext.registerReceiver(mReceiver, filter); + mCheckBoxPreference.setOnPreferenceChangeListener(this); + + handleModeChanged(mLocalManager.getBluetoothManager().getScanMode()); + } + + public void pause() { + if (mLocalManager == null) { + return; + } + + mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable); + mCheckBoxPreference.setOnPreferenceChangeListener(null); + mContext.unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + if (V) { + Log.v(TAG, "Preference changed to " + value); + } + + // Turn on/off BT discoverability + setEnabled((Boolean) value); + + return true; + } + + private void setEnabled(final boolean enable) { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + if (enable) { + + int timeout = getDiscoverableTimeout(); + manager.setDiscoverableTimeout(timeout); + + long endTimestamp = System.currentTimeMillis() + timeout * 1000; + persistDiscoverableEndTimestamp(endTimestamp); + + manager.setScanMode(BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + } else { + manager.setScanMode(BluetoothDevice.SCAN_MODE_CONNECTABLE); + } + } + + private int getDiscoverableTimeout() { + int timeout = SystemProperties.getInt(SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT, -1); + if (timeout <= 0) { + timeout = DISCOVERABLE_TIMEOUT; + } + + return timeout; + } + + private void persistDiscoverableEndTimestamp(long endTimestamp) { + SharedPreferences.Editor editor = mLocalManager.getSharedPreferences().edit(); + editor.putLong(SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, endTimestamp); + editor.commit(); + } + + private void handleModeChanged(int mode) { + if (V) { + Log.v(TAG, "Got mode changed: " + mode); + } + + if (mode == BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + mCheckBoxPreference.setChecked(true); + updateCountdownSummary(); + + } else { + mCheckBoxPreference.setChecked(false); + } + } + + private void updateCountdownSummary() { + int mode = mLocalManager.getBluetoothManager().getScanMode(); + if (mode != BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + return; + } + + long currentTimestamp = System.currentTimeMillis(); + long endTimestamp = mLocalManager.getSharedPreferences().getLong( + SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, 0); + + if (currentTimestamp > endTimestamp) { + // We're still in discoverable mode, but maybe there isn't a timeout. + mCheckBoxPreference.setSummaryOn(null); + return; + } + + String formattedTimeLeft = String.valueOf((endTimestamp - currentTimestamp) / 1000); + + mCheckBoxPreference.setSummaryOn( + mContext.getResources().getString(R.string.bluetooth_is_discoverable, + formattedTimeLeft)); + + synchronized (this) { + mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable); + mUiHandler.postDelayed(mUpdateCountdownSummaryRunnable, 1000); + } + } + + +} diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java new file mode 100644 index 000000000..661700fd3 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothManager.ExtendedBluetoothState; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.preference.Preference; +import android.preference.CheckBoxPreference; +import android.text.TextUtils; +import android.util.Config; + +/** + * BluetoothEnabler is a helper to manage the Bluetooth on/off checkbox + * preference. It is turns on/off Bluetooth and ensures the summary of the + * preference reflects the current state. + */ +public class BluetoothEnabler implements Preference.OnPreferenceChangeListener { + + private static final boolean LOCAL_LOGD = Config.LOGD || false; + private static final String TAG = "BluetoothEnabler"; + + private final Context mContext; + private final CheckBoxPreference mCheckBoxPreference; + private final CharSequence mOriginalSummary; + + private final LocalBluetoothManager mLocalManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleStateChanged(mLocalManager.getBluetoothState()); + } + }; + + public BluetoothEnabler(Context context, CheckBoxPreference checkBoxPreference) { + mContext = context; + mCheckBoxPreference = checkBoxPreference; + + mOriginalSummary = checkBoxPreference.getSummary(); + checkBoxPreference.setPersistent(false); + + mLocalManager = LocalBluetoothManager.getInstance(context); + if (mLocalManager == null) { + // Bluetooth not supported + checkBoxPreference.setEnabled(false); + } + } + + public void resume() { + if (mLocalManager == null) { + return; + } + + ExtendedBluetoothState state = mLocalManager.getBluetoothState(); + // This is the widget enabled state, not the preference toggled state + mCheckBoxPreference.setEnabled(state == ExtendedBluetoothState.ENABLED || + state == ExtendedBluetoothState.DISABLED); + // BT state is not a sticky broadcast, so set it manually + handleStateChanged(state); + + mContext.registerReceiver(mReceiver, + new IntentFilter(LocalBluetoothManager.EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + mCheckBoxPreference.setOnPreferenceChangeListener(this); + } + + public void pause() { + if (mLocalManager == null) { + return; + } + + mContext.unregisterReceiver(mReceiver); + mCheckBoxPreference.setOnPreferenceChangeListener(null); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + // Turn on/off BT + setEnabled((Boolean) value); + + // Don't update UI to opposite state until we're sure + return false; + } + + private void setEnabled(final boolean enable) { + // Disable preference + mCheckBoxPreference.setEnabled(false); + + mLocalManager.setBluetoothEnabled(enable); + } + + private void handleStateChanged(ExtendedBluetoothState state) { + + if (state == ExtendedBluetoothState.DISABLED || state == ExtendedBluetoothState.ENABLED) { + mCheckBoxPreference.setChecked(state == ExtendedBluetoothState.ENABLED); + mCheckBoxPreference + .setSummary(state == ExtendedBluetoothState.DISABLED ? mOriginalSummary : null); + + mCheckBoxPreference.setEnabled(isEnabledByDependency()); + + } else if (state == ExtendedBluetoothState.ENABLING || + state == ExtendedBluetoothState.DISABLING) { + mCheckBoxPreference.setSummary(state == ExtendedBluetoothState.ENABLING + ? R.string.wifi_starting + : R.string.wifi_stopping); + + } else if (state == ExtendedBluetoothState.UNKNOWN) { + mCheckBoxPreference.setChecked(false); + mCheckBoxPreference.setSummary(R.string.wifi_error); + mCheckBoxPreference.setEnabled(true); + } + } + + private boolean isEnabledByDependency() { + Preference dep = getDependencyPreference(); + if (dep == null) { + return true; + } + + return !dep.shouldDisableDependents(); + } + + private Preference getDependencyPreference() { + String depKey = mCheckBoxPreference.getDependency(); + if (TextUtils.isEmpty(depKey)) { + return null; + } + + return mCheckBoxPreference.getPreferenceManager().findPreference(depKey); + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothEventRedirector.java b/src/com/android/settings/bluetooth/BluetoothEventRedirector.java new file mode 100644 index 000000000..2ad5726dd --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothEventRedirector.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.bluetooth.LocalBluetoothManager.ExtendedBluetoothState; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +/** + * BluetoothEventRedirector receives broadcasts and callbacks from the Bluetooth + * API and dispatches the event on the UI thread to the right class in the + * Settings. + */ +public class BluetoothEventRedirector { + private static final String TAG = "BluetoothEventRedirector"; + private static final boolean V = LocalBluetoothManager.V; + + private LocalBluetoothManager mManager; + + private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (V) { + Log.v(TAG, "Received " + intent.getAction()); + } + + String action = intent.getAction(); + String address = intent.getStringExtra(BluetoothIntent.ADDRESS); + + if (action.equals(BluetoothIntent.ENABLED_ACTION)) { + mManager.setBluetoothStateInt(ExtendedBluetoothState.ENABLED); + + } else if (action.equals(BluetoothIntent.DISABLED_ACTION)) { + mManager.setBluetoothStateInt(ExtendedBluetoothState.DISABLED); + + } else if (action.equals(BluetoothIntent.DISCOVERY_STARTED_ACTION)) { + mManager.onScanningStateChanged(true); + + } else if (action.equals(BluetoothIntent.DISCOVERY_COMPLETED_ACTION)) { + mManager.onScanningStateChanged(false); + + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION)) { + short rssi = intent.getShortExtra(BluetoothIntent.RSSI, Short.MIN_VALUE); + mManager.getLocalDeviceManager().onDeviceAppeared(address, rssi); + + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION)) { + mManager.getLocalDeviceManager().onDeviceDisappeared(address); + + } else if (action.equals(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION)) { + mManager.getLocalDeviceManager().onDeviceNameUpdated(address); + + } else if (action.equals(BluetoothIntent.BOND_STATE_CHANGED_ACTION)) { + int bondState = intent.getIntExtra(BluetoothIntent.BOND_STATE, + BluetoothError.ERROR); + mManager.getLocalDeviceManager().onBondingStateChanged(address, bondState); + if (bondState == BluetoothDevice.BOND_NOT_BONDED) { + int reason = intent.getIntExtra(BluetoothIntent.REASON, BluetoothError.ERROR); + if (reason == BluetoothDevice.UNBOND_REASON_AUTH_FAILED || + reason == BluetoothDevice.UNBOND_REASON_AUTH_REJECTED || + reason == BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN) { + mManager.getLocalDeviceManager().onBondingError(address, reason); + } + } + + } else if (action.equals(BluetoothIntent.HEADSET_STATE_CHANGED_ACTION)) { + mManager.getLocalDeviceManager().onProfileStateChanged(address); + + int newState = intent.getIntExtra(BluetoothIntent.HEADSET_STATE, 0); + int oldState = intent.getIntExtra(BluetoothIntent.HEADSET_PREVIOUS_STATE, 0); + if (newState == BluetoothHeadset.STATE_DISCONNECTED && + oldState == BluetoothHeadset.STATE_CONNECTING) { + Log.i(TAG, "Failed to connect BT headset"); + } + + } else if (action.equals(BluetoothA2dp.SINK_STATE_CHANGED_ACTION)) { + mManager.getLocalDeviceManager().onProfileStateChanged(address); + + int newState = intent.getIntExtra(BluetoothA2dp.SINK_STATE, 0); + int oldState = intent.getIntExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, 0); + if (newState == BluetoothA2dp.STATE_DISCONNECTED && + oldState == BluetoothA2dp.STATE_CONNECTING) { + Log.i(TAG, "Failed to connect BT A2DP"); + } + + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION)) { + mManager.getLocalDeviceManager().onBtClassChanged(address); + + } + } + }; + + public BluetoothEventRedirector(LocalBluetoothManager localBluetoothManager) { + mManager = localBluetoothManager; + } + + public void start() { + IntentFilter filter = new IntentFilter(); + + // Bluetooth on/off broadcasts + filter.addAction(BluetoothIntent.ENABLED_ACTION); + filter.addAction(BluetoothIntent.DISABLED_ACTION); + + // Discovery broadcasts + filter.addAction(BluetoothIntent.DISCOVERY_STARTED_ACTION); + filter.addAction(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); + filter.addAction(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); + filter.addAction(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION); + filter.addAction(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); + + // Pairing broadcasts + filter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION); + + // Fine-grained state broadcasts + filter.addAction(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); + filter.addAction(BluetoothIntent.HEADSET_STATE_CHANGED_ACTION); + filter.addAction(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION); + + mManager.getContext().registerReceiver(mBroadcastReceiver, filter); + } + + public void stop() { + mManager.getContext().unregisterReceiver(mBroadcastReceiver); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothNamePreference.java b/src/com/android/settings/bluetooth/BluetoothNamePreference.java new file mode 100644 index 000000000..3065b26c5 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothNamePreference.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.preference.EditTextPreference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +/** + * BluetoothNamePreference is the preference type for editing the device's + * Bluetooth name. It asks the user for a name, and persists it via the + * Bluetooth API. + */ +public class BluetoothNamePreference extends EditTextPreference { + private static final String TAG = "BluetoothNamePreference"; + + private LocalBluetoothManager mLocalManager; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + setSummaryToName(); + } + }; + + public BluetoothNamePreference(Context context, AttributeSet attrs) { + super(context, attrs); + + mLocalManager = LocalBluetoothManager.getInstance(context); + + setSummaryToName(); + } + + public void resume() { + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothIntent.ENABLED_ACTION); + filter.addAction(BluetoothIntent.NAME_CHANGED_ACTION); + getContext().registerReceiver(mReceiver, filter); + } + + public void pause() { + getContext().unregisterReceiver(mReceiver); + } + + private void setSummaryToName() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + if (manager.isEnabled()) { + setSummary(manager.getName()); + } + } + + @Override + protected boolean persistString(String value) { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + manager.setName(value); + return true; + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothPinDialog.java b/src/com/android/settings/bluetooth/BluetoothPinDialog.java new file mode 100644 index 000000000..5e289f756 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPinDialog.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.text.InputFilter.LengthFilter; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.settings.R; + +/** + * BluetoothPinDialog asks the user to enter a PIN for pairing with a remote + * Bluetooth device. It is an activity that appears as a dialog. + */ +public class BluetoothPinDialog extends AlertActivity implements DialogInterface.OnClickListener, + TextWatcher { + private static final String TAG = "BluetoothPinDialog"; + + private LocalBluetoothManager mLocalManager; + private String mAddress; + private EditText mPinView; + private Button mOkButton; + + private static final String INSTANCE_KEY_PAIRING_CANCELED = "received_pairing_canceled"; + private boolean mReceivedPairingCanceled; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!BluetoothIntent.PAIRING_CANCEL_ACTION.equals(intent.getAction())) { + return; + } + + String address = intent.getStringExtra(BluetoothIntent.ADDRESS); + if (address == null || address.equals(mAddress)) { + onReceivedPairingCanceled(); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (!intent.getAction().equals(BluetoothIntent.PAIRING_REQUEST_ACTION)) + { + Log.e(TAG, + "Error: this activity may be started only with intent " + + BluetoothIntent.PAIRING_REQUEST_ACTION); + finish(); + } + + mLocalManager = LocalBluetoothManager.getInstance(this); + mAddress = intent.getStringExtra(BluetoothIntent.ADDRESS); + + // Set up the "dialog" + final AlertController.AlertParams p = mAlertParams; + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.bluetooth_pin_entry); + p.mView = createView(); + p.mPositiveButtonText = getString(android.R.string.ok); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.cancel); + p.mNegativeButtonListener = this; + setupAlert(); + + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setEnabled(false); + + /* + * Leave this registered through pause/resume since we still want to + * finish the activity in the background if pairing is canceled. + */ + registerReceiver(mReceiver, new IntentFilter(BluetoothIntent.PAIRING_CANCEL_ACTION)); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + mReceivedPairingCanceled = savedInstanceState.getBoolean(INSTANCE_KEY_PAIRING_CANCELED); + if (mReceivedPairingCanceled) { + onReceivedPairingCanceled(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(INSTANCE_KEY_PAIRING_CANCELED, mReceivedPairingCanceled); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + unregisterReceiver(mReceiver); + } + + private View createView() { + View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); + + String name = mLocalManager.getLocalDeviceManager().getName(mAddress); + TextView messageView = (TextView) view.findViewById(R.id.message); + messageView.setText(getString(R.string.bluetooth_enter_pin_msg, name)); + + mPinView = (EditText) view.findViewById(R.id.text); + mPinView.addTextChangedListener(this); + // Maximum of 10 characters in a PIN + mPinView.setFilters(new InputFilter[] { new LengthFilter(10) }); + + return view; + } + + public void afterTextChanged(Editable s) { + if (s.length() > 0) { + mOkButton.setEnabled(true); + } + } + + private void onReceivedPairingCanceled() { + mReceivedPairingCanceled = true; + + TextView messageView = (TextView) findViewById(R.id.message); + messageView.setText(getString(R.string.bluetooth_pairing_error_message, + mLocalManager.getLocalDeviceManager().getName(mAddress))); + + mPinView.setVisibility(View.GONE); + mPinView.clearFocus(); + mPinView.removeTextChangedListener(this); + + mOkButton.setEnabled(true); + mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE); + } + + private void onPair(String pin) { + byte[] pinBytes = BluetoothDevice.convertPinToBytes(pin); + + if (pinBytes == null) { + return; + } + + mLocalManager.getBluetoothManager().setPin(mAddress, pinBytes); + } + + private void onCancel() { + mLocalManager.getBluetoothManager().cancelBondProcess(mAddress); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + onPair(mPinView.getText().toString()); + break; + + case DialogInterface.BUTTON_NEGATIVE: + onCancel(); + break; + } + } + + /* Not used */ + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + /* Not used */ + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothPinRequest.java b/src/com/android/settings/bluetooth/BluetoothPinRequest.java new file mode 100644 index 000000000..619052d87 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPinRequest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.text.TextUtils; + +/** + * BluetoothPinRequest is a receiver for any Bluetooth pairing PIN request. It + * checks if the Bluetooth Settings is currently visible and brings up the PIN + * entry dialog. Otherwise it puts a Notification in the status bar, which can + * be clicked to bring up the PIN entry dialog. + */ +public class BluetoothPinRequest extends BroadcastReceiver { + + public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothIntent.PAIRING_REQUEST_ACTION)) { + + LocalBluetoothManager localManager = LocalBluetoothManager.getInstance(context); + + String address = intent.getStringExtra(BluetoothIntent.ADDRESS); + Intent pinIntent = new Intent(); + pinIntent.setClass(context, BluetoothPinDialog.class); + pinIntent.putExtra(BluetoothIntent.ADDRESS, address); + pinIntent.setAction(BluetoothIntent.PAIRING_REQUEST_ACTION); + pinIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (localManager.getForegroundActivity() != null) { + // Since the BT-related activity is in the foreground, just open the dialog + context.startActivity(pinIntent); + + } else { + + // Put up a notification that leads to the dialog + Resources res = context.getResources(); + Notification notification = new Notification( + android.R.drawable.stat_sys_data_bluetooth, + res.getString(R.string.bluetooth_notif_ticker), + System.currentTimeMillis()); + + PendingIntent pending = PendingIntent.getActivity(context, 0, + pinIntent, PendingIntent.FLAG_ONE_SHOT); + + String name = intent.getStringExtra(BluetoothIntent.NAME); + if (TextUtils.isEmpty(name)) { + name = localManager.getLocalDeviceManager().getName(address); + } + + notification.setLatestEventInfo(context, + res.getString(R.string.bluetooth_notif_title), + res.getString(R.string.bluetooth_notif_message) + name, + pending); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + + NotificationManager manager = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.notify(NOTIFICATION_ID, notification); + } + + } else if (action.equals(BluetoothIntent.PAIRING_CANCEL_ACTION)) { + + // Remove the notification + NotificationManager manager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(NOTIFICATION_ID); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java new file mode 100644 index 000000000..5adada3c4 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.ProgressCategory; +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothManager.ExtendedBluetoothState; + +import java.util.List; +import java.util.WeakHashMap; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView.AdapterContextMenuInfo; + +/** + * BluetoothSettings is the Settings screen for Bluetooth configuration and + * connection management. + */ +public class BluetoothSettings extends PreferenceActivity + implements LocalBluetoothManager.Callback { + + private static final String TAG = "BluetoothSettings"; + + private static final int MENU_SCAN = Menu.FIRST; + + private static final String KEY_BT_CHECKBOX = "bt_checkbox"; + private static final String KEY_BT_DISCOVERABLE = "bt_discoverable"; + private static final String KEY_BT_DEVICE_LIST = "bt_device_list"; + private static final String KEY_BT_NAME = "bt_name"; + private static final String KEY_BT_SCAN = "bt_scan"; + + private LocalBluetoothManager mLocalManager; + + private BluetoothEnabler mEnabler; + private BluetoothDiscoverableEnabler mDiscoverableEnabler; + + private BluetoothNamePreference mNamePreference; + + private ProgressCategory mDeviceList; + + private WeakHashMap<LocalBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = + new WeakHashMap<LocalBluetoothDevice, BluetoothDevicePreference>(); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: put this in callback instead of receiving + onBluetoothStateChanged(mLocalManager.getBluetoothState()); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLocalManager = LocalBluetoothManager.getInstance(this); + if (mLocalManager == null) finish(); + + addPreferencesFromResource(R.xml.bluetooth_settings); + + mEnabler = new BluetoothEnabler( + this, + (CheckBoxPreference) findPreference(KEY_BT_CHECKBOX)); + + mDiscoverableEnabler = new BluetoothDiscoverableEnabler( + this, + (CheckBoxPreference) findPreference(KEY_BT_DISCOVERABLE)); + + mNamePreference = (BluetoothNamePreference) findPreference(KEY_BT_NAME); + + mDeviceList = (ProgressCategory) findPreference(KEY_BT_DEVICE_LIST); + + registerForContextMenu(getListView()); + } + + @Override + protected void onResume() { + super.onResume(); + + // Repopulate (which isn't too bad since it's cached in the settings + // bluetooth manager + mDevicePreferenceMap.clear(); + mDeviceList.removeAll(); + addDevices(); + + mEnabler.resume(); + mDiscoverableEnabler.resume(); + mNamePreference.resume(); + mLocalManager.registerCallback(this); + + mLocalManager.startScanning(false); + + registerReceiver(mReceiver, + new IntentFilter(LocalBluetoothManager.EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + + mLocalManager.setForegroundActivity(this); + } + + @Override + protected void onPause() { + super.onPause(); + + mLocalManager.setForegroundActivity(null); + + unregisterReceiver(mReceiver); + + mLocalManager.unregisterCallback(this); + mNamePreference.pause(); + mDiscoverableEnabler.pause(); + mEnabler.pause(); + } + + private void addDevices() { + List<LocalBluetoothDevice> devices = mLocalManager.getLocalDeviceManager().getDevicesCopy(); + for (LocalBluetoothDevice device : devices) { + onDeviceAdded(device); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_SCAN, 0, R.string.bluetooth_scan_for_devices) + .setIcon(com.android.internal.R.drawable.ic_menu_refresh) + .setAlphabeticShortcut('r'); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(MENU_SCAN).setEnabled(mLocalManager.getBluetoothManager().isEnabled()); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case MENU_SCAN: + mLocalManager.startScanning(true); + return true; + + default: + return false; + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + + if (KEY_BT_SCAN.equals(preference.getKey())) { + mLocalManager.startScanning(true); + return true; + } + + if (preference instanceof BluetoothDevicePreference) { + BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference; + btPreference.getDevice().onClicked(); + return true; + } + + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + LocalBluetoothDevice device = getDeviceFromMenuInfo(menuInfo); + if (device == null) return; + + device.onCreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + LocalBluetoothDevice device = getDeviceFromMenuInfo(item.getMenuInfo()); + if (device == null) return false; + + device.onContextItemSelected(item); + return true; + } + + private LocalBluetoothDevice getDeviceFromMenuInfo(ContextMenuInfo menuInfo) { + if ((menuInfo == null) || !(menuInfo instanceof AdapterContextMenuInfo)) { + return null; + } + + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + Preference pref = (Preference) getPreferenceScreen().getRootAdapter().getItem( + adapterMenuInfo.position); + if (pref == null || !(pref instanceof BluetoothDevicePreference)) { + return null; + } + + return ((BluetoothDevicePreference) pref).getDevice(); + } + + public void onDeviceAdded(LocalBluetoothDevice device) { + + if (mDevicePreferenceMap.get(device) != null) { + throw new IllegalStateException("Got onDeviceAdded, but device already exists"); + } + + createDevicePreference(device); + } + + private void createDevicePreference(LocalBluetoothDevice device) { + BluetoothDevicePreference preference = new BluetoothDevicePreference(this, device); + mDeviceList.addPreference(preference); + mDevicePreferenceMap.put(device, preference); + } + + public void onDeviceDeleted(LocalBluetoothDevice device) { + BluetoothDevicePreference preference = mDevicePreferenceMap.remove(device); + if (preference != null) { + mDeviceList.removePreference(preference); + } + } + + public void onScanningStateChanged(boolean started) { + mDeviceList.setProgress(started); + } + + private void onBluetoothStateChanged(ExtendedBluetoothState bluetoothState) { + // When bluetooth is enabled (and we are in the activity, which we are), + // we should start a scan + if (bluetoothState == ExtendedBluetoothState.ENABLED) { + mLocalManager.startScanning(false); + } else if (bluetoothState == ExtendedBluetoothState.DISABLED) { + mDeviceList.setProgress(false); + } + } +} diff --git a/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java b/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java new file mode 100644 index 000000000..7dd1b706d --- /dev/null +++ b/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.text.TextUtils; +import android.util.Log; + +/** + * ConnectSpecificProfilesActivity presents the user with all of the profiles + * for a particular device, and allows him to choose which should be connected + * (or disconnected). + */ +public class ConnectSpecificProfilesActivity extends PreferenceActivity + implements LocalBluetoothDevice.Callback, Preference.OnPreferenceChangeListener { + private static final String TAG = "ConnectSpecificProfilesActivity"; + + private static final String KEY_ONLINE_MODE = "online_mode"; + private static final String KEY_TITLE = "title"; + private static final String KEY_PROFILE_CONTAINER = "profile_container"; + + public static final String EXTRA_ADDRESS = "address"; + + private LocalBluetoothManager mManager; + private LocalBluetoothDevice mDevice; + + private PreferenceGroup mProfileContainer; + private CheckBoxPreference mOnlineModePreference; + + /** + * The current mode of this activity and its checkboxes (either online mode + * or offline mode). In online mode, user interactions with the profile + * checkboxes will also toggle the profile's connectivity. In offline mode, + * they will not, and only the preferred state will be saved for the + * profile. + */ + private boolean mOnlineMode; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String address; + if (savedInstanceState != null) { + address = savedInstanceState.getString(EXTRA_ADDRESS); + } else { + Intent intent = getIntent(); + address = intent.getStringExtra(EXTRA_ADDRESS); + } + + if (TextUtils.isEmpty(address)) { + Log.w(TAG, "Activity started without address"); + finish(); + } + + mManager = LocalBluetoothManager.getInstance(this); + mDevice = mManager.getLocalDeviceManager().findDevice(address); + if (mDevice == null) { + Log.w(TAG, "Device not found, cannot connect to it"); + finish(); + } + + addPreferencesFromResource(R.xml.bluetooth_device_advanced); + mProfileContainer = (PreferenceGroup) findPreference(KEY_PROFILE_CONTAINER); + + // Set the title of the screen + findPreference(KEY_TITLE).setTitle( + getString(R.string.bluetooth_device_advanced_title, mDevice.getName())); + + // Listen for check/uncheck of the online mode checkbox + mOnlineModePreference = (CheckBoxPreference) findPreference(KEY_ONLINE_MODE); + mOnlineModePreference.setOnPreferenceChangeListener(this); + + // Add a preference for each profile + addPreferencesForProfiles(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(EXTRA_ADDRESS, mDevice.getAddress()); + } + + @Override + protected void onResume() { + super.onResume(); + + mManager.setForegroundActivity(this); + mDevice.registerCallback(this); + + refresh(); + } + + @Override + protected void onPause() { + super.onPause(); + + mDevice.unregisterCallback(this); + mManager.setForegroundActivity(null); + } + + private void addPreferencesForProfiles() { + for (Profile profile : mDevice.getProfiles()) { + Preference pref = createProfilePreference(profile); + mProfileContainer.addPreference(pref); + } + } + + /** + * Creates a checkbox preference for the particular profile. The key will be + * the profile's name. + * + * @param profile The profile for which the preference controls. + * @return A preference that allows the user to choose whether this profile + * will be connected to. + */ + private CheckBoxPreference createProfilePreference(Profile profile) { + CheckBoxPreference pref = new CheckBoxPreference(this); + pref.setKey(profile.toString()); + pref.setTitle(profile.localizedString); + pref.setPersistent(false); + pref.setOnPreferenceChangeListener(this); + + refreshProfilePreference(pref, profile); + + return pref; + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (TextUtils.isEmpty(key) || newValue == null) return true; + + if (key.equals(KEY_ONLINE_MODE)) { + onOnlineModeCheckedStateChanged((Boolean) newValue); + + } else { + Profile profile = getProfileOf(preference); + if (profile == null) return false; + onProfileCheckedStateChanged(profile, (Boolean) newValue); + } + + return true; + } + + private void onOnlineModeCheckedStateChanged(boolean checked) { + setOnlineMode(checked, true); + } + + private void onProfileCheckedStateChanged(Profile profile, boolean checked) { + if (mOnlineMode) { + if (checked) { + mDevice.connect(profile); + } else { + mDevice.disconnect(profile); + } + } + + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mManager, profile); + profileManager.setPreferred(mDevice.getAddress(), checked); + } + + public void onDeviceAttributesChanged(LocalBluetoothDevice device) { + refresh(); + } + + private void refresh() { + // We are in 'online mode' if we are connected, connecting, or disconnecting + setOnlineMode(mDevice.isConnected() || mDevice.isBusy(), false); + refreshProfiles(); + } + + /** + * Switches between online/offline mode. + * + * @param onlineMode Whether to be in online mode, or offline mode. + * @param takeAction Whether to take action (i.e., connect or disconnect) + * based on the new online mode. + */ + private void setOnlineMode(boolean onlineMode, boolean takeAction) { + mOnlineMode = onlineMode; + + if (takeAction) { + if (onlineMode) { + mDevice.connect(); + } else { + mDevice.disconnect(); + } + } + + refreshOnlineModePreference(); + } + + private void refreshOnlineModePreference() { + mOnlineModePreference.setChecked(mOnlineMode); + + /* Gray out checkbox while connecting and disconnecting */ + mOnlineModePreference.setEnabled(!mDevice.isBusy()); + + /** + * If the device is online, show status. Otherwise, show a summary that + * describes what the checkbox does. + */ + mOnlineModePreference.setSummary(mOnlineMode ? mDevice.getSummary() + : R.string.bluetooth_device_advanced_online_mode_summary); + } + + private void refreshProfiles() { + for (Profile profile : mDevice.getProfiles()) { + CheckBoxPreference profilePref = + (CheckBoxPreference) findPreference(profile.toString()); + if (profilePref == null) { + profilePref = createProfilePreference(profile); + mProfileContainer.addPreference(profilePref); + } else { + refreshProfilePreference(profilePref, profile); + } + } + } + + private void refreshProfilePreference(CheckBoxPreference profilePref, Profile profile) { + String address = mDevice.getAddress(); + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mManager, profile); + + int connectionStatus = profileManager.getConnectionStatus(address); + + /* Gray out checkbox while connecting and disconnecting */ + profilePref.setEnabled(!mDevice.isBusy()); + + profilePref.setSummary(getProfileSummary(profileManager, profile, address, + connectionStatus, mOnlineMode)); + + profilePref.setChecked(profileManager.isPreferred(address)); + } + + private Profile getProfileOf(Preference pref) { + if (!(pref instanceof CheckBoxPreference)) return null; + String key = pref.getKey(); + if (TextUtils.isEmpty(key)) return null; + + try { + return Profile.valueOf(pref.getKey()); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static int getProfileSummary(LocalBluetoothProfileManager profileManager, + Profile profile, String address, int connectionStatus, boolean onlineMode) { + if (!onlineMode || connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED) { + return getProfileSummaryForSettingPreference(profile); + } else { + return profileManager.getSummary(address); + } + } + + /** + * Gets the summary that describes when checked, it will become a preferred profile. + * + * @param profile The profile to get the summary for. + * @return The summary. + */ + private static final int getProfileSummaryForSettingPreference(Profile profile) { + switch (profile) { + case A2DP: + return R.string.bluetooth_a2dp_profile_summary_use_for; + case HEADSET: + return R.string.bluetooth_headset_profile_summary_use_for; + default: + return 0; + } + } + +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothDevice.java b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java new file mode 100644 index 000000000..a4885401e --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile; + +import android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothClass; +import android.bluetooth.IBluetoothDeviceCallback; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.ArrayList; +import java.util.List; + +/** + * LocalBluetoothDevice represents a remote Bluetooth device. It contains + * attributes of the device (such as the address, name, RSSI, etc.) and + * functionality that can be performed on the device (connect, pair, disconnect, + * etc.). + */ +public class LocalBluetoothDevice implements Comparable<LocalBluetoothDevice> { + private static final String TAG = "LocalBluetoothDevice"; + + private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1; + private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2; + private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3; + private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4; + + private final String mAddress; + private String mName; + private short mRssi; + private int mBtClass = BluetoothClass.ERROR; + + private List<Profile> mProfiles = new ArrayList<Profile>(); + + private boolean mVisible; + + private final LocalBluetoothManager mLocalManager; + + private List<Callback> mCallbacks = new ArrayList<Callback>(); + + /** + * When we connect to multiple profiles, we only want to display a single + * error even if they all fail. This tracks that state. + */ + private boolean mIsConnectingErrorPossible; + + LocalBluetoothDevice(Context context, String address) { + mLocalManager = LocalBluetoothManager.getInstance(context); + if (mLocalManager == null) { + throw new IllegalStateException( + "Cannot use LocalBluetoothDevice without Bluetooth hardware"); + } + + mAddress = address; + + fillData(); + } + + public void onClicked() { + int bondState = getBondState(); + + if (isConnected()) { + askDisconnect(); + } else if (bondState == BluetoothDevice.BOND_BONDED) { + connect(); + } else if (bondState == BluetoothDevice.BOND_NOT_BONDED) { + pair(); + } + } + + public void disconnect() { + for (Profile profile : mProfiles) { + disconnect(profile); + } + } + + public void disconnect(Profile profile) { + LocalBluetoothProfileManager profileManager = + LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); + int status = profileManager.getConnectionStatus(mAddress); + if (SettingsBtStatus.isConnectionStatusConnected(status)) { + profileManager.disconnect(mAddress); + } + } + + public void askDisconnect() { + Context context = mLocalManager.getForegroundActivity(); + if (context == null) { + // Cannot ask, since we need an activity context + disconnect(); + return; + } + + Resources res = context.getResources(); + + String name = getName(); + if (TextUtils.isEmpty(name)) { + name = res.getString(R.string.bluetooth_device); + } + String message = res.getString(R.string.bluetooth_disconnect_blank, name); + + DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + disconnect(); + } + }; + + AlertDialog ad = new AlertDialog.Builder(context) + .setTitle(getName()) + .setMessage(message) + .setPositiveButton(android.R.string.ok, disconnectListener) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + public void connect() { + if (!ensurePaired()) return; + + // Reset the only-show-one-error-dialog tracking variable + mIsConnectingErrorPossible = true; + + Context context = mLocalManager.getContext(); + boolean hasAtLeastOnePreferredProfile = false; + for (Profile profile : mProfiles) { + LocalBluetoothProfileManager profileManager = + LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); + if (profileManager.isPreferred(mAddress)) { + hasAtLeastOnePreferredProfile = true; + connectInt(profile); + } + } + + if (!hasAtLeastOnePreferredProfile) { + connectAndPreferAllProfiles(); + } + } + + private void connectAndPreferAllProfiles() { + if (!ensurePaired()) return; + + // Reset the only-show-one-error-dialog tracking variable + mIsConnectingErrorPossible = true; + + Context context = mLocalManager.getContext(); + for (Profile profile : mProfiles) { + LocalBluetoothProfileManager profileManager = + LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); + profileManager.setPreferred(mAddress, true); + connectInt(profile); + } + } + + public void connect(Profile profile) { + // Reset the only-show-one-error-dialog tracking variable + mIsConnectingErrorPossible = true; + connectInt(profile); + } + + public void connectInt(Profile profile) { + if (!ensurePaired()) return; + + LocalBluetoothProfileManager profileManager = + LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); + int status = profileManager.getConnectionStatus(mAddress); + if (!SettingsBtStatus.isConnectionStatusConnected(status)) { + if (profileManager.connect(mAddress) != BluetoothDevice.RESULT_SUCCESS) { + Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName); + } + } + } + + public void showConnectingError() { + if (!mIsConnectingErrorPossible) return; + mIsConnectingErrorPossible = false; + + mLocalManager.showError(mAddress, R.string.bluetooth_error_title, + R.string.bluetooth_connecting_error_message); + } + + private boolean ensurePaired() { + if (getBondState() == BluetoothDevice.BOND_NOT_BONDED) { + pair(); + return false; + } else { + return true; + } + } + + public void pair() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + // Pairing is unreliable while scanning, so cancel discovery + if (manager.isDiscovering()) { + manager.cancelDiscovery(); + } + + if (!mLocalManager.getBluetoothManager().createBond(mAddress)) { + mLocalManager.showError(mAddress, R.string.bluetooth_error_title, + R.string.bluetooth_pairing_error_message); + } + } + + public void unpair() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + switch (getBondState()) { + case BluetoothDevice.BOND_BONDED: + manager.removeBond(mAddress); + break; + + case BluetoothDevice.BOND_BONDING: + manager.cancelBondProcess(mAddress); + break; + } + } + + private void fillData() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + fetchName(); + fetchBtClass(); + + mVisible = false; + + dispatchAttributesChanged(); + } + + public String getAddress() { + return mAddress; + } + + public String getName() { + return mName; + } + + public void refreshName() { + fetchName(); + dispatchAttributesChanged(); + } + + private void fetchName() { + mName = mLocalManager.getBluetoothManager().getRemoteName(mAddress); + + if (TextUtils.isEmpty(mName)) { + mName = mAddress; + } + } + + public void refresh() { + dispatchAttributesChanged(); + } + + public boolean isVisible() { + return mVisible; + } + + void setVisible(boolean visible) { + if (mVisible != visible) { + mVisible = visible; + dispatchAttributesChanged(); + } + } + + public int getBondState() { + return mLocalManager.getBluetoothManager().getBondState(mAddress); + } + + void setRssi(short rssi) { + if (mRssi != rssi) { + mRssi = rssi; + dispatchAttributesChanged(); + } + } + + /** + * Checks whether we are connected to this device (any profile counts). + * + * @return Whether it is connected. + */ + public boolean isConnected() { + for (Profile profile : mProfiles) { + int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile) + .getConnectionStatus(mAddress); + if (SettingsBtStatus.isConnectionStatusConnected(status)) { + return true; + } + } + + return false; + } + + public boolean isBusy() { + for (Profile profile : mProfiles) { + int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile) + .getConnectionStatus(mAddress); + if (SettingsBtStatus.isConnectionStatusBusy(status)) { + return true; + } + } + + if (getBondState() == BluetoothDevice.BOND_BONDING) { + return true; + } + + return false; + } + + public int getBtClassDrawable() { + + // First try looking at profiles + if (mProfiles.contains(Profile.A2DP)) { + return R.drawable.ic_bt_headphones_a2dp; + } else if (mProfiles.contains(Profile.HEADSET)) { + return R.drawable.ic_bt_headset_hfp; + } + + // Fallback on class + switch (BluetoothClass.Device.Major.getDeviceMajor(mBtClass)) { + case BluetoothClass.Device.Major.COMPUTER: + return R.drawable.ic_bt_laptop; + + case BluetoothClass.Device.Major.PHONE: + return R.drawable.ic_bt_cellphone; + + default: + return 0; + } + } + + /** + * Fetches a new value for the cached BT class. + */ + private void fetchBtClass() { + mBtClass = mLocalManager.getBluetoothManager().getRemoteClass(mAddress); + mProfiles.clear(); + LocalBluetoothProfileManager.fill(mBtClass, mProfiles); + } + + /** + * Refreshes the UI for the BT class, including fetching the latest value + * for the class. + */ + public void refreshBtClass() { + fetchBtClass(); + dispatchAttributesChanged(); + } + + public int getSummary() { + // TODO: clean up + int oneOffSummary = getOneOffSummary(); + if (oneOffSummary != 0) { + return oneOffSummary; + } + + for (Profile profile : mProfiles) { + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mLocalManager, profile); + int connectionStatus = profileManager.getConnectionStatus(mAddress); + + if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) || + connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING || + connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) { + return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); + } + } + + return SettingsBtStatus.getPairingStatusSummary(getBondState()); + } + + /** + * We have special summaries when particular profiles are connected. This + * checks for those states and returns an applicable summary. + * + * @return A one-off summary that is applicable for the current state, or 0. + */ + private int getOneOffSummary() { + boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false; + + if (mProfiles.contains(Profile.A2DP)) { + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mLocalManager, Profile.A2DP); + isConnecting = profileManager.getConnectionStatus(mAddress) == + SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + isA2dpConnected = profileManager.isConnected(mAddress); + } + + if (mProfiles.contains(Profile.HEADSET)) { + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mLocalManager, Profile.HEADSET); + isConnecting |= profileManager.getConnectionStatus(mAddress) == + SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + isHeadsetConnected = profileManager.isConnected(mAddress); + } + + if (isConnecting) { + // If any of these important profiles is connecting, prefer that + return SettingsBtStatus.getConnectionStatusSummary( + SettingsBtStatus.CONNECTION_STATUS_CONNECTING); + } else if (isA2dpConnected && isHeadsetConnected) { + return R.string.bluetooth_summary_connected_to_a2dp_headset; + } else if (isA2dpConnected) { + return R.string.bluetooth_summary_connected_to_a2dp; + } else if (isHeadsetConnected) { + return R.string.bluetooth_summary_connected_to_headset; + } else { + return 0; + } + } + + public List<Profile> getProfiles() { + return new ArrayList<Profile>(mProfiles); + } + + public void onCreateContextMenu(ContextMenu menu) { + // No context menu if it is busy (none of these items are applicable if busy) + if (isBusy()) return; + + int bondState = getBondState(); + boolean isConnected = isConnected(); + boolean hasProfiles = mProfiles.size() > 0; + + menu.setHeaderTitle(getName()); + + if (isConnected) { + menu.add(0, CONTEXT_ITEM_DISCONNECT, 0, R.string.bluetooth_device_context_disconnect); + } else if (hasProfiles) { + // For connection action, show either "Connect" or "Pair & connect" + int connectString = (bondState == BluetoothDevice.BOND_NOT_BONDED) + ? R.string.bluetooth_device_context_pair_connect + : R.string.bluetooth_device_context_connect; + menu.add(0, CONTEXT_ITEM_CONNECT, 0, connectString); + } + + if (bondState == BluetoothDevice.BOND_BONDED) { + // For unpair action, show either "Unpair" or "Disconnect & unpair" + int unpairString = isConnected + ? R.string.bluetooth_device_context_disconnect_unpair + : R.string.bluetooth_device_context_unpair; + menu.add(0, CONTEXT_ITEM_UNPAIR, 0, unpairString); + + // Show the connection options item + menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0, + R.string.bluetooth_device_context_connect_advanced); + } + } + + /** + * Called when a context menu item is clicked. + * + * @param item The item that was clicked. + */ + public void onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case CONTEXT_ITEM_DISCONNECT: + disconnect(); + break; + + case CONTEXT_ITEM_CONNECT: + connect(); + break; + + case CONTEXT_ITEM_UNPAIR: + mLocalManager.getBluetoothManager().disconnectRemoteDeviceAcl(mAddress); + unpair(); + break; + + case CONTEXT_ITEM_CONNECT_ADVANCED: + Intent intent = new Intent(); + // Need an activity context to open this in our task + Context context = mLocalManager.getForegroundActivity(); + if (context == null) { + // Fallback on application context, and open in a new task + context = mLocalManager.getContext(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + intent.setClass(context, ConnectSpecificProfilesActivity.class); + intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_ADDRESS, mAddress); + context.startActivity(intent); + break; + } + } + + public void registerCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.add(callback); + } + } + + public void unregisterCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } + } + + private void dispatchAttributesChanged() { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onDeviceAttributesChanged(this); + } + } + } + + @Override + public String toString() { + return mAddress; + } + + @Override + public boolean equals(Object o) { + if ((o == null) || !(o instanceof LocalBluetoothDevice)) { + throw new ClassCastException(); + } + + return mAddress.equals(((LocalBluetoothDevice) o).mAddress); + } + + @Override + public int hashCode() { + return mAddress.hashCode(); + } + + public int compareTo(LocalBluetoothDevice another) { + int comparison; + + // Connected above not connected + comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); + if (comparison != 0) return comparison; + + // Paired above not paired + comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - + (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); + if (comparison != 0) return comparison; + + // Visible above not visible + comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0); + if (comparison != 0) return comparison; + + // Stronger signal above weaker signal + comparison = another.mRssi - mRssi; + if (comparison != 0) return comparison; + + // Fallback on name + return getName().compareTo(another.getName()); + } + + public interface Callback { + void onDeviceAttributesChanged(LocalBluetoothDevice device); + } +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java b/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java new file mode 100644 index 000000000..6bb2b4afe --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.util.Log; +import android.widget.Toast; +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothManager.Callback; + +import java.util.ArrayList; +import java.util.List; + +/** + * LocalBluetoothDeviceManager manages the set of remote Bluetooth devices. + */ +public class LocalBluetoothDeviceManager { + private static final String TAG = "LocalBluetoothDeviceManager"; + + final LocalBluetoothManager mLocalManager; + final List<Callback> mCallbacks; + + final List<LocalBluetoothDevice> mDevices = new ArrayList<LocalBluetoothDevice>(); + + public LocalBluetoothDeviceManager(LocalBluetoothManager localManager) { + mLocalManager = localManager; + mCallbacks = localManager.getCallbacks(); + readPairedDevices(); + } + + private synchronized boolean readPairedDevices() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + String[] bondedAddresses = manager.listBonds(); + if (bondedAddresses == null) return false; + + boolean deviceAdded = false; + for (String address : bondedAddresses) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + device = new LocalBluetoothDevice(mLocalManager.getContext(), address); + mDevices.add(device); + dispatchDeviceAdded(device); + deviceAdded = true; + } + } + + return deviceAdded; + } + + public synchronized List<LocalBluetoothDevice> getDevicesCopy() { + return new ArrayList<LocalBluetoothDevice>(mDevices); + } + + void onBluetoothStateChanged(boolean enabled) { + if (enabled) { + readPairedDevices(); + } + } + + public synchronized void onDeviceAppeared(String address, short rssi) { + boolean deviceAdded = false; + + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + device = new LocalBluetoothDevice(mLocalManager.getContext(), address); + mDevices.add(device); + deviceAdded = true; + } + + device.setRssi(rssi); + device.setVisible(true); + + if (deviceAdded) { + dispatchDeviceAdded(device); + } + } + + public synchronized void onDeviceDisappeared(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + device.setVisible(false); + checkForDeviceRemoval(device); + } + + private void checkForDeviceRemoval(LocalBluetoothDevice device) { + if (device.getBondState() == BluetoothDevice.BOND_NOT_BONDED && + !device.isVisible()) { + // If device isn't paired, remove it altogether + mDevices.remove(device); + dispatchDeviceDeleted(device); + } + } + + public synchronized void onDeviceNameUpdated(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device != null) { + device.refreshName(); + } + } + + public synchronized LocalBluetoothDevice findDevice(String address) { + + for (int i = mDevices.size() - 1; i >= 0; i--) { + LocalBluetoothDevice device = mDevices.get(i); + + if (device.getAddress().equals(address)) { + return device; + } + } + + return null; + } + + /** + * Attempts to get the name of a remote device, otherwise returns the address. + * + * @param address The address. + * @return The name, or if unavailable, the address. + */ + public String getName(String address) { + LocalBluetoothDevice device = findDevice(address); + return device != null ? device.getName() : address; + } + + private void dispatchDeviceAdded(LocalBluetoothDevice device) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onDeviceAdded(device); + } + } + + // TODO: divider between prev paired/connected and scanned + } + + private void dispatchDeviceDeleted(LocalBluetoothDevice device) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onDeviceDeleted(device); + } + } + } + + public synchronized void onBondingStateChanged(String address, int bondState) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + if (!readPairedDevices()) { + Log.e(TAG, "Got bonding state changed for " + address + + ", but we have no record of that device."); + } + return; + } + + device.refresh(); + + if (bondState == BluetoothDevice.BOND_BONDED) { + // Auto-connect after pairing + device.connect(); + } + } + + /** + * Called when there is a bonding error. + * + * @param address The address of the remote device. + * @param reason The reason, one of the error reasons from + * BluetoothDevice.UNBOND_REASON_* + */ + public synchronized void onBondingError(String address, int reason) { + mLocalManager.showError(address, R.string.bluetooth_error_title, + (reason == BluetoothDevice.UNBOND_REASON_AUTH_FAILED) ? + R.string.bluetooth_pairing_pin_error_message : + R.string.bluetooth_pairing_error_message); + } + + public synchronized void onProfileStateChanged(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + device.refresh(); + } + + public synchronized void onConnectingError(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + /* + * Go through the device's delegate so we don't spam the user with + * errors connecting to different profiles, and instead make sure the + * user sees a single error for his single 'connect' action. + */ + device.showConnectingError(); + } + + public synchronized void onScanningStateChanged(boolean started) { + if (!started) return; + + // If starting a new scan, clear old visibility + for (int i = mDevices.size() - 1; i >= 0; i--) { + LocalBluetoothDevice device = mDevices.get(i); + device.setVisible(false); + checkForDeviceRemoval(device); + } + } + + public synchronized void onBtClassChanged(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device != null) { + device.refreshBtClass(); + } + } +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothManager.java b/src/com/android/settings/bluetooth/LocalBluetoothManager.java new file mode 100644 index 000000000..4671fac6e --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothManager.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.widget.Toast; + +// TODO: have some notion of shutting down. Maybe a minute after they leave BT settings? +/** + * LocalBluetoothManager provides a simplified interface on top of a subset of + * the Bluetooth API. + */ +public class LocalBluetoothManager { + private static final String TAG = "LocalBluetoothManager"; + static final boolean V = true; + + public static final String EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION = + "com.android.settings.bluetooth.intent.action.EXTENDED_BLUETOOTH_STATE_CHANGED"; + private static final String SHARED_PREFERENCES_NAME = "bluetooth_settings"; + + private static LocalBluetoothManager INSTANCE; + /** Used when obtaining a reference to the singleton instance. */ + private static Object INSTANCE_LOCK = new Object(); + private boolean mInitialized; + + private Context mContext; + /** If a BT-related activity is in the foreground, this will be it. */ + private Activity mForegroundActivity; + private AlertDialog mErrorDialog = null; + + private BluetoothDevice mManager; + + private LocalBluetoothDeviceManager mLocalDeviceManager; + private BluetoothEventRedirector mEventRedirector; + private BluetoothA2dp mBluetoothA2dp; + + public static enum ExtendedBluetoothState { ENABLED, ENABLING, DISABLED, DISABLING, UNKNOWN } + private ExtendedBluetoothState mState = ExtendedBluetoothState.UNKNOWN; + + private List<Callback> mCallbacks = new ArrayList<Callback>(); + + private static final int SCAN_EXPIRATION_MS = 5 * 60 * 1000; // 5 mins + private long mLastScan; + + public static LocalBluetoothManager getInstance(Context context) { + synchronized (INSTANCE_LOCK) { + if (INSTANCE == null) { + INSTANCE = new LocalBluetoothManager(); + } + + if (!INSTANCE.init(context)) { + return null; + } + + return INSTANCE; + } + } + + private boolean init(Context context) { + if (mInitialized) return true; + mInitialized = true; + + // This will be around as long as this process is + mContext = context.getApplicationContext(); + + mManager = (BluetoothDevice) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (mManager == null) { + return false; + } + + mLocalDeviceManager = new LocalBluetoothDeviceManager(this); + + mEventRedirector = new BluetoothEventRedirector(this); + mEventRedirector.start(); + + mBluetoothA2dp = new BluetoothA2dp(context); + + return true; + } + + public BluetoothDevice getBluetoothManager() { + return mManager; + } + + public Context getContext() { + return mContext; + } + + public Activity getForegroundActivity() { + return mForegroundActivity; + } + + public void setForegroundActivity(Activity activity) { + if (mErrorDialog != null) { + mErrorDialog.dismiss(); + mErrorDialog = null; + } + mForegroundActivity = activity; + } + + public SharedPreferences getSharedPreferences() { + return mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + } + + public LocalBluetoothDeviceManager getLocalDeviceManager() { + return mLocalDeviceManager; + } + + List<Callback> getCallbacks() { + return mCallbacks; + } + + public void registerCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.add(callback); + } + } + + public void unregisterCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } + } + + public void startScanning(boolean force) { + if (mManager.isDiscovering()) { + /* + * Already discovering, but give the callback that information. + * Note: we only call the callbacks, not the same path as if the + * scanning state had really changed (in that case the device + * manager would clear its list of unpaired scanned devices). + */ + dispatchScanningStateChanged(true); + } else { + if (!force) { + // Don't scan more than frequently than SCAN_EXPIRATION_MS, + // unless forced + if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) { + return; + } + + // If we are playing music, don't scan unless forced. + List<String> sinks = mBluetoothA2dp.listConnectedSinks(); + if (sinks != null) { + for (String address : sinks) { + if (mBluetoothA2dp.getSinkState(address) == BluetoothA2dp.STATE_PLAYING) { + return; + } + } + } + } + + if (mManager.startDiscovery(true)) { + mLastScan = System.currentTimeMillis(); + } + } + } + + public ExtendedBluetoothState getBluetoothState() { + + if (mState == ExtendedBluetoothState.UNKNOWN) { + syncBluetoothState(); + } + + return mState; + } + + void setBluetoothStateInt(ExtendedBluetoothState state) { + mState = state; + + /* + * TODO: change to callback method. originally it was broadcast to + * parallel the framework's method, but it just complicates things here. + */ + // If this were a real API, I'd add as an extra + mContext.sendBroadcast(new Intent(EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + + if (state == ExtendedBluetoothState.ENABLED || state == ExtendedBluetoothState.DISABLED) { + mLocalDeviceManager.onBluetoothStateChanged(state == ExtendedBluetoothState.ENABLED); + } + } + + private void syncBluetoothState() { + setBluetoothStateInt(mManager.isEnabled() + ? ExtendedBluetoothState.ENABLED + : ExtendedBluetoothState.DISABLED); + } + + public void setBluetoothEnabled(boolean enabled) { + boolean wasSetStateSuccessful = enabled + ? mManager.enable() + : mManager.disable(); + + if (wasSetStateSuccessful) { + setBluetoothStateInt(enabled + ? ExtendedBluetoothState.ENABLING + : ExtendedBluetoothState.DISABLING); + } else { + if (V) { + Log.v(TAG, + "setBluetoothEnabled call, manager didn't return success for enabled: " + + enabled); + } + + syncBluetoothState(); + } + } + + /** + * @param started True if scanning started, false if scanning finished. + */ + void onScanningStateChanged(boolean started) { + // TODO: have it be a callback (once we switch bluetooth state changed to callback) + mLocalDeviceManager.onScanningStateChanged(started); + dispatchScanningStateChanged(started); + } + + private void dispatchScanningStateChanged(boolean started) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onScanningStateChanged(started); + } + } + } + + public void showError(String address, int titleResId, int messageResId) { + LocalBluetoothDevice device = mLocalDeviceManager.findDevice(address); + if (device == null) return; + + String name = device.getName(); + String message = mContext.getString(messageResId, name); + + if (mForegroundActivity != null) { + // Need an activity context to show a dialog + mErrorDialog = new AlertDialog.Builder(mForegroundActivity) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(titleResId) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } else { + // Fallback on a toast + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + } + + public interface Callback { + void onScanningStateChanged(boolean started); + void onDeviceAdded(LocalBluetoothDevice device); + void onDeviceDeleted(LocalBluetoothDevice device); + } + +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java new file mode 100644 index 000000000..a1a2af60c --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import com.android.settings.R; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothClass; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * LocalBluetoothProfileManager is an abstract class defining the basic + * functionality related to a profile. + */ +public abstract class LocalBluetoothProfileManager { + + // TODO: close profiles when we're shutting down + private static Map<Profile, LocalBluetoothProfileManager> sProfileMap = + new HashMap<Profile, LocalBluetoothProfileManager>(); + + protected LocalBluetoothManager mLocalManager; + + public static LocalBluetoothProfileManager getProfileManager(LocalBluetoothManager localManager, + Profile profile) { + + LocalBluetoothProfileManager profileManager; + + synchronized (sProfileMap) { + profileManager = sProfileMap.get(profile); + + if (profileManager == null) { + switch (profile) { + case A2DP: + profileManager = new A2dpProfileManager(localManager); + break; + + case HEADSET: + profileManager = new HeadsetProfileManager(localManager); + break; + } + + sProfileMap.put(profile, profileManager); + } + } + + return profileManager; + } + + /** + * Temporary method to fill profiles based on a device's class. + * + * @param btClass The class + * @param profiles The list of profiles to fill + */ + public static void fill(int btClass, List<Profile> profiles) { + profiles.clear(); + + if (BluetoothA2dp.doesClassMatchSink(btClass)) { + profiles.add(Profile.A2DP); + } + + if (BluetoothHeadset.doesClassMatch(btClass)) { + profiles.add(Profile.HEADSET); + } + } + + protected LocalBluetoothProfileManager(LocalBluetoothManager localManager) { + mLocalManager = localManager; + } + + public abstract int connect(String address); + + public abstract int disconnect(String address); + + public abstract int getConnectionStatus(String address); + + public abstract int getSummary(String address); + + public abstract boolean isPreferred(String address); + + public abstract void setPreferred(String address, boolean preferred); + + public boolean isConnected(String address) { + return SettingsBtStatus.isConnectionStatusConnected(getConnectionStatus(address)); + } + + // TODO: int instead of enum + public enum Profile { + HEADSET(R.string.bluetooth_profile_headset), + A2DP(R.string.bluetooth_profile_a2dp); + + public final int localizedString; + + private Profile(int localizedString) { + this.localizedString = localizedString; + } + } + + /** + * A2dpProfileManager is an abstraction for the {@link BluetoothA2dp} service. + */ + private static class A2dpProfileManager extends LocalBluetoothProfileManager { + private BluetoothA2dp mService; + + public A2dpProfileManager(LocalBluetoothManager localManager) { + super(localManager); + mService = new BluetoothA2dp(localManager.getContext()); + } + + @Override + public int connect(String address) { + return mService.connectSink(address); + } + + @Override + public int disconnect(String address) { + return mService.disconnectSink(address); + } + + @Override + public int getConnectionStatus(String address) { + return convertState(mService.getSinkState(address)); + } + + @Override + public int getSummary(String address) { + int connectionStatus = getConnectionStatus(address); + + if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { + return R.string.bluetooth_a2dp_profile_summary_connected; + } else { + return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); + } + } + + @Override + public boolean isPreferred(String address) { + return mService.getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF; + } + + @Override + public void setPreferred(String address, boolean preferred) { + mService.setSinkPriority(address, + preferred ? BluetoothA2dp.PRIORITY_AUTO : BluetoothA2dp.PRIORITY_OFF); + } + + private static int convertState(int a2dpState) { + switch (a2dpState) { + case BluetoothA2dp.STATE_CONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; + case BluetoothA2dp.STATE_CONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + case BluetoothA2dp.STATE_DISCONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + case BluetoothA2dp.STATE_DISCONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING; + case BluetoothA2dp.STATE_PLAYING: + return SettingsBtStatus.CONNECTION_STATUS_ACTIVE; + default: + return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; + } + } + } + + /** + * HeadsetProfileManager is an abstraction for the {@link BluetoothHeadset} service. + */ + private static class HeadsetProfileManager extends LocalBluetoothProfileManager + implements BluetoothHeadset.ServiceListener { + private BluetoothHeadset mService; + private Handler mUiHandler = new Handler(); + + public HeadsetProfileManager(LocalBluetoothManager localManager) { + super(localManager); + mService = new BluetoothHeadset(localManager.getContext(), this); + } + + public void onServiceConnected() { + // This could be called on a non-UI thread, funnel to UI thread. + mUiHandler.post(new Runnable() { + public void run() { + /* + * We just bound to the service, so refresh the UI of the + * headset device. + */ + String address = mService.getHeadsetAddress(); + if (TextUtils.isEmpty(address)) return; + mLocalManager.getLocalDeviceManager().onProfileStateChanged(address); + } + }); + } + + public void onServiceDisconnected() { + } + + @Override + public int connect(String address) { + // Since connectHeadset fails if already connected to a headset, we + // disconnect from any headset first + mService.disconnectHeadset(); + return mService.connectHeadset(address) + ? BluetoothError.SUCCESS : BluetoothError.ERROR; + } + + @Override + public int disconnect(String address) { + if (mService.getHeadsetAddress().equals(address)) { + return mService.disconnectHeadset() ? BluetoothError.SUCCESS : BluetoothError.ERROR; + } else { + return BluetoothError.SUCCESS; + } + } + + @Override + public int getConnectionStatus(String address) { + String headsetAddress = mService.getHeadsetAddress(); + return headsetAddress != null && headsetAddress.equals(address) + ? convertState(mService.getState()) + : SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + } + + @Override + public int getSummary(String address) { + int connectionStatus = getConnectionStatus(address); + + if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { + return R.string.bluetooth_headset_profile_summary_connected; + } else { + return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); + } + } + + @Override + public boolean isPreferred(String address) { + return mService.getPriority(address) > BluetoothHeadset.PRIORITY_OFF; + } + + @Override + public void setPreferred(String address, boolean preferred) { + mService.setPriority(address, + preferred ? BluetoothHeadset.PRIORITY_AUTO : BluetoothHeadset.PRIORITY_OFF); + } + + private static int convertState(int headsetState) { + switch (headsetState) { + case BluetoothHeadset.STATE_CONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; + case BluetoothHeadset.STATE_CONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + case BluetoothHeadset.STATE_DISCONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + default: + return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; + } + } + } +} diff --git a/src/com/android/settings/bluetooth/SettingsBtStatus.java b/src/com/android/settings/bluetooth/SettingsBtStatus.java new file mode 100644 index 000000000..d2cbef5c5 --- /dev/null +++ b/src/com/android/settings/bluetooth/SettingsBtStatus.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.bluetooth.BluetoothDevice; + +import com.android.settings.R; + +/** + * SettingsBtStatus is a helper class that contains constants for various status + * codes. + */ +public class SettingsBtStatus { + private static final String TAG = "SettingsBtStatus"; + + // Connection status + + public static final int CONNECTION_STATUS_UNKNOWN = 0; + public static final int CONNECTION_STATUS_ACTIVE = 1; + /** Use {@link #isConnected} to check for the connected state */ + public static final int CONNECTION_STATUS_CONNECTED = 2; + public static final int CONNECTION_STATUS_CONNECTING = 3; + public static final int CONNECTION_STATUS_DISCONNECTED = 4; + public static final int CONNECTION_STATUS_DISCONNECTING = 5; + + public static final int getConnectionStatusSummary(int connectionStatus) { + switch (connectionStatus) { + case CONNECTION_STATUS_ACTIVE: + return R.string.bluetooth_connected; + case CONNECTION_STATUS_CONNECTED: + return R.string.bluetooth_connected; + case CONNECTION_STATUS_CONNECTING: + return R.string.bluetooth_connecting; + case CONNECTION_STATUS_DISCONNECTED: + return R.string.bluetooth_disconnected; + case CONNECTION_STATUS_DISCONNECTING: + return R.string.bluetooth_disconnecting; + case CONNECTION_STATUS_UNKNOWN: + return R.string.bluetooth_unknown; + default: + return 0; + } + } + + public static final boolean isConnectionStatusConnected(int connectionStatus) { + return connectionStatus == CONNECTION_STATUS_ACTIVE + || connectionStatus == CONNECTION_STATUS_CONNECTED; + } + + public static final boolean isConnectionStatusBusy(int connectionStatus) { + return connectionStatus == CONNECTION_STATUS_CONNECTING + || connectionStatus == CONNECTION_STATUS_DISCONNECTING; + } + + public static final int getPairingStatusSummary(int bondState) { + switch (bondState) { + case BluetoothDevice.BOND_BONDED: + return R.string.bluetooth_paired; + case BluetoothDevice.BOND_BONDING: + return R.string.bluetooth_pairing; + case BluetoothDevice.BOND_NOT_BONDED: + return R.string.bluetooth_not_connected; + default: + return 0; + } + } +} diff --git a/src/com/android/settings/deviceinfo/Memory.java b/src/com/android/settings/deviceinfo/Memory.java new file mode 100644 index 000000000..75a84b719 --- /dev/null +++ b/src/com/android/settings/deviceinfo/Memory.java @@ -0,0 +1,218 @@ +/* + * 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.deviceinfo; + +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.IBinder; +import android.os.RemoteException; +import android.os.Environment; +import android.os.IMountService; +import android.os.ServiceManager; +import android.os.StatFs; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.util.Log; + +import com.android.settings.R; + +import java.io.File; +import java.text.DecimalFormat; + +public class Memory extends PreferenceActivity { + + private static final String TAG = "Memory"; + + private static final String MEMORY_SD_SIZE = "memory_sd_size"; + + private static final String MEMORY_SD_AVAIL = "memory_sd_avail"; + + private static final String MEMORY_SD_UNMOUNT = "memory_sd_unmount"; + + private static final String MEMORY_SD_FORMAT = "memory_sd_format"; + private Resources mRes; + + private Preference mSdSize; + private Preference mSdAvail; + private Preference mSdUnmount; + private Preference mSdFormat; + + // Access using getMountService() + private IMountService mMountService = null; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.device_info_memory); + + mRes = getResources(); + mSdSize = findPreference(MEMORY_SD_SIZE); + mSdAvail = findPreference(MEMORY_SD_AVAIL); + mSdUnmount = findPreference(MEMORY_SD_UNMOUNT); + mSdFormat = findPreference(MEMORY_SD_FORMAT); + } + + @Override + protected void onResume() { + super.onResume(); + + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_REMOVED); + intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + intentFilter.addAction(Intent.ACTION_MEDIA_SHARED); + intentFilter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTABLE); + intentFilter.addAction(Intent.ACTION_MEDIA_NOFS); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); + intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); + intentFilter.addDataScheme("file"); + registerReceiver(mReceiver, intentFilter); + + updateMemoryStatus(); + } + + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(mReceiver); + } + + private synchronized IMountService getMountService() { + if (mMountService == null) { + IBinder service = ServiceManager.getService("mount"); + if (service != null) { + mMountService = IMountService.Stub.asInterface(service); + } else { + Log.e(TAG, "Can't get mount service"); + } + } + return mMountService; + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (preference == mSdUnmount) { + unmount(); + return true; + } else if (preference == mSdFormat) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setClass(this, com.android.settings.MediaFormat.class); + startActivity(intent); + return true; + } + + return false; + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateMemoryStatus(); + } + }; + + private void unmount() { + IMountService mountService = getMountService(); + try { + if (mountService != null) { + mountService.unmountMedia(Environment.getExternalStorageDirectory().toString()); + } else { + Log.e(TAG, "Mount service is null, can't unmount"); + } + } catch (RemoteException ex) { + // Failed for some reason, try to update UI to actual state + updateMemoryStatus(); + } + } + + private void updateMemoryStatus() { + String status = Environment.getExternalStorageState(); + String readOnly = ""; + if (status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { + status = Environment.MEDIA_MOUNTED; + readOnly = mRes.getString(R.string.read_only); + } + + mSdFormat.setEnabled(false); + + if (status.equals(Environment.MEDIA_MOUNTED)) { + try { + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long totalBlocks = stat.getBlockCount(); + long availableBlocks = stat.getAvailableBlocks(); + + mSdSize.setSummary(formatSize(totalBlocks * blockSize)); + mSdAvail.setSummary(formatSize(availableBlocks * blockSize) + readOnly); + mSdUnmount.setEnabled(true); + } catch (IllegalArgumentException e) { + // this can occur if the SD card is removed, but we haven't received the + // ACTION_MEDIA_REMOVED Intent yet. + status = Environment.MEDIA_REMOVED; + } + + } else { + mSdSize.setSummary(mRes.getString(R.string.sd_unavailable)); + mSdAvail.setSummary(mRes.getString(R.string.sd_unavailable)); + mSdUnmount.setEnabled(false); + + if (status.equals(Environment.MEDIA_UNMOUNTED) || + status.equals(Environment.MEDIA_NOFS) || + status.equals(Environment.MEDIA_UNMOUNTABLE) ) { + mSdFormat.setEnabled(true); + } + + + } + + File path = Environment.getDataDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + findPreference("memory_internal_avail").setSummary(formatSize(availableBlocks * blockSize)); + } + + private String formatSize(long size) { + String suffix = null; + + // add KB or MB suffix if size is greater than 1K or 1M + if (size >= 1024) { + suffix = " KB"; + size /= 1024; + if (size >= 1024) { + suffix = " MB"; + size /= 1024; + } + } + + DecimalFormat formatter = new DecimalFormat(); + formatter.setGroupingSize(3); + String result = formatter.format(size); + + if (suffix != null) + result = result + suffix; + return result; + } + +} diff --git a/src/com/android/settings/deviceinfo/Status.java b/src/com/android/settings/deviceinfo/Status.java new file mode 100644 index 000000000..4132ed5a3 --- /dev/null +++ b/src/com/android/settings/deviceinfo/Status.java @@ -0,0 +1,392 @@ +/* + * 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.deviceinfo; + +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.BatteryManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.NetStat; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneFactory; +import com.android.internal.telephony.PhoneStateIntentReceiver; +import com.android.internal.telephony.TelephonyProperties; +import com.android.settings.R; + +import java.lang.ref.WeakReference; + +/** + * Display the following information + * # Phone Number + * # Network + * # Roaming + * # IMEI + * # Network type + * # Signal Strength + * # Battery Strength : TODO + * # Uptime + * # Awake Time + * # XMPP/buzz/tickle status : TODO + * + */ +public class Status extends PreferenceActivity { + + private static final String KEY_WIFI_MAC_ADDRESS = "wifi_mac_address"; + private static final String KEY_BT_ADDRESS = "bt_address"; + private static final String KEY_NETWORK_TRAFFIC_STATS = "network_traffic_stats"; + private static final int EVENT_SIGNAL_STRENGTH_CHANGED = 200; + private static final int EVENT_SERVICE_STATE_CHANGED = 300; + + private static final int EVENT_UPDATE_STATS = 500; + + private TelephonyManager mTelephonyManager; + private Phone mPhone = null; + private PhoneStateIntentReceiver mPhoneStateReceiver; + private Resources mRes; + private Preference mSignalStrength; + private Preference mUptime; + private Preference mAwakeTime; + + private static String sUnknown; + + private Preference mBatteryStatus; + private Preference mBatteryLevel; + + private Handler mHandler; + + private static class MyHandler extends Handler { + private WeakReference<Status> mStatus; + + public MyHandler(Status activity) { + mStatus = new WeakReference<Status>(activity); + } + + @Override + public void handleMessage(Message msg) { + Status status = mStatus.get(); + if (status == null) { + return; + } + + switch (msg.what) { + case EVENT_SIGNAL_STRENGTH_CHANGED: + status.updateSignalStrength(); + break; + + case EVENT_SERVICE_STATE_CHANGED: + ServiceState serviceState = status.mPhoneStateReceiver.getServiceState(); + status.updateServiceState(serviceState); + break; + + case EVENT_UPDATE_STATS: + status.updateTimes(); + status.setNetworkTrafficStats(); + sendEmptyMessageDelayed(EVENT_UPDATE_STATS, 1000); + break; + } + } + } + + private BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + + int level = intent.getIntExtra("level", 0); + int scale = intent.getIntExtra("scale", 100); + + mBatteryLevel.setSummary(String.valueOf(level * 100 / scale) + "%"); + + int plugType = intent.getIntExtra("plugged", 0); + int status = intent.getIntExtra("status", BatteryManager.BATTERY_STATUS_UNKNOWN); + String statusString; + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + statusString = getString(R.string.battery_info_status_charging); + if (plugType > 0) { + statusString = statusString + " " + getString( + (plugType == BatteryManager.BATTERY_PLUGGED_AC) + ? R.string.battery_info_status_charging_ac + : R.string.battery_info_status_charging_usb); + } + } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { + statusString = getString(R.string.battery_info_status_discharging); + } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) { + statusString = getString(R.string.battery_info_status_not_charging); + } else if (status == BatteryManager.BATTERY_STATUS_FULL) { + statusString = getString(R.string.battery_info_status_full); + } else { + statusString = getString(R.string.battery_info_status_unknown); + } + mBatteryStatus.setSummary(statusString); + } + } + }; + + private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onDataConnectionStateChanged(int state) { + updateDataState(); + updateNetworkType(); + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mHandler = new MyHandler(this); + + mTelephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); + + addPreferencesFromResource(R.xml.device_info_status); + mBatteryLevel = findPreference("battery_level"); + mBatteryStatus = findPreference("battery_status"); + + mRes = getResources(); + if (sUnknown == null) { + sUnknown = mRes.getString(R.string.device_info_default); + } + + mPhone = PhoneFactory.getDefaultPhone(); + mSignalStrength = findPreference("signal_strength"); + mUptime = findPreference("up_time"); + mAwakeTime = findPreference("awake_time"); + + setSummaryText("imei", mPhone.getDeviceId()); + setSummaryText("imei_sv", + ((TelephonyManager) getSystemService(TELEPHONY_SERVICE)) + .getDeviceSoftwareVersion()); + setSummaryText("number", mPhone.getLine1Number()); + + mPhoneStateReceiver = new PhoneStateIntentReceiver(this, mHandler); + mPhoneStateReceiver.notifySignalStrength(EVENT_SIGNAL_STRENGTH_CHANGED); + mPhoneStateReceiver.notifyServiceState(EVENT_SERVICE_STATE_CHANGED); + + setWifiStatus(); + setBtStatus(); + } + + @Override + protected void onResume() { + super.onResume(); + + mPhoneStateReceiver.registerIntent(); + registerReceiver(mBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + + updateSignalStrength(); + updateServiceState(mPhone.getServiceState()); + updateDataState(); + + mTelephonyManager.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_DATA_CONNECTION_STATE); + + mHandler.sendEmptyMessage(EVENT_UPDATE_STATS); + } + + @Override + public void onPause() { + super.onPause(); + + mPhoneStateReceiver.unregisterIntent(); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + unregisterReceiver(mBatteryInfoReceiver); + mHandler.removeMessages(EVENT_UPDATE_STATS); + } + + /** + * @param preference The key for the Preference item + * @param property The system property to fetch + * @param alt The default value, if the property doesn't exist + */ + private void setSummary(String preference, String property, String alt) { + try { + findPreference(preference).setSummary( + SystemProperties.get(property, alt)); + } catch (RuntimeException e) { + + } + } + + private void setSummaryText(String preference, String text) { + if (TextUtils.isEmpty(text)) { + text = sUnknown; + } + + findPreference(preference).setSummary(text); + } + + private void updateNetworkType() { + // Whether EDGE, UMTS, etc... + setSummary("network_type", TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, sUnknown); + } + + private void updateDataState() { + int state = mTelephonyManager.getDataState(); + String display = mRes.getString(R.string.radioInfo_unknown); + + switch (state) { + case TelephonyManager.DATA_CONNECTED: + display = mRes.getString(R.string.radioInfo_data_connected); + break; + case TelephonyManager.DATA_SUSPENDED: + display = mRes.getString(R.string.radioInfo_data_suspended); + break; + case TelephonyManager.DATA_CONNECTING: + display = mRes.getString(R.string.radioInfo_data_connecting); + break; + case TelephonyManager.DATA_DISCONNECTED: + display = mRes.getString(R.string.radioInfo_data_disconnected); + break; + } + + setSummaryText("data_state", display); + } + + private void updateServiceState(ServiceState serviceState) { + int state = serviceState.getState(); + String display = mRes.getString(R.string.radioInfo_unknown); + + switch (state) { + case ServiceState.STATE_IN_SERVICE: + display = mRes.getString(R.string.radioInfo_service_in); + break; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + display = mRes.getString(R.string.radioInfo_service_out); + break; + case ServiceState.STATE_POWER_OFF: + display = mRes.getString(R.string.radioInfo_service_off); + break; + } + + setSummaryText("service_state", display); + + if (serviceState.getRoaming()) { + setSummaryText("roaming_state", mRes.getString(R.string.radioInfo_roaming_in)); + } else { + setSummaryText("roaming_state", mRes.getString(R.string.radioInfo_roaming_not)); + } + setSummaryText("operator_name", serviceState.getOperatorAlphaLong()); + } + + void updateSignalStrength() { + int state = + mPhoneStateReceiver.getServiceState().getState(); + Resources r = getResources(); + + if ((ServiceState.STATE_OUT_OF_SERVICE == state) || + (ServiceState.STATE_POWER_OFF == state)) { + mSignalStrength.setSummary("0"); + } + + int signalDbm = mPhoneStateReceiver.getSignalStrengthDbm(); + + if (-1 == signalDbm) signalDbm = 0; + + int signalAsu = mPhoneStateReceiver.getSignalStrength(); + + if (-1 == signalAsu) signalAsu = 0; + + mSignalStrength.setSummary(String.valueOf(signalDbm) + " " + + r.getString(R.string.radioInfo_display_dbm) + " " + + String.valueOf(signalAsu) + " " + + r.getString(R.string.radioInfo_display_asu)); + } + + private void setWifiStatus() { + WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + Preference wifiMacAddressPref = findPreference(KEY_WIFI_MAC_ADDRESS); + String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); + wifiMacAddressPref.setSummary(!TextUtils.isEmpty(macAddress) ? macAddress + : getString(R.string.status_unavailable)); + } + + private void setBtStatus() { + BluetoothDevice bluetooth = (BluetoothDevice) getSystemService(BLUETOOTH_SERVICE); + Preference btAddressPref = findPreference(KEY_BT_ADDRESS); + + if (bluetooth == null) { + // device not BT capable + getPreferenceScreen().removePreference(btAddressPref); + } else { + String address = bluetooth.isEnabled() ? bluetooth.getAddress() : null; + btAddressPref.setSummary(!TextUtils.isEmpty(address) ? address + : getString(R.string.status_unavailable)); + } + } + + private void setNetworkTrafficStats() { + long txPkts = NetStat.getTotalTxPkts(); + long txBytes = NetStat.getTotalTxBytes(); + long rxPkts = NetStat.getTotalRxPkts(); + long rxBytes = NetStat.getTotalRxBytes(); + + Preference netStatsPref = findPreference(KEY_NETWORK_TRAFFIC_STATS); + netStatsPref.setSummary(getString(R.string.status_network_traffic_summary, + txPkts, txBytes, rxPkts, rxBytes)); + } + + void updateTimes() { + long at = SystemClock.uptimeMillis() / 1000; + long ut = SystemClock.elapsedRealtime() / 1000; + long st = ut - at; + + if (ut == 0) { + ut = 1; + } + + mUptime.setSummary(convert(ut)); + mAwakeTime.setSummary(convert(at) + " (" + (((1000 * at / ut) + 5) / 10) + "%)"); + } + + private String pad(int n) { + if (n >= 10) { + return String.valueOf(n); + } else { + return "0" + String.valueOf(n); + } + } + + private String convert(long t) { + int s = (int)(t % 60); + int m = (int)((t / 60) % 60); + int h = (int)((t / 3600)); + + return h + ":" + pad(m) + ":" + pad(s); + } +} diff --git a/src/com/android/settings/quicklaunch/BookmarkPicker.java b/src/com/android/settings/quicklaunch/BookmarkPicker.java new file mode 100644 index 000000000..7152abbaa --- /dev/null +++ b/src/com/android/settings/quicklaunch/BookmarkPicker.java @@ -0,0 +1,331 @@ +/* + * 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.quicklaunch; + +import com.android.settings.R; + +import android.app.ListActivity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SimpleAdapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Activity to pick a bookmark that will be returned to the caller. + * <p> + * Currently, bookmarks are either: + * <li> Activities that are in the launcher + * <li> Activities that are within an app that is capable of being launched with + * the {@link Intent#ACTION_CREATE_SHORTCUT}. + */ +public class BookmarkPicker extends ListActivity implements SimpleAdapter.ViewBinder { + + private static final String TAG = "BookmarkPicker"; + + /** Extra in the returned intent from this activity. */ + public static final String EXTRA_TITLE = "com.android.settings.quicklaunch.TITLE"; + + /** Extra that should be provided, and will be returned. */ + public static final String EXTRA_SHORTCUT = "com.android.settings.quicklaunch.SHORTCUT"; + + /** + * The request code for the screen to create a bookmark that is WITHIN an + * application. For example, Gmail can return a bookmark for the inbox + * folder. + */ + private static final int REQUEST_CREATE_SHORTCUT = 1; + + /** Intent used to get all the activities that are launch-able */ + private static Intent sLaunchIntent; + /** Intent used to get all the activities that are {@link #REQUEST_CREATE_SHORTCUT}-able */ + private static Intent sShortcutIntent; + + /** + * List of ResolveInfo for activities that we can bookmark (either directly + * to the activity, or by launching the activity and it returning a bookmark + * WITHIN that application). + */ + private List<ResolveInfo> mResolveList; + + // List adapter stuff + private static final String KEY_TITLE = "TITLE"; + private static final String KEY_RESOLVE_INFO = "RESOLVE_INFO"; + private static final String sKeys[] = new String[] { KEY_TITLE, KEY_RESOLVE_INFO }; + private static final int sResourceIds[] = new int[] { R.id.title, R.id.icon }; + private SimpleAdapter mMyAdapter; + + /** Display those activities that are launch-able */ + private static final int DISPLAY_MODE_LAUNCH = 0; + /** Display those activities that are able to have bookmarks WITHIN the application */ + private static final int DISPLAY_MODE_SHORTCUT = 1; + private int mDisplayMode = DISPLAY_MODE_LAUNCH; + + private Handler mUiHandler = new Handler(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + updateListAndAdapter(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, DISPLAY_MODE_LAUNCH, 0, R.string.quick_launch_display_mode_applications) + .setIcon(com.android.internal.R.drawable.ic_menu_archive); + menu.add(0, DISPLAY_MODE_SHORTCUT, 0, R.string.quick_launch_display_mode_shortcuts) + .setIcon(com.android.internal.R.drawable.ic_menu_goto); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(DISPLAY_MODE_LAUNCH).setVisible(mDisplayMode != DISPLAY_MODE_LAUNCH); + menu.findItem(DISPLAY_MODE_SHORTCUT).setVisible(mDisplayMode != DISPLAY_MODE_SHORTCUT); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + + case DISPLAY_MODE_LAUNCH: + mDisplayMode = DISPLAY_MODE_LAUNCH; + break; + + case DISPLAY_MODE_SHORTCUT: + mDisplayMode = DISPLAY_MODE_SHORTCUT; + break; + + default: + return false; + } + + updateListAndAdapter(); + return true; + } + + private void ensureIntents() { + if (sLaunchIntent == null) { + sLaunchIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER); + sShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + } + } + + /** + * This should be called from the UI thread. + */ + private void updateListAndAdapter() { + // Get the activities in a separate thread + new Thread("data updater") { + @Override + public void run() { + synchronized (BookmarkPicker.this) { + /* + * Don't touch any of the lists that are being used by the + * adapter in this thread! + */ + ArrayList<ResolveInfo> newResolveList = new ArrayList<ResolveInfo>(); + ArrayList<Map<String, ?>> newAdapterList = new ArrayList<Map<String, ?>>(); + + fillResolveList(newResolveList); + Collections.sort(newResolveList, + new ResolveInfo.DisplayNameComparator(getPackageManager())); + + fillAdapterList(newAdapterList, newResolveList); + + updateAdapterToUseNewLists(newAdapterList, newResolveList); + } + } + }.start(); + } + + private void updateAdapterToUseNewLists(final ArrayList<Map<String, ?>> newAdapterList, + final ArrayList<ResolveInfo> newResolveList) { + // Post this back on the UI thread + mUiHandler.post(new Runnable() { + public void run() { + /* + * SimpleAdapter does not support changing the lists after it + * has been created. We just create a new instance. + */ + mMyAdapter = createResolveAdapter(newAdapterList); + mResolveList = newResolveList; + setListAdapter(mMyAdapter); + } + }); + } + + /** + * Gets all activities matching our current display mode. + * + * @param list The list to fill. + */ + private void fillResolveList(List<ResolveInfo> list) { + ensureIntents(); + PackageManager pm = getPackageManager(); + list.clear(); + + if (mDisplayMode == DISPLAY_MODE_LAUNCH) { + list.addAll(pm.queryIntentActivities(sLaunchIntent, 0)); + } else if (mDisplayMode == DISPLAY_MODE_SHORTCUT) { + list.addAll(pm.queryIntentActivities(sShortcutIntent, 0)); + } + } + + private SimpleAdapter createResolveAdapter(List<Map<String, ?>> list) { + SimpleAdapter adapter = new SimpleAdapter(this, list, + R.layout.bookmark_picker_item, sKeys, sResourceIds); + adapter.setViewBinder(this); + return adapter; + } + + private void fillAdapterList(List<Map<String, ?>> list, + List<ResolveInfo> resolveList) { + list.clear(); + int resolveListSize = resolveList.size(); + for (int i = 0; i < resolveListSize; i++) { + ResolveInfo info = resolveList.get(i); + /* + * Simple adapter craziness. For each item, we need to create a map + * from a key to its value (the value can be any object--the view + * binder will take care of filling the View with a representation + * of that object). + */ + Map<String, Object> map = new TreeMap<String, Object>(); + map.put(KEY_TITLE, getResolveInfoTitle(info)); + map.put(KEY_RESOLVE_INFO, info); + list.add(map); + } + } + + /** Get the title for a resolve info. */ + private String getResolveInfoTitle(ResolveInfo info) { + CharSequence label = info.loadLabel(getPackageManager()); + if (label == null) label = info.activityInfo.name; + return label != null ? label.toString() : null; + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + if (position >= mResolveList.size()) return; + + ResolveInfo info = mResolveList.get(position); + + switch (mDisplayMode) { + + case DISPLAY_MODE_LAUNCH: + // We can go ahead and return the clicked info's intent + Intent intent = getIntentForResolveInfo(info, Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + finish(intent, getResolveInfoTitle(info)); + break; + + case DISPLAY_MODE_SHORTCUT: + // Start the shortcut activity so the user can pick the actual intent + // (example: Gmail's shortcut activity shows a list of mailboxes) + startShortcutActivity(info); + break; + } + + } + + private static Intent getIntentForResolveInfo(ResolveInfo info, String action) { + Intent intent = new Intent(action); + ActivityInfo ai = info.activityInfo; + intent.setClassName(ai.packageName, ai.name); + return intent; + } + + /** + * Starts an activity to get a shortcut. + * <p> + * For example, Gmail has an activity that lists the available labels. It + * returns a shortcut intent for going directly to this label. + */ + private void startShortcutActivity(ResolveInfo info) { + Intent intent = getIntentForResolveInfo(info, Intent.ACTION_CREATE_SHORTCUT); + startActivityForResult(intent, REQUEST_CREATE_SHORTCUT); + + // Will get a callback to onActivityResult + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + return; + } + + switch (requestCode) { + + case REQUEST_CREATE_SHORTCUT: + if (data != null) { + finish((Intent) data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT), + data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME)); + } + break; + + default: + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + + /** + * Finishes the activity and returns the given data. + */ + private void finish(Intent intent, String title) { + // Give back what was given to us (it will have the shortcut, for example) + intent.putExtras(getIntent()); + // Put our information + intent.putExtra(EXTRA_TITLE, title); + setResult(RESULT_OK, intent); + finish(); + } + + /** + * {@inheritDoc} + */ + public boolean setViewValue(View view, Object data, String textRepresentation) { + if (view.getId() == R.id.icon) { + Drawable icon = ((ResolveInfo) data).loadIcon(getPackageManager()); + if (icon != null) { + ((ImageView) view).setImageDrawable(icon); + } + return true; + } else { + return false; + } + } + +} diff --git a/src/com/android/settings/quicklaunch/QuickLaunchSettings.java b/src/com/android/settings/quicklaunch/QuickLaunchSettings.java new file mode 100644 index 000000000..4d44524ac --- /dev/null +++ b/src/com/android/settings/quicklaunch/QuickLaunchSettings.java @@ -0,0 +1,347 @@ +/* + * 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.quicklaunch; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.ContentObserver; +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; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.widget.AdapterView; + +import com.android.settings.R; + +/** + * Settings activity for quick launch. + * <p> + * 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 + AdapterView.OnItemLongClickListener, DialogInterface.OnClickListener { + + private static final String TAG = "QuickLaunchSettings"; + + private static final String KEY_SHORTCUT_CATEGORY = "shortcut_category"; + + private static final int DIALOG_CLEAR_SHORTCUT = 0; + + private static final int REQUEST_PICK_BOOKMARK = 1; + + private static final int COLUMN_SHORTCUT = 0; + private static final int COLUMN_TITLE = 1; + private static final int COLUMN_INTENT = 2; + private static final String[] sProjection = new String[] { + Bookmarks.SHORTCUT, Bookmarks.TITLE, Bookmarks.INTENT + }; + private static final String sShortcutSelection = Bookmarks.SHORTCUT + "=?"; + + private Handler mUiHandler = new Handler(); + + private static final String DEFAULT_BOOKMARK_FOLDER = "@quicklaunch"; + /** Cursor for Bookmarks provider. */ + private Cursor mBookmarksCursor; + /** Listens for changes to Bookmarks provider. */ + private BookmarksObserver mBookmarksObserver; + /** Used to keep track of which shortcuts have bookmarks. */ + private SparseBooleanArray mBookmarkedShortcuts; + + /** Preference category to hold the shortcut preferences. */ + private PreferenceGroup mShortcutGroup; + /** Mapping of a shortcut to its preference. */ + private SparseArray<ShortcutPreference> mShortcutToPreference; + + /** The bookmark title of the shortcut that is being cleared. */ + private CharSequence mClearDialogBookmarkTitle; + private static final String CLEAR_DIALOG_BOOKMARK_TITLE = "CLEAR_DIALOG_BOOKMARK_TITLE"; + /** The shortcut that is being cleared. */ + private char mClearDialogShortcut; + private static final String CLEAR_DIALOG_SHORTCUT = "CLEAR_DIALOG_SHORTCUT"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.quick_launch_settings); + + mShortcutGroup = (PreferenceGroup) findPreference(KEY_SHORTCUT_CATEGORY); + mShortcutToPreference = new SparseArray<ShortcutPreference>(); + mBookmarksObserver = new BookmarksObserver(mUiHandler); + initShortcutPreferences(); + mBookmarksCursor = managedQuery(Bookmarks.CONTENT_URI, sProjection, null, null); + getListView().setOnItemLongClickListener(this); + } + + @Override + protected void onResume() { + super.onResume(); + getContentResolver().registerContentObserver(Bookmarks.CONTENT_URI, true, + mBookmarksObserver); + refreshShortcuts(); + } + + @Override + protected 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); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // Save the clear dialog's info + outState.putCharSequence(CLEAR_DIALOG_BOOKMARK_TITLE, mClearDialogBookmarkTitle); + outState.putInt(CLEAR_DIALOG_SHORTCUT, mClearDialogShortcut); + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + + case DIALOG_CLEAR_SHORTCUT: { + // Create the dialog for clearing a shortcut + return new AlertDialog.Builder(this) + .setTitle(getString(R.string.quick_launch_clear_dialog_title)) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(getString(R.string.quick_launch_clear_dialog_message, + mClearDialogShortcut, mClearDialogBookmarkTitle)) + .setPositiveButton(R.string.quick_launch_clear_ok_button, this) + .setNegativeButton(R.string.quick_launch_clear_cancel_button, this) + .create(); + } + } + + 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; + + mClearDialogBookmarkTitle = pref.getTitle(); + mClearDialogShortcut = pref.getShortcut(); + showDialog(DIALOG_CLEAR_SHORTCUT); + } + + public void onClick(DialogInterface dialog, int which) { + if (mClearDialogShortcut > 0 && which == AlertDialog.BUTTON1) { + // Clear the shortcut + clearShortcut(mClearDialogShortcut); + } + mClearDialogBookmarkTitle = null; + mClearDialogShortcut = 0; + } + + private void clearShortcut(char shortcut) { + getContentResolver().delete(Bookmarks.CONTENT_URI, sShortcutSelection, + new String[] { String.valueOf((int) shortcut) }); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + if (!(preference instanceof ShortcutPreference)) return false; + + // Open the screen to pick a bookmark for this shortcut + ShortcutPreference pref = (ShortcutPreference) preference; + Intent intent = new Intent(this, BookmarkPicker.class); + intent.putExtra(BookmarkPicker.EXTRA_SHORTCUT, pref.getShortcut()); + startActivityForResult(intent, REQUEST_PICK_BOOKMARK); + + return true; + } + + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + + // Open the clear shortcut dialog + Preference pref = (Preference) getPreferenceScreen().getRootAdapter().getItem(position); + if (!(pref instanceof ShortcutPreference)) return false; + showClearDialog((ShortcutPreference) pref); + return true; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + return; + } + + if (requestCode == REQUEST_PICK_BOOKMARK) { + + // Returned from the 'pick bookmark for this shortcut' screen + if (data == null) { + Log.w(TAG, "Result from bookmark picker does not have an intent."); + return; + } + + String title = data.getStringExtra(BookmarkPicker.EXTRA_TITLE); + char shortcut = data.getCharExtra(BookmarkPicker.EXTRA_SHORTCUT, (char) 0); + updateShortcut(shortcut, data); + + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void updateShortcut(char shortcut, Intent intent) { + // Update the bookmark for a shortcut + // Pass an empty title so it gets resolved each time this bookmark is + // displayed (since the locale could change after we insert into the provider). + Bookmarks.add(getContentResolver(), intent, "", DEFAULT_BOOKMARK_FOLDER, shortcut, 0); + } + + private ShortcutPreference getOrCreatePreference(char shortcut) { + ShortcutPreference pref = mShortcutToPreference.get(shortcut); + if (pref != null) { + return pref; + } else { + Log.w(TAG, "Unknown shortcut '" + shortcut + "', creating preference anyway"); + return createPreference(shortcut); + } + } + + private ShortcutPreference createPreference(char shortcut) { + ShortcutPreference pref = new ShortcutPreference(QuickLaunchSettings.this, shortcut); + mShortcutGroup.addPreference(pref); + mShortcutToPreference.put(shortcut, pref); + return pref; + } + + private void initShortcutPreferences() { + + /** Whether the shortcut has been seen already. The array index is the shortcut. */ + SparseBooleanArray shortcutSeen = new SparseBooleanArray(); + KeyCharacterMap keyMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + + // Go through all the key codes and create a preference for the appropriate keys + for (int keyCode = KeyEvent.getMaxKeyCode() - 1; keyCode >= 0; keyCode--) { + // Get the label for the primary char on the key that produces this key code + char shortcut = (char) Character.toLowerCase(keyMap.getDisplayLabel(keyCode)); + if (shortcut == 0 || shortcutSeen.get(shortcut, false)) continue; + // TODO: need a to tell if the current keyboard can produce this key code, for now + // only allow the letter or digits + if (!Character.isLetterOrDigit(shortcut)) continue; + shortcutSeen.put(shortcut, true); + + createPreference(shortcut); + } + } + + private synchronized void refreshShortcuts() { + Cursor c = mBookmarksCursor; + if (c == null) { + // Haven't finished querying yet + return; + } + + if (!c.requery()) { + Log.e(TAG, "Could not requery cursor when refreshing shortcuts."); + return; + } + + /** + * We use the previous bookmarked shortcuts array to filter out those + * shortcuts that had bookmarks before this method call, and don't after + * (so we can set the preferences to be without bookmarks). + */ + SparseBooleanArray noLongerBookmarkedShortcuts = mBookmarkedShortcuts; + SparseBooleanArray newBookmarkedShortcuts = new SparseBooleanArray(); + while (c.moveToNext()) { + char shortcut = Character.toLowerCase((char) c.getInt(COLUMN_SHORTCUT)); + if (shortcut == 0) continue; + + ShortcutPreference pref = getOrCreatePreference(shortcut); + pref.setTitle(Bookmarks.getTitle(this, c)); + pref.setSummary(getString(R.string.quick_launch_shortcut, + String.valueOf(shortcut))); + pref.setHasBookmark(true); + + newBookmarkedShortcuts.put(shortcut, true); + if (noLongerBookmarkedShortcuts != null) { + // After this loop, the shortcuts with value true in this array + // will no longer have bookmarks + noLongerBookmarkedShortcuts.put(shortcut, false); + } + } + + if (noLongerBookmarkedShortcuts != null) { + for (int i = noLongerBookmarkedShortcuts.size() - 1; i >= 0; i--) { + if (noLongerBookmarkedShortcuts.valueAt(i)) { + // True, so there is no longer a bookmark for this shortcut + char shortcut = (char) noLongerBookmarkedShortcuts.keyAt(i); + ShortcutPreference pref = mShortcutToPreference.get(shortcut); + if (pref != null) { + pref.setHasBookmark(false); + } + } + } + } + + mBookmarkedShortcuts = newBookmarkedShortcuts; + + c.deactivate(); + } + + private class BookmarksObserver extends ContentObserver { + + public BookmarksObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + + refreshShortcuts(); + } + } +} diff --git a/src/com/android/settings/quicklaunch/ShortcutPreference.java b/src/com/android/settings/quicklaunch/ShortcutPreference.java new file mode 100644 index 000000000..92efdeb0d --- /dev/null +++ b/src/com/android/settings/quicklaunch/ShortcutPreference.java @@ -0,0 +1,151 @@ +/* + * 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.quicklaunch; + +import com.android.settings.R; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.preference.Preference; +import android.util.TypedValue; +import android.view.View; +import android.widget.TextView; + +/** + * Preference type for a shortcut in {@link QuickLaunchSettings}. + */ +public class ShortcutPreference extends Preference implements Comparable<Preference> { + + private static Object sStaticVarsLock = new Object(); + + // These static fields are used across all instances of ShortcutPreference. + // There will be many ShortcutPreference instances (~36 for US). + private static String STRING_ASSIGN_APPLICATION; + private static String STRING_NO_SHORTCUT; + + private static int sDimAlpha; + private static ColorStateList sRegularTitleColor; + private static ColorStateList sDimTitleColor; + private static ColorStateList sRegularSummaryColor; + private static ColorStateList sDimSummaryColor; + + private char mShortcut; + private boolean mHasBookmark; + + public ShortcutPreference(Context context, char shortcut) { + super(context); + + synchronized (sStaticVarsLock) { + // Init statics. This should only happen for the first ShortcutPreference created, + // the rest will already have them initialized. + if (STRING_ASSIGN_APPLICATION == null) { + STRING_ASSIGN_APPLICATION = context.getString(R.string.quick_launch_assign_application); + STRING_NO_SHORTCUT = context.getString(R.string.quick_launch_no_shortcut); + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); + sDimAlpha = (int) (outValue.getFloat() * 255); + } + } + + mShortcut = shortcut; + + setWidgetLayoutResource(R.layout.preference_widget_shortcut); + } + + public char getShortcut() { + return mShortcut; + } + + public void setShortcut(char shortcut) { + if (shortcut != mShortcut) { + mShortcut = shortcut; + notifyChanged(); + } + } + + public boolean hasBookmark() { + return mHasBookmark; + } + + public void setHasBookmark(boolean hasBookmark) { + if (hasBookmark != mHasBookmark) { + mHasBookmark = hasBookmark; + notifyChanged(); + } + } + + @Override + public CharSequence getTitle() { + return mHasBookmark ? super.getTitle() : STRING_ASSIGN_APPLICATION; + } + + @Override + public CharSequence getSummary() { + return mHasBookmark ? super.getSummary() : STRING_NO_SHORTCUT; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + TextView shortcutView = (TextView) view.findViewById(R.id.shortcut); + if (shortcutView != null) { + shortcutView.setText(String.valueOf(mShortcut)); + } + + TextView titleView = (TextView) view.findViewById(android.R.id.title); + + synchronized (sStaticVarsLock) { + if (sRegularTitleColor == null) { + sRegularTitleColor = titleView.getTextColors(); + sDimTitleColor = sRegularTitleColor.withAlpha(sDimAlpha); + } + } + + ColorStateList color = mHasBookmark ? sRegularTitleColor : sDimTitleColor; + if (color != null) { + titleView.setTextColor(color); + } + + TextView summaryView = (TextView) view.findViewById(android.R.id.summary); + + synchronized (sStaticVarsLock) { + if (sRegularSummaryColor == null) { + sRegularSummaryColor = summaryView.getTextColors(); + sDimSummaryColor = sRegularSummaryColor.withAlpha(sDimAlpha); + } + } + + color = mHasBookmark ? sRegularSummaryColor : sDimSummaryColor; + if (color != null) { + summaryView.setTextColor(color); + } + + } + + public int compareTo(Preference another) { + if (!(another instanceof ShortcutPreference)) return super.compareTo(another); + + // Letters before digits + char other = ((ShortcutPreference) another).mShortcut; + if (Character.isDigit(mShortcut) && Character.isLetter(other)) return 1; + else if (Character.isDigit(other) && Character.isLetter(mShortcut)) return -1; + else return mShortcut - other; + } + +} diff --git a/src/com/android/settings/wifi/AccessPointDialog.java b/src/com/android/settings/wifi/AccessPointDialog.java new file mode 100644 index 000000000..917ed967f --- /dev/null +++ b/src/com/android/settings/wifi/AccessPointDialog.java @@ -0,0 +1,600 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.R; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.text.TextUtils; +import android.text.format.Formatter; +import android.text.method.PasswordTransformationMethod; +import android.text.method.TransformationMethod; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TableLayout; +import android.widget.TextView; + +public class AccessPointDialog extends AlertDialog implements DialogInterface.OnClickListener, + AdapterView.OnItemSelectedListener, View.OnClickListener { + + private static final String TAG = "AccessPointDialog"; + private static final String INSTANCE_KEY_ACCESS_POINT_STATE = + "com.android.settings.wifi.AccessPointDialog:accessPointState"; + private static final String INSTANCE_KEY_MODE = + "com.android.settings.wifi.AccessPointDialog:mode"; + private static final String INSTANCE_KEY_CUSTOM_TITLE = + "com.android.settings.wifi.AccessPointDialog:customTitle"; + private static final String INSTANCE_KEY_AUTO_SECURITY_ALLOWED = + "com.android.settings.wifi.AccessPointDialog:autoSecurityAllowed"; + + private static final int POSITIVE_BUTTON = BUTTON1; + private static final int NEGATIVE_BUTTON = BUTTON2; + private static final int NEUTRAL_BUTTON = BUTTON3; + + /** The dialog should show info connectivity functionality */ + public static final int MODE_INFO = 0; + /** The dialog should configure the detailed AP properties */ + public static final int MODE_CONFIGURE = 1; + /** The dialog should have the password field and connect/cancel */ + public static final int MODE_RETRY_PASSWORD = 2; + + // These should be matched with the XML. Both arrays in XML depend on this + // ordering! + private static final int SECURITY_AUTO = 0; + private static final int SECURITY_NONE = 1; + private static final int SECURITY_WEP = 2; + private static final int SECURITY_WPA_PERSONAL = 3; + private static final int SECURITY_WPA2_PERSONAL = 4; + + private static final int[] WEP_TYPE_VALUES = { + AccessPointState.WEP_PASSWORD_AUTO, AccessPointState.WEP_PASSWORD_ASCII, + AccessPointState.WEP_PASSWORD_HEX + }; + + // Button positions, default to impossible values + private int mConnectButtonPos = Integer.MAX_VALUE; + private int mForgetButtonPos = Integer.MAX_VALUE; + private int mSaveButtonPos = Integer.MAX_VALUE; + + // Client configurable items. Generally, these should be saved in instance state + private int mMode = MODE_INFO; + private boolean mAutoSecurityAllowed = true; + private CharSequence mCustomTitle; + // This does not need to be saved in instance state. + private WifiLayer mWifiLayer; + private AccessPointState mState; + + // General views + private View mView; + private TextView mPasswordText; + private EditText mPasswordEdit; + private CheckBox mShowPasswordCheckBox; + + // Info-specific views + private ViewGroup mTable; + + // Configure-specific views + private EditText mSsidEdit; + private Spinner mSecuritySpinner; + private Spinner mWepTypeSpinner; + + public AccessPointDialog(Context context, WifiLayer wifiLayer) { + super(context); + + mWifiLayer = wifiLayer; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + onLayout(); + onFill(); + + super.onCreate(savedInstanceState); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + // Set to a class loader that can find AccessPointState + savedInstanceState.setClassLoader(getClass().getClassLoader()); + + mState = savedInstanceState.getParcelable(INSTANCE_KEY_ACCESS_POINT_STATE); + mState.setContext(getContext()); + + mMode = savedInstanceState.getInt(INSTANCE_KEY_MODE, mMode); + mAutoSecurityAllowed = savedInstanceState.getBoolean(INSTANCE_KEY_AUTO_SECURITY_ALLOWED, + mAutoSecurityAllowed); + mCustomTitle = savedInstanceState.getCharSequence(INSTANCE_KEY_CUSTOM_TITLE); + if (mCustomTitle != null) { + setTitle(mCustomTitle); + } + + // This is called last since it depends on the above values + super.onRestoreInstanceState(savedInstanceState); + + if (mShowPasswordCheckBox != null) { + // Restore the show-password-state on the edit text + setShowPassword(mShowPasswordCheckBox.isChecked()); + } + } + + @Override + public Bundle onSaveInstanceState() { + Bundle bundle = super.onSaveInstanceState(); + bundle.putParcelable(INSTANCE_KEY_ACCESS_POINT_STATE, mState); + bundle.putInt(INSTANCE_KEY_MODE, mMode); + bundle.putBoolean(INSTANCE_KEY_AUTO_SECURITY_ALLOWED, mAutoSecurityAllowed); + bundle.putCharSequence(INSTANCE_KEY_CUSTOM_TITLE, mCustomTitle); + return bundle; + } + + /** + * Sets state to show in this dialog. + * + * @param state The state. + */ + public void setState(AccessPointState state) { + mState = state; + } + + /** + * Sets the dialog mode. + * @param mode One of {@link #MODE_CONFIGURE} or {@link #MODE_INFO} + */ + public void setMode(int mode) { + mMode = mode; + } + + public void setAutoSecurityAllowed(boolean autoSecurityAllowed) { + mAutoSecurityAllowed = autoSecurityAllowed; + } + + @Override + public void setTitle(CharSequence title) { + super.setTitle(title); + mCustomTitle = title; + } + + @Override + public void setTitle(int titleId) { + setTitle(getContext().getString(titleId)); + } + + /** Called after flags are set, the dialog's layout/etc should be set up here */ + private void onLayout() { + final Context context = getContext(); + final String ssid = mState.getHumanReadableSsid(); + + int positiveButtonResId = 0; + int negativeButtonResId = R.string.cancel; + int neutralButtonResId = 0; + + if (mCustomTitle == null) { + // Generic title is the SSID + // We don't want to trigger this as a custom title, so call super's + super.setTitle(ssid); + } + setInverseBackgroundForced(true); + + boolean defaultPasswordVisibility = true; + + if (mMode == MODE_CONFIGURE) { + setLayout(R.layout.wifi_ap_configure); + + positiveButtonResId = R.string.wifi_save_config; + mSaveButtonPos = POSITIVE_BUTTON; + + } else if (mMode == MODE_INFO) { + setLayout(R.layout.wifi_ap_info); + + if (mState.isConnectable()) { + if (mCustomTitle == null) { + // We don't want to trigger this as a custom title, so call super's + super.setTitle(context.getString(R.string.connect_to_blank, ssid)); + } + positiveButtonResId = R.string.connect; + mConnectButtonPos = POSITIVE_BUTTON; + } + + if (mState.isForgetable()) { + if (positiveButtonResId == 0) { + positiveButtonResId = R.string.forget_network; + mForgetButtonPos = POSITIVE_BUTTON; + } else { + neutralButtonResId = R.string.forget_network; + mForgetButtonPos = NEUTRAL_BUTTON; + } + } + } else if (mMode == MODE_RETRY_PASSWORD) { + setLayout(R.layout.wifi_ap_retry_password); + + positiveButtonResId = R.string.connect; + mConnectButtonPos = POSITIVE_BUTTON; + + setGenericPasswordVisible(true); + defaultPasswordVisibility = false; + } + + if (defaultPasswordVisibility) { + if (!mState.configured && mState.seen && mState.hasSecurity()) { + setGenericPasswordVisible(true); + } else { + setGenericPasswordVisible(false); + } + } + + setButtons(positiveButtonResId, negativeButtonResId, neutralButtonResId); + } + + /** Called when we need to set our member variables to point to the views. */ + private void onReferenceViews(View view) { + mPasswordText = (TextView) view.findViewById(R.id.password_text); + mPasswordEdit = (EditText) view.findViewById(R.id.password_edit); + + mShowPasswordCheckBox = (CheckBox) view.findViewById(R.id.show_password_checkbox); + if (mShowPasswordCheckBox != null) { + mShowPasswordCheckBox.setOnClickListener(this); + } + + if (mMode == MODE_CONFIGURE) { + mSsidEdit = (EditText) view.findViewById(R.id.ssid_edit); + mSecuritySpinner = (Spinner) view.findViewById(R.id.security_spinner); + mSecuritySpinner.setOnItemSelectedListener(this); + setSecuritySpinnerAdapter(); + mWepTypeSpinner = (Spinner) view.findViewById(R.id.wep_type_spinner); + + } else if (mMode == MODE_INFO) { + mTable = (ViewGroup) view.findViewById(R.id.table); + } + + } + + private void setSecuritySpinnerAdapter() { + Context context = getContext(); + int arrayResId = mAutoSecurityAllowed ? R.array.wifi_security_entries + : R.array.wifi_security_without_auto_entries; + + ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(context, + android.R.layout.simple_spinner_item, + context.getResources().getStringArray(arrayResId)); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSecuritySpinner.setAdapter(adapter); + } + + /** Called when the widgets are in-place waiting to be filled with data */ + private void onFill() { + + // Appears in the order added + if (mMode == MODE_INFO) { + if (mState.primary) { + addInfoRow(R.string.wifi_status, mState.getSummarizedStatus()); + addInfoRow(R.string.wifi_link_speed, mState.linkSpeed + WifiInfo.LINK_SPEED_UNITS); + } + + if (mState.seen) { + addInfoRow(R.string.signal, getSignalResId(mState.signal)); + } + + if (mState.security != null) { + addInfoRow(R.string.security, mState.getHumanReadableSecurity()); + } + + if (mState.primary && mState.ipAddress != 0) { + addInfoRow(R.string.ip_address, Formatter.formatIpAddress(mState.ipAddress)); + } + + } else if (mMode == MODE_CONFIGURE) { + String ssid = mState.getHumanReadableSsid(); + if (!TextUtils.isEmpty(ssid)) { + mSsidEdit.setText(ssid); + } + + mPasswordEdit.setHint(R.string.wifi_password_unchanged); + } + + updatePasswordCaption(mState.security); + } + + private void updatePasswordCaption(String security) { + + if (mPasswordText != null && security != null + && security.equals(AccessPointState.WEP)) { + mPasswordText.setText(R.string.please_type_hex_key); + } else { + mPasswordText.setText(R.string.please_type_passphrase); + } + } + + private void addInfoRow(int nameResId, String value) { + View rowView = getLayoutInflater().inflate(R.layout.wifi_ap_info_row, mTable, false); + ((TextView) rowView.findViewById(R.id.name)).setText(nameResId); + ((TextView) rowView.findViewById(R.id.value)).setText(value); + mTable.addView(rowView); + } + + private void addInfoRow(int nameResId, int valueResId) { + addInfoRow(nameResId, getContext().getString(valueResId)); + } + + private void setButtons(int positiveResId, int negativeResId, int neutralResId) { + final Context context = getContext(); + + if (positiveResId > 0) { + setButton(context.getString(positiveResId), this); + } + + if (negativeResId > 0) { + setButton2(context.getString(negativeResId), this); + } + + if (neutralResId > 0) { + setButton3(context.getString(neutralResId), this); + } + } + + private void setLayout(int layoutResId) { + setView(mView = getLayoutInflater().inflate(layoutResId, null)); + onReferenceViews(mView); + } + + public void onClick(DialogInterface dialog, int which) { + if (which == mForgetButtonPos) { + handleForget(); + } else if (which == mConnectButtonPos) { + handleConnect(); + } else if (which == mSaveButtonPos) { + handleSave(); + } + } + + private void handleForget() { + if (!replaceStateWithWifiLayerInstance()) return; + mWifiLayer.forgetNetwork(mState); + } + + private void handleConnect() { + if (!replaceStateWithWifiLayerInstance()) { + Log.w(TAG, "Assuming connecting to a new network."); + } + + /* + * If the network is secured and they haven't entered a password, popup + * an error. Allow empty passwords if the state already has a password + * set (since in that scenario, an empty password means keep the old + * password). + */ + String password = getEnteredPassword(); + boolean passwordIsEmpty = TextUtils.isEmpty(password); + + /* + * When 'retry password', they can not enter a blank password. In any + * other mode, we let them enter a blank password if the state already + * has a password. + */ + if (passwordIsEmpty && (!mState.hasPassword() || mMode == MODE_RETRY_PASSWORD) + && (mState.security != null) && !mState.security.equals(AccessPointState.OPEN)) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.error_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(R.string.wifi_password_incorrect_error) + .setPositiveButton(android.R.string.ok, null) + .show(); + return; + } + + if (!passwordIsEmpty) { + mState.setPassword(password); + } + + mWifiLayer.connectToNetwork(mState); + } + + private void handleSave() { + replaceStateWithWifiLayerInstance(); + + String ssid = mSsidEdit.getText().toString(); + String password = mPasswordEdit.getText().toString(); + + mState.setSsid(ssid); + + int securityType = getSecurityTypeFromSpinner(); + + if (!TextUtils.isEmpty(password)) { + switch (securityType) { + + case SECURITY_WPA_PERSONAL: { + mState.setSecurity(AccessPointState.WPA); + mState.setPassword(password); + break; + } + + case SECURITY_WPA2_PERSONAL: { + mState.setSecurity(AccessPointState.WPA2); + mState.setPassword(password); + break; + } + + case SECURITY_AUTO: { + mState.setPassword(password); + break; + } + + case SECURITY_WEP: { + mState.setSecurity(AccessPointState.WEP); + mState.setPassword(password, + WEP_TYPE_VALUES[mWepTypeSpinner.getSelectedItemPosition()]); + break; + } + + } + } else { + mState.setSecurity(AccessPointState.OPEN); + } + + if (securityType == SECURITY_NONE) { + mState.setSecurity(AccessPointState.OPEN); + } + + if (!mWifiLayer.saveNetwork(mState)) { + return; + } + + // Connect right away if they've touched it + if (!mWifiLayer.connectToNetwork(mState)) { + return; + } + + } + + /** + * Replaces our {@link #mState} with the equal WifiLayer instance. This is useful after + * we unparceled the state previously and before we are calling methods on {@link #mWifiLayer}. + * + * @return Whether WifiLayer was able to find an equal state in its set. + */ + private boolean replaceStateWithWifiLayerInstance() { + AccessPointState state = mWifiLayer.getWifiLayerApInstance(mState); + if (state == null) { + return false; + } + + mState = state; + return true; + } + + private int getSecurityTypeFromSpinner() { + int position = mSecuritySpinner.getSelectedItemPosition(); + // If there is no AUTO choice, the position needs 1 added to get + // to the proper spinner position -> security constants mapping + return mAutoSecurityAllowed ? position : position + 1; + } + + private String getEnteredPassword() { + return mPasswordEdit != null ? mPasswordEdit.getText().toString() : null; + } + + /** + * Call the one you want to hide first. + */ + private void setWepVisible(boolean visible) { + setGenericPasswordVisible(visible); + int visibility = visible ? View.VISIBLE : View.GONE; + mWepTypeSpinner.setVisibility(visibility); + } + + /** + * @see #setWepVisible(boolean) + */ + private void setGenericPasswordVisible(boolean visible) { + int visibility = visible ? View.VISIBLE : View.GONE; + mPasswordText.setVisibility(visibility); + mPasswordEdit.setVisibility(visibility); + mShowPasswordCheckBox.setVisibility(visibility); + } + + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (parent == mSecuritySpinner) { + handleSecurityChange(getSecurityTypeFromSpinner()); + } + } + + public void onNothingSelected(AdapterView parent) { + } + + private void handleSecurityChange(int security) { + + switch (security) { + + case SECURITY_NONE: { + setWepVisible(false); + setGenericPasswordVisible(false); + break; + } + + case SECURITY_WEP: { + setGenericPasswordVisible(false); + setWepVisible(true); + updatePasswordCaption(AccessPointState.WEP); + break; + } + + case SECURITY_AUTO: { + setWepVisible(false); + setGenericPasswordVisible(mState.hasSecurity()); + // Shows the generic 'wireless password' + updatePasswordCaption(AccessPointState.WPA); + break; + } + + case SECURITY_WPA_PERSONAL: + case SECURITY_WPA2_PERSONAL: { + setWepVisible(false); + setGenericPasswordVisible(true); + // Both WPA and WPA2 show the same caption, so either is ok + updatePasswordCaption(AccessPointState.WPA); + break; + } + } + } + + private static int getSignalResId(int signal) { + switch (WifiManager.calculateSignalLevel(signal, 4)) { + case 0: { + return R.string.wifi_signal_0; + } + case 1: { + return R.string.wifi_signal_1; + } + case 2: { + return R.string.wifi_signal_2; + } + case 3: { + return R.string.wifi_signal_3; + } + } + + return 0; + } + + + public void onClick(View v) { + if (v == mShowPasswordCheckBox) { + setShowPassword(mShowPasswordCheckBox.isChecked()); + } + } + + private void setShowPassword(boolean showPassword) { + if (mPasswordEdit != null) { + // Toggle password + mPasswordEdit.setTransformationMethod( + showPassword ? + null : + PasswordTransformationMethod.getInstance()); + } + } + +} diff --git a/src/com/android/settings/wifi/AccessPointPreference.java b/src/com/android/settings/wifi/AccessPointPreference.java new file mode 100644 index 000000000..10e09475e --- /dev/null +++ b/src/com/android/settings/wifi/AccessPointPreference.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.R; + +import android.net.wifi.WifiManager; +import android.preference.Preference; +import android.view.View; +import android.widget.ImageView; + +public class AccessPointPreference extends Preference implements + AccessPointState.AccessPointStateCallback { + + // UI states + private static final int[] STATE_ENCRYPTED = { R.attr.state_encrypted }; + private static final int[] STATE_EMPTY = { }; + + // Signal strength indicator + private static final int UI_SIGNAL_LEVELS = 4; + + private WifiSettings mWifiSettings; + + private AccessPointState mState; + + public AccessPointPreference(WifiSettings wifiSettings, AccessPointState state) { + super(wifiSettings, null); + + mWifiSettings = wifiSettings; + mState = state; + + setWidgetLayoutResource(R.layout.preference_widget_wifi_signal); + + state.setCallback(this); + + refresh(); + } + + public void refresh() { + setTitle(mState.getHumanReadableSsid()); + setSummary(mState.getSummarizedStatus()); + + notifyChanged(); + } + + public void refreshAccessPointState() { + refresh(); + + // The ordering of access points could have changed due to the state change, so + // re-evaluate ordering + notifyHierarchyChanged(); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + ImageView signal = (ImageView) view.findViewById(R.id.signal); + if (mState.seen) { + signal.setImageResource(R.drawable.wifi_signal); + signal.setImageState(mState.hasSecurity() ? STATE_ENCRYPTED : STATE_EMPTY, true); + signal.setImageLevel(getUiSignalLevel()); + } else { + signal.setImageDrawable(null); + } + } + + private int getUiSignalLevel() { + return mState != null ? WifiManager.calculateSignalLevel(mState.signal, UI_SIGNAL_LEVELS) + : 0; + } + + /** + * Returns the {@link AccessPointState} associated with this preference. + * @return The {@link AccessPointState}. + */ + public AccessPointState getAccessPointState() { + return mState; + } + + @Override + public int compareTo(Preference another) { + if (!(another instanceof AccessPointPreference)) { + // Let normal preferences go before us. + // NOTE: we should only be compared to Preference in our + // category. + return 1; + } + + return mState.compareTo(((AccessPointPreference) another).mState); + } + +} + diff --git a/src/com/android/settings/wifi/AccessPointState.java b/src/com/android/settings/wifi/AccessPointState.java new file mode 100644 index 000000000..c22495434 --- /dev/null +++ b/src/com/android/settings/wifi/AccessPointState.java @@ -0,0 +1,879 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.R; + +import android.content.Context; +import android.net.NetworkInfo; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiConfiguration.AuthAlgorithm; +import android.net.wifi.WifiConfiguration.GroupCipher; +import android.net.wifi.WifiConfiguration.KeyMgmt; +import android.net.wifi.WifiConfiguration.PairwiseCipher; +import android.net.wifi.WifiConfiguration.Protocol; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +public final class AccessPointState implements Comparable<AccessPointState>, Parcelable { + + private static final String TAG = "AccessPointState"; + + // Constants used for different security types + public static final String WPA2 = "WPA2"; + public static final String WPA = "WPA"; + public static final String WEP = "WEP"; + public static final String OPEN = "Open"; + + /** String present in capabilities if the scan result is ad-hoc */ + private static final String ADHOC_CAPABILITY = "[IBSS]"; + /** String present in capabilities if the scan result is enterprise secured */ + private static final String ENTERPRISE_CAPABILITY = "-EAP-"; + + // Localized strings for different security types + private static String LOCALIZED_WPA2; + private static String LOCALIZED_WPA; + private static String LOCALIZED_WEP; + private static String LOCALIZED_OPEN; + private static String LOCALIZED_UNKNOWN; + private static String LOCALIZED_VERBOSE_WPA2; + private static String LOCALIZED_VERBOSE_WPA; + private static String LOCALIZED_VERBOSE_WEP; + private static String LOCALIZED_VERBOSE_OPEN; + + + // Localized strings for various messages + private static String SUMMARY_NOT_IN_RANGE; + private static String SUMMARY_REMEMBERED; + private static String SUMMARY_CONNECTION_FAILED; + + public static final String BSSID_ANY = "any"; + public static final int NETWORK_ID_NOT_SET = -1; + /** This should be used with care! */ + static final int NETWORK_ID_ANY = -2; + + public static final int MATCH_NONE = 0; + public static final int MATCH_WEAK = 1; + public static final int MATCH_STRONG = 2; + public static final int MATCH_EXACT = 3; + + // Don't set these directly, use the setters. + public int networkId; + public int priority; + public boolean hiddenSsid; + public int linkSpeed; + public int ipAddress; + public String bssid; + public String ssid; + public int signal; + public boolean primary; + public boolean seen; + public boolean configured; + public NetworkInfo.DetailedState status; + public String security; + public boolean disabled; + + /** + * Use this for sorting based on signal strength. It is a heavily-damped + * time-averaged weighted signal. + */ + private float signalForSorting = Float.MIN_VALUE; + + private static final float DAMPING_FACTOR = 0.2f; + + /** + * This will be a user entered password, and NOT taken from wpa_supplicant + * (since it would give us *) + */ + private String mPassword; + private boolean mConfigHadPassword; + + public static final int WEP_PASSWORD_AUTO = 0; + public static final int WEP_PASSWORD_ASCII = 1; + public static final int WEP_PASSWORD_HEX = 2; + private int mWepPasswordType; + + private Context mContext; + + /** + * If > 0, don't refresh (changes are being batched), use + * {@link #blockRefresh()} and {@link #unblockRefresh()} only. + */ + private int mBlockRefresh; + /** + * This will be set by {@link #requestRefresh} and shouldn't be written to + * elsewhere. + */ + private boolean mNeedsRefresh; + + private AccessPointStateCallback mCallback; + + private StringBuilder mSummaryBuilder = new StringBuilder(); + + interface AccessPointStateCallback { + void refreshAccessPointState(); + } + + public AccessPointState(Context context) { + this(); + + setContext(context); + } + + private AccessPointState() { + bssid = BSSID_ANY; + ssid = ""; + networkId = NETWORK_ID_NOT_SET; + hiddenSsid = false; + } + + void setContext(Context context) { + mContext = context; + setStrings(); + } + + private void setStrings() { + final Context c = mContext; + + if (SUMMARY_NOT_IN_RANGE == null && c != null) { + SUMMARY_NOT_IN_RANGE = c.getString(R.string.summary_not_in_range); + SUMMARY_REMEMBERED = c.getString(R.string.summary_remembered); + SUMMARY_CONNECTION_FAILED = c.getString(R.string.summary_connection_failed); + + LOCALIZED_OPEN = c.getString(R.string.wifi_security_open); + LOCALIZED_WEP = c.getString(R.string.wifi_security_wep); + LOCALIZED_WPA = c.getString(R.string.wifi_security_wpa); + LOCALIZED_WPA2 = c.getString(R.string.wifi_security_wpa2); + + LOCALIZED_VERBOSE_OPEN = c.getString(R.string.wifi_security_verbose_open); + LOCALIZED_VERBOSE_WEP = c.getString(R.string.wifi_security_verbose_wep); + LOCALIZED_VERBOSE_WPA = c.getString(R.string.wifi_security_verbose_wpa); + LOCALIZED_VERBOSE_WPA2 = c.getString(R.string.wifi_security_verbose_wpa2); + + LOCALIZED_UNKNOWN = c.getString(R.string.wifi_security_unknown); + } + } + + public void setNetworkId(int networkId) { + this.networkId = networkId; + } + + public void setBssid(String bssid) { + if (bssid != null) { + // If the BSSID is a wildcard, do NOT let a specific BSSID replace it + if (!this.bssid.equals(BSSID_ANY)) { + this.bssid = bssid; + } + } + } + + private String getWpaSupplicantBssid() { + return bssid.equals(BSSID_ANY) ? null : bssid; + } + + public static String convertToQuotedString(String string) { + if (TextUtils.isEmpty(string)) { + return ""; + } + + final int lastPos = string.length() - 1; + if (lastPos < 0 || (string.charAt(0) == '"' && string.charAt(lastPos) == '"')) { + return string; + } + + return "\"" + string + "\""; + } + + public void setPrimary(boolean primary) { + if (this.primary != primary) { + this.primary = primary; + requestRefresh(); + } + } + + public void setSeen(boolean seen) { + if (this.seen != seen) { + this.seen = seen; + requestRefresh(); + } + } + + public void setDisabled(boolean disabled) { + if (this.disabled != disabled) { + this.disabled = disabled; + requestRefresh(); + } + } + + public void setSignal(int signal) { + + if (signalForSorting == Float.MIN_VALUE) { + signalForSorting = signal; + } else { + signalForSorting = (DAMPING_FACTOR * signal) + ((1-DAMPING_FACTOR) * signalForSorting); + } + + if (this.signal != signal) { + this.signal = signal; + requestRefresh(); + } + } + + public String getHumanReadableSsid() { + if (TextUtils.isEmpty(ssid)) { + return ""; + } + + final int lastPos = ssid.length() - 1; + if (ssid.charAt(0) == '"' && ssid.charAt(lastPos) == '"') { + return ssid.substring(1, lastPos); + } + + return ssid; + } + + public void setSsid(String ssid) { + if (ssid != null) { + this.ssid = convertToQuotedString(ssid); + requestRefresh(); + } + } + + public void setPriority(int priority) { + if (this.priority != priority) { + this.priority = priority; + requestRefresh(); + } + } + + public void setHiddenSsid(boolean hiddenSsid) { + if (this.hiddenSsid != hiddenSsid) { + this.hiddenSsid = hiddenSsid; + requestRefresh(); + } + } + + public void setLinkSpeed(int linkSpeed) { + if (this.linkSpeed != linkSpeed) { + this.linkSpeed = linkSpeed; + requestRefresh(); + } + } + + public void setIpAddress(int address) { + if (ipAddress != address) { + ipAddress = address; + requestRefresh(); + } + } + + public void setConfigured(boolean configured) { + if (this.configured != configured) { + this.configured = configured; + requestRefresh(); + } + } + + public void setStatus(NetworkInfo.DetailedState status) { + if (this.status != status) { + this.status = status; + requestRefresh(); + } + } + + public void setSecurity(String security) { + if (TextUtils.isEmpty(this.security) || !this.security.equals(security)) { + this.security = security; + requestRefresh(); + } + } + + public boolean hasSecurity() { + return security != null && !security.contains(OPEN); + } + + public String getHumanReadableSecurity() { + if (security.equals(OPEN)) return LOCALIZED_OPEN; + else if (security.equals(WEP)) return LOCALIZED_WEP; + else if (security.equals(WPA)) return LOCALIZED_WPA; + else if (security.equals(WPA2)) return LOCALIZED_WPA2; + + return LOCALIZED_UNKNOWN; + } + + public void updateFromScanResult(ScanResult scanResult) { + blockRefresh(); + + // We don't keep specific AP BSSIDs and instead leave that as wildcard + + setSeen(true); + setSsid(scanResult.SSID); + if (networkId == NETWORK_ID_NOT_SET) { + // Since ScanResults don't cross-reference network ID, we set it as a wildcard + setNetworkId(NETWORK_ID_ANY); + } + setSignal(scanResult.level); + setSecurity(getScanResultSecurity(scanResult)); + unblockRefresh(); + } + + /** + * @return The security of a given {@link ScanResult}. + */ + public static String getScanResultSecurity(ScanResult scanResult) { + final String cap = scanResult.capabilities; + final String[] securityModes = { WEP, WPA, WPA2 }; + for (int i = securityModes.length - 1; i >= 0; i--) { + if (cap.contains(securityModes[i])) { + return securityModes[i]; + } + } + + return OPEN; + } + + /** + * @return Whether the given ScanResult represents an adhoc network. + */ + public static boolean isAdhoc(ScanResult scanResult) { + return scanResult.capabilities.contains(ADHOC_CAPABILITY); + } + + /** + * @return Whether the given ScanResult has enterprise security. + */ + public static boolean isEnterprise(ScanResult scanResult) { + return scanResult.capabilities.contains(ENTERPRISE_CAPABILITY); + } + + public void updateFromWifiConfiguration(WifiConfiguration wifiConfig) { + if (wifiConfig != null) { + blockRefresh(); + setBssid(wifiConfig.BSSID); + setNetworkId(wifiConfig.networkId); + setPriority(wifiConfig.priority); + setHiddenSsid(wifiConfig.hiddenSSID); + setSsid(wifiConfig.SSID); + setConfigured(true); + setDisabled(wifiConfig.status == WifiConfiguration.Status.DISABLED); + parseWifiConfigurationSecurity(wifiConfig); + unblockRefresh(); + } + } + + public void setPassword(String password) { + setPassword(password, WEP_PASSWORD_AUTO); + } + + public void setPassword(String password, int wepPasswordType) { + mPassword = password; + mWepPasswordType = wepPasswordType; + } + + public boolean hasPassword() { + return !TextUtils.isEmpty(mPassword) || mConfigHadPassword; + } + + private static boolean hasPassword(WifiConfiguration wifiConfig) { + return !TextUtils.isEmpty(wifiConfig.preSharedKey) + || !TextUtils.isEmpty(wifiConfig.wepKeys[0]) + || !TextUtils.isEmpty(wifiConfig.wepKeys[1]) + || !TextUtils.isEmpty(wifiConfig.wepKeys[2]) + || !TextUtils.isEmpty(wifiConfig.wepKeys[3]); + } + + private void parseWifiConfigurationSecurity(WifiConfiguration wifiConfig) { + setSecurity(getWifiConfigurationSecurity(wifiConfig)); + mConfigHadPassword = hasPassword(wifiConfig); + } + + /** + * @return The security of a given {@link WifiConfiguration}. + */ + public static String getWifiConfigurationSecurity(WifiConfiguration wifiConfig) { + + if (wifiConfig.allowedKeyManagement.get(KeyMgmt.NONE)) { + // If we never set group ciphers, wpa_supplicant puts all of them. + // For open, we don't set group ciphers. + // For WEP, we specifically only set WEP40 and WEP104, so CCMP + // and TKIP should not be there. + if (!wifiConfig.allowedGroupCiphers.get(GroupCipher.CCMP) + && (wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP40) + || wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP104))) { + return WEP; + } else { + return OPEN; + } + } else if (wifiConfig.allowedProtocols.get(Protocol.RSN)) { + return WPA2; + } else if (wifiConfig.allowedProtocols.get(Protocol.WPA)) { + return WPA; + } else { + Log.w(TAG, "Unknown security type from WifiConfiguration, falling back on open."); + return OPEN; + } + } + + public void updateFromWifiInfo(WifiInfo wifiInfo, NetworkInfo.DetailedState state) { + if (wifiInfo != null) { + blockRefresh(); + setBssid(wifiInfo.getBSSID()); + setLinkSpeed(wifiInfo.getLinkSpeed()); + setNetworkId(wifiInfo.getNetworkId()); + setIpAddress(wifiInfo.getIpAddress()); + setSsid(wifiInfo.getSSID()); + if (state != null) { + setStatus(state); + } + setHiddenSsid(wifiInfo.getHiddenSSID()); + unblockRefresh(); + } + } + + /** + * @return Whether this AP can be connected to at the moment. + */ + public boolean isConnectable() { + return !primary && seen; + } + + /** + * @return Whether this AP can be forgotten at the moment. + */ + public boolean isForgetable() { + return configured; + } + + /** + * Updates the state as if it were never configured. + * <p> + * Note: This will not pass the forget call to the Wi-Fi API. + */ + public void forget() { + blockRefresh(); + setConfigured(false); + setNetworkId(NETWORK_ID_NOT_SET); + setPrimary(false); + setStatus(null); + setDisabled(false); + unblockRefresh(); + } + + public void updateWifiConfiguration(WifiConfiguration config) { + config.BSSID = getWpaSupplicantBssid(); + config.priority = priority; + config.hiddenSSID = hiddenSsid; + config.SSID = convertToQuotedString(ssid); + + setupSecurity(config); + } + + private void setupSecurity(WifiConfiguration config) { + config.allowedAuthAlgorithms.clear(); + config.allowedGroupCiphers.clear(); + config.allowedKeyManagement.clear(); + config.allowedPairwiseCiphers.clear(); + config.allowedProtocols.clear(); + + if (TextUtils.isEmpty(security)) { + security = OPEN; + Log.w(TAG, "Empty security, assuming open"); + } + + if (security.equals(WEP)) { + + // If password is empty, it should be left untouched + if (!TextUtils.isEmpty(mPassword)) { + if (mWepPasswordType == WEP_PASSWORD_AUTO) { + if (isHexWepKey(mPassword)) { + config.wepKeys[0] = mPassword; + } else { + config.wepKeys[0] = convertToQuotedString(mPassword); + } + } else { + config.wepKeys[0] = mWepPasswordType == WEP_PASSWORD_ASCII + ? convertToQuotedString(mPassword) + : mPassword; + } + } + + config.wepTxKeyIndex = 0; + + config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN); + config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED); + + config.allowedKeyManagement.set(KeyMgmt.NONE); + + config.allowedGroupCiphers.set(GroupCipher.WEP40); + config.allowedGroupCiphers.set(GroupCipher.WEP104); + + } else if (security.equals(WPA) || security.equals(WPA2)){ + config.allowedGroupCiphers.set(GroupCipher.TKIP); + config.allowedGroupCiphers.set(GroupCipher.CCMP); + + config.allowedKeyManagement.set(KeyMgmt.WPA_PSK); + + config.allowedPairwiseCiphers.set(PairwiseCipher.CCMP); + config.allowedPairwiseCiphers.set(PairwiseCipher.TKIP); + + config.allowedProtocols.set(security.equals(WPA2) ? Protocol.RSN : Protocol.WPA); + + // If password is empty, it should be left untouched + if (!TextUtils.isEmpty(mPassword)) { + if (mPassword.length() == 64 && isHex(mPassword)) { + // Goes unquoted as hex + config.preSharedKey = mPassword; + } else { + // Goes quoted as ASCII + config.preSharedKey = convertToQuotedString(mPassword); + } + } + + } else if (security.equals(OPEN)) { + config.allowedKeyManagement.set(KeyMgmt.NONE); + } + } + + private static boolean isHexWepKey(String wepKey) { + final int len = wepKey.length(); + + // WEP-40, WEP-104, and some vendors using 256-bit WEP (WEP-232?) + if (len != 10 && len != 26 && len != 58) { + return false; + } + + return isHex(wepKey); + } + + private static boolean isHex(String key) { + for (int i = key.length() - 1; i >= 0; i--) { + final char c = key.charAt(i); + if (!(c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f')) { + return false; + } + } + + return true; + } + + public void setCallback(AccessPointStateCallback callback) { + mCallback = callback; + } + + void blockRefresh() { + mBlockRefresh++; + } + + void unblockRefresh() { + if (--mBlockRefresh == 0 && mNeedsRefresh) { + requestRefresh(); + } + } + + private void requestRefresh() { + if (mBlockRefresh > 0) { + mNeedsRefresh = true; + return; + } + + if (mCallback != null) { + mCallback.refreshAccessPointState(); + } + + mNeedsRefresh = false; + } + + /** + * {@inheritDoc} + * @see #hashCode() + * @see #equals(Object) + */ + public int matches(int otherNetworkId, String otherBssid, String otherSsid, + String otherSecurity) { + + // Whenever this method is touched, please ensure #equals and #hashCode + // still work with the changes here! + + if (otherSsid == null) { + if (WifiLayer.LOGV) { + Log.w(TAG, "BSSID: " + otherBssid + ", SSID: " + otherSsid); + } + return MATCH_NONE; + } + + /* + * If we both have 'security' set, it must match (an open network still + * has 'security' set to OPEN) + */ + if (security != null && otherSecurity != null) { + if (!security.equals(otherSecurity)) { + return MATCH_NONE; + } + } + + // WifiConfiguration gives an empty bssid as a BSSID wildcard + if (TextUtils.isEmpty(otherBssid)) { + otherBssid = AccessPointState.BSSID_ANY; + } + + final boolean networkIdMatches = networkId == otherNetworkId; + if (!networkIdMatches && networkId != NETWORK_ID_ANY && otherNetworkId != NETWORK_ID_ANY) { + // Network IDs don't match (e.g., 1 & 2 or unset & 1) and neither is a wildcard + return MATCH_NONE; + } + + if (networkIdMatches && otherNetworkId != NETWORK_ID_NOT_SET + && otherNetworkId != NETWORK_ID_ANY) { + // Network ID matches (they're set to the same ID) + return MATCH_EXACT; + } + + // So now, network IDs aren't set or at least one is a wildcard + + final boolean bssidMatches = bssid.equals(otherBssid); + final boolean otherBssidIsWildcard = otherBssid.equals(BSSID_ANY); + if (bssidMatches && !otherBssidIsWildcard) { + // BSSID matches (and neither is a wildcard) + return MATCH_STRONG; + } + + if (!bssidMatches && !bssid.equals(BSSID_ANY) && !otherBssidIsWildcard) { + // BSSIDs don't match (e.g., 00:24:21:21:42:12 & 42:12:44:21:22:52) + // and neither is a wildcard + return MATCH_NONE; + } + + // So now, BSSIDs are both wildcards + + final boolean ssidMatches = ssid.equals(otherSsid); + if (ssidMatches) { + // SSID matches + return MATCH_WEAK; + } + + return MATCH_NONE; + } + + /** + * {@inheritDoc} + * @see #matches(int, String, String) + * @see #equals(Object) + */ + @Override + public int hashCode() { + // Two equal() objects must have same hashCode. + // With Wi-Fi, the broadest match is if two SSIDs are the same. The finer-grained matches + // imply this (for example, the same network IDs means the same WifiConfiguration which + // means the same SSID). + // See #matches for the exact matching algorithm we use. + return ssid != null ? ssid.hashCode() : 0; + } + + /** + * {@inheritDoc} + * @see #matches(int, String, String) + * @see #hashCode() + */ + @Override + public boolean equals(Object o) { + if (!o.getClass().equals(getClass())) { + return false; + } + + final AccessPointState other = (AccessPointState) o; + + // To see which conditions cause two AccessPointStates to be equal, see + // where #matches returns MATCH_WEAK or greater. + + return matches(other.networkId, other.bssid, other.ssid, other.security) >= MATCH_WEAK; + } + + public int matchesWifiConfiguration(WifiConfiguration wifiConfig) { + String security = getWifiConfigurationSecurity(wifiConfig); + return matches(wifiConfig.networkId, wifiConfig.BSSID, wifiConfig.SSID, security); + } + + String getSummarizedStatus() { + StringBuilder sb = mSummaryBuilder; + sb.delete(0, sb.length()); + + if (primary && status != null) { + buildSummary(sb, WifiStatus.getPrintable(mContext, status), true); + + } else if (!seen) { + buildSummary(sb, SUMMARY_NOT_IN_RANGE, true); + + // Remembered comes second in this case + if (!primary && configured) { + buildSummary(sb, SUMMARY_REMEMBERED, true); + } + + } else { + if (configured && disabled) { + // The connection failure overrides all in this case + return SUMMARY_CONNECTION_FAILED; + } + + // Remembered comes first in this case + if (!primary && configured) { + buildSummary(sb, SUMMARY_REMEMBERED, true); + } + + // If it is seen (and not the primary), show the security type + String verboseSecurity = getVerboseSecurity(); + if (verboseSecurity != null) { + buildSummary(sb, verboseSecurity, true); + } + } + + return sb.toString(); + } + + private String getVerboseSecurity() { + if (WEP.equals(security)) { + return LOCALIZED_VERBOSE_WEP; + } else if (WPA.equals(security)) { + return LOCALIZED_VERBOSE_WPA; + } else if (WPA2.equals(security)) { + return LOCALIZED_VERBOSE_WPA2; + } else if (OPEN.equals(security)) { + return LOCALIZED_VERBOSE_OPEN; + } else { + return null; + } + } + + private void buildSummary(StringBuilder sb, String string, boolean autoLowerCaseFirstLetter) { + if (sb.length() == 0) { + sb.append(string); + } else { + sb.append(", "); + if (autoLowerCaseFirstLetter) { + // Convert first letter to lowercase + sb.append(Character.toLowerCase(string.charAt(0))).append(string, 1, + string.length()); + } else { + sb.append(string); + } + } + } + + public int compareTo(AccessPointState other) { + // This ranks the states for displaying in the AP list, not for + // connecting to (wpa_supplicant does that using the WifiConfiguration's + // priority field). + + // Clarity > efficiency, of this logic: + int comparison; + + // Primary + comparison = (other.primary ? 1 : 0) - (primary ? 1 : 0); + if (comparison != 0) return comparison; + + // Currently seen (similar to, but not always the same as within range) + comparison = (other.seen ? 1 : 0) - (seen ? 1 : 0); + if (comparison != 0) return comparison; + + // Configured + comparison = (other.configured ? 1 : 0) - (configured ? 1 : 0); + if (comparison != 0) return comparison; + + if (!configured) { + // Neither are configured + + // Open network + comparison = (hasSecurity() ? 1 : 0) - (other.hasSecurity() ? 1 : 0); + if (comparison != 0) return comparison; + } + + // Signal strength + comparison = (int) (other.signalForSorting - signalForSorting); + if (comparison != 0) return comparison; + + // Alphabetical + return ssid.compareToIgnoreCase(other.ssid); + } + + public String toString() { + return ssid + " (" + bssid + ", " + networkId + ", " + super.toString() + ")"; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(bssid); + dest.writeInt(configured ? 1 : 0); + dest.writeInt(ipAddress); + dest.writeInt(linkSpeed); + dest.writeInt(networkId); + dest.writeInt(primary ? 1 : 0); + dest.writeInt(priority); + dest.writeInt(hiddenSsid ? 1 : 0); + dest.writeString(security); + dest.writeInt(seen ? 1 : 0); + dest.writeInt(disabled ? 1 : 0); + dest.writeInt(signal); + dest.writeString(ssid); + dest.writeString(status != null ? status.toString() : null); + dest.writeString(mPassword); + dest.writeInt(mConfigHadPassword ? 1 : 0); + dest.writeInt(mWepPasswordType); + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public static final Creator<AccessPointState> CREATOR = + new Creator<AccessPointState>() { + public AccessPointState createFromParcel(Parcel in) { + AccessPointState state = new AccessPointState(); + state.bssid = in.readString(); + state.configured = in.readInt() == 1; + state.ipAddress = in.readInt(); + state.linkSpeed = in.readInt(); + state.networkId = in.readInt(); + state.primary = in.readInt() == 1; + state.priority = in.readInt(); + state.hiddenSsid = in.readInt() == 1; + state.security = in.readString(); + state.seen = in.readInt() == 1; + state.disabled = in.readInt() == 1; + state.signal = in.readInt(); + state.ssid = in.readString(); + String statusStr = in.readString(); + if (statusStr != null) { + state.status = NetworkInfo.DetailedState.valueOf(statusStr); + } + state.mPassword = in.readString(); + state.mConfigHadPassword = in.readInt() == 1; + state.mWepPasswordType = in.readInt(); + return state; + } + + public AccessPointState[] newArray(int size) { + return new AccessPointState[size]; + } + }; + + +} diff --git a/src/com/android/settings/wifi/AdvancedSettings.java b/src/com/android/settings/wifi/AdvancedSettings.java new file mode 100644 index 000000000..323d5c770 --- /dev/null +++ b/src/com/android/settings/wifi/AdvancedSettings.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.R; + +import android.content.ContentResolver; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.provider.Settings; +import android.provider.Settings.System; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +public class AdvancedSettings extends PreferenceActivity + implements Preference.OnPreferenceChangeListener { + + private static final String KEY_MAC_ADDRESS = "mac_address"; + private static final String KEY_USE_STATIC_IP = "use_static_ip"; + private static final String KEY_NUM_CHANNELS = "num_channels"; + private static final String KEY_SLEEP_POLICY = "sleep_policy"; + + private String[] mSettingNames = { + System.WIFI_STATIC_IP, System.WIFI_STATIC_GATEWAY, System.WIFI_STATIC_NETMASK, + System.WIFI_STATIC_DNS1, System.WIFI_STATIC_DNS2 + }; + + private String[] mPreferenceKeys = { + "ip_address", "gateway", "netmask", "dns1", "dns2" + }; + + private CheckBoxPreference mUseStaticIpCheckBox; + + private static final int MENU_ITEM_SAVE = Menu.FIRST; + private static final int MENU_ITEM_CANCEL = Menu.FIRST + 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.wifi_advanced_settings); + + mUseStaticIpCheckBox = (CheckBoxPreference) findPreference(KEY_USE_STATIC_IP); + + for (int i = 0; i < mPreferenceKeys.length; i++) { + Preference preference = findPreference(mPreferenceKeys[i]); + preference.setOnPreferenceChangeListener(this); + } + } + + @Override + protected void onResume() { + super.onResume(); + + updateUi(); + initNumChannelsPreference(); + initSleepPolicyPreference(); + refreshMacAddress(); + } + + private void initNumChannelsPreference() { + ListPreference pref = (ListPreference) findPreference(KEY_NUM_CHANNELS); + pref.setOnPreferenceChangeListener(this); + + WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); + /* + * Generate the list of valid channel counts to show in the ListPreference. + * The values are numerical, so the only text to be localized is the + * "channel_word" resource. + */ + int[] validChannelCounts = wifiManager.getValidChannelCounts(); + if (validChannelCounts == null) { + Toast.makeText(this, R.string.wifi_setting_num_channels_error, + Toast.LENGTH_SHORT).show(); + pref.setEnabled(false); + return; + } + String[] entries = new String[validChannelCounts.length]; + String[] entryValues = new String[validChannelCounts.length]; + + for (int i = 0; i < validChannelCounts.length; i++) { + entryValues[i] = String.valueOf(validChannelCounts[i]); + entries[i] = getString(R.string.wifi_setting_num_channels_channel_phrase, + validChannelCounts[i]); + } + pref.setEntries(entries); + pref.setEntryValues(entryValues); + pref.setEnabled(true); + int numChannels = wifiManager.getNumAllowedChannels(); + if (numChannels >= 0) { + pref.setValue(String.valueOf(numChannels)); + } + } + + private void initSleepPolicyPreference() { + ListPreference pref = (ListPreference) findPreference(KEY_SLEEP_POLICY); + pref.setOnPreferenceChangeListener(this); + int value = Settings.System.getInt(getContentResolver(), + Settings.System.WIFI_SLEEP_POLICY,Settings. System.WIFI_SLEEP_POLICY_DEFAULT); + pref.setValue(String.valueOf(value)); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + + if (keyCode == KeyEvent.KEYCODE_BACK) { + updateSettingsProvider(); + } + + return super.onKeyDown(keyCode, event); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (key == null) return true; + + if (key.equals(KEY_NUM_CHANNELS)) { + try { + int numChannels = Integer.parseInt((String) newValue); + WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); + if (!wifiManager.setNumAllowedChannels(numChannels)) { + Toast.makeText(this, R.string.wifi_setting_num_channels_error, + Toast.LENGTH_SHORT).show(); + } + } catch (NumberFormatException e) { + Toast.makeText(this, R.string.wifi_setting_num_channels_error, + Toast.LENGTH_SHORT).show(); + return false; + } + + } else if (key.equals(KEY_SLEEP_POLICY)) { + try { + Settings.System.putInt(getContentResolver(), + Settings.System.WIFI_SLEEP_POLICY, Integer.parseInt(((String) newValue))); + } catch (NumberFormatException e) { + Toast.makeText(this, R.string.wifi_setting_sleep_policy_error, + Toast.LENGTH_SHORT).show(); + return false; + } + + } else { + String value = (String) newValue; + + if (!isIpAddress(value)) { + Toast.makeText(this, R.string.wifi_ip_settings_invalid_ip, Toast.LENGTH_LONG).show(); + return false; + } + + preference.setSummary(value); + } + + return true; + } + + private boolean isIpAddress(String value) { + + int start = 0; + int end = value.indexOf('.'); + int numBlocks = 0; + + while (start < value.length()) { + + if (end == -1) { + end = value.length(); + } + + try { + int block = Integer.parseInt(value.substring(start, end)); + if ((block > 255) || (block < 0)) { + return false; + } + } catch (NumberFormatException e) { + return false; + } + + numBlocks++; + + start = end + 1; + end = value.indexOf('.', start); + } + + return numBlocks == 4; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + menu.add(0, MENU_ITEM_SAVE, 0, R.string.wifi_ip_settings_menu_save) + .setIcon(android.R.drawable.ic_menu_save); + + menu.add(0, MENU_ITEM_CANCEL, 0, R.string.wifi_ip_settings_menu_cancel) + .setIcon(android.R.drawable.ic_menu_close_clear_cancel); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + + case MENU_ITEM_SAVE: + updateSettingsProvider(); + finish(); + return true; + + case MENU_ITEM_CANCEL: + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void updateUi() { + ContentResolver contentResolver = getContentResolver(); + + mUseStaticIpCheckBox.setChecked(System.getInt(contentResolver, + System.WIFI_USE_STATIC_IP, 0) != 0); + + for (int i = 0; i < mSettingNames.length; i++) { + EditTextPreference preference = (EditTextPreference) findPreference(mPreferenceKeys[i]); + String settingValue = System.getString(contentResolver, mSettingNames[i]); + preference.setText(settingValue); + preference.setSummary(settingValue); + } + } + + private void updateSettingsProvider() { + ContentResolver contentResolver = getContentResolver(); + + System.putInt(contentResolver, System.WIFI_USE_STATIC_IP, + mUseStaticIpCheckBox.isChecked() ? 1 : 0); + + for (int i = 0; i < mSettingNames.length; i++) { + EditTextPreference preference = (EditTextPreference) findPreference(mPreferenceKeys[i]); + System.putString(contentResolver, mSettingNames[i], preference.getText()); + } + } + + private void refreshMacAddress() { + WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + Preference wifiMacAddressPref = findPreference(KEY_MAC_ADDRESS); + String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); + wifiMacAddressPref.setSummary(!TextUtils.isEmpty(macAddress) ? macAddress + : getString(R.string.status_unavailable)); + } + +} diff --git a/src/com/android/settings/wifi/WifiEnabler.java b/src/com/android/settings/wifi/WifiEnabler.java new file mode 100644 index 000000000..88cfe06b7 --- /dev/null +++ b/src/com/android/settings/wifi/WifiEnabler.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import static android.net.wifi.WifiManager.WIFI_STATE_DISABLED; +import static android.net.wifi.WifiManager.WIFI_STATE_DISABLING; +import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED; +import static android.net.wifi.WifiManager.WIFI_STATE_ENABLING; +import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN; + +import com.android.settings.R; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.preference.Preference; +import android.preference.CheckBoxPreference; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +public class WifiEnabler implements Preference.OnPreferenceChangeListener { + + private static final boolean LOCAL_LOGD = Config.LOGD || WifiLayer.LOGV; + private static final String TAG = "SettingsWifiEnabler"; + + private final Context mContext; + private final WifiManager mWifiManager; + private final CheckBoxPreference mWifiCheckBoxPref; + private final CharSequence mOriginalSummary; + + private final IntentFilter mWifiStateFilter; + private final BroadcastReceiver mWifiStateReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + handleWifiStateChanged( + intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WIFI_STATE_UNKNOWN), + intent.getIntExtra(WifiManager.EXTRA_PREVIOUS_WIFI_STATE, + WIFI_STATE_UNKNOWN)); + } else if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + handleNetworkStateChanged( + (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO)); + } + } + }; + + public WifiEnabler(Context context, WifiManager wifiManager, + CheckBoxPreference wifiCheckBoxPreference) { + mContext = context; + mWifiCheckBoxPref = wifiCheckBoxPreference; + mWifiManager = wifiManager; + + mOriginalSummary = wifiCheckBoxPreference.getSummary(); + wifiCheckBoxPreference.setPersistent(false); + + mWifiStateFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); + mWifiStateFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + } + + public void resume() { + int state = mWifiManager.getWifiState(); + // This is the widget enabled state, not the preference toggled state + mWifiCheckBoxPref.setEnabled(state == WIFI_STATE_ENABLED || state == WIFI_STATE_DISABLED + || state == WIFI_STATE_UNKNOWN); + + mContext.registerReceiver(mWifiStateReceiver, mWifiStateFilter); + mWifiCheckBoxPref.setOnPreferenceChangeListener(this); + } + + public void pause() { + mContext.unregisterReceiver(mWifiStateReceiver); + mWifiCheckBoxPref.setOnPreferenceChangeListener(null); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + // Turn on/off Wi-Fi + setWifiEnabled((Boolean) value); + + // Don't update UI to opposite state until we're sure + return false; + } + + private void setWifiEnabled(final boolean enable) { + // Disable button + mWifiCheckBoxPref.setEnabled(false); + + if (!mWifiManager.setWifiEnabled(enable)) { + mWifiCheckBoxPref.setSummary(enable ? R.string.error_starting : R.string.error_stopping); + } + } + + private void handleWifiStateChanged(int wifiState, int previousWifiState) { + + if (LOCAL_LOGD) { + Log.d(TAG, "Received wifi state changed from " + + getHumanReadableWifiState(previousWifiState) + " to " + + getHumanReadableWifiState(wifiState)); + } + + if (wifiState == WIFI_STATE_DISABLED || wifiState == WIFI_STATE_ENABLED) { + mWifiCheckBoxPref.setChecked(wifiState == WIFI_STATE_ENABLED); + mWifiCheckBoxPref + .setSummary(wifiState == WIFI_STATE_DISABLED ? mOriginalSummary : null); + + mWifiCheckBoxPref.setEnabled(isEnabledByDependency()); + + } else if (wifiState == WIFI_STATE_DISABLING || wifiState == WIFI_STATE_ENABLING) { + mWifiCheckBoxPref.setSummary(wifiState == WIFI_STATE_ENABLING ? R.string.wifi_starting + : R.string.wifi_stopping); + + } else if (wifiState == WIFI_STATE_UNKNOWN) { + int message = R.string.wifi_error; + if (previousWifiState == WIFI_STATE_ENABLING) message = R.string.error_starting; + else if (previousWifiState == WIFI_STATE_DISABLING) message = R.string.error_stopping; + + mWifiCheckBoxPref.setChecked(false); + mWifiCheckBoxPref.setSummary(message); + mWifiCheckBoxPref.setEnabled(true); + } + } + + private void handleNetworkStateChanged(NetworkInfo networkInfo) { + + if (LOCAL_LOGD) { + Log.d(TAG, "Received network state changed to " + networkInfo); + } + + if (mWifiManager.isWifiEnabled()) { + String summary = WifiStatus.getStatus(mContext, + mWifiManager.getConnectionInfo().getSSID(), networkInfo.getDetailedState()); + mWifiCheckBoxPref.setSummary(summary); + } + } + + private boolean isEnabledByDependency() { + Preference dep = getDependencyPreference(); + if (dep == null) { + return true; + } + + return !dep.shouldDisableDependents(); + } + + private Preference getDependencyPreference() { + String depKey = mWifiCheckBoxPref.getDependency(); + if (TextUtils.isEmpty(depKey)) { + return null; + } + + return mWifiCheckBoxPref.getPreferenceManager().findPreference(depKey); + } + + private static String getHumanReadableWifiState(int wifiState) { + switch (wifiState) { + case WIFI_STATE_DISABLED: + return "Disabled"; + case WIFI_STATE_DISABLING: + return "Disabling"; + case WIFI_STATE_ENABLED: + return "Enabled"; + case WIFI_STATE_ENABLING: + return "Enabling"; + case WIFI_STATE_UNKNOWN: + return "Unknown"; + default: + return "Some other state!"; + } + } +} diff --git a/src/com/android/settings/wifi/WifiLayer.java b/src/com/android/settings/wifi/WifiLayer.java new file mode 100644 index 000000000..b0857d2c8 --- /dev/null +++ b/src/com/android/settings/wifi/WifiLayer.java @@ -0,0 +1,1302 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import static android.net.wifi.WifiManager.WIFI_STATE_DISABLED; +import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED; + +import com.android.settings.R; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; +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.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Helper class for abstracting Wi-Fi. + * <p> + * Client must call {@link #onCreate()}, {@link #onCreatedCallback()}, + * {@link #onPause()}, {@link #onResume()}. + */ +public class WifiLayer { + + private static final String TAG = "SettingsWifiLayer"; + static final boolean LOGV = false || Config.LOGV; + + //============================ + // Other member variables + //============================ + + private Context mContext; + private Callback mCallback; + + static final int MESSAGE_ATTEMPT_SCAN = 1; + private Handler mHandler = new MyHandler(); + + //============================ + // Wifi member variables + //============================ + + private WifiManager mWifiManager; + private IntentFilter mIntentFilter; + private List<AccessPointState> mApScanList = new ArrayList<AccessPointState>(); + private List<AccessPointState> mApOtherList = new ArrayList<AccessPointState>(); + private AccessPointState mCurrentPrimaryAp; + + /** The last access point that we were authenticating with. */ + private AccessPointState mLastAuthenticatingAp; + + /** The delay between scans when we're continually scanning. */ + private static final int CONTINUOUS_SCAN_DELAY_MS = 6000; + /** On failure, the maximum retries for scanning. */ + private static final int SCAN_MAX_RETRY = 5; + /** On failure, the delay between each scan retry. */ + private static final int SCAN_RETRY_DELAY_MS = 1000; + /** On failure, the number of retries so far. */ + private int mScanRetryCount = 0; + /** + * Whether we're currently obtaining an address. Continuous scanning will be + * disabled in this state. + */ + private boolean mIsObtainingAddress; + + /** + * See {@link android.provider.Settings.Secure#WIFI_NUM_OPEN_NETWORKS_KEPT}. + */ + private int WIFI_NUM_OPEN_NETWORKS_KEPT; + /** + * Once the highest priority exceeds this value, all networks will be + * wrapped around starting at 0. This is so another client of the Wi-Fi + * API can have access points that aren't managed by us. (If the other + * client wants lower-priority access points than ours, it can use negative + * priority.) + */ + private static final int HIGHEST_PRIORITY_MAX_VALUE = 99999; + /** + * Never access directly, only the related methods should. + */ + private int mNextHighestPriority; + + /** + * This is used to track when the user wants to connect to a specific AP. We + * disable all other APs, set this to true, and let wpa_supplicant connect. + * Once we get a network state change, we re-enable the rest of them. + */ + private boolean mReenableApsOnNetworkStateChange = false; + + /** + * The current supplicant state, as broadcasted. + */ + private SupplicantState mCurrentSupplicantState; + + //============================ + // Inner classes + //============================ + + interface Callback { + void onError(int messageResId); + + /** + * Called when an AP is added or removed. + * + * @param ap The AP. + * @param added {@code true} if added, {@code false} if removed. + */ + void onAccessPointSetChanged(AccessPointState ap, boolean added); + + /** + * Called when the scanning status changes. + * + * @param started {@code true} if the scanning just started, + * {@code false} if it just ended. + */ + void onScanningStatusChanged(boolean started); + + /** + * Called when the access points should be enabled or disabled. This is + * called from both wpa_supplicant being connected/disconnected and Wi-Fi + * being enabled/disabled. + * + * @param enabled {@code true} if they should be enabled, {@code false} + * if they should be disabled. + */ + void onAccessPointsStateChanged(boolean enabled); + + /** + * Called when there is trouble authenticating and the retry-password + * dialog should be shown. + * + * @param ap The access point. + */ + void onRetryPassword(AccessPointState ap); + } + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + handleNetworkStateChanged( + (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO), + intent.getStringExtra(WifiManager.EXTRA_BSSID)); + } else if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + handleScanResultsAvailable(); + } else if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) { + handleSupplicantConnectionChanged( + intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)); + } else if (action.equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) { + handleSupplicantStateChanged( + (SupplicantState) intent.getParcelableExtra(WifiManager.EXTRA_NEW_STATE), + intent.hasExtra(WifiManager.EXTRA_SUPPLICANT_ERROR), + intent.getIntExtra(WifiManager.EXTRA_SUPPLICANT_ERROR, 0)); + } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + handleWifiStateChanged(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN)); + } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { + handleSignalChanged(intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, 0)); + } else if (action.equals(WifiManager.NETWORK_IDS_CHANGED_ACTION)) { + handleNetworkIdsChanged(); + } + } + }; + + /** + * If using this class, make sure to call the callbacks of this class, such + * as {@link #onCreate()}, {@link #onCreatedCallback()}, + * {@link #onPause()}, {@link #onResume()}. + * + * @param context The context. + * @param callback The interface that will be invoked when events from this + * class are generated. + */ + public WifiLayer(Context context, Callback callback) { + mContext = context; + mCallback = callback; + } + + //============================ + // Lifecycle + //============================ + + /** + * The client MUST call this. + * <p> + * This shouldn't have any dependency on the callback. + */ + public void onCreate() { + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + mIntentFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION); + mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); + mIntentFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION); + + WIFI_NUM_OPEN_NETWORKS_KEPT = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.WIFI_NUM_OPEN_NETWORKS_KEPT, 10); + } + + /** + * The client MUST call this. + * <p> + * Callback is ready, this can do whatever it wants with it. + */ + public void onCreatedCallback() { + if (isWifiEnabled()) { + refreshAll(false); + } + } + + /** + * The client MUST call this. + * + * @see android.app.Activity#onResume + */ + public void onResume() { + mContext.registerReceiver(mReceiver, mIntentFilter); + + if (isWifiEnabled()) { + // Kick start the continual scan + queueContinuousScan(); + } + } + + /** + * The client MUST call this. + * + * @see android.app.Activity#onPause + */ + public void onPause() { + mContext.unregisterReceiver(mReceiver); + + attemptReenableAllAps(); + + removeFutureScans(); + } + + //============================ + // "Public" API + //============================ + + /** + * Returns an AccessPointState instance (that we track locally in WifiLayer) + * for the given state. First, we check if we track the given instance. If + * not, we find an equal AccessPointState instance that we track. + * + * @param state An AccessPointState instance that does not necessarily have + * to be one that this WifiLayer class tracks. For example, it + * could be the result of unparceling. + * @return An AccessPointState instance that this WifiLayer class tracks. + */ + public AccessPointState getWifiLayerApInstance(AccessPointState state) { + synchronized (this) { + + if (hasApInstanceLocked(state)) { + return state; + } + + return findApLocked(state.networkId, state.bssid, state.ssid, state.security); + } + } + + /** + * Connects to the network, and creates the Wi-Fi API config if necessary. + * + * @param state The state of the network to connect to. This MUST be an + * instance that was given to you by this class. If you + * constructed the instance yourself (for example, after + * unparceling it), you should use + * {@link #getWifiLayerApInstance(AccessPointState)}. + * @return Whether the operation was successful. + */ + public boolean connectToNetwork(AccessPointState state) { + if (LOGV) { + Log.v(TAG, "Connecting to " + state); + } + + // Need WifiConfiguration for the AP + WifiConfiguration config = findConfiguredNetwork(state); + + if (LOGV) { + Log.v(TAG, " Found configured network " + config); + } + + if (config == null) { + /* + * Connecting for the first time, need to create it. We will enable + * and save it below (when we set priority). + */ + config = addConfiguration(state, 0); + + if (config == null) { + Log.e(TAG, "Config is still null, even after attempting to add it."); + error(R.string.error_connecting); + return false; + } + + /* + * We could reload the configured networks, but instead just + * shortcut and add this state to our list in memory. + */ + ensureTrackingState(state); + } else { + // Make sure the configuration has the latest from the state + state.updateWifiConfiguration(config); + } + + // Enable this network before we save to storage + if (!managerEnableNetwork(state, false)) { + Log.e(TAG, "Could not enable network ID " + state.networkId); + error(R.string.error_connecting); + return false; + } + + /* + * Give it highest priority, this could cause a network ID change, so do + * it after any modifications to the network we're connecting to + */ + setHighestPriorityStateAndSave(state, config); + + /* + * We force supplicant to connect to this network by disabling the + * others. We do this AFTER we save above so this disabled flag isn't + * persisted. + */ + mReenableApsOnNetworkStateChange = true; + if (!managerEnableNetwork(state, true)) { + Log.e(TAG, "Could not enable network ID " + state.networkId); + error(R.string.error_connecting); + return false; + } + + if (LOGV) { + Log.v(TAG, " Enabled network " + state.networkId); + } + + if (mCurrentSupplicantState == SupplicantState.DISCONNECTED || + mCurrentSupplicantState == SupplicantState.SCANNING) { + mWifiManager.reconnect(); + } + + // Check for too many configured open networks + if (!state.hasSecurity()) { + checkForExcessOpenNetworks(); + } + + return true; + } + + /** + * Saves a network, and creates the Wi-Fi API config if necessary. + * + * @param state The state of the network to save. If you constructed the + * instance yourself (for example, after unparceling it), you + * should use {@link #getWifiLayerApInstance(AccessPointState)}. + * @return Whether the operation was successful. + */ + public boolean saveNetwork(AccessPointState state) { + WifiConfiguration config = findConfiguredNetwork(state); + + if (config == null) { + // if the user is adding a new network, assume that it is hidden + state.setHiddenSsid(true); + + config = addConfiguration(state, ADD_CONFIGURATION_ENABLE); + + if (config == null) { + Log.e(TAG, "Could not save configuration, call to addConfiguration failed."); + error(R.string.error_saving); + return false; + } + + } else { + state.updateWifiConfiguration(config); + if (mWifiManager.updateNetwork(config) == -1) { + Log.e(TAG, "Could not update configuration, call to WifiManager failed."); + error(R.string.error_saving); + return false; + } + } + + // Successfully added network, go ahead and persist + if (!managerSaveConfiguration()) { + Log.e(TAG, "Could not save configuration, call to WifiManager failed."); + error(R.string.error_saving); + return false; + } + + /* + * We could reload the configured networks, but instead just shortcut + * and add this state to our list in memory + */ + ensureTrackingState(state); + + return true; + } + + /** + * Forgets a network. + * + * @param state The state of the network to forget. If you constructed the + * instance yourself (for example, after unparceling it), you + * should use {@link #getWifiLayerApInstance(AccessPointState)}. + * @return Whether the operation was succesful. + */ + public boolean forgetNetwork(AccessPointState state) { + if (!state.configured) { + Log.w(TAG, "Inconsistent state: Forgetting a network that is not configured."); + return true; + } + + int oldNetworkId = state.networkId; + state.forget(); + + if (!state.seen) { + // If it is not seen, it should be removed from the UI + removeApFromUi(state); + } + + synchronized (this) { + mApOtherList.remove(state); + // It should not be removed from the scan list, since if it was + // there that means it's still seen + } + + if (!mWifiManager.removeNetwork(oldNetworkId)) { + Log.e(TAG, "Removing network " + state.ssid + " (network ID " + oldNetworkId + + ") failed."); + return false; + } + + if (!managerSaveConfiguration()) { + error(R.string.error_saving); + return false; + } + + return true; + } + + /** + * This ensures this class is tracking the given state. This means it is in + * our list of access points, either in the scanned list or in the + * remembered list. + * + * @param state The state that will be checked for tracking, and if not + * tracking will be added to the remembered list in memory. + */ + private void ensureTrackingState(AccessPointState state) { + synchronized (this) { + if (hasApInstanceLocked(state)) { + return; + } + + mApOtherList.add(state); + } + } + + /** + * Attempts to scan networks. This has a retry mechanism. + */ + public void attemptScan() { + + // Remove any future scans since we're scanning right now + removeFutureScans(); + + if (!mWifiManager.isWifiEnabled()) return; + + if (!mWifiManager.startScan()) { + postAttemptScan(); + } else { + mScanRetryCount = 0; + } + } + + private void queueContinuousScan() { + mHandler.removeMessages(MESSAGE_ATTEMPT_SCAN); + + if (!mIsObtainingAddress) { + // Don't do continuous scan while in obtaining IP state + mHandler.sendEmptyMessageDelayed(MESSAGE_ATTEMPT_SCAN, CONTINUOUS_SCAN_DELAY_MS); + } + } + + private void removeFutureScans() { + mHandler.removeMessages(MESSAGE_ATTEMPT_SCAN); + } + + public boolean isWifiEnabled() { + return mWifiManager.isWifiEnabled(); + } + + public void error(int messageResId) { + Log.e(TAG, mContext.getResources().getString(messageResId)); + + if (mCallback != null) { + mCallback.onError(messageResId); + } + } + + //============================ + // Wifi logic + //============================ + + private void refreshAll(boolean attemptScan) { + loadConfiguredAccessPoints(); + refreshStatus(); + + if (attemptScan) { + attemptScan(); + } + } + + private void postAttemptScan() { + onScanningStarted(); + + if (++mScanRetryCount < SCAN_MAX_RETRY) { + // Just in case, remove previous ones first + removeFutureScans(); + mHandler.sendEmptyMessageDelayed(MESSAGE_ATTEMPT_SCAN, SCAN_RETRY_DELAY_MS); + } else { + // Show an error once we run out of attempts + error(R.string.error_scanning); + onScanningEnded(); + } + } + + private void onScanningStarted() { + if (mCallback != null) { + mCallback.onScanningStatusChanged(true); + } + } + + private void onScanningEnded() { + queueContinuousScan(); + + if (mCallback != null) { + mCallback.onScanningStatusChanged(false); + } + } + + private void clearApLists() { + List<AccessPointState> accessPoints = new ArrayList<AccessPointState>(); + + synchronized(this) { + // Clear the logic's list of access points + accessPoints.addAll(mApScanList); + accessPoints.addAll(mApOtherList); + mApScanList.clear(); + mApOtherList.clear(); + } + + for (int i = accessPoints.size() - 1; i >= 0; i--) { + removeApFromUi(accessPoints.get(i)); + } + } + + private boolean managerSaveConfiguration() { + boolean retValue = mWifiManager.saveConfiguration(); + + /* + * We need to assume the network IDs have changed, so handle this. Note: + * we also have a receiver on the broadcast intent in case another wifi + * framework client caused the change. In this case, we will handle the + * possible network ID change twice (but it's not too costly). + */ + handleNetworkIdsChanged(); + + return retValue; + } + + private boolean managerEnableNetwork(AccessPointState state, boolean disableOthers) { + if (!mWifiManager.enableNetwork(state.networkId, disableOthers)) { + return false; + } + + // Enabling was successful, make sure the state is not disabled + state.setDisabled(false); + + return true; + } + + private static final int ADD_CONFIGURATION_ENABLE = 1; + private static final int ADD_CONFIGURATION_SAVE = 2; + private WifiConfiguration addConfiguration(AccessPointState state, int flags) { + // Create and add + WifiConfiguration config = new WifiConfiguration(); + + state.updateWifiConfiguration(config); + + final int networkId = mWifiManager.addNetwork(config); + if (networkId == -1) { + return null; + } + + state.setNetworkId(networkId); + state.setConfigured(true); + + // If we should, then enable it, since it comes disabled by default + if ((flags & ADD_CONFIGURATION_ENABLE) != 0 + && !managerEnableNetwork(state, false)) { + return null; + } + + // If we should, then save it + if ((flags & ADD_CONFIGURATION_SAVE) != 0 && !managerSaveConfiguration()) { + return null; + } + + if (mCallback != null) { + mCallback.onAccessPointSetChanged(state, true); + } + + return config; + } + + private WifiConfiguration findConfiguredNetwork(AccessPointState state) { + final List<WifiConfiguration> wifiConfigs = getConfiguredNetworks(); + + for (int i = wifiConfigs.size() - 1; i >= 0; i--) { + final WifiConfiguration wifiConfig = wifiConfigs.get(i); + if (state.matchesWifiConfiguration(wifiConfig) >= AccessPointState.MATCH_WEAK) { + return wifiConfig; + } + } + + return null; + } + + private List<WifiConfiguration> getConfiguredNetworks() { + final List<WifiConfiguration> wifiConfigs = mWifiManager.getConfiguredNetworks(); + return wifiConfigs; + } + + /** + * Must call while holding the lock for the list, which is usually the + * WifiLayer instance. + */ + private static AccessPointState findApLocked(List<AccessPointState> list, int networkId, + String bssid, String ssid, String security) { + AccessPointState ap; + for (int i = list.size() - 1; i >= 0; i--) { + ap = list.get(i); + if (ap.matches(networkId, bssid, ssid, security) >= AccessPointState.MATCH_WEAK) { + return ap; + } + } + + return null; + } + + /** + * Must call while holding the lock for the lists, which is usually this + * WifiLayer instance. + */ + private AccessPointState findApLocked(int networkId, String bssid, String ssid, + String security) { + AccessPointState ap = findApLocked(mApScanList, networkId, bssid, ssid, security); + if (ap == null) { + ap = findApLocked(mApOtherList, networkId, bssid, ssid, security); + } + return ap; + } + + /** + * Returns whether we have the exact instance of the access point state + * given. This is useful in cases where an AccessPointState has been + * parceled by the client and the client is attempting to use it to + * connect/forget/save. + * <p> + * Must call while holding the lock for the lists, which is usually this + * WifiLayer instance. + */ + private boolean hasApInstanceLocked(AccessPointState state) { + + for (int i = mApScanList.size() - 1; i >= 0; i--) { + if (mApScanList.get(i) == state) { + return true; + } + } + + for (int i = mApOtherList.size() - 1; i >= 0; i--) { + if (mApOtherList.get(i) == state) { + return true; + } + } + + return false; + } + + private void loadConfiguredAccessPoints() { + final List<WifiConfiguration> configs = getConfiguredNetworks(); + + for (int i = configs.size() - 1; i >= 0; i--) { + final WifiConfiguration config = configs.get(i); + + AccessPointState ap; + synchronized(this) { + ap = findApLocked(config.networkId, config.BSSID, config.SSID, + AccessPointState.getWifiConfigurationSecurity(config)); + + if (ap != null) { + // We already know about this one + continue; + } + + ap = new AccessPointState(mContext); + ap.updateFromWifiConfiguration(config); + if (LOGV) Log.v(TAG, "Created " + ap + " in loadConfiguredAccessPoints"); + mApOtherList.add(ap); + } + + // Make sure our next highest priority is greater than this + checkNextHighestPriority(ap.priority); + + if (mCallback != null) { + mCallback.onAccessPointSetChanged(ap, true); + } + } + } + + private AccessPointState getCurrentAp() { + final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + + String ssid = wifiInfo.getSSID(); + if (ssid != null) { + /* + * We pass null for security since we have a network ID (i.e., it's + * not a wildcard), and rely on it matching. + */ + return findApLocked(wifiInfo.getNetworkId(), wifiInfo.getBSSID(), ssid, null); + } else { + return null; + } + } + + private void setPrimaryAp(AccessPointState ap) { + synchronized (this) { + // Unset other + if (mCurrentPrimaryAp != null) { + mCurrentPrimaryAp.setPrimary(false); + } + + mCurrentPrimaryAp = ap; + } + + if (ap != null) { + ap.setPrimary(true); + } + } + + private void attemptReenableAllAps() { + if (mReenableApsOnNetworkStateChange) { + mReenableApsOnNetworkStateChange = false; + enableAllAps(); + } + } + + private void enableAllAps() { + synchronized(this) { + if (LOGV) { + Log.v(TAG, " Enabling all APs"); + } + + enableApsLocked(mApOtherList); + enableApsLocked(mApScanList); + } + } + + private void enableApsLocked(List<AccessPointState> apList) { + for (int i = apList.size() - 1; i >= 0; i--) { + AccessPointState state = apList.get(i); + int networkId = state.networkId; + if (networkId != AccessPointState.NETWORK_ID_NOT_SET && + networkId != AccessPointState.NETWORK_ID_ANY) { + managerEnableNetwork(state, false); + } + } + } + + private void removeApFromUi(AccessPointState ap) { + if (mCallback != null) { + mCallback.onAccessPointSetChanged(ap, false); + } + } + + /** + * Sets the access point state to the highest priority. + * <p> + * If you have a list of configured networks from WifiManager, you probably + * shouldn't call this until you're done traversing the list. + * + * @param state The state to set as the highest priority. + * @param reusableConfiguration An optional WifiConfiguration that will be + * given to the WifiManager as updated data for the network ID. + * This will be filled with the new priority. + * @return Whether the operation was successful. + */ + private boolean setHighestPriorityStateAndSave(AccessPointState state, + WifiConfiguration reusableConfiguration) { + + if (!isConsideredForHighestPriority(state)) { + Log.e(TAG, + "Could not set highest priority on state because state is not being considered."); + return false; + } + + if (reusableConfiguration == null) { + reusableConfiguration = new WifiConfiguration(); + } + + int oldPriority = reusableConfiguration.priority; + reusableConfiguration.priority = getNextHighestPriority(); + reusableConfiguration.networkId = state.networkId; + + if (mWifiManager.updateNetwork(reusableConfiguration) == -1) { + // Rollback priority + reusableConfiguration.priority = oldPriority; + Log.e(TAG, + "Could not set highest priority on state because updating the supplicant network failed."); + return false; + } + + if (!managerSaveConfiguration()) { + reusableConfiguration.priority = oldPriority; + Log.e(TAG, + "Could not set highest priority on state because saving config failed."); + return false; + } + + state.priority = reusableConfiguration.priority; + + if (LOGV) { + Log.v(TAG, " Set highest priority to " + + state.priority + " from " + oldPriority); + } + + return true; + } + + /** + * Makes sure the next highest priority is larger than the given priority. + */ + private void checkNextHighestPriority(int priority) { + if (priority > HIGHEST_PRIORITY_MAX_VALUE || priority < 0) { + // This is a priority that we aren't managing + return; + } + + if (mNextHighestPriority <= priority) { + mNextHighestPriority = priority + 1; + } + } + + /** + * Checks if there are too many open networks, and removes the excess ones. + */ + private void checkForExcessOpenNetworks() { + synchronized(this) { + ArrayList<AccessPointState> allAps = getApsSortedByPriorityLocked(); + + // Walk from highest to lowest priority + int openConfiguredCount = 0; + for (int i = allAps.size() - 1; i >= 0; i--) { + AccessPointState state = allAps.get(i); + if (state.configured && !state.hasSecurity()) { + openConfiguredCount++; + if (openConfiguredCount > WIFI_NUM_OPEN_NETWORKS_KEPT) { + // Remove this network + forgetNetwork(state); + } + } + } + } + } + + private boolean isConsideredForHighestPriority(AccessPointState state) { + return state.configured && state.networkId != AccessPointState.NETWORK_ID_ANY && + state.networkId != AccessPointState.NETWORK_ID_NOT_SET; + } + + /** + * Gets the next highest priority. If this value is larger than the max, + * shift all the priorities so the lowest starts at 0. + * <p> + * Only + * {@link #setHighestPriorityStateAndSave(AccessPointState, WifiConfiguration)} + * should call this. + * + * @return The next highest priority to use. + */ + private int getNextHighestPriority() { + if (mNextHighestPriority > HIGHEST_PRIORITY_MAX_VALUE) { + shiftPriorities(); + } + + return mNextHighestPriority++; + } + + /** + * Shift all the priorities so the lowest starts at 0. + * + * @return Whether the operation was successful. + */ + private boolean shiftPriorities() { + synchronized(this) { + + ArrayList<AccessPointState> allAps = getApsSortedByPriorityLocked(); + + // Re-usable WifiConfiguration for setting priority + WifiConfiguration updatePriorityConfig = new WifiConfiguration(); + + // Set new priorities + mNextHighestPriority = 0; + int size = allAps.size(); + for (int i = 0; i < size; i++) { + AccessPointState state = allAps.get(i); + + if (!isConsideredForHighestPriority(state)) { + continue; + } + + if (!setHighestPriorityStateAndSave(state, updatePriorityConfig)) { + Log.e(TAG, + "Could not shift priorities because setting the new priority failed."); + return false; + } + } + + return true; + } + } + + private ArrayList<AccessPointState> getApsSortedByPriorityLocked() { + // Get all of the access points we have + ArrayList<AccessPointState> allAps = new ArrayList<AccessPointState>(mApScanList.size() + + mApOtherList.size()); + allAps.addAll(mApScanList); + allAps.addAll(mApOtherList); + + // Sort them based on priority + Collections.sort(allAps, new Comparator<AccessPointState>() { + public int compare(AccessPointState object1, AccessPointState object2) { + return object1.priority - object2.priority; + } + }); + + return allAps; + } + + //============================ + // Status related + //============================ + + private void refreshStatus() { + refreshStatus(null, null); + } + + private void refreshStatus(AccessPointState ap, NetworkInfo.DetailedState detailedState) { + final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + if (detailedState == null) { + detailedState = WifiInfo.getDetailedStateOf(wifiInfo.getSupplicantState()); + } + + if (ap == null && WifiStatus.isLiveConnection(detailedState)) { + /* + * We pass null for security since we have a network ID (i.e., it's + * not a wildcard), and rely on it matching. + */ + ap = findApLocked(wifiInfo.getNetworkId(), wifiInfo.getBSSID(), wifiInfo + .getSSID(), null); + } + + if (ap != null) { + ap.blockRefresh(); + + // Let the AP get the latest info from the WifiInfo + ap.updateFromWifiInfo(wifiInfo, detailedState); + + // The detailed state from the Intent has more states than the WifiInfo's detailed + // state can have (for example, DHCP completion). Set the status using + // the Intent's detailed state. + ap.setStatus(detailedState); + ap.unblockRefresh(); + } + } + + //============================ + // Wifi callbacks + //============================ + + private void handleNetworkStateChanged(NetworkInfo info, String bssid) { + final AccessPointState ap = getCurrentAp(); + NetworkInfo.DetailedState detailedState = info.getDetailedState(); + + if (LOGV) { + Log.v(TAG, "State change received " + info.toString() + ", or " + + detailedState + " on " + bssid + " matched to " + ap); + } + + handleDisablingScanWhileObtainingAddress(detailedState); + + // This will update the AP with its new info + refreshStatus(ap, detailedState); + + boolean isDisconnected = info.getState().equals(State.DISCONNECTED); + if (ap != null && info.isConnectedOrConnecting()) { + setPrimaryAp(ap); + + if (LOGV) { + Log.v(TAG, " Updated " + ap + " to be primary"); + } + + } else if (isDisconnected) { + + /* + * When we drop off a network (for example, the router is powered + * down when we were connected), we received a DISCONNECT event + * without a BSSID. We should not have a primary AP anymore. + */ + setPrimaryAp(null); + + if (LOGV) { + Log.v(TAG, " Cleared primary"); + } + + } else if (detailedState.equals(DetailedState.FAILED)) { + + /* + * Doh, failed for whatever reason. Unset the primary AP, but set + * failed status on the AP that failed. + */ + setPrimaryAp(null); + ap.setStatus(DetailedState.FAILED); + + // Bring up error dialog + error(R.string.wifi_generic_connection_error); + + } else if (LOGV) { + Log.v(TAG, " Did not update any AP to primary, could have updated " + + ap + " but we aren't connected or connecting"); + } + + if ((ap != null) && (info.isConnected() + || (detailedState == DetailedState.OBTAINING_IPADDR))) { + /* + * Sometimes the scan results do not contain the AP even though it's + * clearly connected. This may be because we do passive background + * scanning that isn't as 'strong' as active scanning, so even + * though a network is nearby, it won't be seen by the passive + * scanning. If we are connected (or obtaining IP) then we know it + * is seen. + */ + ap.setSeen(true); + } + + attemptReenableAllAps(); + } + + private void handleDisablingScanWhileObtainingAddress(DetailedState detailedState) { + + if (detailedState == DetailedState.OBTAINING_IPADDR) { + mIsObtainingAddress = true; + + // We will not scan while obtaining an IP address + removeFutureScans(); + + } else { + mIsObtainingAddress = false; + + // Start continuous scan + queueContinuousScan(); + } + } + + private void handleScanResultsAvailable() { + synchronized(this) { + // In the end, we'll moved the ones no longer seen into the mApOtherList + List<AccessPointState> oldScanList = mApScanList; + List<AccessPointState> newScanList = + new ArrayList<AccessPointState>(oldScanList.size()); + + List<ScanResult> list = mWifiManager.getScanResults(); + if (list != null) { + for (int i = list.size() - 1; i >= 0; i--) { + final ScanResult scanResult = list.get(i); + + if (LOGV) { +// Log.v(TAG, " " + scanResult); + } + + if (scanResult == null) { + continue; + } + + /* + * Ignore adhoc, enterprise-secured, or hidden networks. + * Hidden networks show up with empty SSID. + */ + if (AccessPointState.isAdhoc(scanResult) + || AccessPointState.isEnterprise(scanResult) + || TextUtils.isEmpty(scanResult.SSID)) { + continue; + } + + final String ssid = AccessPointState.convertToQuotedString(scanResult.SSID); + String security = AccessPointState.getScanResultSecurity(scanResult); + + // See if this AP is part of a group of APs (e.g., any large + // wifi network has many APs, we'll only show one) that we've + // seen in this scan + AccessPointState ap = findApLocked(newScanList, AccessPointState.NETWORK_ID_ANY, + AccessPointState.BSSID_ANY, ssid, security); + + // Yup, we've seen this network. + if (ap != null) { + // Use the better signal + if (WifiManager.compareSignalLevel(scanResult.level, ap.signal) > 0) { + ap.setSignal(scanResult.level); + } + + if (LOGV) { +// Log.v(TAG, " Already seen, continuing.."); + } + + continue; + } + + // Find the AP in either our old scan list, or our non-seen + // configured networks list + ap = findApLocked(AccessPointState.NETWORK_ID_ANY, AccessPointState.BSSID_ANY, + ssid, security); + + if (ap != null) { + // Remove the AP from both (no harm if one doesn't contain it) + oldScanList.remove(ap); + mApOtherList.remove(ap); + } else { + ap = new AccessPointState(mContext); +// if (LOGV) Log.v(TAG, "Created " + ap); + } + + // Give it the latest state + ap.updateFromScanResult(scanResult); + + if (mCallback != null) { + mCallback.onAccessPointSetChanged(ap, true); + } + + newScanList.add(ap); + } + } + + // oldScanList contains the ones no longer seen + List<AccessPointState> otherList = mApOtherList; + for (int i = oldScanList.size() - 1; i >= 0; i--) { + final AccessPointState ap = oldScanList.get(i); + + if (ap.configured) { + + // Keep it around, since it is configured + ap.setSeen(false); + otherList.add(ap); + + } else { + + // Remove it since it is not configured and not seen + removeApFromUi(ap); + } + } + + mApScanList = newScanList; + } + + onScanningEnded(); + } + + private void handleSupplicantConnectionChanged(boolean connected) { + if (mCallback != null) { + mCallback.onAccessPointsStateChanged(connected); + } + + if (connected) { + refreshAll(true); + } + } + + private void handleWifiStateChanged(int wifiState) { + + if (wifiState == WIFI_STATE_ENABLED) { + loadConfiguredAccessPoints(); + attemptScan(); + + } else if (wifiState == WIFI_STATE_DISABLED) { + removeFutureScans(); + if (LOGV) Log.v(TAG, "Clearing AP lists because wifi is disabled"); + clearApLists(); + } + + if (mCallback != null) { + mCallback.onAccessPointsStateChanged(wifiState == WIFI_STATE_ENABLED); + } + } + + private void handleSignalChanged(int rssi) { + + if (mCurrentPrimaryAp != null) { + mCurrentPrimaryAp.setSignal(rssi); + } + } + + private void handleSupplicantStateChanged(SupplicantState state, boolean hasError, int error) { + mCurrentSupplicantState = state; + + if (SupplicantState.FOUR_WAY_HANDSHAKE.equals(state)) { + mLastAuthenticatingAp = getCurrentAp(); + } + + if (hasError) { + handleSupplicantStateError(error); + } + } + + private void handleSupplicantStateError(int supplicantError) { + if (supplicantError == WifiManager.ERROR_AUTHENTICATING) { + if (mCallback != null) { + if (mLastAuthenticatingAp != null) { + mCallback.onRetryPassword(mLastAuthenticatingAp); + } + } + } + } + + private void handleNetworkIdsChanged() { + synchronized (this) { + final List<WifiConfiguration> configs = getConfiguredNetworks(); + + for (int i = configs.size() - 1; i >= 0; i--) { + final WifiConfiguration config = configs.get(i); + + AccessPointState ap; + // Since network IDs have changed, we can't use it to find our previous AP state + ap = findApLocked(AccessPointState.NETWORK_ID_ANY, config.BSSID, config.SSID, + AccessPointState.getWifiConfigurationSecurity(config)); + + if (ap == null) { + continue; + } + + ap.setNetworkId(config.networkId); + } + } + } + + private class MyHandler extends Handler { + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_ATTEMPT_SCAN: + attemptScan(); + break; + } + } + } + +} diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java new file mode 100644 index 000000000..d2683c0f4 --- /dev/null +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.ProgressCategory; +import com.android.settings.R; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.preference.CheckBoxPreference; +import android.provider.Settings; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView; +import android.widget.Toast; +import android.widget.AdapterView.AdapterContextMenuInfo; + +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Settings screen for WiFi. This will be launched from the main system settings. + */ +public class WifiSettings extends PreferenceActivity implements WifiLayer.Callback, + DialogInterface.OnDismissListener { + + private static final String TAG = "WifiSettings"; + + //============================ + // Preference/activity member variables + //============================ + + private static final String INSTANCE_KEY_DIALOG_BUNDLE = + "com.android.settings.wifi.WifiSettings:dialogBundle"; + /* + * We don't use Activity's dialog management because AlertDialog isn't fully + * able to change many of its features after it's been created, and the + * dialog management only creates once. + */ + private Dialog mDialog; + + private static final String KEY_ADD_OTHER_NETWORK = "add_other_network"; + + private static final int CONTEXT_MENU_ID_CONNECT = Menu.FIRST; + private static final int CONTEXT_MENU_ID_FORGET = Menu.FIRST + 1; + private static final int CONTEXT_MENU_ID_CHANGE_PASSWORD = Menu.FIRST + 2; + + private static final int MENU_ID_SCAN = Menu.FIRST; + private static final int MENU_ID_ADVANCED = Menu.FIRST + 1; + + private static final String KEY_WIFI_ENABLED = "wifi_enabled"; + private static final String KEY_OPEN_NETWORK_NOTIFICATIONS_ENABLED = + "open_network_notifications_enabled"; + private static final String KEY_ACCESS_POINTS = "access_points"; + + private ProgressCategory mApCategory; + private CheckBoxPreference mWifiEnabled; + private WifiEnabler mWifiEnabler; + private CheckBoxPreference mOpenNetworkNotificationsEnabled; + private Preference mAddOtherNetwork; + + private WeakHashMap<AccessPointState, AccessPointPreference> mAps; + + //============================ + // Wifi member variables + //============================ + + private WifiLayer mWifiLayer; + + //============================ + // Activity lifecycle + //============================ + + public WifiSettings() { + mAps = new WeakHashMap<AccessPointState, AccessPointPreference>(); + mWifiLayer = new WifiLayer(this, this); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + onCreatePreferences(); + mWifiLayer.onCreate(); + + onCreatedWifi(); + mWifiLayer.onCreatedCallback(); + } + + /** + * Shouldn't have any dependency on the wifi layer. + */ + private void onCreatePreferences() { + addPreferencesFromResource(R.xml.wifi_settings); + + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + + mApCategory = (ProgressCategory) preferenceScreen.findPreference(KEY_ACCESS_POINTS); + // We don't want the ordering to be the order preferences are added, + // instead we want*: + // 1) preferred, visible APs + // 2) visible APs + // 3) preferred, APs out of range + // * this ordering logic is in AccessPointPreference's compareTo + mApCategory.setOrderingAsAdded(false); + + mWifiEnabled = (CheckBoxPreference) preferenceScreen.findPreference(KEY_WIFI_ENABLED); + mWifiEnabler = new WifiEnabler(this, (WifiManager) getSystemService(WIFI_SERVICE), + mWifiEnabled); + + mOpenNetworkNotificationsEnabled = (CheckBoxPreference) preferenceScreen + .findPreference(KEY_OPEN_NETWORK_NOTIFICATIONS_ENABLED); + mOpenNetworkNotificationsEnabled.setChecked(Settings.Secure.getInt(getContentResolver(), + Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 0) == 1); + + mAddOtherNetwork = preferenceScreen.findPreference(KEY_ADD_OTHER_NETWORK); + + registerForContextMenu(getListView()); + } + + private void onCreatedWifi() { + } + + @Override + protected void onResume() { + super.onResume(); + mWifiLayer.onResume(); + mWifiEnabler.resume(); + } + + @Override + protected void onPause() { + super.onPause(); + mWifiLayer.onPause(); + mWifiEnabler.pause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mDialog != null) { + mDialog.dismiss(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + menu.add(0, MENU_ID_SCAN, 0, R.string.scan_wifi) + .setIcon(R.drawable.ic_menu_scan_network); + + menu.add(0, MENU_ID_ADVANCED, 0, R.string.wifi_menu_advanced) + .setIcon(android.R.drawable.ic_menu_manage); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + + case MENU_ID_SCAN: + mWifiLayer.attemptScan(); + return true; + + case MENU_ID_ADVANCED: + Intent intent = new Intent(this, AdvancedSettings.class); + startActivity(intent); + return true; + + default: + return false; + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (mDialog != null) { + Bundle dialogBundle = mDialog.onSaveInstanceState(); + outState.putBundle(INSTANCE_KEY_DIALOG_BUNDLE, dialogBundle); + } + } + + @Override + protected void onRestoreInstanceState(Bundle state) { + super.onRestoreInstanceState(state); + + Bundle dialogBundle = state.getBundle(INSTANCE_KEY_DIALOG_BUNDLE); + if (dialogBundle != null) { + mDialog = new AccessPointDialog(this, mWifiLayer); + mDialog.onRestoreInstanceState(dialogBundle); + showDialog(mDialog); + } + } + + /** + * {@inheritDoc} + */ + public void onDismiss(DialogInterface dialog) { + if (dialog == mDialog) { + mDialog = null; + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + AccessPointState state = getStateFromMenuInfo(menuInfo); + if (state == null) { + return; + } + + menu.setHeaderTitle(state.getHumanReadableSsid()); + + if (state.isConnectable()) { + menu.add(0, CONTEXT_MENU_ID_CONNECT, 0, R.string.wifi_context_menu_connect); + } + + if (state.isForgetable()) { + menu.add(0, CONTEXT_MENU_ID_FORGET, 1, R.string.wifi_context_menu_forget); + + if (state.hasPassword()) { + menu.add(0, CONTEXT_MENU_ID_CHANGE_PASSWORD, 2, + R.string.wifi_context_menu_change_password); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + + AccessPointState state = getStateFromMenuInfo(item.getMenuInfo()); + if (state == null) { + return false; + } + + switch (item.getItemId()) { + + case CONTEXT_MENU_ID_CONNECT: + connectToNetwork(state); + return true; + + case CONTEXT_MENU_ID_FORGET: + mWifiLayer.forgetNetwork(state); + return true; + + case CONTEXT_MENU_ID_CHANGE_PASSWORD: + showAccessPointDialog(state, AccessPointDialog.MODE_CONFIGURE); + return true; + + default: + return false; + } + } + + /** + * Decides what needs to happen to connect to a particular access point. If + * it is secured and doesn't already have a password, it will bring up a + * password box. Otherwise it will just connect. + */ + private void connectToNetwork(AccessPointState state) { + if (state.hasSecurity() && !state.hasPassword()) { + showAccessPointDialog(state, AccessPointDialog.MODE_INFO); + } else { + mWifiLayer.connectToNetwork(state); + } + } + + private AccessPointState getStateFromMenuInfo(ContextMenuInfo menuInfo) { + if ((menuInfo == null) || !(menuInfo instanceof AdapterContextMenuInfo)) { + return null; + } + + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + Preference pref = (Preference) getPreferenceScreen().getRootAdapter().getItem( + adapterMenuInfo.position); + if (pref == null || !(pref instanceof AccessPointPreference)) { + return null; + } + + return ((AccessPointPreference) pref).getAccessPointState(); + } + + //============================ + // Preference callbacks + //============================ + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { + super.onPreferenceTreeClick(preferenceScreen, preference); + + if (preference == mAddOtherNetwork) { + showAddOtherNetworkDialog(); + } else if (preference == mOpenNetworkNotificationsEnabled) { + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, + mOpenNetworkNotificationsEnabled.isChecked() ? 1 : 0); + } else if (preference instanceof AccessPointPreference) { + AccessPointState state = ((AccessPointPreference) preference).getAccessPointState(); + showAccessPointDialog(state, AccessPointDialog.MODE_INFO); + } + + return false; + } + + //============================ + // Wifi-related + //============================ + + public WifiLayer getWifiLayer() { + return mWifiLayer; + } + + private void showAddOtherNetworkDialog() { + AccessPointDialog dialog = new AccessPointDialog(this, mWifiLayer); + dialog.setState(new AccessPointState(this)); + dialog.setMode(AccessPointDialog.MODE_CONFIGURE); + dialog.setTitle(R.string.wifi_add_other_network); + dialog.setAutoSecurityAllowed(false); + showDialog(dialog); + } + + public void showAccessPointDialog(AccessPointState state, int mode) { + AccessPointDialog dialog = new AccessPointDialog(this, mWifiLayer); + dialog.setMode(mode); + dialog.setState(state); + showDialog(dialog); + } + + private void showDialog(Dialog dialog) { + // Have only one dialog open at a time + if (mDialog != null) { + mDialog.dismiss(); + } + + mDialog = dialog; + dialog.setOnDismissListener(this); + if (dialog != null) { + dialog.show(); + } + } + + //============================ + // Wifi callbacks + //============================ + + public void onError(int messageResId) { + Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show(); + } + + public void onScanningStatusChanged(boolean started) { + mApCategory.setProgress(started); + } + + public void onAccessPointSetChanged(AccessPointState ap, boolean added) { + + AccessPointPreference pref = mAps.get(ap); + + if (WifiLayer.LOGV) { + Log.v(TAG, "onAccessPointSetChanged with " + ap + " and " + + (added ? "added" : "removed") + ", found pref " + pref); + } + + if (added) { + + if (pref == null) { + pref = new AccessPointPreference(this, ap); + mAps.put(ap, pref); + } else { + pref.setEnabled(true); + } + + mApCategory.addPreference(pref); + + } else { + + mAps.remove(ap); + + if (pref != null) { + mApCategory.removePreference(pref); + } + + } + } + + public void onAccessPointsStateChanged(boolean enabled) { + if (enabled) { + mApCategory.setEnabled(true); + } else { + mApCategory.removeAll(); + mAps.clear(); + } + + mAddOtherNetwork.setEnabled(enabled); + } + + public void onRetryPassword(AccessPointState ap) { + + if ((mDialog != null) && mDialog.isShowing()) { + // If we're already showing a dialog, ignore this request + return; + } + + showAccessPointDialog(ap, AccessPointDialog.MODE_RETRY_PASSWORD); + } + +} diff --git a/src/com/android/settings/wifi/WifiStatus.java b/src/com/android/settings/wifi/WifiStatus.java new file mode 100644 index 000000000..d4b6431d0 --- /dev/null +++ b/src/com/android/settings/wifi/WifiStatus.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.wifi; + +import com.android.settings.R; + +import android.content.Context; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.text.TextUtils; + +public class WifiStatus { + + // e.g., "Connecting" + public static String sScanning; + public static String sConnecting; + public static String sAuthenticating; + public static String sObtainingIp; + public static String sConnected; + public static String sDisconnecting; + public static String sDisconnected; + public static String sFailed; + + // e.g., "Connecting to %1$s" + public static String sScanningFragment; + public static String sConnectingFragment; + public static String sAuthenticatingFragment; + public static String sObtainingIpFragment; + public static String sConnectedFragment; + public static String sDisconnectingFragment; + public static String sDisconnectedFragment; + public static String sFailedFragment; + + private static void fillStrings(Context context) { + sScanning = context.getString(R.string.status_scanning); + sConnecting = context.getString(R.string.status_connecting); + sAuthenticating = context.getString(R.string.status_authenticating); + sObtainingIp = context.getString(R.string.status_obtaining_ip); + sConnected = context.getString(R.string.status_connected); + sDisconnecting = context.getString(R.string.status_disconnecting); + sDisconnected = context.getString(R.string.status_disconnected); + sFailed = context.getString(R.string.status_failed); + + sScanningFragment = context.getString(R.string.fragment_status_scanning); + sConnectingFragment = context.getString(R.string.fragment_status_connecting); + sAuthenticatingFragment = context.getString(R.string.fragment_status_authenticating); + sObtainingIpFragment = context.getString(R.string.fragment_status_obtaining_ip); + sConnectedFragment = context.getString(R.string.fragment_status_connected); + sDisconnectingFragment = context.getString(R.string.fragment_status_disconnecting); + sDisconnectedFragment = context.getString(R.string.fragment_status_disconnected); + sFailedFragment = context.getString(R.string.fragment_status_failed); + } + + public static String getStatus(Context context, String ssid, + NetworkInfo.DetailedState detailedState) { + + if (!TextUtils.isEmpty(ssid) && isLiveConnection(detailedState)) { + return getPrintableFragment(context, detailedState, ssid); + } else { + return getPrintable(context, detailedState); + } + } + + public static boolean isLiveConnection(NetworkInfo.DetailedState detailedState) { + return detailedState != NetworkInfo.DetailedState.DISCONNECTED + && detailedState != NetworkInfo.DetailedState.FAILED + && detailedState != NetworkInfo.DetailedState.IDLE + && detailedState != NetworkInfo.DetailedState.SCANNING; + } + + public static String getPrintable(Context context, + NetworkInfo.DetailedState detailedState) { + + if (sScanning == null) { + fillStrings(context); + } + + switch (detailedState) { + case AUTHENTICATING: + return sAuthenticating; + case CONNECTED: + return sConnected; + case CONNECTING: + return sConnecting; + case DISCONNECTED: + return sDisconnected; + case DISCONNECTING: + return sDisconnecting; + case FAILED: + return sFailed; + case OBTAINING_IPADDR: + return sObtainingIp; + case SCANNING: + return sScanning; + default: + return null; + } + } + + public static String getPrintableFragment(Context context, + NetworkInfo.DetailedState detailedState, String apName) { + + if (sScanningFragment == null) { + fillStrings(context); + } + + String fragment = null; + switch (detailedState) { + case AUTHENTICATING: + fragment = sAuthenticatingFragment; + break; + case CONNECTED: + fragment = sConnectedFragment; + break; + case CONNECTING: + fragment = sConnectingFragment; + break; + case DISCONNECTED: + fragment = sDisconnectedFragment; + break; + case DISCONNECTING: + fragment = sDisconnectingFragment; + break; + case FAILED: + fragment = sFailedFragment; + break; + case OBTAINING_IPADDR: + fragment = sObtainingIpFragment; + break; + case SCANNING: + fragment = sScanningFragment; + break; + } + + return String.format(fragment, apName); + } + +} |