************************************************************************ strings.xml ************************************************************************ WeatherViewer YOUR_API_KEY http://api.openweathermap.org/data/2.5/forecast/daily?q= Invalid URL A graphical representation of the weather conditions High: %s Low: %s %1$s: %2$s Humidity: %s Enter city (e.g, Boston, MA, US) Unable to read weather data Unable to connect to OpenWeatherMap.org ************************************************************************ Weather.java ************************************************************************ package at.htl.weatherviewer; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.TimeZone; public class Weather { public final String dayOfWeek; public final String minTemp; public final String maxTemp; public final String humidity; public final String description; public final String iconURL; public Weather(long timeStamp, double minTemp, double maxTemp, double humiditiy, String description, String iconName) { //NumberFormat to format double temperatures rounded to integers NumberFormat numberFormat = NumberFormat.getInstance() ; numberFormat.setMaximumFractionDigits(0); this.dayOfWeek = convertTimeStampToDay(timeStamp); this.minTemp = numberFormat.format(minTemp) + "\u00B0C"; this.maxTemp = numberFormat.format(maxTemp) + "\u00B0C"; this.humidity = NumberFormat.getPercentInstance().format(humiditiy / 100.0); this.description = description; this.iconURL = "http://openweathermap.org/img/w/" + iconName + ".png"; } // convert timestamp to a day's name (e.g. Monday, Tuesday, ..) private static String convertTimeStampToDay(long timeStamp) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timeStamp * 1000); TimeZone tz = TimeZone.getDefault(); // get device's time zone // adjust time for device's time zone calendar.add(Calendar.MILLISECOND, tz.getOffset(calendar.getTimeInMillis())); // SimpleDateFormat that returns the day's name SimpleDateFormat dateFormatter = new SimpleDateFormat("EEEE"); return dateFormatter.format(calendar.getTime()); } } ************************************************************************ WeatherArrayAdpter.java ************************************************************************ package at.htl.weatherviewer; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; public class WeatherArrayAdapter extends ArrayAdapter { private static final String LOG_TAG = WeatherArrayAdapter.class.getSimpleName(); // class for reusing views as list items scroll off and onto the screen private static class ViewHolder { ImageView conditionImageView; TextView dayTextView; TextView lowTextView; TextView hiTextView; TextView humidityTextView; } // stores already downloaded Bitmaps for reuse private Map bitmaps = new HashMap<>(); // constructor to initialize superclass inherited members public WeatherArrayAdapter(Context context, List forecast) { super(context, -1, forecast); } // creates the custom view for the ListView's items @Override public View getView(int position, View convertView, ViewGroup parent) { View rowView = convertView; // get Weather object for this specified ListView position Weather day = getItem(position); ViewHolder viewHolder; // object that references's list item's views // check for reusable ViewHolder from ListView item that scrolled // offscreen; otherwise create a new ViewHolder if (rowView == null) { // no reusable ViewHolder, so create one viewHolder = new ViewHolder(); LayoutInflater inflater = LayoutInflater.from(getContext()); rowView = inflater.inflate(R.layout.list_item, parent, false); viewHolder.conditionImageView = (ImageView) rowView.findViewById(R.id.conditionImageView); viewHolder.dayTextView = (TextView) rowView.findViewById(R.id.dayTextView); viewHolder.lowTextView = (TextView) rowView.findViewById(R.id.lowTextView); viewHolder.hiTextView = (TextView) rowView.findViewById(R.id.hiTextView); viewHolder.humidityTextView = (TextView) rowView.findViewById(R.id.humidityTextView); rowView.setTag(viewHolder); // assign the data to the item as a tag } else { // reuse existing ViewHolder stored as the list item's tag viewHolder = (ViewHolder) rowView.getTag(); } // if weather condition icon already downloaded use it; // otherwise, download icon in a separate thread if (bitmaps.containsKey(day.iconURL)) { viewHolder.conditionImageView.setImageBitmap(bitmaps.get(day.iconURL)); } else { // download and display weather condition image Log.d(LOG_TAG, "day.iconURL = " + day.iconURL); new LoadImageTask(viewHolder.conditionImageView).execute(day.iconURL); } // get other data from Weather object and place into views Context context = getContext(); // for loading String resources viewHolder.dayTextView.setText(context.getString( R.string.day_description, day.dayOfWeek, day.description)); viewHolder.lowTextView.setText( context.getString(R.string.low_temp, day.minTemp)); viewHolder.hiTextView.setText( context.getString(R.string.high_temp, day.maxTemp)); viewHolder.humidityTextView.setText( context.getString(R.string.humidity, day.humidity)); return rowView; // return completed list item to display } // AsyncTask to load weather conditions icons in a separate thread private class LoadImageTask extends AsyncTask { private ImageView imageView; // displays the thumbnail // store ImageView on which to set the download Bitmap public LoadImageTask(ImageView imageView) { this.imageView = imageView; } // load image: params[0] is the String URL representing the image @Override protected Bitmap doInBackground(String... params) { Bitmap bitmap = null; HttpURLConnection connection = null; try { URL url = new URL(params[0]); // create URL for image // open an HttpURLConnection, get its InputStream // and download the image connection = (HttpURLConnection) url.openConnection(); try (InputStream inputStream = connection.getInputStream()) { bitmap = BitmapFactory.decodeStream(inputStream); bitmaps.put(params[0], bitmap); // cache for later use } catch (Exception e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } finally { connection.disconnect(); } return bitmap; } // set weather condition image in list item @Override protected void onPostExecute(Bitmap bitmap) { imageView.setImageBitmap(bitmap); } } } ************************************************************************ MainActivity.java ************************************************************************ package at.htl.weatherviewer; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ListView; import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private static final String LOG_TAG = MainActivity.class.getSimpleName(); // List of Weather objects representing the forecast private List weatherList = new ArrayList<>(); // ArrayAdapter for binding Weather objects to a ListView private WeatherArrayAdapter weatherArrayAdapter; private ListView weatherListView; // configure Toolbar, ListView and FAB @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // autogenerated code to inflate layout and configure Toolbar setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // create ArrayAdapter to bind weatherList to the weatherListView weatherListView = (ListView) findViewById(R.id.weatherListView); weatherArrayAdapter = new WeatherArrayAdapter(this, weatherList); weatherListView.setAdapter(weatherArrayAdapter); weatherListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(getBaseContext(), "Item clicked", Toast.LENGTH_SHORT).show(); Snackbar.make(findViewById(R.id.coordinatorLayout), "item clicked", Snackbar.LENGTH_SHORT).show(); } }); // Configure FAB to hide keyboard and initiate web service request FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // get next from locationEditText and create web service URL EditText locationEditText = (EditText) findViewById(R.id.locationEditText); URL url = createURL(locationEditText.getText().toString()); // hide keyboard and initiate a GetWeatherTask to download // weather data from OpenWeatherMap.org in a separate thread if (url != null) { dismissKeyboard(locationEditText); GetWeatherTask getLocalWeatherTask = new GetWeatherTask(); getLocalWeatherTask.execute(url); } else { Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.invalid_url, Snackbar.LENGTH_LONG).show(); } } }); } private void dismissKeyboard(View view) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } // create openweathermap.org web service URL using city private URL createURL(String city) { String apiKey = getString(R.string.api_key); String baseUrl = getString(R.string.web_service_url); try { // create URL for specified city and metric units (Celsius) String urlString = baseUrl + URLEncoder.encode(city, "UTF-8") + "&units=metric&cnt=16&APPID=" + apiKey; // "&units=metric&lang=de&cnt=16&APPID=" + apiKey; // deutsche Übersetzung return new URL(urlString); } catch (Exception e) { e.printStackTrace(); } return null; // URL was malformed } // makes the REST web service call to get weather data and // saves the data to a local HTML file private class GetWeatherTask extends AsyncTask { private final String LOG_TAG = GetWeatherTask.class.getSimpleName(); @Override protected JSONObject doInBackground(URL... params) { HttpURLConnection connection = null; try { connection = (HttpURLConnection) params[0].openConnection(); int response = connection.getResponseCode(); if (response == HttpURLConnection.HTTP_OK) { StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { builder.append(line); } } catch (IOException e) { Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.read_error, Snackbar.LENGTH_LONG).show(); e.printStackTrace(); } Log.d(LOG_TAG, builder.toString()); return new JSONObject(builder.toString()); } else { Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.connect_error, Snackbar.LENGTH_LONG).show(); } } catch (Exception e) { Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.connect_error, Snackbar.LENGTH_LONG).show(); e.printStackTrace(); } finally { connection.disconnect(); // close the HttpURLConnection } return null; } // process JSON response and update ListView @Override protected void onPostExecute(JSONObject weather) { convertJSONtoArrayList(weather); // repopulate weatherList weatherArrayAdapter.notifyDataSetChanged(); // rebind to ListView weatherListView.smoothScrollToPosition(0); // scroll to top } } // create Weather objects from JSONObject containing the forecast private void convertJSONtoArrayList(JSONObject forecast) { weatherList.clear(); // clear old weather data try { // get forecast's "list" JSONArray JSONArray list = forecast.getJSONArray("list"); // convert each element of list to a Weather object for (int i = 0; i < list.length(); ++i) { JSONObject day = list.getJSONObject(i); // get one day's data // get the day's temperatures ("temp") JSONObject JSONObject temperatures = day.getJSONObject("temp"); // get day's "weather" JSONObject for the description and icon JSONObject weather = day.getJSONArray("weather").getJSONObject(0); // add new Weather object to weatherList weatherList.add(new Weather( day.getLong("dt"), // date/time timestamp temperatures.getDouble("min"), // minimum temperature temperatures.getDouble("max"), // maximum temperature day.getDouble("humidity"), // percent humidity weather.getString("description"), // weather conditions weather.getString("icon"))); // icon name } } catch (JSONException e) { e.printStackTrace(); } } } ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************