/* * Copyright 2015. Appsi Mobile * * 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.appsimobile.appsii.module.home; import android.app.Activity; import android.app.Dialog; import android.app.DialogFragment; import android.app.LoaderManager; import android.content.AsyncTaskLoader; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.content.SharedPreferences; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.provider.Settings; import android.support.annotation.IntDef; import android.support.annotation.RequiresPermission; import android.support.v4.util.CircularArray; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.text.format.DateUtils; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import com.appsimobile.appsii.LocationLoader; import com.appsimobile.appsii.LocationReceiver; import com.appsimobile.appsii.R; import com.appsimobile.appsii.dagger.AppInjector; import com.appsimobile.appsii.module.weather.loader.YahooWeatherApiClient; import com.appsimobile.appsii.permissions.PermissionUtils; import javax.inject.Inject; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; /** * Dialog fragment that pops up when touching the preference. */ public class YahooLocationChooserDialogFragment extends DialogFragment implements TextWatcher, Handler.Callback, LoaderManager.LoaderCallbacks<CircularArray<YahooWeatherApiClient.LocationSearchResult>>, View.OnClickListener { public static final int MESSAGE_RELOAD = 0; public static final int LOCATION_REQUEST_RESULT_DISABLED = 0; public static final int LOCATION_REQUEST_RESULT_READY = 1; public static final int LOCATION_REQUEST_RESULT_UPDATING_LOCATION = 2; public static final int LOCATION_REQUEST_RESULT_PERMISSION_DENIED = 3; /** * Time between search queries while typing. */ private static final int QUERY_DELAY_MILLIS = 500; @Inject LocationUpdateHelper mLocationUpdateHelper; View mGrantAccessContainer; Button mCancelGrantLocationAccessButton; Button mConfirmGrantLocationAccessButton; boolean mHasUpdatedLocationInfo; String mCurrentTownName; String mCurrentCountry; String mCurrentTimezone; String mCurrentWoeid; TextView mSearchView; TextView mHeaderTextView; View mCurrentLocationContainer; @Inject SharedPreferences mPreferences; final Runnable mShowLocationRunnable = new Runnable() { @Override public void run() { Activity activity = getActivity(); if (activity == null) return; mCurrentTownName = mPreferences.getString("last_location_update_town", null); mCurrentWoeid = mPreferences.getString("last_location_update_woeid", null); mCurrentCountry = mPreferences.getString("last_location_update_country", null); mCurrentTimezone = mPreferences.getString("last_location_update_timezone", null); onLocationInfoAvailable(mCurrentCountry, mCurrentTownName); } }; @Inject PermissionUtils mPermissionUtils; View mEnableLocationContainer; LocationResultListener mLocationResultListener; SearchResultsListAdapter mSearchResultsAdapter; ListView mSearchResultsList; Button mApplyCurrentLocationButton; Button mCancelCurrentLocationButton; Button mCancelUseLocationButton; Button mConfirmUseLocationButton; boolean mNoHints; private Handler mHandler; private String mQuery; private Handler mRestartLoaderHandler; public YahooLocationChooserDialogFragment() { } public static YahooLocationChooserDialogFragment newInstance() { return new YahooLocationChooserDialogFragment(); } public static YahooLocationChooserDialogFragment newInstance(boolean noHints) { YahooLocationChooserDialogFragment result = new YahooLocationChooserDialogFragment(); if (noHints) { Bundle args = new Bundle(); args.putBoolean("no_hints", true); result.setArguments(args); } return result; } @Override public boolean handleMessage(Message msg) { Bundle args = new Bundle(); args.putString("query", mQuery); getLoaderManager().restartLoader(0, args, YahooLocationChooserDialogFragment.this); return true; } public void setLocationResultListener(LocationResultListener locationResultListener) { mLocationResultListener = locationResultListener; tryBindList(); } private void tryBindList() { if (mLocationResultListener == null) { return; } if (isAdded() && mSearchResultsAdapter == null) { mSearchResultsAdapter = new SearchResultsListAdapter(); } if (mSearchResultsAdapter != null && mSearchResultsList != null) { mSearchResultsList.setAdapter(mSearchResultsAdapter); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); tryBindList(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppInjector.inject(this); // first see if we need to show hints Bundle args = getArguments(); mNoHints = args != null && args.getBoolean("no_hints"); mHandler = new Handler(); mRestartLoaderHandler = new Handler(this); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Context layoutContext = new ContextThemeWrapper(getActivity(), R.style.Appsi_Sidebar_Material_Teal); LayoutInflater layoutInflater = LayoutInflater.from(layoutContext); View rootView = layoutInflater.inflate(R.layout.fragment_location_chooser, null); mSearchView = (TextView) rootView.findViewById(R.id.location_query); mCurrentLocationContainer = rootView.findViewById(R.id.location_search); mApplyCurrentLocationButton = (Button) rootView.findViewById(R.id.apply_current_location_button); mCancelCurrentLocationButton = (Button) rootView.findViewById(R.id.cancel_current_location_button); mEnableLocationContainer = rootView.findViewById(R.id.location_unavailable); mCancelUseLocationButton = (Button) rootView.findViewById(R.id.cancel_enable_location_button); mConfirmUseLocationButton = (Button) rootView.findViewById(R.id.enable_location_button); mGrantAccessContainer = rootView.findViewById(R.id.location_permission_denied); mCancelGrantLocationAccessButton = (Button) rootView.findViewById(R.id.cancel_grant_location_access_button); mConfirmGrantLocationAccessButton = (Button) rootView.findViewById(R.id.grant_location_access_button); mHeaderTextView = (TextView) rootView.findViewById(R.id.your_location_title); mSearchView.addTextChangedListener(this); mApplyCurrentLocationButton.setOnClickListener(this); mCancelCurrentLocationButton.setOnClickListener(this); mCancelUseLocationButton.setOnClickListener(this); mConfirmUseLocationButton.setOnClickListener(this); mCancelGrantLocationAccessButton.setOnClickListener(this); mConfirmGrantLocationAccessButton.setOnClickListener(this); // Set up apps mSearchResultsList = (ListView) rootView.findViewById(android.R.id.list); mSearchResultsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> listView, View view, int position, long itemId) { YahooWeatherApiClient.LocationSearchResult value = (YahooWeatherApiClient.LocationSearchResult) mSearchResultsAdapter .getItem(position); mLocationResultListener.onLocationSearchResult(value); dismiss(); } }); tryBindList(); AlertDialog dialog = new AlertDialog.Builder(getActivity()) .setView(rootView) .create(); dialog.getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); return dialog; } @Override public void onStart() { super.onStart(); // if we already have a location, or we should not show hints, // return immediately. if (mHasUpdatedLocationInfo || mNoHints) return; @LocationRequestResult int locationResult = startLocationUpdateIfNeeded(); switch (locationResult) { case LOCATION_REQUEST_RESULT_PERMISSION_DENIED: showLocationPermissionDenied(); break; case LOCATION_REQUEST_RESULT_DISABLED: showLocationProviderDisabled(); break; case LOCATION_REQUEST_RESULT_READY: mHasUpdatedLocationInfo = true; // act as if we are updating the location mHandler.postDelayed(mShowLocationRunnable, 1); break; case LOCATION_REQUEST_RESULT_UPDATING_LOCATION: break; } } @LocationRequestResult private int startLocationUpdateIfNeeded() { Activity activity = getActivity(); mHasUpdatedLocationInfo = true; return mLocationUpdateHelper.startLocationUpdateIfNeeded(activity); } void showLocationPermissionDenied() { mHasUpdatedLocationInfo = false; mGrantAccessContainer.setVisibility(View.VISIBLE); } void showLocationProviderDisabled() { mHasUpdatedLocationInfo = false; mEnableLocationContainer.setVisibility(View.VISIBLE); } @Override public void onStop() { super.onStop(); if (mLocationUpdateHelper != null) { mLocationUpdateHelper.onStop(); } // remove the pending requests. This may crash when the user types // something and immediately closes the dialog mRestartLoaderHandler.removeMessages(MESSAGE_RELOAD); } @Override public void onDestroy() { super.onDestroy(); mLocationUpdateHelper = null; } @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { mQuery = charSequence.toString(); if (mRestartLoaderHandler.hasMessages(MESSAGE_RELOAD)) { return; } mRestartLoaderHandler.sendMessageDelayed( mRestartLoaderHandler.obtainMessage(MESSAGE_RELOAD), QUERY_DELAY_MILLIS); } @Override public void afterTextChanged(Editable editable) { } @Override public Loader<CircularArray<YahooWeatherApiClient.LocationSearchResult>> onCreateLoader(int id, Bundle args) { final String query = args.getString("query"); return new ResultsLoader(query, getActivity()); } @Override public void onLoadFinished(Loader<CircularArray<YahooWeatherApiClient.LocationSearchResult>> loader, CircularArray<YahooWeatherApiClient.LocationSearchResult> results) { mSearchResultsAdapter.changeArray(results); } @Override public void onLoaderReset(Loader<CircularArray<YahooWeatherApiClient.LocationSearchResult>> loader) { mSearchResultsAdapter.changeArray(null); } public void onCurrentLocationInfoReady(String woeid, String country, String town, String timezone) { // TODO: create a test for this case if (woeid == null || town == null) return; mHasUpdatedLocationInfo = true; mCurrentTownName = town; mCurrentWoeid = woeid; mCurrentCountry = country; mCurrentTimezone = timezone; Activity activity = getActivity(); if (activity != null) { SharedPreferences sharedPreferences = mPreferences; sharedPreferences.edit(). putString("last_location_update_town", town). putString("last_location_update_woeid", woeid). putString("last_location_update_country", country). putLong("last_location_update_millis", System.currentTimeMillis()). apply(); onLocationInfoAvailable(country, town); } } void onLocationInfoAvailable(String country, String currentTownName) { updateCurrentLocationFields(currentTownName, country); mCurrentLocationContainer.setVisibility(View.VISIBLE); } private void updateCurrentLocationFields(String header, String summary) { mCurrentLocationContainer.setVisibility(View.VISIBLE); mHeaderTextView.setText(header); String text = getResources().getString(R.string.user_location, header, summary); CharSequence styled = Html.fromHtml(text); mHeaderTextView.setText(styled); } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.apply_current_location_button: onCurrentLocationClicked(); break; case R.id.cancel_current_location_button: onCurrentLocationCancelClicked(); break; case R.id.enable_location_button: onEnableLocationClicked(); break; case R.id.cancel_enable_location_button: onCancelEnableLocationClicked(); break; case R.id.grant_location_access_button: onGrantLocationAccessClicked(); break; case R.id.cancel_grant_location_access_button: onCancelLocationAccessClicked(); break; } } void onCurrentLocationClicked() { // communicate the selected result back to the fragment YahooWeatherApiClient.LocationSearchResult result = new YahooWeatherApiClient.LocationSearchResult(); result.country = mCurrentCountry; result.woeid = mCurrentWoeid; result.displayName = mCurrentTownName; result.timezone = mCurrentTimezone; mLocationResultListener.onLocationSearchResult(result); dismiss(); } void onCurrentLocationCancelClicked() { mCurrentLocationContainer.setVisibility(View.GONE); } void onEnableLocationClicked() { Intent locationIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); startActivity(locationIntent); dismiss(); } void onCancelEnableLocationClicked() { mEnableLocationContainer.setVisibility(View.GONE); } void onGrantLocationAccessClicked() { mPermissionUtils.requestPermission(this, 7, ACCESS_COARSE_LOCATION); dismiss(); } void onCancelLocationAccessClicked() { mGrantAccessContainer.setVisibility(View.GONE); } @IntDef({LOCATION_REQUEST_RESULT_DISABLED, LOCATION_REQUEST_RESULT_READY, LOCATION_REQUEST_RESULT_UPDATING_LOCATION, LOCATION_REQUEST_RESULT_PERMISSION_DENIED}) public @interface LocationRequestResult { } public interface LocationUpdateHelper { @LocationRequestResult int startLocationUpdateIfNeeded(Context context); void setFragment(YahooLocationChooserDialogFragment fragment); void onStop(); } public interface LocationResultListener { void onLocationSearchResult(YahooWeatherApiClient.LocationSearchResult result); } /** * Loader that fetches location search results from {@link YahooWeatherApiClient}. */ private static class ResultsLoader extends AsyncTaskLoader<CircularArray<YahooWeatherApiClient.LocationSearchResult>> { private final String mQuery; private CircularArray<YahooWeatherApiClient.LocationSearchResult> mResults; public ResultsLoader(String query, Context context) { super(context); mQuery = query; } @Override public CircularArray<YahooWeatherApiClient.LocationSearchResult> loadInBackground() { return YahooWeatherApiClient.findLocationsAutocomplete(mQuery); } @Override public void deliverResult(CircularArray<YahooWeatherApiClient.LocationSearchResult> apps) { mResults = apps; if (isStarted()) { // If the Loader is currently started, we can immediately // deliver its results. super.deliverResult(apps); } } @Override protected void onStartLoading() { if (mResults != null) { deliverResult(mResults); } if (takeContentChanged() || mResults == null) { // If the data has changed since the last time it was loaded // or is not currently available, start a load. forceLoad(); } } @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } @Override protected void onReset() { super.onReset(); onStopLoading(); } } static abstract class AbstractDefaultLocationUpdate implements LocationUpdateHelper { final SharedPreferences mPrefs; public AbstractDefaultLocationUpdate(SharedPreferences preferences) { mPrefs = preferences; } @Override @LocationRequestResult public final int startLocationUpdateIfNeeded(Context context) { // request location updates if needed long lastLocationUpdate = mPrefs.getLong("last_location_update_millis", 0); long passedMillis = System.currentTimeMillis() - lastLocationUpdate; int passedMinutes = (int) (passedMillis / DateUtils.MINUTE_IN_MILLIS); Log.i("WeatherFragment", "passed time since last location request: " + passedMinutes); return doStartLocationUpdateIfNeeded(context, passedMinutes); } @LocationRequestResult protected abstract int doStartLocationUpdateIfNeeded(Context context, int passedMinutes); } public static class DefaultLocationUpdate extends AbstractDefaultLocationUpdate implements LocationReceiver { final PermissionUtils mPermissionUtils; final LocationManager mLocationManager; LocationLoader mLocationLoader; YahooLocationChooserDialogFragment mFragment; @Inject public DefaultLocationUpdate(SharedPreferences sharedPreferences, PermissionUtils permissionUtils, LocationManager locationManager) { super(sharedPreferences); mPermissionUtils = permissionUtils; mLocationManager = locationManager; } @Override @LocationRequestResult @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}) protected int doStartLocationUpdateIfNeeded( Context context, int passedMinutes) throws SecurityException { if (!mPermissionUtils.holdsPermission(context, ACCESS_COARSE_LOCATION)) { return LOCATION_REQUEST_RESULT_PERMISSION_DENIED; } LocationManager locationManager = mLocationManager; if (!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { // mark the location as received otherwise the // ui will start the animation anyway return LOCATION_REQUEST_RESULT_DISABLED; } if (passedMinutes > 3) { mLocationLoader = new LocationLoader(this); mLocationLoader.requestLocationUpdate(context); return LOCATION_REQUEST_RESULT_UPDATING_LOCATION; } return LOCATION_REQUEST_RESULT_READY; } @Override public void onCurrentLocationInfoReady(String woeid, String country, String town, String timezone) { if (mFragment != null) { mFragment.onCurrentLocationInfoReady(woeid, country, town, timezone); } } @Override public void setFragment(YahooLocationChooserDialogFragment fragment) { mFragment = fragment; } @Override public void onStop() { if (mLocationLoader != null) { mLocationLoader.destroy(); } mFragment = null; } } private class SearchResultsListAdapter extends BaseAdapter { private CircularArray<YahooWeatherApiClient.LocationSearchResult> mResults; SearchResultsListAdapter() { mResults = new CircularArray<>(); } public void changeArray(CircularArray<YahooWeatherApiClient.LocationSearchResult> results) { if (results == null) { results = new CircularArray<>(); } mResults = results; notifyDataSetChanged(); } @Override public int getCount() { return Math.max(1, mResults.size()); } @Override public Object getItem(int position) { if (position == 0 && mResults.size() == 0) { return null; } return mResults.get(position); } @Override public long getItemId(int position) { if (position == 0 && mResults.size() == 0) { return -1; } return mResults.get(position).woeid.hashCode(); } @Override public View getView(int position, View convertView, ViewGroup container) { if (convertView == null) { convertView = LayoutInflater.from(getActivity()) .inflate(R.layout.list_item_weather_location_result, container, false); } if (position == 0 && mResults.size() == 0) { ((TextView) convertView.findViewById(android.R.id.text1)) .setText(R.string.weather_auto_location); ((TextView) convertView.findViewById(android.R.id.text2)) .setText(R.string.weather_auto_location_summary); } else { YahooWeatherApiClient.LocationSearchResult result = mResults.get(position); ((TextView) convertView.findViewById(android.R.id.text1)) .setText(result.displayName); ((TextView) convertView.findViewById(android.R.id.text2)) .setText(result.country); } return convertView; } public String getPrefValueAt(int position) { if (position == 0 && mResults.size() == 0) { return ""; } YahooWeatherApiClient.LocationSearchResult result = mResults.get(position); return result.woeid + "," + result.displayName; } } }