diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba225043..f8ce186c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ - = 23 + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + String[] PERMISSIONS_STORAGE = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}; + + checkSelfPermission(PERMISSIONS_STORAGE[0]); + checkSelfPermission(PERMISSIONS_STORAGE[1]); + requestPermissions(PERMISSIONS_STORAGE, PERMISSIONS_REQUEST_CODE); + } else readWriteAccepted = true; + } + + //--------------------------------------BOTTOM NAV BAR METHODS---------------------------------- + private void paginationEnabled(boolean enabled) { + firstPage.setEnabled(enabled); + previousPage.setEnabled(enabled); + nextPage.setEnabled(enabled); + lastPage.setEnabled(enabled); + } + private void initIncrementButton(ImageButton increment, final int step) { // Increment once for a click increment.setOnClickListener(new View.OnClickListener() { @@ -335,7 +367,7 @@ public class TopicActivity extends BaseActivity { protected void onPreExecute() { progressBar.setVisibility(ProgressBar.VISIBLE); - paginationEnable(false); + paginationEnabled(false); if (replyFAB.getVisibility() != View.GONE) replyFAB.setEnabled(false); } @@ -403,7 +435,7 @@ public class TopicActivity extends BaseActivity { pageIndicator.setText(String.valueOf(thisPage) + "/" + String.valueOf(numberOfPages)); pageRequestValue = thisPage; - paginationEnable(true); + paginationEnabled(true); if (topicTitle == null || Objects.equals(topicTitle, "")) toolbar.setTitle(parsedTitle); @@ -488,11 +520,4 @@ public class TopicActivity extends BaseActivity { } } } - - private void paginationEnable(boolean enabled) { - firstPage.setEnabled(enabled); - previousPage.setEnabled(enabled); - nextPage.setEnabled(enabled); - lastPage.setEnabled(enabled); - } } \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java index a9627b25..05a2779f 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java +++ b/app/src/main/java/gr/thmmy/mthmmy/activities/topic/TopicAdapter.java @@ -10,7 +10,6 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.PowerManager; import android.support.annotation.NonNull; import android.support.v4.content.res.ResourcesCompat; import android.support.v7.widget.CardView; @@ -93,7 +92,7 @@ class TopicAdapter extends RecyclerView.Adapter { * Index of state indicator in the boolean array. If true quote button for this post is checked. */ private static final int isQuoteButtonChecked = 2; - private final MaterialProgressBar progressBar; + //private final MaterialProgressBar progressBar; private DownloadTask downloadTask; private TopicActivity.TopicTask topicTask; @@ -167,7 +166,7 @@ class TopicAdapter extends RecyclerView.Adapter { //Initializes properties, array's values will be false by default viewProperties.add(new boolean[3]); } - this.progressBar = progressBar; + //this.progressBar = progressBar; downloadTask = new DownloadTask(); this.topicTask = topicTask; } @@ -244,8 +243,12 @@ class TopicAdapter extends RecyclerView.Adapter { attached.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - downloadTask = new DownloadTask(); - downloadTask.execute(attachedFile); + if (TopicActivity.readWriteAccepted) { + downloadTask = new DownloadTask(); + downloadTask.execute(attachedFile); + } else + Toast.makeText(context, "Persmissions missing!", Toast.LENGTH_SHORT) + .show(); } }); @@ -616,17 +619,11 @@ class TopicAdapter extends RecyclerView.Adapter { * Debug Tag for logging debug output to LogCat */ private static final String TAG = "DownloadTask"; //Separate tag for AsyncTask - private PowerManager.WakeLock mWakeLock; @Override protected void onPreExecute() { super.onPreExecute(); - //Locks CPU to prevent going off - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - getClass().getName()); - mWakeLock.acquire(); - progressBar.setVisibility(View.VISIBLE); + Toast.makeText(context, "Downloading", Toast.LENGTH_SHORT).show(); } @Override @@ -647,7 +644,10 @@ class TopicAdapter extends RecyclerView.Adapter { Report.e(TAG, "Error while trying to download a file", e); return e.toString(); } catch (OutOfMemoryError e) { - Report.e(TAG, "Error while trying to download a file", e); + Report.e(TAG, e.toString(), e); + return e.toString(); + } catch (IllegalStateException e) { + Report.e(TAG, e.toString(), e); return e.toString(); } return null; @@ -655,12 +655,10 @@ class TopicAdapter extends RecyclerView.Adapter { @Override protected void onPostExecute(String result) { - mWakeLock.release(); - if (result != null) - Toast.makeText(context, result, Toast.LENGTH_SHORT).show(); - else - Toast.makeText(context, "Download complete", Toast.LENGTH_SHORT).show(); - progressBar.setVisibility(View.INVISIBLE); + if (result != null) { + Toast.makeText(context, "Download failed!", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, result, Toast.LENGTH_LONG).show(); + } } } } \ No newline at end of file diff --git a/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java b/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java index 16ee1d0f..15245e2f 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java +++ b/app/src/main/java/gr/thmmy/mthmmy/session/SessionManager.java @@ -27,11 +27,10 @@ import okhttp3.RequestBody; import okhttp3.Response; /** - This class handles all session related operations (e.g. login, logout) - and stores data to SharedPreferences (session information and cookies). -*/ -public class SessionManager -{ + * This class handles all session related operations (e.g. login, logout) + * and stores data to SharedPreferences (session information and cookies). + */ +public class SessionManager { //Class TAG private static final String TAG = "SessionManager"; @@ -66,27 +65,25 @@ public class SessionManager //Constructor public SessionManager(OkHttpClient client, PersistentCookieJar cookieJar, - SharedPrefsCookiePersistor cookiePersistor, SharedPreferences sharedPrefs) - { + SharedPrefsCookiePersistor cookiePersistor, SharedPreferences sharedPrefs) { this.client = client; - this.cookiePersistor=cookiePersistor; + this.cookiePersistor = cookiePersistor; this.cookieJar = cookieJar; this.sharedPrefs = sharedPrefs; } //------------------------------------AUTH BEGINS---------------------------------------------- + /** - * Login function with two options: (username, password) or nothing (using saved cookies). - * Always call it in a separate thread. + * Login function with two options: (username, password) or nothing (using saved cookies). + * Always call it in a separate thread. */ - public int login(String... strings) - { + public int login(String... strings) { Report.i(TAG, "Logging in..."); //Build the login request for each case Request request; - if (strings.length == 2) - { + if (strings.length == 2) { clearSessionData(); String loginName = strings[0]; @@ -101,9 +98,7 @@ public class SessionManager .url(loginUrl) .post(formBody) .build(); - } - else - { + } else { request = new Request.Builder() .url(loginUrl) .build(); @@ -125,20 +120,16 @@ public class SessionManager sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply(); sharedPrefs.edit().putString(USERNAME, extractUserName(document)).apply(); String avatar = extractAvatarLink(document); - if (avatar!=null) - { - sharedPrefs.edit().putBoolean(HAS_AVATAR,true).apply(); + if (avatar != null) { + sharedPrefs.edit().putBoolean(HAS_AVATAR, true).apply(); sharedPrefs.edit().putString(AVATAR_LINK, extractAvatarLink(document)).apply(); - } - else - sharedPrefs.edit().putBoolean(HAS_AVATAR,false).apply(); + } else + sharedPrefs.edit().putBoolean(HAS_AVATAR, false).apply(); sharedPrefs.edit().putString(LOGOUT_LINK, HttpUrl.parse(logoutButton.attr("href")).toString()).apply(); return SUCCESS; - } - else - { + } else { Report.i(TAG, "Login failed."); //Investigate login failure @@ -159,41 +150,35 @@ public class SessionManager return FAILURE; } //Handle exception - } - catch (InterruptedIOException e){ + } catch (InterruptedIOException e) { Report.i(TAG, "Login InterruptedIOException"); //users cancels LoginTask return CANCELLED; - } - catch (IOException e) { + } catch (IOException e) { Report.w(TAG, "Login IOException", e); return CONNECTION_ERROR; - } - catch (Exception e) { + } catch (Exception e) { Report.w(TAG, "Login Exception (other)", e); return EXCEPTION; } } /** - * A function that checks the validity of the current saved session (if it exists). - * If isLoggedIn() is true, it will call login() with cookies. On failure, this can only return - * the code FAILURE. CANCELLED, CONNECTION_ERROR and EXCEPTION are simply considered a SUCCESS - * (e.g. no internet connection), at least until a more thorough handling of different - * exceptions is implemented (if considered mandatory). - * Always call it in a separate thread in a way that won't hinder performance (e.g. after - * fragments' data are retrieved). + * A function that checks the validity of the current saved session (if it exists). + * If isLoggedIn() is true, it will call login() with cookies. On failure, this can only return + * the code FAILURE. CANCELLED, CONNECTION_ERROR and EXCEPTION are simply considered a SUCCESS + * (e.g. no internet connection), at least until a more thorough handling of different + * exceptions is implemented (if considered mandatory). + * Always call it in a separate thread in a way that won't hinder performance (e.g. after + * fragments' data are retrieved). */ - public void validateSession() - { + public void validateSession() { Report.i(TAG, "Validating session..."); - if(isLoggedIn()) - { + if (isLoggedIn()) { int loginResult = login(); - if(loginResult != FAILURE) + if (loginResult != FAILURE) return; - } - else if(isLoginScreenDefault()) + } else if (isLoginScreenDefault()) return; sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, true).apply(); @@ -201,10 +186,9 @@ public class SessionManager } /** - * Call this function when user explicitly chooses to continue as a guest (UI thread). + * Call this function when user explicitly chooses to continue as a guest (UI thread). */ - public void guestLogin() - { + public void guestLogin() { Report.i("TAG", "Continuing as a guest, as chosen by the user."); clearSessionData(); sharedPrefs.edit().putBoolean(LOGIN_SCREEN_AS_DEFAULT, false).apply(); @@ -212,14 +196,13 @@ public class SessionManager /** - * Logout function. Always call it in a separate thread. + * Logout function. Always call it in a separate thread. */ - public int logout() - { + public int logout() { Report.i(TAG, "Logging out..."); Request request = new Request.Builder() - .url(sharedPrefs.getString(LOGOUT_LINK,"LogoutLink")) + .url(sharedPrefs.getString(LOGOUT_LINK, "LogoutLink")) .build(); try { @@ -271,15 +254,18 @@ public class SessionManager return sharedPrefs.getBoolean(LOGIN_SCREEN_AS_DEFAULT, true); } + //TODO FIX METHOD, THIS MIGHT BE A SECURITY FLAW!! SEE ISSUE #2 MERGED WITH #16 + public String getCookieHeader() { + return cookiePersistor.loadAll().get(0).toString(); + } + //--------------------------------------GETTERS END--------------------------------------------- //------------------------------------OTHER FUNCTIONS------------------------------------------- - private void setPersistentCookieSession() - { + private void setPersistentCookieSession() { List cookieList = cookieJar.loadForRequest(indexUrl); - if (cookieList.size() == 2) - { + if (cookieList.size() == 2) { if ((cookieList.get(0).name().equals("THMMYgrC00ki3")) && (cookieList.get(1).name().equals("PHPSESSID"))) { Cookie.Builder builder = new Cookie.Builder(); @@ -295,18 +281,16 @@ public class SessionManager } } - private void clearSessionData() - { + private void clearSessionData() { cookieJar.clear(); sharedPrefs.edit().clear().apply(); //Clear session data sharedPrefs.edit().putString(USERNAME, guestName).apply(); sharedPrefs.edit().putBoolean(LOGGED_IN, false).apply(); //User logs out - Report.i(TAG,"Session data cleared."); + Report.i(TAG, "Session data cleared."); } @Nullable - private String extractUserName(Document doc) - { + private String extractUserName(Document doc) { if (doc != null) { Elements user = doc.select("div[id=myuser] > h3"); @@ -319,13 +303,12 @@ public class SessionManager return matcher.group(1); } } - Report.w(TAG,"Extracting username failed!"); + Report.w(TAG, "Extracting username failed!"); return null; } @Nullable - private String extractAvatarLink(Document doc) - { + private String extractAvatarLink(Document doc) { if (doc != null) { Elements avatar = doc.select("#ava img"); @@ -333,7 +316,7 @@ public class SessionManager return avatar.attr("src"); } } - Report.w(TAG,"Extracting avatar's link failed!"); + Report.w(TAG, "Extracting avatar's link failed!"); return null; } //----------------------------------OTHER FUNCTIONS END----------------------------------------- diff --git a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java index 8e8414f6..5de8cbc8 100644 --- a/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java +++ b/app/src/main/java/gr/thmmy/mthmmy/utils/FileManager/ThmmyFile.java @@ -1,10 +1,18 @@ package gr.thmmy.mthmmy.utils.FileManager; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; import android.os.Environment; import android.os.StatFs; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Log; import android.webkit.MimeTypeMap; +import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; @@ -12,6 +20,7 @@ import java.io.IOException; import java.net.URL; import java.util.Objects; +import gr.thmmy.mthmmy.base.BaseActivity; import mthmmy.utils.Report; import okhttp3.Request; import okhttp3.Response; @@ -22,6 +31,7 @@ import static gr.thmmy.mthmmy.base.BaseActivity.getClient; * Used for downloading and storing a file from the forum using {@link okhttp3}. *

