/*
* 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;
}
}
}