diff --git a/app/build.gradle b/app/build.gradle index fb17597..cf0e993 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,6 +24,7 @@ repositories { } dependencies { + implementation 'com.android.support:support-annotations:28.0.0' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab5cdae..7600201 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Book.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Book.java new file mode 100644 index 0000000..34ecd81 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Book.java @@ -0,0 +1,67 @@ +package ua.kpi.comsys.io8227.jackshen; + +/** An {@link Book} object contains information related to a single book. */ + +class Book { + + /** Title of the book */ + private String mTitle; + + /** Subtitle of the book */ + private String mSubtitle; + + /** ISBN of the book*/ + private String mIsbn; + + /** Retail price of the book */ + private String mPrice; + + /** Cover of the book */ + private String mImageUrl; + + /** + * Create book object + * + * @param title - title of the book + * @param subtitle - subtitle of the book + * @param isbn - isbn number of the book + * @param price - retail price of the book + * @param imageUrl - the URL to find cover of the book + */ + Book(String title, String subtitle, String isbn, String price, String imageUrl) { + this.mTitle = title; + this.mSubtitle = subtitle; + this.mIsbn = isbn; + this.mPrice = price; + this.mImageUrl = imageUrl; + } + + /** + * Return the title information of the book + * + * @return the title of the book + */ + String getTitle() { + return mTitle; + } + + /** Return the subtitle of the book */ + String getSubtitle() { + return mSubtitle; + } + + /** Return the ISBN number of the book */ + String getISBN() { + return mIsbn; + } + + /** Return the retail price of the book */ + String getPrice() { + return mPrice; + } + + /** Return the URL to find cover of the book */ + String getImageUrl() { + return mImageUrl; + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java new file mode 100644 index 0000000..dbdbedb --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java @@ -0,0 +1,123 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.app.LoaderManager; +import android.content.Context; +import android.content.Loader; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.widget.ListView; +import android.widget.TextView; + +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + +public class BookActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { + + /** URL for book data */ + private static final String REQUEST_URL = "https://api.jsonbin.io/b/6023ffbd87173a3d2f5b37e9"; + + /** + * Constant value for the book loader ID. + * We can choose any int, it`s really needed for multiple loaders + */ + private static final int BOOK_LOADER_ID = 1; + + /** Adapter for the list of books */ + private BookAdapter mAdapter; + + /** TextView that is displayed when the list is empty */ + private TextView mEmptyTextView; + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + protected void onCreate(Bundle savedInstanceState) { + // Initialize activity on main thread. + // Bundle holds previous state when re-initialized + super.onCreate(savedInstanceState); + + // Inflate the activity's UI + setContentView(R.layout.activity_book); + + // Find a reference to the {@link ListView} in the layout + ListView bookListView = findViewById(R.id.list); + + // Set empty view when there is no data + mEmptyTextView = findViewById(R.id.empty_view); + bookListView.setEmptyView(mEmptyTextView); + + // Create a new adapter that takes an empty list of books as input + mAdapter = new BookAdapter(this, new ArrayList()); + + // Set the adapter on the {@link ListView} so the list can be populated in UI + bookListView.setAdapter(mAdapter); + + // Get a reference to the ConnectivityManager to check state of network connectivity + ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + // Get details on the currently active default data network + NetworkInfo networkInfo = Objects.requireNonNull(connMgr).getActiveNetworkInfo(); + + // If there is a network connection -> get data + if (networkInfo != null && networkInfo.isConnected()) { + // Get a reference to the LoaderManager, in order to interact with loaders. + LoaderManager loaderManager = getLoaderManager(); + + // Get the loader initialized. Go through the above specified int ID constant + // and pass the bundle to null. + loaderManager.initLoader(BOOK_LOADER_ID, null, this); + } else { + + // Otherwise, display error and hide loading indicator so error message will be visible + View loadingIndicator = findViewById(R.id.progress_bar); + loadingIndicator.setVisibility(View.GONE); + + // Update empty state with no connection error message + mEmptyTextView.setText("No internet connection."); + } + } + + @Override + public Loader> onCreateLoader(int i, Bundle bundle) { + // Create a new loader for the given URL + Uri baseUri = Uri.parse(REQUEST_URL); + + return new BookLoader(this, baseUri.toString()); + } + + /** + * The loader requests and parses information downloader from the internet on a background + * thread pool, keeping the UI thread unblocked + */ + @Override + public void onLoadFinished(Loader> loader, List books) { + + // Hide progress bar + View loadingIndicator = findViewById(R.id.progress_bar); + loadingIndicator.setVisibility(View.GONE); + + // Set empty state text to display "No books to display." + mEmptyTextView.setText("No books to display."); + + // Clear the adapter of previous data + mAdapter.clear(); + + // Add valid list of books to the adapter + if (books != null && !books.isEmpty()) + mAdapter.addAll(books); + } + + @Override + public void onLoaderReset(Loader> loader) { + // Clear existing data on adapter as loader is reset + mAdapter.clear(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java new file mode 100644 index 0000000..53a6643 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java @@ -0,0 +1,119 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.os.Build; +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 androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + + +/** + * Class {@link BookAdapter} is used to create a list item layout for each book + * in the data source (a list of {@link Book} objects). + * + * An adapter view such as ListView will be provided with these + * list item layouts to be presented to the user. + */ + +public class BookAdapter extends ArrayAdapter { + + /** + * Constructs a new {@link BookAdapter}. + * + * @param context of the app + * @param books - list of books + */ + + BookAdapter(Context context, List books) { + super(context, 0, books); + } + + /** + * Returns the view of the list item that shows information about the book in the book list. + */ + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + // Check if there is an existing convertView that we can reuse, otherwise, + // if convertView is null, then inflate a new list item layout. + View listItemView = convertView; + if (listItemView == null) { + listItemView = LayoutInflater.from(getContext()).inflate(R.layout.book_list_item, parent, false); + } + + // Find the book at the given position in the list of books + Book currentBook = getItem(position); + + // Find the TextView with view ID titleBook + TextView titleView = listItemView.findViewById(R.id.titleBook); + // Find the TextView with view ID subtitleBook + TextView subtitleView = listItemView.findViewById(R.id.subtitleBook); + // Find the TextView with view ID isbnBook + TextView isbnView = listItemView.findViewById(R.id.isbnBook); + // Find the TextView with view ID priceBook + TextView priceView = listItemView.findViewById(R.id.priceBook); + // Find the ImageView with view ID imageBook + ImageView imageView = listItemView.findViewById(R.id.imageBook); + + // Display the title of the current book in that TextView + titleView.setText(Objects.requireNonNull(currentBook).getTitle()); + // Display the subtitle of the current book in that TextView + subtitleView.setText(currentBook.getSubtitle()); + // Display the ISBN of the current book in that TextView + isbnView.setText(currentBook.getISBN()); + // Display the price of the current book in that TextView + priceView.setText(currentBook.getPrice()); + + // Display image in the ImageView widget + if (currentBook.getImageUrl().equals("")) { + imageView.setImageResource(R.drawable.noimage); + } else { + new DownloadImage(imageView).execute(currentBook.getImageUrl()); + } + + return listItemView; + } + + /** Class to download an image from URL */ + private class DownloadImage extends AsyncTask { + final ImageView bmImage; + + DownloadImage(ImageView bmImage) { + this.bmImage = bmImage; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + protected Bitmap doInBackground(String... urls) { + String urlDisplay = urls[0]; + Bitmap mIcon = null; + try { + InputStream in = new java.net.URL(urlDisplay).openStream(); + mIcon = BitmapFactory.decodeStream(in); + } catch (Exception e) { + Log.e("Error", Objects.requireNonNull(e.getMessage())); + e.printStackTrace(); + } + return mIcon; + } + + protected void onPostExecute(Bitmap result) { + bmImage.setImageBitmap(result); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java new file mode 100644 index 0000000..538e8d4 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java @@ -0,0 +1,204 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** Helper methods to request and retrieve book data from a specified JSON */ +final class BookJSONParser { + + /** Tag for the log messages */ + private static final String LOG_MSG = BookJSONParser.class.getSimpleName(); + + /** + * We are creating a private constructor because no one else should create + * the {@link BookJSONParser} object. + */ + private BookJSONParser() { + } + + /** + * Query given JSON and return a list of {@link String} objects. + * + * @param requestUrl - our URL as a {@link String} object + * @return the list of {@link Book}s + */ + static List getBookData(String requestUrl) { + URL url = makeUrl(requestUrl); + + // Perform HTTP request to the URL and receive a JSON response back + String jsonResponse = null; + try { + jsonResponse = makeHttpRequest(url); + } catch (IOException err) { + Log.e(LOG_MSG, "Problem making the HTTP request.", err); + } + + // Extract relevant fields from the JSON response and create a list of {@link Book}s + // Then return the list of {@link Book}s + return extractDataFromJson(jsonResponse); + } + + /** Returns new URL object from the given string URL. */ + private static URL makeUrl(String strUrl) { + // Initialize an empty {@link URL} object to hold the parsed URL from the strUrl + URL url = null; + + // Parse valid URL from param strUrl and handle Malformed urls + try { + url = new URL(strUrl); + } catch (MalformedURLException err) { + Log.e(LOG_MSG, "Problem building the URL!", err); + } + + // Return valid URL + return url; + } + + /** Make an HTTP request to the given URL and return a String as the response. */ + private static String makeHttpRequest(URL url) throws IOException { + // Initialize variable to hold the parsed JSON response + String jsonResponse = ""; + + // Return response if URL is null + if (url == null) { + return jsonResponse; + } + + // Initialize HTTP connection object + HttpURLConnection connection = null; + + // Initialize {@link InputStream} to hold response from request + InputStream inputStream = null; + try { + // Establish connection to the URL + connection = (HttpURLConnection) url.openConnection(); + + // Set request type + connection.setRequestMethod("GET"); + + // Setting how long to wait on the request (in milliseconds) + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + + // Establish connection to the URL + connection.connect(); + + // Check for successful connection + if (connection.getResponseCode() == 200) { + inputStream = connection.getInputStream(); + jsonResponse = readFromStream(inputStream); + } else { + Log.e(LOG_MSG, "Error response code: " + connection.getResponseCode()); + } + } catch (IOException err) { + Log.e(LOG_MSG, "Problem retrieving the book JSON results.", err); + } finally { + if (connection != null) { + // Disconnect the connection after successfully making the HTTP request + connection.disconnect(); + } + + if (inputStream != null) { + // Close the stream after successfully parsing the request + // This also can throw an IOException + inputStream.close(); + } + } + + // Return JSON as a {@link String} + return jsonResponse; + } + + /** + * Convert the {@link InputStream} into a String which contains the whole JSON response. + */ + private static String readFromStream(InputStream inputStream) throws IOException { + StringBuilder output = new StringBuilder(); + if (inputStream != null) { + // Decode the bits + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); + + // Buffer the decoded characters + BufferedReader reader = new BufferedReader(inputStreamReader); + + // Store a line of characters from the BufferedReader + String line = reader.readLine(); + + // If not end of buffered input stream, read next line and add to output + while (line != null) { + output.append(line); + line = reader.readLine(); + } + } + + // Convert chars sequence from the builder into a string and return + return output.toString(); + } + + private static List extractDataFromJson(String bookJSON) { + // Exit if no data was returned from the HTTP request + if (TextUtils.isEmpty(bookJSON)) + return null; + + // Initialize list of strings to hold the extracted books + List books = new ArrayList<>(); + + try { + // Create JSON object from response + JSONObject rawJSONResponse = new JSONObject(bookJSON); + + // Extract the array that holds the books + JSONArray bookArray = rawJSONResponse.getJSONArray("books"); + + for (int i = 0; i < bookArray.length(); i++) { + // Get the current book + JSONObject currentBook = bookArray.getJSONObject(i); + + // Get the book's title + String title = currentBook.getString("title"); + + // Get the book's subtitle + String subtitle = currentBook.getString("subtitle"); + + // Get the book's ISBN number + String isbn13 = currentBook.getString("isbn13"); + + // Get the book's price + String price = currentBook.getString("price"); + + // Get the URL of book's cover + String imageUrl = currentBook.getString("image"); + + // Create a new {@link Book} object with the title, subtitle, isbn13, price + // and imageUrl from the JSON response. + Book book = new Book(title, subtitle, isbn13, price, imageUrl); + + // Add the new {@link Book} to the list of books. + books.add(book); + } + + } catch (JSONException e) { + // Catch the exception from the 'try' block and print a log message + Log.e("BookJSONParser", "Problem parsing the book JSON results", e); + } + + // Return the list of books + return books; + } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java new file mode 100644 index 0000000..79fb6fb --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java @@ -0,0 +1,47 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.AsyncTaskLoader; +import android.content.Context; + +import java.util.List; + + +/** Using AsyncTask to load a list of books by network request to a certain URL. */ +public class BookLoader extends AsyncTaskLoader> { + + /** Query URL **/ + final private String mUrl; + + + /** + * Constructs a new {@link BookLoader}. + * + * @param context of the activity + * @param url to load data from + */ + BookLoader(Context context, String url) { + super(context); + mUrl = url; + } + + + /** Forcing the loader to make an HTTP request and start downloading the required data */ + @Override + protected void onStartLoading() { + forceLoad(); + } + + /** + * This method is called in a background thread and takes care of the + * generating new data from the given JSON file + */ + @Override + public List loadInBackground() { + // Check for valid string url + if (mUrl == null) { + return null; + } + // Perform the network request, parse the response, and extract a list of books. + return BookJSONParser.getBookData(mUrl); + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java index 215ab1b..c6c7111 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java @@ -3,12 +3,10 @@ import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.view.View; import android.view.Window; import android.view.WindowManager; -import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; @@ -81,14 +79,19 @@ public void onSwipeLeft() { }); - Button button = findViewById(R.id.open_portfolio); - button.setOnClickListener(new View.OnClickListener(){ - @Override - public void onClick(View view) { - Intent openSite = new Intent(Intent.ACTION_VIEW, Uri.parse("https://jackshen.herokuapp.com/")); - startActivity(openSite); - } - }); +// Button button = findViewById(R.id.open_portfolio); +// button.setOnClickListener(new View.OnClickListener(){ +// @Override +// public void onClick(View view) { +// Intent openSite = new Intent(Intent.ACTION_VIEW, Uri.parse("https://jackshen.herokuapp.com/")); +// startActivity(openSite); +// } +// }); + } + + public void openBooks(View v) { + Intent intent = new Intent(this, BookActivity.class); + startActivity(intent); } public void openInfograph(View v) { diff --git a/app/src/main/res/drawable/noimage.png b/app/src/main/res/drawable/noimage.png new file mode 100644 index 0000000..bda4bcb Binary files /dev/null and b/app/src/main/res/drawable/noimage.png differ diff --git a/app/src/main/res/layout/activity_book.xml b/app/src/main/res/layout/activity_book.xml new file mode 100644 index 0000000..c8d6b78 --- /dev/null +++ b/app/src/main/res/layout/activity_book.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3ab363e..9462c0c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -100,18 +100,18 @@ android:paddingLeft="32dp" android:paddingRight="32dp"> - + + + + + + + + + + + +