Class has one constructor, {@link #ThmmyFile(URL, String, String)}. */ +@SuppressWarnings("unused") public class ThmmyFile { /** * Debug Tag for logging debug output to LogCat @@ -46,8 +56,8 @@ public class ThmmyFile { } /** - * This constructor only creates a ThmmyFile object and does not download the file. To download - * the file use {@link #download(Context)}! + * This constructor only creates a ThmmyFile object and does not download the file. To + * download the file use {@link #download(Context)} after you provide a url! * * @param fileUrl {@link URL} object with file's url * @param filename {@link String} with desired file name @@ -115,23 +125,64 @@ public class ThmmyFile { /** * Used to download the file. If download is successful file's extension and path will be assigned * to object's fields and can be accessed using getter methods. - *

File is stored in sdcard1/Android/data/Downloads/packageName

* - * @return the {@link File} if successful, null otherwise - * @throws IOException if the request could not be executed due to cancellation, a connectivity - * problem or timeout. Because networks can fail during an exchange, it is possible that the - * remote server accepted the request before the failure. - * @throws SecurityException if the requested file is not hosted by the forum. + * @return null if downloaded with the download service, otherwise the {@link File} + * @throws IOException if the request could not be executed due to cancellation, a + * connectivity problem or timeout. Because networks can fail + * during an exchange, it is possible that the remote server + * accepted the request before the failure. + * @throws SecurityException if the requested file is not hosted by the forum. + * @throws IllegalStateException if file's url or filename is not yet set */ @Nullable - public File download(Context context) throws IOException, SecurityException, OutOfMemoryError { - if (fileUrl == null) { - return null; - } - if (!Objects.equals(fileUrl.getHost(), "www.thmmy.gr")) + public File download(Context context) throws IOException, IllegalStateException, OutOfMemoryError { + if (fileUrl == null) + throw new IllegalStateException("Internal error!\nNo url was provided."); + else if (!Objects.equals(fileUrl.getHost(), "www.thmmy.gr")) throw new SecurityException("Downloading files from other sources is not supported"); + else if (filename == null || Objects.equals(filename, "")) + throw new IllegalStateException("Internal error!\nNo filename was provided."); + + try { + downloadWithManager(context, fileUrl); + } catch (IllegalStateException e) { + return downloadWithoutManager(context, fileUrl); + } + return null; + } + + private void downloadWithManager(Context context, @NonNull URL pFileUrl) throws IllegalStateException, IOException { + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(pFileUrl.toString())); + request.addRequestHeader("Cookie", BaseActivity.getSessionManager().getCookieHeader()); + request.setDescription("mThmmy"); + request.setMimeType(MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(filename))); + request.setTitle(filename); + request.allowScanningByMediaScanner(); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + try { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); + } catch (IllegalStateException e) { + Report.d(TAG, "External directory not available!", e); + Log.d(TAG, "External directory not available!", e); + throw e; + } - Request request = new Request.Builder().url(fileUrl).build(); + DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + manager.enqueue(request); + context.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Toast.makeText(context, "Download complete", Toast.LENGTH_SHORT).show(); + context.unregisterReceiver(this); + } + }, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } + + @Nullable + private File downloadWithoutManager(Context context, @NonNull URL pFileUrl) throws IOException + , SecurityException, OutOfMemoryError { + Request request = new Request.Builder().url(pFileUrl).build(); Response response = getClient().newCall(request).execute(); if (!response.isSuccessful()) { @@ -153,7 +204,8 @@ public class ThmmyFile { } @Nullable - private File getOutputMediaFile(Context context, String fileName, String fileInfo) throws OutOfMemoryError, IOException { + private File getOutputMediaFile(Context context, String fileName, String fileInfo) throws + OutOfMemoryError, IOException { File mediaStorageDir; String extState = Environment.getExternalStorageState(); if (Environment.isExternalStorageRemovable() &&