diff --git a/app/build.gradle b/app/build.gradle index 649471f..8be59da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 + compileSdkVersion 28 defaultConfig { applicationId "by.paranoidandroid.threadsexample" minSdkVersion 14 - targetSdkVersion 26 + targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -16,11 +16,15 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'androidx.appcompat:appcompat:1.0.0-rc01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/app/src/androidTest/java/by/paranoidandroid/threadsexample/utils/SimpleAsyncTaskImplTest.java b/app/src/androidTest/java/by/paranoidandroid/threadsexample/utils/SimpleAsyncTaskImplTest.java new file mode 100644 index 0000000..573fbd8 --- /dev/null +++ b/app/src/androidTest/java/by/paranoidandroid/threadsexample/utils/SimpleAsyncTaskImplTest.java @@ -0,0 +1,47 @@ +package by.paranoidandroid.threadsexample.utils; + +import android.os.Looper; + +import org.junit.Test; + +import utils.SimpleAsyncTaskImpl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Created by rl98880 on 19/11/2017. + * Edited by Ksenia on 13/08/2018. + */ +public class SimpleAsyncTaskImplTest { + int counter = 0; + + @Test + public void execute() throws Exception { + new SimpleAsyncTaskImpl() { + @Override + protected void onPreExecute() { + assertTrue(isOnUiThread()); + assertEquals(counter++, 0); + } + + @Override + protected Object doInBackground(Object[] obj) { + assertFalse(isOnUiThread()); + assertEquals(counter++, 1); + return new Object(); + } + + @Override + protected void onPostExecute(Object obj) { + assertTrue(isOnUiThread()); + assertEquals(counter++, 2); + } + }.execute(); + } + + public boolean isOnUiThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9dd8f3f..205d1f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,5 +7,21 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme" /> - + android:theme="@style/AppTheme"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/by/paranoidandroid/threadsexample/activities/AsyncTaskActivity.java b/app/src/main/java/by/paranoidandroid/threadsexample/activities/AsyncTaskActivity.java new file mode 100644 index 0000000..b0ff285 --- /dev/null +++ b/app/src/main/java/by/paranoidandroid/threadsexample/activities/AsyncTaskActivity.java @@ -0,0 +1,144 @@ +package by.paranoidandroid.threadsexample.activities; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import by.paranoidandroid.threadsexample.R; +import utils.IAsyncTaskEvents; + +import static android.os.AsyncTask.Status.FINISHED; +import static android.os.AsyncTask.Status.PENDING; +import static android.os.AsyncTask.Status.RUNNING; + +/** + * Activity that demonstrates work with AsyncTask. + */ + +public class AsyncTaskActivity extends Activity implements IAsyncTaskEvents { + private final String STATE_COUNTER = "COUNTER", + STATE_RUNNING = "RUNNING", + STATE_TEXTVIEW = "TEXTVIEW"; + private TextView textView; + private int counter; + private boolean isRunning; + CounterAsyncTask counterAsyncTask; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.actvity_asynctask); + + Button createBtn = findViewById(R.id.btn_create); + Button startBtn = findViewById(R.id.btn_start); + Button cancelBtn = findViewById(R.id.btn_cancel); + textView = findViewById(R.id.text_view_counter); + + if (savedInstanceState != null) { + textView.setText(savedInstanceState.getString(STATE_TEXTVIEW, "")); + isRunning = savedInstanceState.getBoolean(STATE_RUNNING); + if (isRunning) { + counter = savedInstanceState.getInt(STATE_COUNTER); + counterAsyncTask = new CounterAsyncTask(AsyncTaskActivity.this); + isRunning = true; + counterAsyncTask.execute(counter); + } + } + + View.OnClickListener btnListener = view -> { + switch (view.getId()) { + case R.id.btn_create: + if (counterAsyncTask == null + || counterAsyncTask.isCancelled() + || counterAsyncTask.getStatus() == FINISHED) { + counterAsyncTask = new CounterAsyncTask(AsyncTaskActivity.this); + } + break; + case R.id.btn_start: + if (counterAsyncTask != null + && counterAsyncTask.getStatus() == PENDING) { + isRunning = true; + counterAsyncTask.execute(); + } + break; + case R.id.btn_cancel: + if (counterAsyncTask != null) { + isRunning = false; + counterAsyncTask.cancel(true); + counterAsyncTask = null; + } + break; + } + }; + + createBtn.setOnClickListener(btnListener); + startBtn.setOnClickListener(btnListener); + cancelBtn.setOnClickListener(btnListener); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + if (counterAsyncTask != null && counterAsyncTask.getStatus() == RUNNING) { + counterAsyncTask.cancel(true); + counterAsyncTask = null; + } + outState.putInt(STATE_COUNTER, counter); + outState.putBoolean(STATE_RUNNING, isRunning); + outState.putString(STATE_TEXTVIEW, textView.getText().toString()); + + super.onSaveInstanceState(outState); + } + + @Override + public void onProgressUpdate(Integer counter) { + this.counter = counter; + textView.setText(String.valueOf(counter)); + } + + @Override + public void onPostExecute() { + textView.setText(getString(R.string.done)); + isRunning = true; + } + + static class CounterAsyncTask extends AsyncTask { + private final static String LOG_TAG = "CounterAsyncTask"; + private final static int MAX_COUNT = 10, TIMEOUT = 500; + private int counter = 1; + private IAsyncTaskEvents listener; + + CounterAsyncTask(IAsyncTaskEvents events) { + listener = events; + } + + @Override + protected void onProgressUpdate(Integer... values) { + listener.onProgressUpdate(values[0]); + } + + @Override + protected Void doInBackground(Integer... values) { + if (values.length > 0 && values[0] != null) { + counter = values[0]; + counter++; + } + while (counter <= MAX_COUNT) { + publishProgress(counter++); + try { + Thread.sleep(TIMEOUT); + } catch (Exception ex) { + Log.e(LOG_TAG, "Exception: ", ex); + } + } + return null; + } + + @Override + protected void onPostExecute(Void s) { + listener.onPostExecute(); + } + } +} diff --git a/app/src/main/java/by/paranoidandroid/threadsexample/activities/LoaderActivity.java b/app/src/main/java/by/paranoidandroid/threadsexample/activities/LoaderActivity.java new file mode 100644 index 0000000..1e2e8d3 --- /dev/null +++ b/app/src/main/java/by/paranoidandroid/threadsexample/activities/LoaderActivity.java @@ -0,0 +1,75 @@ +package by.paranoidandroid.threadsexample.activities; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import by.paranoidandroid.threadsexample.R; +import utils.CounterLoader; + +/** + * Activity that demonstrates work with Loader. + */ + +public class LoaderActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks { + final int COUNTER_LOADER_ID = 0, MAX_COUNT = 10; + Button startBtn, cancelBtn; + TextView textView; + View.OnClickListener btnListener; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_loader); + + startBtn = findViewById(R.id.btn_start); + cancelBtn = findViewById(R.id.btn_cancel); + textView = findViewById(R.id.text_view_counter); + + if (savedInstanceState != null) { + if (LoaderManager.getInstance(LoaderActivity.this).getLoader(COUNTER_LOADER_ID) != null) { + // Prepare the loader. Either re-connect with an existing one, or start a new one. + LoaderManager.getInstance(LoaderActivity.this) + .initLoader(COUNTER_LOADER_ID, null, LoaderActivity.this); + } + } + + btnListener = view -> { + switch (view.getId()) { + case R.id.btn_start: + // Prepare the loader. Either re-connect with an existing one, or start a new one. + LoaderManager.getInstance(LoaderActivity.this) + .initLoader(COUNTER_LOADER_ID, null, LoaderActivity.this); + break; + case R.id.btn_cancel: + LoaderManager.getInstance(LoaderActivity.this) + .destroyLoader(COUNTER_LOADER_ID); + break; + } + }; + + startBtn.setOnClickListener(btnListener); + cancelBtn.setOnClickListener(btnListener); + } + + + @Override + public Loader onCreateLoader(int id, Bundle args) { + textView.setText(""); + return new CounterLoader(this); + } + + @Override + public void onLoadFinished(Loader loader, Integer data) { + if (data > MAX_COUNT) { + textView.setText(getString(R.string.done)); + } + } + + @Override + public void onLoaderReset(Loader loader) {} +} diff --git a/app/src/main/java/by/paranoidandroid/threadsexample/activities/MainActivity.java b/app/src/main/java/by/paranoidandroid/threadsexample/activities/MainActivity.java new file mode 100644 index 0000000..30e846c --- /dev/null +++ b/app/src/main/java/by/paranoidandroid/threadsexample/activities/MainActivity.java @@ -0,0 +1,53 @@ +package by.paranoidandroid.threadsexample.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import by.paranoidandroid.threadsexample.R; + +/** + * Main activity with three buttons. + */ + +public class MainActivity extends AppCompatActivity { + Button asynctaskBtn, loaderBtn, threadsBtn; + View.OnClickListener btnListener; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + asynctaskBtn = findViewById(R.id.btn_asynctask); + loaderBtn = findViewById(R.id.btn_loader); + threadsBtn = findViewById(R.id.btn_threads); + + btnListener = new View.OnClickListener() { + Intent intent; + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.btn_asynctask: + intent = new Intent(MainActivity.this, AsyncTaskActivity.class); + break; + case R.id.btn_loader: + intent = new Intent(MainActivity.this, LoaderActivity.class); + break; + case R.id.btn_threads: + intent = new Intent(MainActivity.this, ThreadsActivity.class); + break; + } + startActivity(intent); + } + }; + + asynctaskBtn.setOnClickListener(btnListener); + loaderBtn.setOnClickListener(btnListener); + threadsBtn.setOnClickListener(btnListener); + } + +} diff --git a/app/src/main/java/by/paranoidandroid/threadsexample/activities/ThreadsActivity.java b/app/src/main/java/by/paranoidandroid/threadsexample/activities/ThreadsActivity.java new file mode 100644 index 0000000..0fe7aed --- /dev/null +++ b/app/src/main/java/by/paranoidandroid/threadsexample/activities/ThreadsActivity.java @@ -0,0 +1,149 @@ +package by.paranoidandroid.threadsexample.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import by.paranoidandroid.threadsexample.R; +import utils.IAsyncTaskEvents; +import utils.SimpleAsyncTaskImpl; + +import static utils.SimpleAsyncTask.Status.FINISHED; +import static utils.SimpleAsyncTask.Status.PENDING; +import static utils.SimpleAsyncTask.Status.RUNNING; + +/** + * Activity that demonstrates advanced work with threads. + */ + +public class ThreadsActivity extends Activity implements IAsyncTaskEvents { + private final String STATE_COUNTER = "COUNTER", + STATE_RUNNING = "RUNNING", + STATE_TEXTVIEW = "TEXTVIEW"; + private Button createBtn, startBtn, cancelBtn; + private TextView textView; + private View.OnClickListener btnListener; + private int counter; + private boolean isRunning; + ThreadsActivity.CounterAsyncTask counterAsyncTask; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.actvity_asynctask); + + createBtn = findViewById(R.id.btn_create); + startBtn = findViewById(R.id.btn_start); + cancelBtn = findViewById(R.id.btn_cancel); + textView = findViewById(R.id.text_view_counter); + + if (savedInstanceState != null) { + textView.setText(savedInstanceState.getString(STATE_TEXTVIEW, "")); + isRunning = savedInstanceState.getBoolean(STATE_RUNNING); + if (isRunning) { + counter = savedInstanceState.getInt(STATE_COUNTER); + counterAsyncTask = new ThreadsActivity.CounterAsyncTask(ThreadsActivity.this); + isRunning = true; + counterAsyncTask.execute(counter); + } + } + + btnListener = view -> { + switch (view.getId()) { + case R.id.btn_create: + if (counterAsyncTask == null + || counterAsyncTask.isCancelled() + || counterAsyncTask.getStatus() == FINISHED) { + counterAsyncTask = new CounterAsyncTask(ThreadsActivity.this); + } + break; + case R.id.btn_start: + if (counterAsyncTask != null + && counterAsyncTask.getStatus() == PENDING) { + isRunning = true; + counterAsyncTask.execute(); + } + break; + case R.id.btn_cancel: + if (counterAsyncTask != null) { + isRunning = false; + counterAsyncTask.cancel(); + counterAsyncTask = null; + } + break; + } + }; + + createBtn.setOnClickListener(btnListener); + startBtn.setOnClickListener(btnListener); + cancelBtn.setOnClickListener(btnListener); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + if (counterAsyncTask != null && counterAsyncTask.getStatus() == RUNNING) { + counterAsyncTask.cancel(); + counterAsyncTask = null; + } + outState.putInt(STATE_COUNTER, counter); + outState.putBoolean(STATE_RUNNING, isRunning); + outState.putString(STATE_TEXTVIEW, textView.getText().toString()); + + super.onSaveInstanceState(outState); + } + + @Override + public void onProgressUpdate(Integer counter) { + this.counter = counter; + textView.setText(String.valueOf(counter)); + } + + @Override + public void onPostExecute() { + textView.setText(getString(R.string.done)); + isRunning = true; + } + + static class CounterAsyncTask extends SimpleAsyncTaskImpl { + private final static String LOG_TAG = "Own AsyncTask"; + private final static int MAX_COUNT = 10, TIMEOUT = 500; + private int counter = 1; + private IAsyncTaskEvents listener; + + CounterAsyncTask(IAsyncTaskEvents events) { + listener = events; + } + + @Override + protected void onProgressUpdate(Integer... values) { + listener.onProgressUpdate(values[0]); + } + + @Override + protected Void doInBackground(Integer... values) { + if (values != null && values[0] != null) { + counter = values[0]; + counter++; + } + while (counter <= MAX_COUNT) { + if (!isCancelled()) { + publishProgress(counter++); + try { + Thread.sleep(TIMEOUT); + } catch (Exception ex) { + Log.e(LOG_TAG, "Exception: ", ex); + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void s) { + listener.onPostExecute(); + } + } +} diff --git a/app/src/main/java/utils/CounterLoader.java b/app/src/main/java/utils/CounterLoader.java new file mode 100644 index 0000000..39e75b4 --- /dev/null +++ b/app/src/main/java/utils/CounterLoader.java @@ -0,0 +1,54 @@ +package utils; + +import android.content.Context; +import android.util.Log; + +import androidx.loader.content.AsyncTaskLoader; + +public class CounterLoader extends AsyncTaskLoader { + private final static String LOG_TAG = "CounterLoader"; + private final static int MAX_COUNT = 10, TIMEOUT = 500; + private int counter = 0; + + public CounterLoader(Context context) { + super(context); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + if (counter != 0) { + deliverResult(counter); + } else if (isStarted()) { + forceLoad(); + } + } + + @Override + public Integer loadInBackground() { + counter++; + while (counter <= MAX_COUNT) { + if (!isLoadInBackgroundCanceled()) { + Log.e(LOG_TAG, "Counter: " + counter); + counter++; + try { + Thread.sleep(TIMEOUT); + } catch (Exception ex) { + Log.e(LOG_TAG, "Exception: ", ex); + } + } + } + return counter; + } + + @Override + public void deliverResult(Integer data) { + counter = data; + super.deliverResult(data); + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + } +} diff --git a/app/src/main/java/utils/IAsyncTaskEvents.java b/app/src/main/java/utils/IAsyncTaskEvents.java new file mode 100644 index 0000000..94a7892 --- /dev/null +++ b/app/src/main/java/utils/IAsyncTaskEvents.java @@ -0,0 +1,11 @@ +package utils; + +/** + * Interface that helps to communicate between the task (the worker thread) + * and the Activity (UI thread). + */ + +public interface IAsyncTaskEvents { + void onProgressUpdate(Integer counter); + void onPostExecute(); +} diff --git a/app/src/main/java/utils/SimpleAsyncTask.java b/app/src/main/java/utils/SimpleAsyncTask.java new file mode 100644 index 0000000..837d2eb --- /dev/null +++ b/app/src/main/java/utils/SimpleAsyncTask.java @@ -0,0 +1,43 @@ +package utils; + +/** + * Abstract class that is interface for own implementation of AsyncTask with java threads + * and communication to the UI thread's’ Handler. + */ + +public abstract class SimpleAsyncTask { + public enum Status { + FINISHED, + PENDING, + RUNNING + } + + volatile Status status; + volatile boolean isCancelled = false; + + public final Status getStatus() { + return status; + } + + protected abstract void onPreExecute(); + + protected abstract Result doInBackground(Params[] params); + + protected abstract void onPostExecute(Result result); + + protected abstract void execute(); + + protected final void publishProgress(Progress... values) { + onProgressUpdate(values); + } + + protected abstract void onProgressUpdate(Progress... values); + + protected abstract void cancel(); + + protected abstract void onCancelled(); + + public final boolean isCancelled() { + return isCancelled; + } +} diff --git a/app/src/main/java/utils/SimpleAsyncTaskImpl.java b/app/src/main/java/utils/SimpleAsyncTaskImpl.java new file mode 100644 index 0000000..ca07ea0 --- /dev/null +++ b/app/src/main/java/utils/SimpleAsyncTaskImpl.java @@ -0,0 +1,79 @@ +package utils; + +import android.os.Handler; +import android.os.Looper; + +/** + * Own implementation of AsyncTask with java threads + * and communication to the UI thread's’ Handler. + */ + +public class SimpleAsyncTaskImpl extends SimpleAsyncTask { + private Handler handler; + private Params[] params; + + protected SimpleAsyncTaskImpl() { + status = Status.PENDING; + handler = new Handler(Looper.getMainLooper()); + } + + @Override + protected void onPreExecute() {} + + @Override + protected Result doInBackground(Params[] params) { + return null; + } + + @Override + protected void onPostExecute(Result o) {} + + @Override + public void execute() { + status = Status.RUNNING; + + handler.post(this::onPreExecute); + + new Thread(() -> { + if (isCancelled) { + handler.post(this::onCancelled); + } else { + Result result = doInBackground(params); + handler.post(() -> { + if (isCancelled) { + onCancelled(); + } else { + onPostExecute(result); + } + }); + } + status = Status.FINISHED; + }).start(); + } + + public void execute(Params... values) { + params = values; + execute(); + } + + @Override + protected void onProgressUpdate(Progress... values) { } + + /** + * Invoking this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled()}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. + * IMPORTANT TO NOTE: + * To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.) + * + */ + @Override + public void cancel() { + isCancelled = true; + } + + protected void onCancelled() {} +} diff --git a/app/src/main/res/layout/activity_loader.xml b/app/src/main/res/layout/activity_loader.xml new file mode 100644 index 0000000..d0313cb --- /dev/null +++ b/app/src/main/res/layout/activity_loader.xml @@ -0,0 +1,46 @@ + + + +