diff options
author | cretin45 <cretin45@gmail.com> | 2015-01-15 16:04:44 -0800 |
---|---|---|
committer | cretin45 <cretin45@gmail.com> | 2015-01-15 16:04:44 -0800 |
commit | 0328b87bf68f6389049991c68caa515f4230f95f (patch) | |
tree | 556b0a23df4bb849eada991b01f4861c651f25e8 /src/com/cyanogenmod/setupwizard | |
download | packages_apps_SetupWizard-0328b87bf68f6389049991c68caa515f4230f95f.zip packages_apps_SetupWizard-0328b87bf68f6389049991c68caa515f4230f95f.tar.gz packages_apps_SetupWizard-0328b87bf68f6389049991c68caa515f4230f95f.tar.bz2 |
SetupWizard: Initial commit
Diffstat (limited to 'src/com/cyanogenmod/setupwizard')
21 files changed, 5272 insertions, 0 deletions
diff --git a/src/com/cyanogenmod/setupwizard/SetupWizardApp.java b/src/com/cyanogenmod/setupwizard/SetupWizardApp.java new file mode 100644 index 0000000..4df1194 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/SetupWizardApp.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard; + + +import android.app.Application; +import android.app.StatusBarManager; +import android.content.Context; + +public class SetupWizardApp extends Application { + + public static final String TAG = SetupWizardApp.class.getSimpleName(); + // Leave this off for release + public static final boolean DEBUG = false; + + public static final String ACCOUNT_TYPE_CYANOGEN = "com.cyanogen"; + public static final String ACCOUNT_TYPE_GMS = "com.google"; + + public static final String ACTION_SETUP_WIFI = "com.android.net.wifi.SETUP_WIFI_NETWORK"; + + public static final String EXTRA_FIRST_RUN = "firstRun"; + public static final String EXTRA_ALLOW_SKIP = "allowSkip"; + public static final String EXTRA_AUTO_FINISH = "wifi_auto_finish_on_connect"; + + public static final int REQUEST_CODE_SETUP_WIFI = 0; + + private StatusBarManager mStatusBarManager; + + @Override + public void onCreate() { + super.onCreate(); + mStatusBarManager = (StatusBarManager)getSystemService(Context.STATUS_BAR_SERVICE); + } + + public void disableStatusBar() { + mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND | StatusBarManager.DISABLE_NOTIFICATION_ALERTS + | StatusBarManager.DISABLE_NOTIFICATION_TICKER | StatusBarManager.DISABLE_RECENT | StatusBarManager.DISABLE_HOME + | StatusBarManager.DISABLE_SEARCH); + } + + public void enableStatusBar() { + mStatusBarManager.disable(StatusBarManager.DISABLE_NONE); + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/AbstractSetupData.java b/src/com/cyanogenmod/setupwizard/setup/AbstractSetupData.java new file mode 100644 index 0000000..fed8732 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/AbstractSetupData.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; + +import java.util.ArrayList; + +public abstract class AbstractSetupData implements SetupDataCallbacks { + + private static final String TAG = AbstractSetupData.class.getSimpleName(); + + protected Context mContext; + private ArrayList<SetupDataCallbacks> mListeners = new ArrayList<SetupDataCallbacks>(); + private PageList mPageList; + + private int mCurrentPageIndex = 0; + + public AbstractSetupData(Context context) { + mContext = context; + mPageList = onNewPageList(); + } + + protected abstract PageList onNewPageList(); + + @Override + public void onPageLoaded(Page page) { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onPageLoaded(page); + } + } + + @Override + public void onPageTreeChanged() { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onPageTreeChanged(); + } + } + + @Override + public void onFinish() { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onFinish(); + } + } + + @Override + public Page getPage(String key) { + return mPageList.getPage(key); + } + + @Override + public Page getPage(int index) { + return mPageList.getPage(index); + } + + public Page getCurrentPage() { + return mPageList.getPage(mCurrentPageIndex); + } + + public boolean isFirstPage() { + return mCurrentPageIndex == 0; + } + + public boolean isLastPage() { + return mCurrentPageIndex == mPageList.size() - 1; + } + + @Override + public void onNextPage() { + if (getCurrentPage().doNextAction() == false) { + if (advanceToNextUncompleted()) { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onNextPage(); + } + } + } + } + + @Override + public void onPreviousPage() { + if (getCurrentPage().doPreviousAction() == false) { + if (advanceToPreviousUncompleted()) { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onPreviousPage(); + } + } + } + } + + @Override + public void onPageViewCreated(LayoutInflater inflater, Bundle savedInstanceState, + int layoutResource) {} + + private boolean advanceToNextUncompleted() { + while (mCurrentPageIndex < mPageList.size()) { + mCurrentPageIndex++; + if (!getCurrentPage().isCompleted()) { + return true; + } + } + return false; + } + + private boolean advanceToPreviousUncompleted() { + while (mCurrentPageIndex > 0) { + mCurrentPageIndex--; + if (!getCurrentPage().isCompleted()) { + return true; + } + } + return false; + } + + public void load(Bundle savedValues) { + for (String key : savedValues.keySet()) { + Page page = mPageList.getPage(key); + if (page != null) { + page.resetData(savedValues.getBundle(key)); + } + } + } + + public Bundle save() { + Bundle bundle = new Bundle(); + for (Page page : mPageList.values()) { + bundle.putBundle(page.getKey(), page.getData()); + } + return bundle; + } + + public void registerListener(SetupDataCallbacks listener) { + mListeners.add(listener); + } + + public void unregisterListener(SetupDataCallbacks listener) { + mListeners.remove(listener); + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/CMSetupWizardData.java b/src/com/cyanogenmod/setupwizard/setup/CMSetupWizardData.java new file mode 100644 index 0000000..e50f1ac --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/CMSetupWizardData.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import com.cyanogenmod.setupwizard.util.SetupWizardUtils; + +import android.content.Context; +import android.telephony.SubscriptionManager; + +import java.util.ArrayList; + +public class CMSetupWizardData extends AbstractSetupData { + + public CMSetupWizardData(Context context) { + super(context); + } + + @Override + protected PageList onNewPageList() { + ArrayList<SetupPage> pages = new ArrayList<SetupPage>(); + pages.add(new WelcomePage(mContext, this)); + pages.add(new WifiSetupPage(mContext, this)); + if (SetupWizardUtils.isGSMPhone(mContext) && SetupWizardUtils.isSimMissing(mContext)) { + pages.add(new SimCardMissingPage(mContext, this)); + } + if (SetupWizardUtils.isMultiSimDevice(mContext) + && SubscriptionManager.getActiveSubInfoCount() > 1) { + pages.add(new ChooseDataSimPage(mContext, this)); + } + if (SetupWizardUtils.hasTelephony(mContext) && + !SetupWizardUtils.isMobileDataEnabled(mContext)) { + pages.add(new MobileDataPage(mContext, this)); + } + if (SetupWizardUtils.hasGMS(mContext)) { + pages.add(new GmsAccountPage(mContext, this)); + } + pages.add(new CyanogenAccountPage(mContext, this)); + pages.add(new LocationSettingsPage(mContext, this)); + pages.add(new DateTimePage(mContext, this)); + pages.add(new FinishPage(mContext, this)); + return new PageList(pages.toArray(new SetupPage[pages.size()])); + } + + +}
\ No newline at end of file diff --git a/src/com/cyanogenmod/setupwizard/setup/ChooseDataSimPage.java b/src/com/cyanogenmod/setupwizard/setup/ChooseDataSimPage.java new file mode 100644 index 0000000..820edc6 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/ChooseDataSimPage.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.SubInfoRecord; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +import java.util.List; + +public class ChooseDataSimPage extends SetupPage { + + public static final String TAG = "ChooseDataSimPage"; + + public ChooseDataSimPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + ChooseDataSimFragment fragment = new ChooseDataSimFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_choose_data_sim; + } + + @Override + public int getNextButtonTitleResId() { + return R.string.skip; + } + + + public static class ChooseDataSimFragment extends SetupPageFragment { + + private ViewGroup mPageView; + private SparseArray<TextView> mNameViews; + private SparseArray<ImageView> mSignalViews; + private SparseArray<CheckBox> mCheckBoxes; + + private TelephonyManager mPhone; + private List<SubInfoRecord> mSubInfoRecords; + private SparseArray<SignalStrength> mSignalStrengths; + private SparseArray<ServiceState> mServiceStates; + private SparseArray<PhoneStateListener> mPhoneStateListeners; + + private View.OnClickListener mSetDataSimClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + SubInfoRecord subInfoRecord = (SubInfoRecord)view.getTag(); + if (subInfoRecord != null) { + SubscriptionManager.setDefaultDataSubId(subInfoRecord.subId); + setDataSubChecked(subInfoRecord); + } + } + }; + + @Override + protected void initializePage() { + mPageView = (ViewGroup)mRootView.findViewById(R.id.page_view); + mSubInfoRecords = SubscriptionManager.getActiveSubInfoList(); + int simCount = mSubInfoRecords.size(); + mNameViews = new SparseArray<TextView>(simCount); + mSignalViews = new SparseArray<ImageView>(simCount); + mCheckBoxes = new SparseArray<CheckBox>(simCount); + mServiceStates = new SparseArray<ServiceState>(simCount); + mSignalStrengths = new SparseArray<SignalStrength>(simCount); + mPhoneStateListeners = new SparseArray<PhoneStateListener>(simCount); + LayoutInflater inflater = LayoutInflater.from(getActivity()); + for (int i = 0; i < simCount; i++) { + View simRow = inflater.inflate(R.layout.data_sim_row, null); + mPageView.addView(simRow); + SubInfoRecord subInfoRecord = mSubInfoRecords.get(i); + simRow.setTag(subInfoRecord); + simRow.setOnClickListener(mSetDataSimClickListener); + mNameViews.put(i, (TextView) simRow.findViewById(R.id.sim_title)); + mSignalViews.put(i, (ImageView) simRow.findViewById(R.id.signal)); + mCheckBoxes.put(i, (CheckBox) simRow.findViewById(R.id.enable_check)); + mPhoneStateListeners.put(i, createPhoneStateListener(subInfoRecord)); + mPageView.addView(inflater.inflate(R.layout.divider, null)); + } + mPhone = (TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE); + for (int i = 0; i < mPhoneStateListeners.size(); i++) { + mPhone.listen(mPhoneStateListeners.get(i), + PhoneStateListener.LISTEN_SERVICE_STATE + | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); + } + updateSignalStrengths(); + updateCurrentDataSub(); + } + + @Override + protected int getLayoutResource() { + return R.layout.choose_data_sim_page; + } + + @Override + public void onResume() { + super.onResume(); + updateSignalStrengths(); + updateCurrentDataSub(); + } + + @Override + public void onDetach() { + super.onDetach(); + for (int i = 0; i < mPhoneStateListeners.size(); i++) { + mPhone.listen(mPhoneStateListeners.get(i), PhoneStateListener.LISTEN_NONE); + } + } + + private PhoneStateListener createPhoneStateListener(final SubInfoRecord subInfoRecord) { + return new PhoneStateListener(subInfoRecord.subId) { + + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + mSignalStrengths.put(subInfoRecord.slotId, signalStrength); + updateSignalStrength(subInfoRecord); + } + + @Override + public void onServiceStateChanged(ServiceState state) { + mServiceStates.put(subInfoRecord.slotId, state); + updateSignalStrength(subInfoRecord); + } + }; + } + + private void updateSignalStrengths() { + for (int i = 0; i < mSubInfoRecords.size(); i++) { + updateSignalStrength(mSubInfoRecords.get(i)); + } + } + + private void setDataSubChecked(SubInfoRecord subInfoRecord) { + for (int i = 0; i < mCheckBoxes.size(); i++) { + mCheckBoxes.get(i).setChecked(subInfoRecord.slotId == i); + + } + } + + private void updateCurrentDataSub() { + for (int i = 0; i < mSubInfoRecords.size(); i++) { + SubInfoRecord subInfoRecord = mSubInfoRecords.get(i); + mCheckBoxes.get(i).setChecked(SubscriptionManager.getDefaultDataSubId() + == subInfoRecord.subId); + + } + } + + private void updateCarrierText(SubInfoRecord subInfoRecord) { + String name = mPhone.getNetworkOperatorName(subInfoRecord.subId); + ServiceState serviceState = mServiceStates.get(subInfoRecord.slotId); + if (TextUtils.isEmpty(name)) { + if (serviceState != null && serviceState.isEmergencyOnly()) { + name = getString(R.string.setup_mobile_data_emergency_only); + } else { + name = getString(R.string.setup_mobile_data_no_service); + } + } + String formattedName = + getString(R.string.data_sim_name, subInfoRecord.slotId + 1, name); + mNameViews.get(subInfoRecord.slotId).setText(formattedName); + } + + private void updateSignalStrength(SubInfoRecord subInfoRecord) { + ImageView signalView = mSignalViews.get(subInfoRecord.slotId); + SignalStrength signalStrength = mSignalStrengths.get(subInfoRecord.slotId); + if (!hasService(subInfoRecord)) { + signalView.setImageResource(R.drawable.ic_signal_no_signal); + } else { + if (signalStrength != null) { + int resId; + switch (signalStrength.getLevel()) { + case 4: + resId = R.drawable.ic_signal_4; + break; + case 3: + resId = R.drawable.ic_signal_3; + break; + case 2: + resId = R.drawable.ic_signal_2; + break; + case 1: + resId = R.drawable.ic_signal_1; + break; + default: + resId = R.drawable.ic_signal_0; + break; + } + signalView.setImageResource(resId); + } + } + updateCarrierText(subInfoRecord); + } + + private boolean hasService(SubInfoRecord subInfoRecord) { + boolean retVal; + ServiceState serviceState = mServiceStates.get(subInfoRecord.slotId); + if (serviceState != null) { + // Consider the device to be in service if either voice or data service is available. + // Some SIM cards are marketed as data-only and do not support voice service, and on + // these SIM cards, we want to show signal bars for data service as well as the "no + // service" or "emergency calls only" text that indicates that voice is not available. + switch(serviceState.getVoiceRegState()) { + case ServiceState.STATE_POWER_OFF: + retVal = false; + break; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + retVal = serviceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; + break; + default: + retVal = true; + } + } else { + retVal = false; + } + Log.d(TAG, "hasService: mServiceState=" + serviceState + " retVal=" + retVal); + return retVal; + } + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/CyanogenAccountPage.java b/src/com/cyanogenmod/setupwizard/setup/CyanogenAccountPage.java new file mode 100644 index 0000000..dc48faa --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/CyanogenAccountPage.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import com.cyanogenmod.setupwizard.SetupWizardApp; +import com.cyanogenmod.setupwizard.R; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; + +public class CyanogenAccountPage extends SetupPage { + + public static final String TAG = "CyanogenAccountPage"; + + public CyanogenAccountPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public int getNextButtonTitleResId() { + return R.string.skip; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return -1; + } + + @Override + public void doLoadAction(Activity context, int action) { + launchCyanogenAccountSetup(context, action); + } + + public void launchCyanogenAccountSetup(final Activity activity, final int action) { + Bundle bundle = new Bundle(); + bundle.putBoolean(SetupWizardApp.EXTRA_FIRST_RUN, true); + AccountManager + .get(activity).addAccount(SetupWizardApp.ACCOUNT_TYPE_CYANOGEN, null, null, bundle, + activity, new AccountManagerCallback<Bundle>() { + @Override + public void run(AccountManagerFuture<Bundle> bundleAccountManagerFuture) { + if (activity == null) return; //There is a chance this activity has been torn down. + if (accountExists(activity, SetupWizardApp.ACCOUNT_TYPE_CYANOGEN)) { + setCompleted(true); + getCallbacks().onNextPage(); + } else { + if (action == Page.ACTION_NEXT) { + getCallbacks().onNextPage(); + } else { + getCallbacks().onPreviousPage(); + } + } + } + }, null); + } + + private boolean accountExists(Activity activity, String accountType) { + return AccountManager.get(activity).getAccountsByType(accountType).length > 0; + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/DateTimePage.java b/src/com/cyanogenmod/setupwizard/setup/DateTimePage.java new file mode 100644 index 0000000..40eda95 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/DateTimePage.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.TimePickerDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.os.Handler; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.DatePicker; +import android.widget.SimpleAdapter; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.TimePicker; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +public class DateTimePage extends SetupPage { + + public static final String TAG = "DateTimePage"; + + private static final String KEY_ID = "id"; // value: String + private static final String KEY_DISPLAYNAME = "name"; // value: String + private static final String KEY_GMT = "gmt"; // value: String + private static final String KEY_OFFSET = "offset"; // value: int (Integer) + private static final String XMLTAG_TIMEZONE = "timezone"; + + private static final int HOURS_1 = 60 * 60000; + + + public DateTimePage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + DateTimeFragment fragment = new DateTimeFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_datetime; + } + + public static class DateTimeFragment extends SetupPageFragment + implements TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener { + + private TimeZone mCurrentTimeZone; + private View mDateView; + private View mTimeView; + private TextView mDateTextView; + private TextView mTimeTextView; + + + private final Handler mHandler = new Handler(); + + @Override + public void onResume() { + super.onResume(); + // 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); + getActivity().registerReceiver(mIntentReceiver, filter, null, null); + + updateTimeAndDateDisplay(getActivity()); + } + + @Override + public void onPause() { + super.onPause(); + getActivity().unregisterReceiver(mIntentReceiver); + } + + @Override + protected void initializePage() { + final Spinner spinner = (Spinner) mRootView.findViewById(R.id.timezone_list); + final SimpleAdapter adapter = constructTimezoneAdapter(getActivity(), false); + mCurrentTimeZone = TimeZone.getDefault(); + mDateView = mRootView.findViewById(R.id.date_item); + mDateView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDatePicker(); + } + }); + mTimeView = mRootView.findViewById(R.id.time_item); + mTimeView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showTimePicker(); + } + }); + mDateTextView = (TextView)mRootView.findViewById(R.id.date_text); + mTimeTextView = (TextView)mRootView.findViewById(R.id.time_text); + // Pre-select current/default timezone + mHandler.post(new Runnable() { + @Override + public void run() { + int tzIndex = getTimeZoneIndex(adapter, mCurrentTimeZone); + spinner.setAdapter(adapter); + if (tzIndex != -1) { + spinner.setSelection(tzIndex); + } + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) { + final Map<?, ?> map = (Map<?, ?>) adapterView.getItemAtPosition(position); + final String tzId = (String) map.get(KEY_ID); + if (mCurrentTimeZone != null && !mCurrentTimeZone.getID().equals(tzId)) { + // Update the system timezone value + final Activity activity = getActivity(); + final AlarmManager alarm = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone(tzId); + mCurrentTimeZone = TimeZone.getTimeZone(tzId); + } + + } + + @Override + public void onNothingSelected(AdapterView<?> adapterView) { + } + }); + } + }); + } + + private void showDatePicker() { + DatePickerFragment datePickerFragment = DatePickerFragment.newInstance(); + datePickerFragment.setOnDateSetListener(this); + datePickerFragment.show(getFragmentManager(), DatePickerFragment.TAG); + } + + private void showTimePicker() { + TimePickerFragment timePickerFragment = TimePickerFragment.newInstance(); + timePickerFragment.setOnTimeSetListener(this); + timePickerFragment.show(getFragmentManager(), TimePickerFragment.TAG); + } + + public void updateTimeAndDateDisplay(Context context) { + java.text.DateFormat shortDateFormat = DateFormat.getDateFormat(context); + final Calendar now = Calendar.getInstance(); + mTimeTextView.setText(DateFormat.getTimeFormat(getActivity()).format(now.getTime())); + mDateTextView.setText(shortDateFormat.format(now.getTime())); + } + + @Override + protected int getLayoutResource() { + return R.layout.setup_datetime_page; + } + + @Override + public void onDateSet(DatePicker view, int year, int month, int day) { + final Activity activity = getActivity(); + if (activity != null) { + setDate(activity, year, month, day); + updateTimeAndDateDisplay(activity); + } + } + + @Override + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + final Activity activity = getActivity(); + if (activity != null) { + setTime(activity, hourOfDay, minute); + updateTimeAndDateDisplay(activity); + } + } + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final Activity activity = getActivity(); + if (activity != null) { + updateTimeAndDateDisplay(activity); + } + } + }; + + } + + private static SimpleAdapter constructTimezoneAdapter(Context context, + boolean sortedByName) { + final String[] from = new String[] {KEY_DISPLAYNAME, KEY_GMT}; + final int[] to = new int[] {android.R.id.text1, android.R.id.text2}; + + final String sortKey = (sortedByName ? KEY_DISPLAYNAME : KEY_OFFSET); + final TimeZoneComparator comparator = new TimeZoneComparator(sortKey); + final List<HashMap<String, Object>> sortedList = getZones(context); + Collections.sort(sortedList, comparator); + final SimpleAdapter adapter = new SimpleAdapter(context, + sortedList, + R.layout.date_time_setup_custom_list_item_2, + from, + to); + + return adapter; + } + + private static List<HashMap<String, Object>> getZones(Context context) { + final List<HashMap<String, Object>> myData = new ArrayList<HashMap<String, Object>>(); + final long date = Calendar.getInstance().getTimeInMillis(); + try { + XmlResourceParser xrp = context.getResources().getXml(R.xml.timezones); + while (xrp.next() != XmlResourceParser.START_TAG) + continue; + 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; + } + + private static void addItem( + List<HashMap<String, Object>> myData, String id, String displayName, long date) { + final HashMap<String, Object> map = new HashMap<String, Object>(); + map.put(KEY_ID, id); + map.put(KEY_DISPLAYNAME, displayName); + final TimeZone tz = TimeZone.getTimeZone(id); + final int offset = tz.getOffset(date); + final int p = Math.abs(offset); + final 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); + + myData.add(map); + } + + private static int getTimeZoneIndex(SimpleAdapter adapter, TimeZone tz) { + final String defaultId = tz.getID(); + final int listSize = adapter.getCount(); + for (int i = 0; i < listSize; i++) { + // Using HashMap<String, Object> induces unnecessary warning. + final HashMap<?,?> map = (HashMap<?,?>)adapter.getItem(i); + final String id = (String)map.get(KEY_ID); + if (defaultId.equals(id)) { + // If current timezone is in this list, move focus to it + return i; + } + } + return -1; + } + + private static void setDate(Context context, 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) { + ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when); + } + } + + private static void setTime(Context context, int hourOfDay, int minute) { + Calendar c = Calendar.getInstance(); + + c.set(Calendar.HOUR_OF_DAY, hourOfDay); + c.set(Calendar.MINUTE, minute); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + long when = c.getTimeInMillis(); + + if (when / 1000 < Integer.MAX_VALUE) { + ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when); + } + } + + private static class TimeZoneComparator implements Comparator<HashMap<?, ?>> { + private String mSortingKey; + + public TimeZoneComparator(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); + } + } + + private static class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener { + + private static String TAG = TimePickerFragment.class.getSimpleName(); + + private TimePickerDialog.OnTimeSetListener mOnTimeSetListener; + + public static TimePickerFragment newInstance() { + TimePickerFragment frag = new TimePickerFragment(); + return frag; + } + + private void setOnTimeSetListener(TimePickerDialog.OnTimeSetListener onTimeSetListener) { + mOnTimeSetListener = onTimeSetListener; + } + + @Override + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + if (mOnTimeSetListener != null) { + mOnTimeSetListener.onTimeSet(view, hourOfDay, minute); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Calendar calendar = Calendar.getInstance(); + return new TimePickerDialog( + getActivity(), + this, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + DateFormat.is24HourFormat(getActivity())); + + } + } + + private static class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener { + + private static String TAG = DatePickerFragment.class.getSimpleName(); + + private DatePickerDialog.OnDateSetListener mOnDateSetListener; + + public static DatePickerFragment newInstance() { + DatePickerFragment frag = new DatePickerFragment(); + return frag; + } + + private void setOnDateSetListener(DatePickerDialog.OnDateSetListener onDateSetListener) { + mOnDateSetListener = onDateSetListener; + } + + @Override + public void onDateSet(DatePicker view, int year, int month, int day) { + if (mOnDateSetListener != null) { + mOnDateSetListener.onDateSet(view, year, month, day); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Calendar calendar = Calendar.getInstance(); + return new DatePickerDialog( + getActivity(), + this, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)); + + } + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/FinishPage.java b/src/com/cyanogenmod/setupwizard/setup/FinishPage.java new file mode 100644 index 0000000..1ab0df9 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/FinishPage.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; + +public class FinishPage extends SetupPage { + + public static final String TAG = "FinishPage"; + + public FinishPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + FinishFragment fragment = new FinishFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_complete; + } + + @Override + public boolean doNextAction() { + getCallbacks().onFinish(); + return true; + } + + @Override + public int getNextButtonTitleResId() { + return R.string.start; + } + + @Override + public int getPrevButtonTitleResId() { + return -1; + } + + public static class FinishFragment extends SetupPageFragment { + + @Override + protected void initializePage() {} + + @Override + protected int getLayoutResource() { + return R.layout.setup_finished_page; + } + + @Override + protected int getHeaderLayoutResource() { + return -1; + } + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/GmsAccountPage.java b/src/com/cyanogenmod/setupwizard/setup/GmsAccountPage.java new file mode 100644 index 0000000..a7d2aa8 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/GmsAccountPage.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.SetupWizardApp; + +import java.io.IOException; + +public class GmsAccountPage extends SetupPage { + + public static final String TAG = "GmsAccountPage"; + + public GmsAccountPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_gms_account; + } + + @Override + public int getNextButtonTitleResId() { + return R.string.skip; + } + + @Override + public void doLoadAction(Activity context, int action) { + launchGmsAccountSetup(context, action); + } + + public void launchGmsAccountSetup(final Activity activity, final int action) { + Bundle bundle = new Bundle(); + bundle.putBoolean(SetupWizardApp.EXTRA_FIRST_RUN, true); + bundle.putBoolean(SetupWizardApp.EXTRA_ALLOW_SKIP, true); + AccountManager + .get(activity).addAccount(SetupWizardApp.ACCOUNT_TYPE_GMS, null, null, + bundle, activity, new AccountManagerCallback<Bundle>() { + @Override + public void run(AccountManagerFuture<Bundle> bundleAccountManagerFuture) { + //There is a chance this activity has been torn down. + if (activity == null) return; + String token = null; + try { + token = bundleAccountManagerFuture.getResult().getString(AccountManager.KEY_AUTHTOKEN); + } catch (OperationCanceledException e) { + } catch (IOException e) { + } catch (AuthenticatorException e) { + } + if (token != null) { + setCompleted(true); + } + if (action == Page.ACTION_NEXT) { + getCallbacks().onNextPage(); + } else { + getCallbacks().onPreviousPage(); + } + } + }, null); + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/LocationSettingsPage.java b/src/com/cyanogenmod/setupwizard/setup/LocationSettingsPage.java new file mode 100644 index 0000000..00865f4 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/LocationSettingsPage.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Fragment; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.location.LocationManager; +import android.os.Bundle; +import android.provider.Settings; +import android.view.View; +import android.widget.CheckBox; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +import java.util.Observable; +import java.util.Observer; + +public class LocationSettingsPage extends SetupPage { + + private static final String TAG = "LocationSettingsPage"; + + public LocationSettingsPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(Page.KEY_PAGE_ARGUMENT, getKey()); + + LocationSettingsFragment fragment = new LocationSettingsFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_location; + } + + public static class LocationSettingsFragment extends SetupPageFragment { + + private View mLocationRow; + private View mGpsRow; + private View mNetworkRow; + private CheckBox mNetwork; + private CheckBox mGps; + private CheckBox mLocationAccess; + + private ContentResolver mContentResolver; + + // These provide support for receiving notification when Location Manager settings change. + // This is necessary because the Network Location Provider can change settings + // if the user does not confirm enabling the provider. + private ContentQueryMap mContentQueryMap; + private Observer mSettingsObserver; + + + private View.OnClickListener mLocationClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + onToggleLocationAccess(!mLocationAccess.isChecked()); + } + }; + + private View.OnClickListener mGpsClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Settings.Secure.setLocationProviderEnabled(mContentResolver, + LocationManager.GPS_PROVIDER, !mGps.isChecked()); + } + }; + + private View.OnClickListener mNetworkClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Settings.Secure.setLocationProviderEnabled(mContentResolver, + LocationManager.NETWORK_PROVIDER, !mNetwork.isChecked()); + } + }; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mContentResolver = getActivity().getContentResolver(); + getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.primary_dark)); + } + + @Override + protected void initializePage() { + mLocationRow = mRootView.findViewById(R.id.location); + mLocationRow.setOnClickListener(mLocationClickListener); + mLocationAccess = (CheckBox) mRootView.findViewById(R.id.location_checkbox); + mGpsRow = mRootView.findViewById(R.id.gps); + mGpsRow.setOnClickListener(mGpsClickListener); + mGps = (CheckBox) mRootView.findViewById(R.id.gps_checkbox); + mNetworkRow = mRootView.findViewById(R.id.network); + mNetworkRow.setOnClickListener(mNetworkClickListener); + mNetwork = (CheckBox) mRootView.findViewById(R.id.network_checkbox); + } + + @Override + protected int getLayoutResource() { + return R.layout.location_settings; + } + + @Override + protected int getHeaderLayoutResource() { + return R.layout.header_condensed; + } + + @Override + public void onResume() { + super.onResume(); + updateLocationToggles(); + if (mSettingsObserver == null) { + mSettingsObserver = new Observer() { + public void update(Observable o, Object arg) { + updateLocationToggles(); + } + }; + } + + mContentQueryMap.addObserver(mSettingsObserver); + } + + @Override + public void onStart() { + super.onStart(); + // listen for Location Manager settings changes + Cursor settingsCursor = getActivity().getContentResolver().query(Settings.Secure.CONTENT_URI, null, + "(" + Settings.System.NAME + "=?)", + new String[]{Settings.Secure.LOCATION_PROVIDERS_ALLOWED}, + null); + mContentQueryMap = new ContentQueryMap(settingsCursor, Settings.System.NAME, true, null); + } + + @Override + public void onStop() { + super.onStop(); + if (mSettingsObserver != null) { + mContentQueryMap.deleteObserver(mSettingsObserver); + } + mContentQueryMap.close(); + } + + + private void updateLocationToggles() { + boolean gpsEnabled = Settings.Secure.isLocationProviderEnabled( + mContentResolver, LocationManager.GPS_PROVIDER); + boolean networkEnabled = Settings.Secure.isLocationProviderEnabled( + mContentResolver, LocationManager.NETWORK_PROVIDER); + mGps.setChecked(gpsEnabled); + mNetwork.setChecked(networkEnabled); + mLocationAccess.setChecked(gpsEnabled || networkEnabled); + } + + private void onToggleLocationAccess(boolean checked) { + Settings.Secure.setLocationProviderEnabled(mContentResolver, + LocationManager.GPS_PROVIDER, checked); + mGps.setEnabled(checked); + mGpsRow.setEnabled(checked); + Settings.Secure.setLocationProviderEnabled(mContentResolver, + LocationManager.NETWORK_PROVIDER, checked); + mNetwork.setEnabled(checked); + mNetworkRow.setEnabled(checked); + updateLocationToggles(); + } + + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/MobileDataPage.java b/src/com/cyanogenmod/setupwizard/setup/MobileDataPage.java new file mode 100644 index 0000000..9223128 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/MobileDataPage.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; +import com.cyanogenmod.setupwizard.util.SetupWizardUtils; + +public class MobileDataPage extends SetupPage { + + public static final String TAG = "MobileDataPage"; + + public MobileDataPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + MobileDataFragment fragment = new MobileDataFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_mobile_data; + } + + public static class MobileDataFragment extends SetupPageFragment { + + private View mEnableDataRow; + private Switch mEnableMobileData; + private ImageView mSignalView; + private TextView mNameView; + + private TelephonyManager mPhone; + private SignalStrength mSignalStrength; + private ServiceState mServiceState; + + private PhoneStateListener mPhoneStateListener = + new PhoneStateListener(SubscriptionManager.getDefaultDataSubId()) { + + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + mSignalStrength = signalStrength; + updateSignalStrength(); + } + + @Override + public void onServiceStateChanged(ServiceState state) { + mServiceState = state; + updateSignalStrength(); + } + + }; + + private View.OnClickListener mEnableDataClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean checked = !mEnableMobileData.isChecked(); + SetupWizardUtils.setMobileDataEnabled(getActivity(), checked); + mEnableMobileData.setChecked(checked); + } + }; + + @Override + protected void initializePage() { + mEnableDataRow = mRootView.findViewById(R.id.data); + mEnableDataRow.setOnClickListener(mEnableDataClickListener); + mEnableMobileData = (Switch) mRootView.findViewById(R.id.data_switch); + mSignalView = (ImageView) mRootView.findViewById(R.id.signal); + mNameView = (TextView) mRootView.findViewById(R.id.enable_data_title); + updateDataConnectionStatus(); + updateSignalStrength(); + } + + @Override + protected int getLayoutResource() { + return R.layout.mobile_data_settings; + } + + @Override + public void onResume() { + super.onResume(); + updateDataConnectionStatus(); + updateSignalStrength(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mPhone = (TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE); + mPhone.listen(mPhoneStateListener, + PhoneStateListener.LISTEN_SERVICE_STATE + | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); + } + + @Override + public void onDetach() { + super.onDetach(); + mPhone.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + } + + private void updateCarrierText() { + String name = mPhone.getNetworkOperatorName(SubscriptionManager.getDefaultDataSubId()); + if (TextUtils.isEmpty(name)) { + if (mServiceState != null && mServiceState.isEmergencyOnly()) { + name = getString(R.string.setup_mobile_data_emergency_only); + } else { + name = getString(R.string.setup_mobile_data_no_service); + } + } + mNameView.setText(name); + } + + private void updateSignalStrength() { + if (!hasService()) { + mSignalView.setImageResource(R.drawable.ic_signal_no_signal); + } else { + if (mSignalStrength != null) { + int resId; + switch (mSignalStrength.getLevel()) { + case 4: + resId = R.drawable.ic_signal_4; + break; + case 3: + resId = R.drawable.ic_signal_3; + break; + case 2: + resId = R.drawable.ic_signal_2; + break; + case 1: + resId = R.drawable.ic_signal_1; + break; + default: + resId = R.drawable.ic_signal_0; + break; + } + mSignalView.setImageResource(resId); + } + } + updateCarrierText(); + } + + private void updateDataConnectionStatus() { + mEnableMobileData.setChecked(SetupWizardUtils.isMobileDataEnabled(getActivity())); + } + + private boolean hasService() { + boolean retVal; + if (mServiceState != null) { + // Consider the device to be in service if either voice or data service is available. + // Some SIM cards are marketed as data-only and do not support voice service, and on + // these SIM cards, we want to show signal bars for data service as well as the "no + // service" or "emergency calls only" text that indicates that voice is not available. + switch(mServiceState.getVoiceRegState()) { + case ServiceState.STATE_POWER_OFF: + retVal = false; + break; + case ServiceState.STATE_OUT_OF_SERVICE: + case ServiceState.STATE_EMERGENCY_ONLY: + retVal = mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; + break; + default: + retVal = true; + } + } else { + retVal = false; + } + return retVal; + } + + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/Page.java b/src/com/cyanogenmod/setupwizard/setup/Page.java new file mode 100644 index 0000000..31a02a7 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/Page.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; + +public interface Page { + + public static final String KEY_PAGE_ARGUMENT = "key_arg"; + + public static final int ACTION_NEXT = 1; + public static final int ACTION_PREVIOUS = 2; + + public String getKey(); + public int getTitleResId(); + public int getPrevButtonTitleResId(); + public int getNextButtonTitleResId(); + public Fragment getFragment(); + public Bundle getData(); + public void resetData(Bundle data); + public boolean isRequired(); + public Page setRequired(boolean required); + public boolean isCompleted(); + public void setCompleted(boolean completed); + public boolean doPreviousAction(); + public boolean doNextAction(); + public void doLoadAction(Activity context, int action); + public abstract boolean onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/src/com/cyanogenmod/setupwizard/setup/PageList.java b/src/com/cyanogenmod/setupwizard/setup/PageList.java new file mode 100644 index 0000000..6709d47 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/PageList.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import java.util.LinkedHashMap; + +public class PageList extends LinkedHashMap<String, Page> { + + public PageList(Page... pages) { + for (Page page : pages) { + put(page.getKey(), page); + } + } + + public Page getPage(String key) { + return get(key); + } + + public Page getPage(int index) { + int i=0; + for (Page page : values()) { + if (i == index) { + return page; + } + i++; + } + return null; + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/SetupDataCallbacks.java b/src/com/cyanogenmod/setupwizard/setup/SetupDataCallbacks.java new file mode 100644 index 0000000..2e787b4 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/SetupDataCallbacks.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.os.Bundle; +import android.view.LayoutInflater; + +public interface SetupDataCallbacks { + void onNextPage(); + void onPreviousPage(); + void onPageLoaded(Page page); + void onPageTreeChanged(); + void onFinish(); + Page getPage(String key); + Page getPage(int key); + void onPageViewCreated(LayoutInflater inflater, Bundle savedInstanceState, int layoutResource); +} diff --git a/src/com/cyanogenmod/setupwizard/setup/SetupPage.java b/src/com/cyanogenmod/setupwizard/setup/SetupPage.java new file mode 100644 index 0000000..c607857 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/SetupPage.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import com.cyanogenmod.setupwizard.R; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + + +public abstract class SetupPage implements Page { + + private final SetupDataCallbacks mCallbacks; + + private Bundle mData = new Bundle(); + private boolean mRequired = false; + private boolean mCompleted = false; + + protected final Context mContext; + + protected SetupPage(Context context, SetupDataCallbacks callbacks) { + mContext = context; + mCallbacks = callbacks; + } + + @Override + public Fragment getFragment() { + return null; + } + + @Override + public int getPrevButtonTitleResId() { + return -1; + } + + @Override + public int getNextButtonTitleResId() { + return R.string.next; + } + + @Override + public boolean doNextAction() { + return false; + } + + @Override + public boolean doPreviousAction() { + return false; + } + + @Override + public void doLoadAction(Activity context, int action) { + if (context == null || context.isFinishing()) { return; } + final FragmentManager fragmentManager = context.getFragmentManager(); + if (action == Page.ACTION_NEXT) { + FragmentTransaction transaction = fragmentManager.beginTransaction(); + transaction.replace(R.id.content, getFragment(), getKey()); + transaction.commit(); + } else { + FragmentTransaction transaction = fragmentManager.beginTransaction(); + transaction.replace(R.id.content, getFragment(), getKey()); + transaction.commit(); + } + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + return false; + } + + @Override + public boolean isRequired() { + return mRequired; + } + + @Override + public Page setRequired(boolean required) { + mRequired = required; + return this; + } + + @Override + public boolean isCompleted() { + return mCompleted; + } + + @Override + public void setCompleted(boolean completed) { + mCompleted = completed; + mCallbacks.onNextPage(); + } + + @Override + public Bundle getData() { + return mData; + } + + @Override + public void resetData(Bundle data) { + mData = data; + mCallbacks.onPageLoaded(this); + } + + protected SetupDataCallbacks getCallbacks() { + return mCallbacks; + } +} diff --git a/src/com/cyanogenmod/setupwizard/setup/SimCardMissingPage.java b/src/com/cyanogenmod/setupwizard/setup/SimCardMissingPage.java new file mode 100644 index 0000000..8f74e24 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/SimCardMissingPage.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +public class SimCardMissingPage extends SetupPage { + + public static final String TAG = "SimCardMissingPage"; + + public SimCardMissingPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + FinishFragment fragment = new FinishFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.setup_sim_missing; + } + + @Override + public int getNextButtonTitleResId() { + return R.string.skip; + } + + + public static class FinishFragment extends SetupPageFragment { + + @Override + protected void initializePage() {} + + @Override + protected int getLayoutResource() { + return R.layout.sim_missing_page; + } + + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/WelcomePage.java b/src/com/cyanogenmod/setupwizard/setup/WelcomePage.java new file mode 100644 index 0000000..ca3bf99 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/WelcomePage.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Fragment; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.widget.ArrayAdapter; +import android.widget.NumberPicker; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.ui.LocalePicker; +import com.cyanogenmod.setupwizard.ui.SetupPageFragment; + +import java.util.Locale; + +public class WelcomePage extends SetupPage { + + public static final String TAG = "WelcomePage"; + + public WelcomePage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public Fragment getFragment() { + Bundle args = new Bundle(); + args.putString(SetupPage.KEY_PAGE_ARGUMENT, getKey()); + + WelcomeFragment fragment = new WelcomeFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public int getTitleResId() { + return R.string.setup_welcome; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getPrevButtonTitleResId() { + return R.string.emergency_call; + } + + public static class WelcomeFragment extends SetupPageFragment { + + private ArrayAdapter<com.android.internal.app.LocalePicker.LocaleInfo> mLocaleAdapter; + private Locale mInitialLocale; + private Locale mCurrentLocale; + private int[] mAdapterIndices; + + private LocalePicker mLanguagePicker; + + private final Handler mHandler = new Handler(); + + private final Runnable mUpdateLocale = new Runnable() { + public void run() { + if (mCurrentLocale != null) { + com.android.internal.app.LocalePicker.updateLocale(mCurrentLocale); + } + } + }; + + @Override + protected void initializePage() { + mLanguagePicker = (LocalePicker) mRootView.findViewById(R.id.locale_list); + loadLanguages(); + } + + private void loadLanguages() { + mLocaleAdapter = com.android.internal.app.LocalePicker.constructAdapter(getActivity(), R.layout.locale_picker_item, R.id.locale); + mInitialLocale = Locale.getDefault(); + mCurrentLocale = mInitialLocale; + mAdapterIndices = new int[mLocaleAdapter.getCount()]; + int currentLocaleIndex = 0; + String [] labels = new String[mLocaleAdapter.getCount()]; + for (int i=0; i<mAdapterIndices.length; i++) { + com.android.internal.app.LocalePicker.LocaleInfo localLocaleInfo = mLocaleAdapter.getItem(i); + Locale localLocale = localLocaleInfo.getLocale(); + if (localLocale.equals(mCurrentLocale)) { + currentLocaleIndex = i; + } + mAdapterIndices[i] = i; + labels[i] = localLocaleInfo.getLabel(); + } + mLanguagePicker.setDisplayedValues(labels); + mLanguagePicker.setMaxValue(labels.length - 1); + mLanguagePicker.setValue(currentLocaleIndex); + mLanguagePicker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + mLanguagePicker.setOnValueChangedListener(new LocalePicker.OnValueChangeListener() { + public void onValueChange(LocalePicker picker, int oldVal, int newVal) { + setLocaleFromPicker(); + } + }); + } + + private void setLocaleFromPicker() { + int i = mAdapterIndices[mLanguagePicker.getValue()]; + final com.android.internal.app.LocalePicker.LocaleInfo localLocaleInfo = mLocaleAdapter.getItem(i); + onLocaleChanged(localLocaleInfo.getLocale()); + } + + private void onLocaleChanged(Locale paramLocale) { + Resources localResources = getActivity().getResources(); + Configuration localConfiguration1 = localResources.getConfiguration(); + Configuration localConfiguration2 = new Configuration(); + localConfiguration2.locale = paramLocale; + localResources.updateConfiguration(localConfiguration2, null); + localResources.updateConfiguration(localConfiguration1, null); + mHandler.removeCallbacks(mUpdateLocale); + mCurrentLocale = paramLocale; + mHandler.postDelayed(mUpdateLocale, 1000); + } + + @Override + protected int getLayoutResource() { + return R.layout.setup_welcome_page; + } + + @Override + protected int getHeaderLayoutResource() { + return R.layout.logo_header; + } + } + +} diff --git a/src/com/cyanogenmod/setupwizard/setup/WifiSetupPage.java b/src/com/cyanogenmod/setupwizard/setup/WifiSetupPage.java new file mode 100644 index 0000000..705b932 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/setup/WifiSetupPage.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.setup; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.SetupWizardApp; +import com.cyanogenmod.setupwizard.util.SetupWizardUtils; + +public class WifiSetupPage extends SetupPage { + + public static final String TAG = "WifiSetupPage"; + + public WifiSetupPage(Context context, SetupDataCallbacks callbacks) { + super(context, callbacks); + } + + @Override + public int getNextButtonTitleResId() { + return R.string.skip; + } + + @Override + public String getKey() { + return TAG; + } + + @Override + public int getTitleResId() { + return R.string.existing; + } + + @Override + public void doLoadAction(Activity context, int action) { + if (action == Page.ACTION_PREVIOUS) { + getCallbacks().onPreviousPage(); + } else { + SetupWizardUtils.launchWifiSetup(context); + } + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != SetupWizardApp.REQUEST_CODE_SETUP_WIFI) return false; + getCallbacks().onNextPage(); + return true; + } +} diff --git a/src/com/cyanogenmod/setupwizard/ui/LocalePicker.java b/src/com/cyanogenmod/setupwizard/ui/LocalePicker.java new file mode 100644 index 0000000..049476c --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/ui/LocalePicker.java @@ -0,0 +1,2607 @@ +/* + * 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.cyanogenmod.setupwizard.ui; + +import com.android.internal.R; + +import android.annotation.Widget; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.InputFilter; +import android.text.InputType; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.NumberKeyListener; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.Scroller; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import libcore.icu.LocaleData; + +/** + * A widget that enables the user to select a number form a predefined range. + * There are two flavors of this widget and which one is presented to the user + * depends on the current theme. + * <ul> + * <li> + * If the current theme is derived from {@link android.R.style#Theme} the widget + * presents the current value as an editable input field with an increment button + * above and a decrement button below. Long pressing the buttons allows for a quick + * change of the current value. Tapping on the input field allows to type in + * a desired value. + * </li> + * <li> + * If the current theme is derived from {@link android.R.style#Theme_Holo} or + * {@link android.R.style#Theme_Holo_Light} the widget presents the current + * value as an editable input field with a lesser value above and a greater + * value below. Tapping on the lesser or greater value selects it by animating + * the number axis up or down to make the chosen value current. Flinging up + * or down allows for multiple increments or decrements of the current value. + * Long pressing on the lesser and greater values also allows for a quick change + * of the current value. Tapping on the current value allows to type in a + * desired value. + * </li> + * </ul> + * <p> + * For an example of using this widget, see {@link android.widget.TimePicker}. + * </p> + */ +@Widget +public class LocalePicker extends LinearLayout { + + /** + * The number of items show in the selector wheel. + */ + private static int SELECTOR_WHEEL_ITEM_COUNT = 3; + + /** + * The default update interval during long press. + */ + private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; + + /** + * The index of the middle selector item. + */ + private static int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; + + /** + * The coefficient by which to adjust (divide) the max fling velocity. + */ + private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; + + /** + * The the duration for adjusting the selector wheel. + */ + private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; + + /** + * The duration of scrolling while snapping to a given position. + */ + private static final int SNAP_SCROLL_DURATION = 300; + + /** + * The strength of fading in the top and bottom while drawing the selector. + */ + private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; + + /** + * The default unscaled height of the selection divider. + */ + private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 1; + + /** + * The default unscaled distance between the selection dividers. + */ + private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; + + /** + * The resource id for the default layout. + */ + private static final int DEFAULT_LAYOUT_RESOURCE_ID = + com.cyanogenmod.setupwizard.R.layout.locale_picker; + + /** + * Constant for unspecified size. + */ + private static final int SIZE_UNSPECIFIED = -1; + + /** + * Use a custom NumberPicker formatting callback to use two-digit minutes + * strings like "01". Keeping a static formatter etc. is the most efficient + * way to do this; it avoids creating temporary objects on every call to + * format(). + */ + private static class TwoDigitFormatter implements LocalePicker.Formatter { + final StringBuilder mBuilder = new StringBuilder(); + + char mZeroDigit; + java.util.Formatter mFmt; + + final Object[] mArgs = new Object[1]; + + TwoDigitFormatter() { + final Locale locale = Locale.getDefault(); + init(locale); + } + + private void init(Locale locale) { + mFmt = createFormatter(locale); + mZeroDigit = getZeroDigit(locale); + } + + public String format(int value) { + final Locale currentLocale = Locale.getDefault(); + if (mZeroDigit != getZeroDigit(currentLocale)) { + init(currentLocale); + } + mArgs[0] = value; + mBuilder.delete(0, mBuilder.length()); + mFmt.format("%02d", mArgs); + return mFmt.toString(); + } + + private static char getZeroDigit(Locale locale) { + return LocaleData.get(locale).zeroDigit; + } + + private java.util.Formatter createFormatter(Locale locale) { + return new java.util.Formatter(mBuilder, locale); + } + } + + private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); + + /** + * @hide + */ + public static final Formatter getTwoDigitFormatter() { + return sTwoDigitFormatter; + } + + /** + * The increment button. + */ + private final ImageButton mIncrementButton; + + /** + * The decrement button. + */ + private final ImageButton mDecrementButton; + + /** + * The text for showing the current value. + */ + private final EditText mInputText; + + /** + * The distance between the two selection dividers. + */ + private final int mSelectionDividersDistance; + + /** + * The min height of this widget. + */ + private final int mMinHeight; + + /** + * The max height of this widget. + */ + private final int mMaxHeight; + + /** + * The max width of this widget. + */ + private final int mMinWidth; + + /** + * The max width of this widget. + */ + private int mMaxWidth; + + /** + * Flag whether to compute the max width. + */ + private final boolean mComputeMaxWidth; + + /** + * The height of the text. + */ + private final int mTextSize; + + /** + * The height of the gap between text elements if the selector wheel. + */ + private int mSelectorTextGapHeight; + + /** + * The values to be displayed instead the indices. + */ + private String[] mDisplayedValues; + + /** + * Lower value of the range of numbers allowed for the NumberPicker + */ + private int mMinValue; + + /** + * Upper value of the range of numbers allowed for the NumberPicker + */ + private int mMaxValue; + + /** + * Current value of this NumberPicker + */ + private int mValue; + + /** + * Listener to be notified upon current value change. + */ + private OnValueChangeListener mOnValueChangeListener; + + /** + * Listener to be notified upon scroll state change. + */ + private OnScrollListener mOnScrollListener; + + /** + * Formatter for for displaying the current value. + */ + private Formatter mFormatter; + + /** + * The speed for updating the value form long press. + */ + private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; + + /** + * Cache for the string representation of selector indices. + */ + private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); + + /** + * The selector indices whose value are show by the selector. + */ + private final int[] mSelectorIndices; + + /** + * The {@link android.graphics.Paint} for drawing the selector. + */ + private final Paint mSelectorWheelPaint; + + /** + * The {@link android.graphics.drawable.Drawable} for pressed virtual (increment/decrement) buttons. + */ + private final Drawable mVirtualButtonPressedDrawable; + + /** + * The height of a selector element (text + gap). + */ + private int mSelectorElementHeight; + + /** + * The initial offset of the scroll selector. + */ + private int mInitialScrollOffset = Integer.MIN_VALUE; + + /** + * The current offset of the scroll selector. + */ + private int mCurrentScrollOffset; + + /** + * The {@link android.widget.Scroller} responsible for flinging the selector. + */ + private final Scroller mFlingScroller; + + /** + * The {@link android.widget.Scroller} responsible for adjusting the selector. + */ + private final Scroller mAdjustScroller; + + /** + * The previous Y coordinate while scrolling the selector. + */ + private int mPreviousScrollerY; + + /** + * Handle to the reusable command for setting the input text selection. + */ + private SetSelectionCommand mSetSelectionCommand; + + /** + * Handle to the reusable command for changing the current value from long + * press by one. + */ + private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; + + /** + * Command for beginning an edit of the current value via IME on long press. + */ + private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand; + + /** + * The Y position of the last down event. + */ + private float mLastDownEventY; + + /** + * The time of the last down event. + */ + private long mLastDownEventTime; + + /** + * The Y position of the last down or move event. + */ + private float mLastDownOrMoveEventY; + + /** + * Determines speed during touch scrolling. + */ + private VelocityTracker mVelocityTracker; + + /** + * @see android.view.ViewConfiguration#getScaledTouchSlop() + */ + private int mTouchSlop; + + /** + * @see android.view.ViewConfiguration#getScaledMinimumFlingVelocity() + */ + private int mMinimumFlingVelocity; + + /** + * @see android.view.ViewConfiguration#getScaledMaximumFlingVelocity() + */ + private int mMaximumFlingVelocity; + + /** + * Flag whether the selector should wrap around. + */ + private boolean mWrapSelectorWheel; + + /** + * The back ground color used to optimize scroller fading. + */ + private final int mSolidColor; + + /** + * Flag whether this widget has a selector wheel. + */ + private final boolean mHasSelectorWheel; + + /** + * Divider for showing item to be selected while scrolling + */ + private final Drawable mSelectionDivider; + + /** + * The height of the selection divider. + */ + private final int mSelectionDividerHeight; + + /** + * The current scroll state of the number picker. + */ + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * Flag whether to ignore move events - we ignore such when we show in IME + * to prevent the content from scrolling. + */ + private boolean mIngonreMoveEvents; + + /** + * Flag whether to show soft input on tap. + */ + private boolean mShowSoftInputOnTap; + + /** + * The top of the top selection divider. + */ + private int mTopSelectionDividerTop; + + /** + * The bottom of the bottom selection divider. + */ + private int mBottomSelectionDividerBottom; + + /** + * The virtual id of the last hovered child. + */ + private int mLastHoveredChildVirtualViewId; + + /** + * Whether the increment virtual button is pressed. + */ + private boolean mIncrementVirtualButtonPressed; + + /** + * Whether the decrement virtual button is pressed. + */ + private boolean mDecrementVirtualButtonPressed; + + /** + * Provider to report to clients the semantic structure of this widget. + */ + private AccessibilityNodeProviderImpl mAccessibilityNodeProvider; + + /** + * Helper class for managing pressed state of the virtual buttons. + */ + private final PressedStateHelper mPressedStateHelper; + + /** + * The keycode of the last handled DPAD down event. + */ + private int mLastHandledDownDpadKeyCode = -1; + + /** + * Interface to listen for changes of the current value. + */ + public interface OnValueChangeListener { + + /** + * Called upon a change of the current value. + * + * @param picker The NumberPicker associated with this listener. + * @param oldVal The previous value. + * @param newVal The new value. + */ + void onValueChange(LocalePicker picker, int oldVal, int newVal); + } + + /** + * Interface to listen for the picker scroll state. + */ + public interface OnScrollListener { + + /** + * The view is not scrolling. + */ + public static int SCROLL_STATE_IDLE = 0; + + /** + * The user is scrolling using touch, and his finger is still on the screen. + */ + public static int SCROLL_STATE_TOUCH_SCROLL = 1; + + /** + * The user had previously been scrolling using touch and performed a fling. + */ + public static int SCROLL_STATE_FLING = 2; + + /** + * Callback invoked while the number picker scroll state has changed. + * + * @param view The view whose scroll state is being reported. + * @param scrollState The current scroll state. One of + * {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_TOUCH_SCROLL} or + * {@link #SCROLL_STATE_IDLE}. + */ + public void onScrollStateChange(LocalePicker view, int scrollState); + } + + /** + * Interface used to format current value into a string for presentation. + */ + public interface Formatter { + + /** + * Formats a string representation of the current value. + * + * @param value The currently selected value. + * @return A formatted string representation. + */ + public String format(int value); + } + + /** + * Create a new number picker. + * + * @param context The application environment. + */ + public LocalePicker(Context context) { + this(context, null); + } + + /** + * Create a new number picker. + * + * @param context The application environment. + * @param attrs A collection of attributes. + */ + public LocalePicker(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.numberPickerStyle); + } + + /** + * Create a new number picker + * + * @param context the application environment. + * @param attrs a collection of attributes. + * @param defStyle The default style to apply to this view. + */ + public LocalePicker(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + SELECTOR_WHEEL_ITEM_COUNT = context.getResources().getInteger(com.cyanogenmod.setupwizard.R.integer.local_picker_items); + SELECTOR_MIDDLE_ITEM_INDEX = context.getResources().getInteger(com.cyanogenmod.setupwizard.R.integer.local_picker_items)/2; + mSelectorIndices= new int[SELECTOR_WHEEL_ITEM_COUNT]; + // process style attributes + TypedArray attributesArray = context.obtainStyledAttributes( + attrs, R.styleable.NumberPicker, defStyle, 0); + final int layoutResId = attributesArray.getResourceId( + R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); + + mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID); + + mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); + + mSelectionDivider = context.getDrawable(com.cyanogenmod.setupwizard.R.drawable.divider); + + final int defSelectionDividerHeight = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, + getResources().getDisplayMetrics()); + mSelectionDividerHeight = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight); + + final int defSelectionDividerDistance = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE, + getResources().getDisplayMetrics()); + mSelectionDividersDistance = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance); + + mMinHeight = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED); + + mMaxHeight = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED); + if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED + && mMinHeight > mMaxHeight) { + throw new IllegalArgumentException("minHeight > maxHeight"); + } + + mMinWidth = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED); + + mMaxWidth = attributesArray.getDimensionPixelSize( + R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED); + if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED + && mMinWidth > mMaxWidth) { + throw new IllegalArgumentException("minWidth > maxWidth"); + } + + mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED); + + mVirtualButtonPressedDrawable = attributesArray.getDrawable( + R.styleable.NumberPicker_virtualButtonPressedDrawable); + + attributesArray.recycle(); + + mPressedStateHelper = new PressedStateHelper(); + + // By default Linearlayout that we extend is not drawn. This is + // its draw() method is not called but dispatchDraw() is called + // directly (see ViewGroup.drawChild()). However, this class uses + // the fading edge effect implemented by View and we need our + // draw() method to be called. Therefore, we declare we will draw. + setWillNotDraw(!mHasSelectorWheel); + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResId, this, true); + + OnClickListener onClickListener = new OnClickListener() { + public void onClick(View v) { + hideSoftInput(); + mInputText.clearFocus(); + if (v.getId() == R.id.increment) { + changeValueByOne(true); + } else { + changeValueByOne(false); + } + } + }; + + OnLongClickListener onLongClickListener = new OnLongClickListener() { + public boolean onLongClick(View v) { + hideSoftInput(); + mInputText.clearFocus(); + if (v.getId() == R.id.increment) { + postChangeCurrentByOneFromLongPress(true, 0); + } else { + postChangeCurrentByOneFromLongPress(false, 0); + } + return true; + } + }; + + // increment button + if (!mHasSelectorWheel) { + mIncrementButton = (ImageButton) findViewById(R.id.increment); + mIncrementButton.setOnClickListener(onClickListener); + mIncrementButton.setOnLongClickListener(onLongClickListener); + } else { + mIncrementButton = null; + } + + // decrement button + if (!mHasSelectorWheel) { + mDecrementButton = (ImageButton) findViewById(R.id.decrement); + mDecrementButton.setOnClickListener(onClickListener); + mDecrementButton.setOnLongClickListener(onLongClickListener); + } else { + mDecrementButton = null; + } + + // input text + mInputText = (EditText) findViewById(R.id.numberpicker_input); + mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + mInputText.selectAll(); + } else { + mInputText.setSelection(0, 0); + validateInputTextView(v); + } + } + }); + mInputText.setFilters(new InputFilter[] { + new InputTextFilter() + }); + + mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); + mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE); + + // initialize constants + ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() + / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; + mTextSize = (int) mInputText.getTextSize(); + + // create the selector wheel paint + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextAlign(Align.CENTER); + paint.setTextSize(mTextSize); + paint.setTypeface(mInputText.getTypeface()); + ColorStateList colors = mInputText.getTextColors(); + int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); + paint.setColor(color); + mSelectorWheelPaint = paint; + + // create the fling and adjust scrollers + mFlingScroller = new Scroller(getContext(), null, true); + mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); + + updateInputTextView(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mHasSelectorWheel) { + super.onLayout(changed, left, top, right, bottom); + return; + } + final int msrdWdth = getMeasuredWidth(); + final int msrdHght = getMeasuredHeight(); + + // Input text centered horizontally. + final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); + final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); + final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; + final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; + final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; + final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; + mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); + + if (changed) { + // need to do all this when we know our size + initializeSelectorWheel(); + initializeFadingEdges(); + mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 + - mSelectionDividerHeight; + mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight + + mSelectionDividersDistance; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!mHasSelectorWheel) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + // Try greedily to fit the max width and height. + final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); + final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); + super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); + // Flag if we are measured with width or height less than the respective min. + final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), + widthMeasureSpec); + final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), + heightMeasureSpec); + setMeasuredDimension(widthSize, heightSize); + } + + /** + * Move to the final position of a scroller. Ensures to force finish the scroller + * and if it is not at its final position a scroll of the selector wheel is + * performed to fast forward to the final position. + * + * @param scroller The scroller to whose final position to get. + * @return True of the a move was performed, i.e. the scroller was not in final position. + */ + private boolean moveToFinalScrollerPosition(Scroller scroller) { + scroller.forceFinished(true); + int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); + int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight; + int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; + if (overshootAdjustment != 0) { + if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) { + if (overshootAdjustment > 0) { + overshootAdjustment -= mSelectorElementHeight; + } else { + overshootAdjustment += mSelectorElementHeight; + } + } + amountToScroll += overshootAdjustment; + scrollBy(0, amountToScroll); + return true; + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (!mHasSelectorWheel || !isEnabled()) { + return false; + } + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + removeAllCallbacks(); + mInputText.setVisibility(View.INVISIBLE); + mLastDownOrMoveEventY = mLastDownEventY = event.getY(); + mLastDownEventTime = event.getEventTime(); + mIngonreMoveEvents = false; + mShowSoftInputOnTap = false; + // Handle pressed state before any state change. + if (mLastDownEventY < mTopSelectionDividerTop) { + if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { + mPressedStateHelper.buttonPressDelayed( + PressedStateHelper.BUTTON_DECREMENT); + } + } else if (mLastDownEventY > mBottomSelectionDividerBottom) { + if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { + mPressedStateHelper.buttonPressDelayed( + PressedStateHelper.BUTTON_INCREMENT); + } + } + // Make sure we support flinging inside scrollables. + getParent().requestDisallowInterceptTouchEvent(true); + if (!mFlingScroller.isFinished()) { + mFlingScroller.forceFinished(true); + mAdjustScroller.forceFinished(true); + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else if (!mAdjustScroller.isFinished()) { + mFlingScroller.forceFinished(true); + mAdjustScroller.forceFinished(true); + } else if (mLastDownEventY < mTopSelectionDividerTop) { + hideSoftInput(); + postChangeCurrentByOneFromLongPress( + false, ViewConfiguration.getLongPressTimeout()); + } else if (mLastDownEventY > mBottomSelectionDividerBottom) { + hideSoftInput(); + postChangeCurrentByOneFromLongPress( + true, ViewConfiguration.getLongPressTimeout()); + } else { + mShowSoftInputOnTap = true; + postBeginSoftInputOnLongPressCommand(); + } + return true; + } + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled() || !mHasSelectorWheel) { + return false; + } + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_MOVE: { + if (mIngonreMoveEvents) { + break; + } + float currentMoveY = event.getY(); + if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); + if (deltaDownY > mTouchSlop) { + removeAllCallbacks(); + onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + } + } else { + int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY)); + scrollBy(0, deltaMoveY); + invalidate(); + } + mLastDownOrMoveEventY = currentMoveY; + } break; + case MotionEvent.ACTION_UP: { + removeBeginSoftInputCommand(); + removeChangeCurrentByOneFromLongPress(); + mPressedStateHelper.cancel(); + VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + int initialVelocity = (int) velocityTracker.getYVelocity(); + if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { + fling(initialVelocity); + onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + } else { + int eventY = (int) event.getY(); + int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); + long deltaTime = event.getEventTime() - mLastDownEventTime; + if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) { + if (mShowSoftInputOnTap) { + mShowSoftInputOnTap = false; + showSoftInput(); + } else { + int selectorIndexOffset = (eventY / mSelectorElementHeight) + - SELECTOR_MIDDLE_ITEM_INDEX; + if (selectorIndexOffset > 0) { + changeValueByOne(true); + mPressedStateHelper.buttonTapped( + PressedStateHelper.BUTTON_INCREMENT); + } else if (selectorIndexOffset < 0) { + changeValueByOne(false); + mPressedStateHelper.buttonTapped( + PressedStateHelper.BUTTON_DECREMENT); + } + } + } else { + ensureScrollWheelAdjusted(); + } + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + } break; + } + return true; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + removeAllCallbacks(); + break; + } + return super.dispatchTouchEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + removeAllCallbacks(); + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + if (!mHasSelectorWheel) { + break; + } + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + if (mWrapSelectorWheel || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) + ? getValue() < getMaxValue() : getValue() > getMinValue()) { + requestFocus(); + mLastHandledDownDpadKeyCode = keyCode; + removeAllCallbacks(); + if (mFlingScroller.isFinished()) { + changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN); + } + return true; + } + break; + case KeyEvent.ACTION_UP: + if (mLastHandledDownDpadKeyCode == keyCode) { + mLastHandledDownDpadKeyCode = -1; + return true; + } + break; + } + } + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + removeAllCallbacks(); + break; + } + return super.dispatchTrackballEvent(event); + } + + @Override + protected boolean dispatchHoverEvent(MotionEvent event) { + if (!mHasSelectorWheel) { + return super.dispatchHoverEvent(event); + } + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + final int eventY = (int) event.getY(); + final int hoveredVirtualViewId; + if (eventY < mTopSelectionDividerTop) { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT; + } else if (eventY > mBottomSelectionDividerBottom) { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT; + } else { + hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT; + } + final int action = event.getActionMasked(); + AccessibilityNodeProviderImpl provider = + (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider(); + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: { + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mLastHoveredChildVirtualViewId = hoveredVirtualViewId; + provider.performAction(hoveredVirtualViewId, + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } break; + case MotionEvent.ACTION_HOVER_MOVE: { + if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId + && mLastHoveredChildVirtualViewId != View.NO_ID) { + provider.sendAccessibilityEventForVirtualView( + mLastHoveredChildVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mLastHoveredChildVirtualViewId = hoveredVirtualViewId; + provider.performAction(hoveredVirtualViewId, + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } break; + case MotionEvent.ACTION_HOVER_EXIT: { + provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, + AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mLastHoveredChildVirtualViewId = View.NO_ID; + } break; + } + } + return false; + } + + @Override + public void computeScroll() { + Scroller scroller = mFlingScroller; + if (scroller.isFinished()) { + scroller = mAdjustScroller; + if (scroller.isFinished()) { + return; + } + } + scroller.computeScrollOffset(); + int currentScrollerY = scroller.getCurrY(); + if (mPreviousScrollerY == 0) { + mPreviousScrollerY = scroller.getStartY(); + } + scrollBy(0, currentScrollerY - mPreviousScrollerY); + mPreviousScrollerY = currentScrollerY; + if (scroller.isFinished()) { + onScrollerFinished(scroller); + } else { + invalidate(); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (!mHasSelectorWheel) { + mIncrementButton.setEnabled(enabled); + } + if (!mHasSelectorWheel) { + mDecrementButton.setEnabled(enabled); + } + mInputText.setEnabled(enabled); + } + + @Override + public void scrollBy(int x, int y) { + int[] selectorIndices = mSelectorIndices; + if (!mWrapSelectorWheel && y > 0 + && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { + mCurrentScrollOffset = mInitialScrollOffset; + return; + } + if (!mWrapSelectorWheel && y < 0 + && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { + mCurrentScrollOffset = mInitialScrollOffset; + return; + } + mCurrentScrollOffset += y; + while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { + mCurrentScrollOffset -= mSelectorElementHeight; + decrementSelectorIndices(selectorIndices); + setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); + if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { + mCurrentScrollOffset = mInitialScrollOffset; + } + } + while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { + mCurrentScrollOffset += mSelectorElementHeight; + incrementSelectorIndices(selectorIndices); + setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); + if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { + mCurrentScrollOffset = mInitialScrollOffset; + } + } + } + + @Override + public int getSolidColor() { + return mSolidColor; + } + + /** + * Sets the listener to be notified on change of the current value. + * + * @param onValueChangedListener The listener. + */ + public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { + mOnValueChangeListener = onValueChangedListener; + } + + /** + * Set listener to be notified for scroll state changes. + * + * @param onScrollListener The listener. + */ + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + } + + /** + * Set the formatter to be used for formatting the current value. + * <p> + * Note: If you have provided alternative values for the values this + * formatter is never invoked. + * </p> + * + * @param formatter The formatter object. If formatter is <code>null</code>, + * {@link String#valueOf(int)} will be used. + *@see #setDisplayedValues(String[]) + */ + public void setFormatter(Formatter formatter) { + if (formatter == mFormatter) { + return; + } + mFormatter = formatter; + initializeSelectorWheelIndices(); + updateInputTextView(); + } + + /** + * Set the current value for the number picker. + * <p> + * If the argument is less than the {@link LocalePicker#getMinValue()} and + * {@link LocalePicker#getWrapSelectorWheel()} is <code>false</code> the + * current value is set to the {@link LocalePicker#getMinValue()} value. + * </p> + * <p> + * If the argument is less than the {@link LocalePicker#getMinValue()} and + * {@link LocalePicker#getWrapSelectorWheel()} is <code>true</code> the + * current value is set to the {@link LocalePicker#getMaxValue()} value. + * </p> + * <p> + * If the argument is less than the {@link LocalePicker#getMaxValue()} and + * {@link LocalePicker#getWrapSelectorWheel()} is <code>false</code> the + * current value is set to the {@link LocalePicker#getMaxValue()} value. + * </p> + * <p> + * If the argument is less than the {@link LocalePicker#getMaxValue()} and + * {@link LocalePicker#getWrapSelectorWheel()} is <code>true</code> the + * current value is set to the {@link LocalePicker#getMinValue()} value. + * </p> + * + * @param value The current value. + * @see #setWrapSelectorWheel(boolean) + * @see #setMinValue(int) + * @see #setMaxValue(int) + */ + public void setValue(int value) { + setValueInternal(value, false); + } + + /** + * Shows the soft input for its input text. + */ + private void showSoftInput() { + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (mHasSelectorWheel) { + mInputText.setVisibility(View.VISIBLE); + } + mInputText.requestFocus(); + inputMethodManager.showSoftInput(mInputText, 0); + } + } + + /** + * Hides the soft input if it is active for the input text. + */ + private void hideSoftInput() { + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { + inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (mHasSelectorWheel) { + mInputText.setVisibility(View.INVISIBLE); + } + } + } + + /** + * Computes the max width if no such specified as an attribute. + */ + private void tryComputeMaxWidth() { + if (!mComputeMaxWidth) { + return; + } + int maxTextWidth = 0; + if (mDisplayedValues == null) { + float maxDigitWidth = 0; + for (int i = 0; i <= 9; i++) { + final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i)); + if (digitWidth > maxDigitWidth) { + maxDigitWidth = digitWidth; + } + } + int numberOfDigits = 0; + int current = mMaxValue; + while (current > 0) { + numberOfDigits++; + current = current / 10; + } + maxTextWidth = (int) (numberOfDigits * maxDigitWidth); + } else { + final int valueCount = mDisplayedValues.length; + for (int i = 0; i < valueCount; i++) { + final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]); + if (textWidth > maxTextWidth) { + maxTextWidth = (int) textWidth; + } + } + } + maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight(); + if (mMaxWidth != maxTextWidth) { + if (maxTextWidth > mMinWidth) { + mMaxWidth = maxTextWidth; + } else { + mMaxWidth = mMinWidth; + } + invalidate(); + } + } + + /** + * Gets whether the selector wheel wraps when reaching the min/max value. + * + * @return True if the selector wheel wraps. + * + * @see #getMinValue() + * @see #getMaxValue() + */ + public boolean getWrapSelectorWheel() { + return mWrapSelectorWheel; + } + + /** + * Sets whether the selector wheel shown during flinging/scrolling should + * wrap around the {@link LocalePicker#getMinValue()} and + * {@link LocalePicker#getMaxValue()} values. + * <p> + * By default if the range (max - min) is more than the number of items shown + * on the selector wheel the selector wheel wrapping is enabled. + * </p> + * <p> + * <strong>Note:</strong> If the number of items, i.e. the range ( + * {@link #getMaxValue()} - {@link #getMinValue()}) is less than + * the number of items shown on the selector wheel, the selector wheel will + * not wrap. Hence, in such a case calling this method is a NOP. + * </p> + * + * @param wrapSelectorWheel Whether to wrap. + */ + public void setWrapSelectorWheel(boolean wrapSelectorWheel) { + final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; + if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) { + mWrapSelectorWheel = wrapSelectorWheel; + } + } + + /** + * Sets the speed at which the numbers be incremented and decremented when + * the up and down buttons are long pressed respectively. + * <p> + * The default value is 300 ms. + * </p> + * + * @param intervalMillis The speed (in milliseconds) at which the numbers + * will be incremented and decremented. + */ + public void setOnLongPressUpdateInterval(long intervalMillis) { + mLongPressUpdateInterval = intervalMillis; + } + + /** + * Returns the value of the picker. + * + * @return The value. + */ + public int getValue() { + return mValue; + } + + /** + * Returns the min value of the picker. + * + * @return The min value + */ + public int getMinValue() { + return mMinValue; + } + + /** + * Sets the min value of the picker. + * + * @param minValue The min value inclusive. + * + * <strong>Note:</strong> The length of the displayed values array + * set via {@link #setDisplayedValues(String[])} must be equal to the + * range of selectable numbers which is equal to + * {@link #getMaxValue()} - {@link #getMinValue()} + 1. + */ + public void setMinValue(int minValue) { + if (mMinValue == minValue) { + return; + } + if (minValue < 0) { + throw new IllegalArgumentException("minValue must be >= 0"); + } + mMinValue = minValue; + if (mMinValue > mValue) { + mValue = mMinValue; + } + boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; + setWrapSelectorWheel(wrapSelectorWheel); + initializeSelectorWheelIndices(); + updateInputTextView(); + tryComputeMaxWidth(); + invalidate(); + } + + /** + * Returns the max value of the picker. + * + * @return The max value. + */ + public int getMaxValue() { + return mMaxValue; + } + + /** + * Sets the max value of the picker. + * + * @param maxValue The max value inclusive. + * + * <strong>Note:</strong> The length of the displayed values array + * set via {@link #setDisplayedValues(String[])} must be equal to the + * range of selectable numbers which is equal to + * {@link #getMaxValue()} - {@link #getMinValue()} + 1. + */ + public void setMaxValue(int maxValue) { + if (mMaxValue == maxValue) { + return; + } + if (maxValue < 0) { + throw new IllegalArgumentException("maxValue must be >= 0"); + } + mMaxValue = maxValue; + if (mMaxValue < mValue) { + mValue = mMaxValue; + } + boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length; + setWrapSelectorWheel(wrapSelectorWheel); + initializeSelectorWheelIndices(); + updateInputTextView(); + tryComputeMaxWidth(); + invalidate(); + } + + /** + * Gets the values to be displayed instead of string values. + * + * @return The displayed values. + */ + public String[] getDisplayedValues() { + return mDisplayedValues; + } + + /** + * Sets the values to be displayed. + * + * @param displayedValues The displayed values. + * + * <strong>Note:</strong> The length of the displayed values array + * must be equal to the range of selectable numbers which is equal to + * {@link #getMaxValue()} - {@link #getMinValue()} + 1. + */ + public void setDisplayedValues(String[] displayedValues) { + if (mDisplayedValues == displayedValues) { + return; + } + mDisplayedValues = displayedValues; + if (mDisplayedValues != null) { + // Allow text entry rather than strictly numeric entry. + mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } else { + mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); + } + updateInputTextView(); + initializeSelectorWheelIndices(); + tryComputeMaxWidth(); + } + + @Override + protected float getTopFadingEdgeStrength() { + return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; + } + + @Override + protected float getBottomFadingEdgeStrength() { + return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; + } + + @Override + protected void onDetachedFromWindow() { + removeAllCallbacks(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (!mHasSelectorWheel) { + super.onDraw(canvas); + return; + } + float x = (mRight - mLeft) / 2; + float y = mCurrentScrollOffset; + + // draw the virtual buttons pressed state if needed + if (mVirtualButtonPressedDrawable != null + && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { + if (mDecrementVirtualButtonPressed) { + mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); + mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop); + mVirtualButtonPressedDrawable.draw(canvas); + } + if (mIncrementVirtualButtonPressed) { + mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); + mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight, + mBottom); + mVirtualButtonPressedDrawable.draw(canvas); + } + } + + // draw the selector wheel + int[] selectorIndices = mSelectorIndices; + for (int i = 0; i < selectorIndices.length; i++) { + int selectorIndex = selectorIndices[i]; + String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); + // Do not draw the middle item if input is visible since the input + // is shown only if the wheel is static and it covers the middle + // item. Otherwise, if the user starts editing the text via the + // IME he may see a dimmed version of the old value intermixed + // with the new one. + if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) { + canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); + } + y += mSelectorElementHeight; + } + + // draw the selection dividers + if (mSelectionDivider != null) { + // draw the top divider + int topOfTopDivider = mTopSelectionDividerTop; + int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; + mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); + mSelectionDivider.draw(canvas); + + // draw the bottom divider + int bottomOfBottomDivider = mBottomSelectionDividerBottom; + int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; + mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); + mSelectionDivider.draw(canvas); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(LocalePicker.class.getName()); + event.setScrollable(true); + event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); + event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + if (!mHasSelectorWheel) { + return super.getAccessibilityNodeProvider(); + } + if (mAccessibilityNodeProvider == null) { + mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); + } + return mAccessibilityNodeProvider; + } + + /** + * Makes a measure spec that tries greedily to use the max value. + * + * @param measureSpec The measure spec. + * @param maxSize The max value for the size. + * @return A measure spec greedily imposing the max size. + */ + private int makeMeasureSpec(int measureSpec, int maxSize) { + if (maxSize == SIZE_UNSPECIFIED) { + return measureSpec; + } + final int size = MeasureSpec.getSize(measureSpec); + final int mode = MeasureSpec.getMode(measureSpec); + switch (mode) { + case MeasureSpec.EXACTLY: + return measureSpec; + case MeasureSpec.AT_MOST: + return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); + case MeasureSpec.UNSPECIFIED: + return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); + default: + throw new IllegalArgumentException("Unknown measure mode: " + mode); + } + } + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Tries to respect the min size, unless a different size + * is imposed by the constraints. + * + * @param minSize The minimal desired size. + * @param measuredSize The currently measured size. + * @param measureSpec The current measure spec. + * @return The resolved size and state. + */ + private int resolveSizeAndStateRespectingMinSize( + int minSize, int measuredSize, int measureSpec) { + if (minSize != SIZE_UNSPECIFIED) { + final int desiredWidth = Math.max(minSize, measuredSize); + return resolveSizeAndState(desiredWidth, measureSpec, 0); + } else { + return measuredSize; + } + } + + /** + * Resets the selector indices and clear the cached string representation of + * these indices. + */ + private void initializeSelectorWheelIndices() { + mSelectorIndexToStringCache.clear(); + int[] selectorIndices = mSelectorIndices; + int current = getValue(); + for (int i = 0; i < mSelectorIndices.length; i++) { + int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); + if (mWrapSelectorWheel) { + selectorIndex = getWrappedSelectorIndex(selectorIndex); + } + selectorIndices[i] = selectorIndex; + ensureCachedScrollSelectorValue(selectorIndices[i]); + } + } + + /** + * Sets the current value of this NumberPicker. + * + * @param current The new value of the NumberPicker. + * @param notifyChange Whether to notify if the current value changed. + */ + private void setValueInternal(int current, boolean notifyChange) { + if (mValue == current) { + return; + } + // Wrap around the values if we go past the start or end + if (mWrapSelectorWheel) { + current = getWrappedSelectorIndex(current); + } else { + current = Math.max(current, mMinValue); + current = Math.min(current, mMaxValue); + } + int previous = mValue; + mValue = current; + updateInputTextView(); + if (notifyChange) { + notifyChange(previous, current); + } + initializeSelectorWheelIndices(); + invalidate(); + } + + /** + * Changes the current value by one which is increment or + * decrement based on the passes argument. + * decrement the current value. + * + * @param increment True to increment, false to decrement. + */ + private void changeValueByOne(boolean increment) { + if (mHasSelectorWheel) { + mInputText.setVisibility(View.INVISIBLE); + if (!moveToFinalScrollerPosition(mFlingScroller)) { + moveToFinalScrollerPosition(mAdjustScroller); + } + mPreviousScrollerY = 0; + if (increment) { + mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION); + } else { + mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION); + } + invalidate(); + } else { + if (increment) { + setValueInternal(mValue + 1, true); + } else { + setValueInternal(mValue - 1, true); + } + } + } + + private void initializeSelectorWheel() { + initializeSelectorWheelIndices(); + int[] selectorIndices = mSelectorIndices; + int totalTextHeight = selectorIndices.length * mTextSize; + float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; + float textGapCount = selectorIndices.length; + mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); + mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; + // Ensure that the middle item is positioned the same as the text in + // mInputText + int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); + mInitialScrollOffset = editTextTextPosition + - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); + mCurrentScrollOffset = mInitialScrollOffset; + updateInputTextView(); + } + + private void initializeFadingEdges() { + setVerticalFadingEdgeEnabled(true); + setFadingEdgeLength((mBottom - mTop - mTextSize) / 2); + } + + /** + * Callback invoked upon completion of a given <code>scroller</code>. + */ + private void onScrollerFinished(Scroller scroller) { + if (scroller == mFlingScroller) { + if (!ensureScrollWheelAdjusted()) { + updateInputTextView(); + } + onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else { + if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + updateInputTextView(); + } + } + } + + /** + * Handles transition to a given <code>scrollState</code> + */ + private void onScrollStateChange(int scrollState) { + if (mScrollState == scrollState) { + return; + } + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChange(this, scrollState); + } + } + + /** + * Flings the selector with the given <code>velocityY</code>. + */ + private void fling(int velocityY) { + mPreviousScrollerY = 0; + + if (velocityY > 0) { + mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); + } else { + mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); + } + + invalidate(); + } + + /** + * @return The wrapped index <code>selectorIndex</code> value. + */ + private int getWrappedSelectorIndex(int selectorIndex) { + if (selectorIndex > mMaxValue) { + return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1; + } else if (selectorIndex < mMinValue) { + return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1; + } + return selectorIndex; + } + + /** + * Increments the <code>selectorIndices</code> whose string representations + * will be displayed in the selector. + */ + private void incrementSelectorIndices(int[] selectorIndices) { + for (int i = 0; i < selectorIndices.length - 1; i++) { + selectorIndices[i] = selectorIndices[i + 1]; + } + int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; + if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) { + nextScrollSelectorIndex = mMinValue; + } + selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; + ensureCachedScrollSelectorValue(nextScrollSelectorIndex); + } + + /** + * Decrements the <code>selectorIndices</code> whose string representations + * will be displayed in the selector. + */ + private void decrementSelectorIndices(int[] selectorIndices) { + for (int i = selectorIndices.length - 1; i > 0; i--) { + selectorIndices[i] = selectorIndices[i - 1]; + } + int nextScrollSelectorIndex = selectorIndices[1] - 1; + if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) { + nextScrollSelectorIndex = mMaxValue; + } + selectorIndices[0] = nextScrollSelectorIndex; + ensureCachedScrollSelectorValue(nextScrollSelectorIndex); + } + + /** + * Ensures we have a cached string representation of the given <code> + * selectorIndex</code> to avoid multiple instantiations of the same string. + */ + private void ensureCachedScrollSelectorValue(int selectorIndex) { + SparseArray<String> cache = mSelectorIndexToStringCache; + String scrollSelectorValue = cache.get(selectorIndex); + if (scrollSelectorValue != null) { + return; + } + if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { + scrollSelectorValue = ""; + } else { + if (mDisplayedValues != null) { + int displayedValueIndex = selectorIndex - mMinValue; + scrollSelectorValue = mDisplayedValues[displayedValueIndex]; + } else { + scrollSelectorValue = formatNumber(selectorIndex); + } + } + cache.put(selectorIndex, scrollSelectorValue); + } + + private String formatNumber(int value) { + return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value); + } + + private void validateInputTextView(View v) { + String str = String.valueOf(((TextView) v).getText()); + if (TextUtils.isEmpty(str)) { + // Restore to the old value as we don't allow empty values + updateInputTextView(); + } else { + // Check the new value and ensure it's in range + int current = getSelectedPos(str.toString()); + setValueInternal(current, true); + } + } + + /** + * Updates the view of this NumberPicker. If displayValues were specified in + * the string corresponding to the index specified by the current value will + * be returned. Otherwise, the formatter specified in {@link #setFormatter} + * will be used to format the number. + * + * @return Whether the text was updated. + */ + private boolean updateInputTextView() { + /* + * If we don't have displayed values then use the current number else + * find the correct value in the displayed values for the current + * number. + */ + String text = (mDisplayedValues == null) ? formatNumber(mValue) + : mDisplayedValues[mValue - mMinValue]; + if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) { + mInputText.setText(text); + return true; + } + + return false; + } + + /** + * Notifies the listener, if registered, of a change of the value of this + * NumberPicker. + */ + private void notifyChange(int previous, int current) { + if (mOnValueChangeListener != null) { + mOnValueChangeListener.onValueChange(this, previous, mValue); + } + } + + /** + * Posts a command for changing the current value by one. + * + * @param increment Whether to increment or decrement the value. + */ + private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { + if (mChangeCurrentByOneFromLongPressCommand == null) { + mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); + } else { + removeCallbacks(mChangeCurrentByOneFromLongPressCommand); + } + mChangeCurrentByOneFromLongPressCommand.setStep(increment); + postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); + } + + /** + * Removes the command for changing the current value by one. + */ + private void removeChangeCurrentByOneFromLongPress() { + if (mChangeCurrentByOneFromLongPressCommand != null) { + removeCallbacks(mChangeCurrentByOneFromLongPressCommand); + } + } + + /** + * Posts a command for beginning an edit of the current value via IME on + * long press. + */ + private void postBeginSoftInputOnLongPressCommand() { + if (mBeginSoftInputOnLongPressCommand == null) { + mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand(); + } else { + removeCallbacks(mBeginSoftInputOnLongPressCommand); + } + postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout()); + } + + /** + * Removes the command for beginning an edit of the current value via IME. + */ + private void removeBeginSoftInputCommand() { + if (mBeginSoftInputOnLongPressCommand != null) { + removeCallbacks(mBeginSoftInputOnLongPressCommand); + } + } + + /** + * Removes all pending callback from the message queue. + */ + private void removeAllCallbacks() { + if (mChangeCurrentByOneFromLongPressCommand != null) { + removeCallbacks(mChangeCurrentByOneFromLongPressCommand); + } + if (mSetSelectionCommand != null) { + removeCallbacks(mSetSelectionCommand); + } + if (mBeginSoftInputOnLongPressCommand != null) { + removeCallbacks(mBeginSoftInputOnLongPressCommand); + } + mPressedStateHelper.cancel(); + } + + /** + * @return The selected index given its displayed <code>value</code>. + */ + private int getSelectedPos(String value) { + if (mDisplayedValues == null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + // Ignore as if it's not a number we don't care + } + } else { + for (int i = 0; i < mDisplayedValues.length; i++) { + // Don't force the user to type in jan when ja will do + value = value.toLowerCase(); + if (mDisplayedValues[i].toLowerCase().startsWith(value)) { + return mMinValue + i; + } + } + + /* + * The user might have typed in a number into the month field i.e. + * 10 instead of OCT so support that too. + */ + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + + // Ignore as if it's not a number we don't care + } + } + return mMinValue; + } + + /** + * Posts an {@link SetSelectionCommand} from the given <code>selectionStart + * </code> to <code>selectionEnd</code>. + */ + private void postSetSelectionCommand(int selectionStart, int selectionEnd) { + if (mSetSelectionCommand == null) { + mSetSelectionCommand = new SetSelectionCommand(); + } else { + removeCallbacks(mSetSelectionCommand); + } + mSetSelectionCommand.mSelectionStart = selectionStart; + mSetSelectionCommand.mSelectionEnd = selectionEnd; + post(mSetSelectionCommand); + } + + /** + * The numbers accepted by the input text's {@link android.view.LayoutInflater.Filter} + */ + private static final char[] DIGIT_CHARACTERS = new char[] { + // Latin digits are the common case + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + // Arabic-Indic + '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668' + , '\u0669', + // Extended Arabic-Indic + '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8' + , '\u06f9' + }; + + /** + * Filter for accepting only valid indices or prefixes of the string + * representation of valid indices. + */ + class InputTextFilter extends NumberKeyListener { + + // XXX This doesn't allow for range limits when controlled by a + // soft input method! + public int getInputType() { + return InputType.TYPE_CLASS_TEXT; + } + + @Override + protected char[] getAcceptedChars() { + return DIGIT_CHARACTERS; + } + + @Override + public CharSequence filter( + CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + if (mDisplayedValues == null) { + CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); + if (filtered == null) { + filtered = source.subSequence(start, end); + } + + String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + + dest.subSequence(dend, dest.length()); + + if ("".equals(result)) { + return result; + } + int val = getSelectedPos(result); + + /* + * Ensure the user can't type in a value greater than the max + * allowed. We have to allow less than min as the user might + * want to delete some numbers and then type a new number. + * And prevent multiple-"0" that exceeds the length of upper + * bound number. + */ + if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) { + return ""; + } else { + return filtered; + } + } else { + CharSequence filtered = String.valueOf(source.subSequence(start, end)); + if (TextUtils.isEmpty(filtered)) { + return ""; + } + String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + + dest.subSequence(dend, dest.length()); + String str = String.valueOf(result).toLowerCase(); + for (String val : mDisplayedValues) { + String valLowerCase = val.toLowerCase(); + if (valLowerCase.startsWith(str)) { + postSetSelectionCommand(result.length(), val.length()); + return val.subSequence(dstart, val.length()); + } + } + return ""; + } + } + } + + /** + * Ensures that the scroll wheel is adjusted i.e. there is no offset and the + * middle element is in the middle of the widget. + * + * @return Whether an adjustment has been made. + */ + private boolean ensureScrollWheelAdjusted() { + // adjust to the closest value + int deltaY = mInitialScrollOffset - mCurrentScrollOffset; + if (deltaY != 0) { + mPreviousScrollerY = 0; + if (Math.abs(deltaY) > mSelectorElementHeight / 2) { + deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; + } + mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); + invalidate(); + return true; + } + return false; + } + + class PressedStateHelper implements Runnable { + public static final int BUTTON_INCREMENT = 1; + public static final int BUTTON_DECREMENT = 2; + + private final int MODE_PRESS = 1; + private final int MODE_TAPPED = 2; + + private int mManagedButton; + private int mMode; + + public void cancel() { + mMode = 0; + mManagedButton = 0; + LocalePicker.this.removeCallbacks(this); + if (mIncrementVirtualButtonPressed) { + mIncrementVirtualButtonPressed = false; + invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); + } + mDecrementVirtualButtonPressed = false; + if (mDecrementVirtualButtonPressed) { + invalidate(0, 0, mRight, mTopSelectionDividerTop); + } + } + + public void buttonPressDelayed(int button) { + cancel(); + mMode = MODE_PRESS; + mManagedButton = button; + LocalePicker.this.postDelayed(this, ViewConfiguration.getTapTimeout()); + } + + public void buttonTapped(int button) { + cancel(); + mMode = MODE_TAPPED; + mManagedButton = button; + LocalePicker.this.post(this); + } + + @Override + public void run() { + switch (mMode) { + case MODE_PRESS: { + switch (mManagedButton) { + case BUTTON_INCREMENT: { + mIncrementVirtualButtonPressed = true; + invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); + } break; + case BUTTON_DECREMENT: { + mDecrementVirtualButtonPressed = true; + invalidate(0, 0, mRight, mTopSelectionDividerTop); + } + } + } break; + case MODE_TAPPED: { + switch (mManagedButton) { + case BUTTON_INCREMENT: { + if (!mIncrementVirtualButtonPressed) { + LocalePicker.this.postDelayed(this, + ViewConfiguration.getPressedStateDuration()); + } + mIncrementVirtualButtonPressed ^= true; + invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); + } break; + case BUTTON_DECREMENT: { + if (!mDecrementVirtualButtonPressed) { + LocalePicker.this.postDelayed(this, + ViewConfiguration.getPressedStateDuration()); + } + mDecrementVirtualButtonPressed ^= true; + invalidate(0, 0, mRight, mTopSelectionDividerTop); + } + } + } break; + } + } + } + + /** + * Command for setting the input text selection. + */ + class SetSelectionCommand implements Runnable { + private int mSelectionStart; + + private int mSelectionEnd; + + public void run() { + mInputText.setSelection(mSelectionStart, mSelectionEnd); + } + } + + /** + * Command for changing the current value from a long press by one. + */ + class ChangeCurrentByOneFromLongPressCommand implements Runnable { + private boolean mIncrement; + + private void setStep(boolean increment) { + mIncrement = increment; + } + + @Override + public void run() { + changeValueByOne(mIncrement); + postDelayed(this, mLongPressUpdateInterval); + } + } + + /** + * @hide + */ + public static class CustomEditText extends EditText { + + public CustomEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onEditorAction(int actionCode) { + super.onEditorAction(actionCode); + if (actionCode == EditorInfo.IME_ACTION_DONE) { + clearFocus(); + } + } + } + + /** + * Command for beginning soft input on long press. + */ + class BeginSoftInputOnLongPressCommand implements Runnable { + + @Override + public void run() { + showSoftInput(); + mIngonreMoveEvents = true; + } + } + + /** + * Class for managing virtual view tree rooted at this picker. + */ + class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider { + private static final int UNDEFINED = Integer.MIN_VALUE; + + private static final int VIRTUAL_VIEW_ID_INCREMENT = 1; + + private static final int VIRTUAL_VIEW_ID_INPUT = 2; + + private static final int VIRTUAL_VIEW_ID_DECREMENT = 3; + + private final Rect mTempRect = new Rect(); + + private final int[] mTempArray = new int[2]; + + private int mAccessibilityFocusedView = UNDEFINED; + + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY, + mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); + case VIRTUAL_VIEW_ID_DECREMENT: + return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT, + getVirtualDecrementButtonText(), mScrollX, mScrollY, + mScrollX + (mRight - mLeft), + mTopSelectionDividerTop + mSelectionDividerHeight); + case VIRTUAL_VIEW_ID_INPUT: + return createAccessibiltyNodeInfoForInputText(mScrollX, + mTopSelectionDividerTop + mSelectionDividerHeight, + mScrollX + (mRight - mLeft), + mBottomSelectionDividerBottom - mSelectionDividerHeight); + case VIRTUAL_VIEW_ID_INCREMENT: + return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT, + getVirtualIncrementButtonText(), mScrollX, + mBottomSelectionDividerBottom - mSelectionDividerHeight, + mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); + } + return super.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched, + int virtualViewId) { + if (TextUtils.isEmpty(searched)) { + return Collections.emptyList(); + } + String searchedLowerCase = searched.toLowerCase(); + List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>(); + switch (virtualViewId) { + case View.NO_ID: { + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_DECREMENT, result); + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_INPUT, result); + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, + VIRTUAL_VIEW_ID_INCREMENT, result); + return result; + } + case VIRTUAL_VIEW_ID_DECREMENT: + case VIRTUAL_VIEW_ID_INCREMENT: + case VIRTUAL_VIEW_ID_INPUT: { + findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId, + result); + return result; + } + } + return super.findAccessibilityNodeInfosByText(searched, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView != virtualViewId) { + mAccessibilityFocusedView = virtualViewId; + requestAccessibilityFocus(); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView == virtualViewId) { + mAccessibilityFocusedView = UNDEFINED; + clearAccessibilityFocus(); + return true; + } + return false; + } + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (LocalePicker.this.isEnabled() + && (getWrapSelectorWheel() || getValue() < getMaxValue())) { + changeValueByOne(true); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (LocalePicker.this.isEnabled() + && (getWrapSelectorWheel() || getValue() > getMinValue())) { + changeValueByOne(false); + return true; + } + } return false; + } + } break; + case VIRTUAL_VIEW_ID_INPUT: { + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (LocalePicker.this.isEnabled() && !mInputText.isFocused()) { + return mInputText.requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (LocalePicker.this.isEnabled() && mInputText.isFocused()) { + mInputText.clearFocus(); + return true; + } + return false; + } + case AccessibilityNodeInfo.ACTION_CLICK: { + if (LocalePicker.this.isEnabled()) { + showSoftInput(); + return true; + } + return false; + } + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView != virtualViewId) { + mAccessibilityFocusedView = virtualViewId; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + mInputText.invalidate(); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView == virtualViewId) { + mAccessibilityFocusedView = UNDEFINED; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + mInputText.invalidate(); + return true; + } + } return false; + default: { + return mInputText.performAccessibilityAction(action, arguments); + } + } + } return false; + case VIRTUAL_VIEW_ID_INCREMENT: { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: { + if (LocalePicker.this.isEnabled()) { + LocalePicker.this.changeValueByOne(true); + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_CLICKED); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView != virtualViewId) { + mAccessibilityFocusedView = virtualViewId; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView == virtualViewId) { + mAccessibilityFocusedView = UNDEFINED; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); + return true; + } + } return false; + } + } return false; + case VIRTUAL_VIEW_ID_DECREMENT: { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: { + if (LocalePicker.this.isEnabled()) { + final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT); + LocalePicker.this.changeValueByOne(increment); + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_CLICKED); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView != virtualViewId) { + mAccessibilityFocusedView = virtualViewId; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + invalidate(0, 0, mRight, mTopSelectionDividerTop); + return true; + } + } return false; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (mAccessibilityFocusedView == virtualViewId) { + mAccessibilityFocusedView = UNDEFINED; + sendAccessibilityEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + invalidate(0, 0, mRight, mTopSelectionDividerTop); + return true; + } + } return false; + } + } return false; + } + return super.performAction(virtualViewId, action, arguments); + } + + public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) { + switch (virtualViewId) { + case VIRTUAL_VIEW_ID_DECREMENT: { + if (hasVirtualDecrementButton()) { + sendAccessibilityEventForVirtualButton(virtualViewId, eventType, + getVirtualDecrementButtonText()); + } + } break; + case VIRTUAL_VIEW_ID_INPUT: { + sendAccessibilityEventForVirtualText(eventType); + } break; + case VIRTUAL_VIEW_ID_INCREMENT: { + if (hasVirtualIncrementButton()) { + sendAccessibilityEventForVirtualButton(virtualViewId, eventType, + getVirtualIncrementButtonText()); + } + } break; + } + } + + private void sendAccessibilityEventForVirtualText(int eventType) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + mInputText.onInitializeAccessibilityEvent(event); + mInputText.onPopulateAccessibilityEvent(event); + event.setSource(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT); + requestSendAccessibilityEvent(LocalePicker.this, event); + } + } + + private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, + String text) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setClassName(Button.class.getName()); + event.setPackageName(mContext.getPackageName()); + event.getText().add(text); + event.setEnabled(LocalePicker.this.isEnabled()); + event.setSource(LocalePicker.this, virtualViewId); + requestSendAccessibilityEvent(LocalePicker.this, event); + } + } + + private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, + int virtualViewId, List<AccessibilityNodeInfo> outResult) { + switch (virtualViewId) { + case VIRTUAL_VIEW_ID_DECREMENT: { + String text = getVirtualDecrementButtonText(); + if (!TextUtils.isEmpty(text) + && text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT)); + } + } return; + case VIRTUAL_VIEW_ID_INPUT: { + CharSequence text = mInputText.getText(); + if (!TextUtils.isEmpty(text) && + text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); + return; + } + CharSequence contentDesc = mInputText.getText(); + if (!TextUtils.isEmpty(contentDesc) && + contentDesc.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); + return; + } + } break; + case VIRTUAL_VIEW_ID_INCREMENT: { + String text = getVirtualIncrementButtonText(); + if (!TextUtils.isEmpty(text) + && text.toString().toLowerCase().contains(searchedLowerCase)) { + outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT)); + } + } return; + } + } + + private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText( + int left, int top, int right, int bottom) { + AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo(); + info.setSource(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT); + if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) { + info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) { + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } + Rect boundsInParent = mTempRect; + boundsInParent.set(left, top, right, bottom); + info.setVisibleToUser(isVisibleToUser(boundsInParent)); + info.setBoundsInParent(boundsInParent); + Rect boundsInScreen = boundsInParent; + int[] locationOnScreen = mTempArray; + getLocationOnScreen(locationOnScreen); + boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); + info.setBoundsInScreen(boundsInScreen); + return info; + } + + private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId, + String text, int left, int top, int right, int bottom) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setClassName(Button.class.getName()); + info.setPackageName(mContext.getPackageName()); + info.setSource(LocalePicker.this, virtualViewId); + info.setParent(LocalePicker.this); + info.setText(text); + info.setClickable(true); + info.setLongClickable(true); + info.setEnabled(LocalePicker.this.isEnabled()); + Rect boundsInParent = mTempRect; + boundsInParent.set(left, top, right, bottom); + info.setVisibleToUser(isVisibleToUser(boundsInParent)); + info.setBoundsInParent(boundsInParent); + Rect boundsInScreen = boundsInParent; + int[] locationOnScreen = mTempArray; + getLocationOnScreen(locationOnScreen); + boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); + info.setBoundsInScreen(boundsInScreen); + + if (mAccessibilityFocusedView != virtualViewId) { + info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + if (mAccessibilityFocusedView == virtualViewId) { + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } + if (LocalePicker.this.isEnabled()) { + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); + } + + return info; + } + + private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top, + int right, int bottom) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setClassName(LocalePicker.class.getName()); + info.setPackageName(mContext.getPackageName()); + info.setSource(LocalePicker.this); + + if (hasVirtualDecrementButton()) { + info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_DECREMENT); + } + info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT); + if (hasVirtualIncrementButton()) { + info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_INCREMENT); + } + + info.setParent((View) getParentForAccessibility()); + info.setEnabled(LocalePicker.this.isEnabled()); + info.setScrollable(true); + + final float applicationScale = + getContext().getResources().getCompatibilityInfo().applicationScale; + + Rect boundsInParent = mTempRect; + boundsInParent.set(left, top, right, bottom); + boundsInParent.scale(applicationScale); + info.setBoundsInParent(boundsInParent); + + info.setVisibleToUser(isVisibleToUser()); + + Rect boundsInScreen = boundsInParent; + int[] locationOnScreen = mTempArray; + getLocationOnScreen(locationOnScreen); + boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); + boundsInScreen.scale(applicationScale); + info.setBoundsInScreen(boundsInScreen); + + if (mAccessibilityFocusedView != View.NO_ID) { + info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + if (mAccessibilityFocusedView == View.NO_ID) { + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } + if (LocalePicker.this.isEnabled()) { + if (getWrapSelectorWheel() || getValue() < getMaxValue()) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (getWrapSelectorWheel() || getValue() > getMinValue()) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + return info; + } + + private boolean hasVirtualDecrementButton() { + return getWrapSelectorWheel() || getValue() > getMinValue(); + } + + private boolean hasVirtualIncrementButton() { + return getWrapSelectorWheel() || getValue() < getMaxValue(); + } + + private String getVirtualDecrementButtonText() { + int value = mValue - 1; + if (mWrapSelectorWheel) { + value = getWrappedSelectorIndex(value); + } + if (value >= mMinValue) { + return (mDisplayedValues == null) ? formatNumber(value) + : mDisplayedValues[value - mMinValue]; + } + return null; + } + + private String getVirtualIncrementButtonText() { + int value = mValue + 1; + if (mWrapSelectorWheel) { + value = getWrappedSelectorIndex(value); + } + if (value <= mMaxValue) { + return (mDisplayedValues == null) ? formatNumber(value) + : mDisplayedValues[value - mMinValue]; + } + return null; + } + } + + static private String formatNumberWithLocale(int value) { + return String.format(Locale.getDefault(), "%d", value); + } +} diff --git a/src/com/cyanogenmod/setupwizard/ui/SetupPageFragment.java b/src/com/cyanogenmod/setupwizard/ui/SetupPageFragment.java new file mode 100644 index 0000000..c16ad37 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/ui/SetupPageFragment.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.ui; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.setup.Page; +import com.cyanogenmod.setupwizard.setup.SetupDataCallbacks; + +public abstract class SetupPageFragment extends Fragment { + + protected SetupDataCallbacks mCallbacks; + protected String mKey; + protected Page mPage; + protected View mRootView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle args = getArguments(); + mKey = args.getString(Page.KEY_PAGE_ARGUMENT); + if (mKey == null) { + throw new IllegalArgumentException("No KEY_PAGE_ARGUMENT given"); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mRootView = inflater.inflate(getLayoutResource(), container, false); + mCallbacks.onPageViewCreated(inflater, savedInstanceState, getHeaderLayoutResource()); + return mRootView; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mPage = mCallbacks.getPage(mKey); + initializePage(); + mCallbacks.onPageLoaded(mPage); + getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.primary)); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof SetupDataCallbacks)) { + throw new ClassCastException("Activity implement SetupDataCallbacks"); + } + mCallbacks = (SetupDataCallbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + protected int getHeaderLayoutResource() { + return R.layout.header; + } + + protected abstract void initializePage(); + protected abstract int getLayoutResource(); + +} diff --git a/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java b/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java new file mode 100644 index 0000000..a65c65c --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.ui; + +import android.app.Activity; +import android.app.AppGlobals; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.cyanogenmod.setupwizard.R; +import com.cyanogenmod.setupwizard.SetupWizardApp; +import com.cyanogenmod.setupwizard.setup.AbstractSetupData; +import com.cyanogenmod.setupwizard.setup.CMSetupWizardData; +import com.cyanogenmod.setupwizard.setup.Page; +import com.cyanogenmod.setupwizard.setup.SetupDataCallbacks; +import com.cyanogenmod.setupwizard.util.SetupWizardUtils; + + +public class SetupWizardActivity extends Activity implements SetupDataCallbacks { + + private static final String TAG = SetupWizardActivity.class.getSimpleName(); + + private View mRootView; + private View mPageView; + private Button mNextButton; + private Button mPrevButton; + private TextView mTitleView; + private ViewGroup mHeaderView; + + private AbstractSetupData mSetupData; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.setup_main); + mRootView = findViewById(R.id.root); + mPageView = findViewById(R.id.page); + ((SetupWizardApp)getApplicationContext()).disableStatusBar(); + mSetupData = (AbstractSetupData)getLastNonConfigurationInstance(); + if (mSetupData == null) { + mSetupData = new CMSetupWizardData(this); + } + mHeaderView = (ViewGroup)findViewById(R.id.header); + mNextButton = (Button) findViewById(R.id.next_button); + mPrevButton = (Button) findViewById(R.id.prev_button); + mSetupData.registerListener(this); + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mSetupData.onNextPage(); + } + }); + mPrevButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mSetupData.onPreviousPage(); + } + }); + if (savedInstanceState == null) { + Page page = mSetupData.getCurrentPage(); + page.doLoadAction(this, Page.ACTION_NEXT); + } + if (savedInstanceState != null && savedInstanceState.containsKey("data")) { + mSetupData.load(savedInstanceState.getBundle("data")); + } + } + + @Override + protected void onResume() { + super.onResume(); + onPageTreeChanged(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mSetupData.unregisterListener(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mSetupData.getCurrentPage().onActivityResult(requestCode, resultCode, data); + } + + @Override + public Object onRetainNonConfigurationInstance() { + return mSetupData; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBundle("data", mSetupData.save()); + } + + @Override + public void onBackPressed() { + mSetupData.onPreviousPage(); + } + + @Override + public void onPageViewCreated(LayoutInflater inflater, Bundle savedInstanceState, + int layoutResource) { + if (layoutResource != -1) { + mHeaderView.setVisibility(View.VISIBLE); + mHeaderView.removeAllViews(); + inflater.inflate(layoutResource, mHeaderView, true); + } else { + mHeaderView.setVisibility(View.GONE); + } + mTitleView = (TextView) findViewById(android.R.id.title); + if (mTitleView != null) { + Page page = mSetupData.getCurrentPage(); + mTitleView.setText(page.getTitleResId()); + } + } + + @Override + public void onNextPage() { + Page page = mSetupData.getCurrentPage(); + page.doLoadAction(this, Page.ACTION_NEXT); + } + + @Override + public void onPreviousPage() { + Page page = mSetupData.getCurrentPage(); + page.doLoadAction(this, Page.ACTION_PREVIOUS); + } + + @Override + public void onPageLoaded(Page page) { + updateButtonBar(); + } + + @Override + public void onPageTreeChanged() { + updateButtonBar(); + } + + private void updateButtonBar() { + Page page = mSetupData.getCurrentPage(); + mNextButton.setText(page.getNextButtonTitleResId()); + if (page.getPrevButtonTitleResId() != -1) { + mPrevButton.setText(page.getPrevButtonTitleResId()); + } else { + mPrevButton.setText(""); + } + if (mSetupData.isFirstPage()) { + mPrevButton.setCompoundDrawables(null, null, null, null); + } else { + mPrevButton.setCompoundDrawablesWithIntrinsicBounds( + getDrawable(R.drawable.ic_chevron_left_dark), + null, null, null); + } + final Resources resources = getResources(); + if (mSetupData.isLastPage()) { + mPrevButton.setVisibility(View.INVISIBLE); + mRootView.setBackgroundColor(resources.getColor(R.color.primary)); + mPageView.setBackgroundColor(resources.getColor(R.color.primary)); + mNextButton.setCompoundDrawablesWithIntrinsicBounds(null, null, + getDrawable(R.drawable.ic_chevron_right_wht), null); + mNextButton.setTextColor(resources.getColor(R.color.white)); + } else { + mPrevButton.setVisibility(View.VISIBLE); + mPageView.setBackgroundColor(resources.getColor(R.color.page_background)); + if (mSetupData.isFirstPage()) { + mRootView.setBackgroundColor(resources.getColor(R.color.page_background)); + } else { + mRootView.setBackgroundColor(resources.getColor(R.color.window_background)); + } + mNextButton.setCompoundDrawablesWithIntrinsicBounds(null, null, + getDrawable(R.drawable.ic_chevron_right_dark), null); + mNextButton.setTextColor(resources.getColor(R.color.primary_text)); + } + } + + @Override + public Page getPage(String key) { + return mSetupData.getPage(key); + } + + @Override + public Page getPage(int key) { + return mSetupData.getPage(key); + } + + @Override + public void onFinish() { + finishSetup(); + } + + private void finishSetup() { + Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1); + Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1); + ((SetupWizardApp)AppGlobals.getInitialApplication()).enableStatusBar(); + SetupWizardUtils.disableSetupWizards(this); + finish(); + } +} diff --git a/src/com/cyanogenmod/setupwizard/util/SetupWizardUtils.java b/src/com/cyanogenmod/setupwizard/util/SetupWizardUtils.java new file mode 100644 index 0000000..e31cff0 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/util/SetupWizardUtils.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.cyanogenmod.setupwizard.util; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.cyanogenmod.setupwizard.SetupWizardApp; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; + +import java.util.List; + +public class SetupWizardUtils { + + private static final String TAG = SetupWizardUtils.class.getSimpleName(); + + private static final String GOOGLE_SETUPWIZARD_PACKAGE = "com.google.android.setupwizard"; + + private SetupWizardUtils(){} + + public static void tryEnablingWifi(Context context) { + WifiManager wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE); + if (!wifiManager.isWifiEnabled()) { + wifiManager.setWifiEnabled(true); + } + } + + public static void launchWifiSetup(Activity context) { + SetupWizardUtils.tryEnablingWifi(context); + Intent intent = new Intent(SetupWizardApp.ACTION_SETUP_WIFI); + intent.putExtra(SetupWizardApp.EXTRA_FIRST_RUN, true); + intent.putExtra(SetupWizardApp.EXTRA_ALLOW_SKIP, true); + intent.putExtra("theme", "material_light"); + intent.putExtra(SetupWizardApp.EXTRA_AUTO_FINISH, true); + context.startActivityForResult(intent, SetupWizardApp.REQUEST_CODE_SETUP_WIFI); + } + + public static boolean isNetworkConnected(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); + } + + public static boolean isWifiConnected(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return mWifi != null && mWifi.isConnected(); + } + + public static boolean isMobileDataEnabled(Context context) { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (tm.isMultiSimEnabled()) { + int subscription = SubscriptionManager.getDefaultDataPhoneId(); + return android.provider.Settings.Global.getInt(context.getContentResolver(), + android.provider.Settings.Global.MOBILE_DATA + subscription, 0) != 0; + } else { + return android.provider.Settings.Global.getInt(context.getContentResolver(), + android.provider.Settings.Global.MOBILE_DATA, 0) != 0; + } + } + + public static void setMobileDataEnabled(Context context, boolean enabled) { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + tm.setDataEnabled(enabled); + if (tm.isMultiSimEnabled()) { + int subscription = SubscriptionManager.getDefaultDataPhoneId(); + android.provider.Settings.Global.putInt(context.getContentResolver(), + android.provider.Settings.Global.MOBILE_DATA + subscription, enabled ? 1 : 0); + } else { + android.provider.Settings.Global.putInt(context.getContentResolver(), + android.provider.Settings.Global.MOBILE_DATA, enabled ? 1 : 0); + } + } + + public static boolean hasTelephony(Context context) { + PackageManager packageManager = context.getPackageManager(); + return packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + } + + public static boolean isMultiSimDevice(Context context) { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return tm.isMultiSimEnabled(); + } + + public static boolean isGSMPhone(Context context) { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + int phoneType = tm.getPhoneType(); + return phoneType == TelephonyManager.PHONE_TYPE_GSM; + } + + public static boolean isSimMissing(Context context) { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + int simCount = SubscriptionManager.getActiveSubInfoCount(); + for (int i = 0; i < simCount; i++) { + int simState = tm.getSimState(i); + if (simState != TelephonyManager.SIM_STATE_ABSENT && + simState != TelephonyManager.SIM_STATE_UNKNOWN) { + return false; + } + } + return true; + } + + public static boolean hasGMS(Context context) { + return GooglePlayServicesUtil.isGooglePlayServicesAvailable(context) != + ConnectionResult.SERVICE_MISSING; + } + + public static void disableSetupWizards(Activity context) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_HOME); + final PackageManager pm = context.getPackageManager(); + final List<ResolveInfo> resolveInfos = + pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo info : resolveInfos) { + if (GOOGLE_SETUPWIZARD_PACKAGE.equals(info.activityInfo.packageName)) { + final ComponentName componentName = + new ComponentName(info.activityInfo.packageName, info.activityInfo.name); + pm.setComponentEnabledSetting(componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } + } + pm.setComponentEnabledSetting(context.getComponentName(), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(intent); + } +} |