├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── drawable
│ │ │ │ └── priority_circle.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── task_layout.xml
│ │ │ │ └── activity_add_task.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── udacity
│ │ │ └── todolist
│ │ │ ├── data
│ │ │ ├── TaskDbHelper.java
│ │ │ ├── TaskContract.java
│ │ │ └── TaskContentProvider.java
│ │ │ ├── AddTaskActivity.java
│ │ │ ├── CustomCursorAdapter.java
│ │ │ └── MainActivity.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── udacity
│ │ │ └── todolist
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── udacity
│ │ └── todolist
│ │ ├── utils
│ │ └── PollingCheck.java
│ │ └── data
│ │ ├── TestUtilities.java
│ │ └── TestTaskContentProvider.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/udacity/ToDoList/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Aug 22 13:04:16 PDT 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/priority_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/udacity/todolist/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.assertEquals;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #3F51B5
5 | #303F9F
6 | #C5CAE9
7 | #303F9F
8 |
9 |
10 | #E74C3C
11 | #E67E22
12 | #F1C40F
13 |
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Archival Note
3 | This repository is deprecated; therefore, we are going to archive it. However, learners will be able to fork it to their personal Github account but cannot submit PRs to this repository. If you have any issues or suggestions to make, feel free to:
4 | - Utilize the https://knowledge.udacity.com/ forum to seek help on content-specific issues.
5 | - Submit a support ticket along with the link to your forked repository if (learners are) blocked for other reasons. Here are the links for the [retail consumers](https://udacity.zendesk.com/hc/en-us/requests/new) and [enterprise learners](https://udacityenterprise.zendesk.com/hc/en-us/requests/new?ticket_form_id=360000279131).
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/cezannec/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | To-Do List
3 | All Tasks
4 | Add a New Task
5 |
6 |
7 | 1
8 | 2
9 | 3
10 |
11 |
12 |
13 | Describe your task…
14 | Priority
15 |
16 | High
17 | Medium
18 | Low
19 |
20 | Add
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
11 |
12 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.3"
6 | defaultConfig {
7 | applicationId "com.example.udacity.todolist"
8 | minSdkVersion 15
9 | targetSdkVersion 24
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(dir: 'libs', include: ['*.jar'])
24 | compile 'com.android.support:appcompat-v7:24.2.1'
25 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha8'
26 |
27 | //add Recycler view dependencies; must match SDK version (24)
28 | compile 'com.android.support:recyclerview-v7:24.2.1'
29 |
30 | //FAB dependencies
31 | compile 'com.android.support:design:24.2.1'
32 |
33 | //Testing
34 | // Instrumentation dependencies use androidTestCompile
35 | // (as opposed to testCompile for local unit tests run in the JVM)
36 | androidTestCompile 'junit:junit:4.12'
37 | androidTestCompile 'com.android.support:support-annotations:24.2.1'
38 | androidTestCompile 'com.android.support.test:runner:0.5'
39 | androidTestCompile 'com.android.support.test:rules:0.5'
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/udacity/todolist/utils/PollingCheck.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.utils;
2 |
3 | import junit.framework.Assert;
4 |
5 | import java.util.concurrent.Callable;
6 |
7 |
8 | public abstract class PollingCheck {
9 |
10 | private static final long TIME_SLICE = 50;
11 | private long mTimeout = 3000;
12 |
13 | public PollingCheck() {
14 | }
15 |
16 | public PollingCheck(long timeout) {
17 | mTimeout = timeout;
18 | }
19 |
20 | public static void check(CharSequence message, long timeout, Callable condition)
21 | throws Exception {
22 | while (timeout > 0) {
23 | if (condition.call()) {
24 | return;
25 | }
26 |
27 | Thread.sleep(TIME_SLICE);
28 | timeout -= TIME_SLICE;
29 | }
30 |
31 | Assert.fail(message.toString());
32 | }
33 |
34 | protected abstract boolean check();
35 |
36 | public void run() {
37 | if (check()) {
38 | return;
39 | }
40 |
41 | long timeout = mTimeout;
42 | while (timeout > 0) {
43 | try {
44 | Thread.sleep(TIME_SLICE);
45 | } catch (InterruptedException e) {
46 | Assert.fail("Notification error, unexpected InterruptedException");
47 | }
48 |
49 | if (check()) {
50 | return;
51 | }
52 |
53 | timeout -= TIME_SLICE;
54 | }
55 |
56 | Assert.fail("Notification not set, unexpected timeout");
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/data/TaskDbHelper.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.data;
2 |
3 | import android.content.Context;
4 | import android.database.sqlite.SQLiteDatabase;
5 | import android.database.sqlite.SQLiteOpenHelper;
6 |
7 | import com.example.udacity.todolist.data.TaskContract.TaskEntry;
8 |
9 |
10 | public class TaskDbHelper extends SQLiteOpenHelper {
11 |
12 | // The name of the database
13 | private static final String DATABASE_NAME = "TasksDb.db";
14 |
15 | // If you change the database schema, you must increment the database version
16 | private static final int VERSION = 10;
17 |
18 |
19 | // Constructor
20 | TaskDbHelper(Context context) {
21 | super(context, DATABASE_NAME, null, VERSION);
22 | }
23 |
24 |
25 | /**
26 | * Called when the tasks database is created for the first time.
27 | */
28 | @Override
29 | public void onCreate(SQLiteDatabase db) {
30 |
31 | // Create tasks table (careful to follow SQL formatting rules)
32 | final String CREATE_TABLE = "CREATE TABLE " + TaskEntry.TABLE_NAME + " (" +
33 | TaskEntry._ID + " INTEGER PRIMARY KEY, " +
34 | TaskEntry.COLUMN_DESCRIPTION + " TEXT NOT NULL, " +
35 | TaskEntry.COLUMN_PRIORITY + " INTEGER NOT NULL);";
36 |
37 | db.execSQL(CREATE_TABLE);
38 | }
39 |
40 |
41 | /**
42 | * This method discards the old table of data and calls onCreate to recreate a new one.
43 | * This only occurs when the version number for this database (DATABASE_VERSION) is incremented.
44 | */
45 | @Override
46 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
47 | db.execSQL("DROP TABLE IF EXISTS " + TaskEntry.TABLE_NAME);
48 | onCreate(db);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/task_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 |
12 |
16 |
23 |
24 |
25 |
31 |
32 |
33 |
37 |
38 |
39 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/AddTaskActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist;
2 |
3 | import android.content.ContentValues;
4 | import android.net.Uri;
5 | import android.os.Bundle;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.view.View;
8 | import android.widget.EditText;
9 | import android.widget.RadioButton;
10 | import android.widget.Toast;
11 |
12 | import com.example.udacity.todolist.data.TaskContract;
13 |
14 |
15 | public class AddTaskActivity extends AppCompatActivity {
16 |
17 | // Declare a member variable to keep track of a task's selected mPriority
18 | private int mPriority;
19 |
20 |
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.activity_add_task);
24 |
25 | // Initialize to highest mPriority by default (mPriority = 1)
26 | ((RadioButton) findViewById(R.id.radButton1)).setChecked(true);
27 | mPriority = 1;
28 | }
29 |
30 |
31 | /**
32 | * onClickAddTask is called when the "ADD" button is clicked.
33 | * It retrieves user input and inserts that new task data into the underlying database.
34 | */
35 | public void onClickAddTask(View view) {
36 | // Check if the EditText input is empty - (don't create an entry if there is no input)
37 | String input = ((EditText) findViewById(R.id.editTextTaskDescription)).getText().toString();
38 | if (input.length() == 0) {
39 | return;
40 | }
41 |
42 | // TODO: 5.6. Retrieve user input and store it in a ContentValues object
43 | // [Hint] Don't forget to call finish() to return to MainActivity after this insert is complete
44 |
45 | // Create new empty ContentValues object
46 | ContentValues contentValues = new ContentValues();
47 |
48 | // Put the task description and selected mPriority into the ContentValues
49 | contentValues.put(TaskContract.TaskEntry.COLUMN_DESCRIPTION, input);
50 | contentValues.put(TaskContract.TaskEntry.COLUMN_PRIORITY, mPriority);
51 |
52 | // TODO: 5.7. Insert new task data via a ContentResolver
53 | Uri uri = getContentResolver().insert(TaskContract.TaskEntry.CONTENT_URI, contentValues);
54 |
55 | // TODO: 5.8. Display the URI that's returned with a Toast
56 | if(uri != null) {
57 | Toast.makeText(getBaseContext(), uri.toString(), Toast.LENGTH_LONG).show();
58 | }
59 |
60 | // Finish activity (this returns back to MainActivity)
61 | finish();
62 | }
63 |
64 |
65 | /**
66 | * onPrioritySelected is called whenever a priority button is clicked.
67 | * It changes the value of mPriority based on the selected button.
68 | */
69 | public void onPrioritySelected(View view) {
70 | if (((RadioButton) findViewById(R.id.radButton1)).isChecked()) {
71 | mPriority = 1;
72 | } else if (((RadioButton) findViewById(R.id.radButton2)).isChecked()) {
73 | mPriority = 2;
74 | } else if (((RadioButton) findViewById(R.id.radButton3)).isChecked()) {
75 | mPriority = 3;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/udacity/todolist/data/TestUtilities.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.data;
2 |
3 | import android.database.ContentObserver;
4 | import android.net.Uri;
5 | import android.os.Handler;
6 | import android.os.HandlerThread;
7 |
8 | import com.example.udacity.todolist.utils.PollingCheck;
9 |
10 |
11 | class TestUtilities {
12 |
13 | static TestContentObserver getTestContentObserver() {
14 | return TestContentObserver.getTestContentObserver();
15 | }
16 |
17 | /**
18 | * Students: The test functions for insert and delete use TestContentObserver to test
19 | * the ContentObserver callbacks using the PollingCheck class from the Android Compatibility
20 | * Test Suite tests.
21 | * NOTE: This only tests that the onChange function is called; it DOES NOT test that the
22 | * correct Uri is returned.
23 | */
24 | static class TestContentObserver extends ContentObserver {
25 | final HandlerThread mHT;
26 | boolean mContentChanged;
27 |
28 | private TestContentObserver(HandlerThread ht) {
29 | super(new Handler(ht.getLooper()));
30 | mHT = ht;
31 | }
32 |
33 | static TestContentObserver getTestContentObserver() {
34 | HandlerThread ht = new HandlerThread("ContentObserverThread");
35 | ht.start();
36 | return new TestContentObserver(ht);
37 | }
38 |
39 | /**
40 | * Called when a content change occurs.
41 | *
42 | * To ensure correct operation on older versions of the framework that did not provide a
43 | * Uri argument, applications should also implement this method whenever they implement
44 | * the { #onChange(boolean, Uri)} overload.
45 | *
46 | * @param selfChange True if this is a self-change notification.
47 | */
48 | @Override
49 | public void onChange(boolean selfChange) {
50 | onChange(selfChange, null);
51 | }
52 |
53 | /**
54 | * Called when a content change occurs. Includes the changed content Uri when available.
55 | *
56 | * @param selfChange True if this is a self-change notification.
57 | * @param uri The Uri of the changed content, or null if unknown.
58 | */
59 | @Override
60 | public void onChange(boolean selfChange, Uri uri) {
61 | mContentChanged = true;
62 | }
63 |
64 | /**
65 | * Note: The PollingCheck class is taken from the Android CTS (Compatibility Test Suite).
66 | * It's useful to look at the Android CTS source for ideas on how to test your Android
67 | * applications. The reason that PollingCheck works is that, by default, the JUnit testing
68 | * framework is not running on the main Android application thread.
69 | */
70 | void waitForNotificationOrFail() {
71 |
72 | new PollingCheck(5000) {
73 | @Override
74 | protected boolean check() {
75 | return mContentChanged;
76 | }
77 | }.run();
78 | mHT.quit();
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/data/TaskContract.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.data;
2 |
3 | import android.content.ContentResolver;
4 | import android.net.Uri;
5 | import android.provider.BaseColumns;
6 |
7 |
8 | public class TaskContract {
9 |
10 | /* TODO: 3.1. Add content provider constants to the Contract
11 | Clients need to know how to access the task data, and it's your job to provide
12 | these content URI's for the path to that data:
13 | 1) Content authority,
14 | 2) Base content URI,
15 | 3) Path(s) to the tasks directory
16 | 4) Content URI for data in the TaskEntry class
17 | */
18 |
19 | // The authority, which is how your code knows which Content Provider to access
20 | public static final String AUTHORITY = "com.example.udacity.todolist";
21 |
22 | // The base content URI = "content://" +
23 | public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);
24 |
25 | // Define the possible paths for accessing data in this contract
26 | // This is the path for the "tasks" directory
27 | public static final String PATH_TASKS = "tasks";
28 |
29 |
30 | /* TaskEntry is an inner class that defines the contents of the task table */
31 | public static final class TaskEntry implements BaseColumns {
32 |
33 | // TaskEntry content URI = base content URI + path
34 | public static final Uri CONTENT_URI =
35 | BASE_CONTENT_URI.buildUpon().appendPath(PATH_TASKS).build();
36 |
37 | // Task table and column names
38 | public static final String TABLE_NAME = "tasks";
39 |
40 | // Since TaskEntry implements the interface "BaseColumns", it has an automatically produced
41 | // "_ID" column in addition to the two below
42 | public static final String COLUMN_DESCRIPTION = "description";
43 | public static final String COLUMN_PRIORITY = "priority";
44 |
45 |
46 | /*
47 | The above table structure looks something like the sample table below.
48 | With the name of the table and columns on top, and potential contents in rows
49 |
50 | Note: Because this implements BaseColumns, the _id column is generated automatically
51 |
52 | tasks
53 | - - - - - - - - - - - - - - - - - - - - - -
54 | | _id | description | priority |
55 | - - - - - - - - - - - - - - - - - - - - - -
56 | | 1 | Complete lesson | 1 |
57 | - - - - - - - - - - - - - - - - - - - - - -
58 | | 2 | Go shopping | 3 |
59 | - - - - - - - - - - - - - - - - - - - - - -
60 | .
61 | .
62 | .
63 | - - - - - - - - - - - - - - - - - - - - - -
64 | | 43 | Learn guitar | 2 |
65 | - - - - - - - - - - - - - - - - - - - - - -
66 |
67 | */
68 |
69 |
70 | /*
71 | (Included) MIME types used in TaskContentProvider's getType() method .
72 | There are generally two MIME types of data we will be working with:
73 | 1) a directory of items, which can be a row or set of rows
74 | OR 2) a single item, which is a single row of data
75 | */
76 |
77 | static final String CONTENT_TYPE =
78 | ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + AUTHORITY + "/" + PATH_TASKS;
79 |
80 | static final String CONTENT_ITEM_TYPE =
81 | ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + AUTHORITY + "/" + PATH_TASKS;
82 |
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
166 | cd "$(dirname "$0")"
167 | fi
168 |
169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
170 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/CustomCursorAdapter.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist;
2 |
3 | import android.content.Context;
4 | import android.database.Cursor;
5 | import android.graphics.drawable.GradientDrawable;
6 | import android.support.v4.content.ContextCompat;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.TextView;
12 |
13 | import com.example.udacity.todolist.data.TaskContract;
14 |
15 |
16 | /**
17 | * This CustomCursorAdapter creates and binds ViewHolders, that hold the description and priority of a task,
18 | * to a RecyclerView to efficiently display data.
19 | */
20 | public class CustomCursorAdapter extends RecyclerView.Adapter {
21 |
22 | // Class variables for the Cursor that holds task data and the Context
23 | private Cursor mCursor;
24 | private Context mContext;
25 |
26 |
27 | /**
28 | * Constructor for the CustomCursorAdapter that initializes the Context.
29 | *
30 | * @param mContext the current Context
31 | */
32 | public CustomCursorAdapter(Context mContext) {
33 | this.mContext = mContext;
34 | }
35 |
36 |
37 | /**
38 | * Called when ViewHolders are created to fill a RecyclerView.
39 | *
40 | * @return A new TaskViewHolder that holds the view for each task
41 | */
42 | @Override
43 | public TaskViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
44 |
45 | // Inflate the task_layout to a view
46 | View view = LayoutInflater.from(mContext)
47 | .inflate(R.layout.task_layout, parent, false);
48 |
49 | return new TaskViewHolder(view);
50 | }
51 |
52 |
53 | /**
54 | * Called by the RecyclerView to display data at a specified position in the Cursor.
55 | *
56 | * @param holder The ViewHolder to bind Cursor data to
57 | * @param position The position of the data in the Cursor
58 | */
59 | @Override
60 | public void onBindViewHolder(TaskViewHolder holder, int position) {
61 |
62 | // Indices for the _id, description, and priority columns
63 | int idIndex = mCursor.getColumnIndex(TaskContract.TaskEntry._ID);
64 | int descriptionIndex = mCursor.getColumnIndex(TaskContract.TaskEntry.COLUMN_DESCRIPTION);
65 | int priorityIndex = mCursor.getColumnIndex(TaskContract.TaskEntry.COLUMN_PRIORITY);
66 |
67 | mCursor.moveToPosition(position); // get to the right location in the cursor
68 |
69 | // Determine the values of the wanted data
70 | final int id = mCursor.getInt(idIndex);
71 | String description = mCursor.getString(descriptionIndex);
72 | int priority = mCursor.getInt(priorityIndex);
73 |
74 | //Set values
75 | holder.itemView.setTag(id);
76 | holder.taskDescriptionView.setText(description);
77 |
78 | // Programmatically set the text and color for the priority TextView
79 | String priorityString = "" + priority; // converts int to String
80 | holder.priorityView.setText(priorityString);
81 |
82 | GradientDrawable priorityCircle = (GradientDrawable) holder.priorityView.getBackground();
83 | // Get the appropriate background color based on the priority
84 | int priorityColor = getPriorityColor(priority);
85 | priorityCircle.setColor(priorityColor);
86 |
87 | }
88 |
89 |
90 | /*
91 | Helper method for selecting the correct priority circle color.
92 | P1 = red, P2 = orange, P3 = yellow
93 | */
94 | private int getPriorityColor(int priority) {
95 | int priorityColor = 0;
96 |
97 | switch(priority) {
98 | case 1: priorityColor = ContextCompat.getColor(mContext, R.color.materialRed);
99 | break;
100 | case 2: priorityColor = ContextCompat.getColor(mContext, R.color.materialOrange);
101 | break;
102 | case 3: priorityColor = ContextCompat.getColor(mContext, R.color.materialYellow);
103 | break;
104 | default: break;
105 | }
106 | return priorityColor;
107 | }
108 |
109 |
110 | /**
111 | * Returns the number of items to display.
112 | */
113 | @Override
114 | public int getItemCount() {
115 | if (mCursor == null) {
116 | return 0;
117 | }
118 | return mCursor.getCount();
119 | }
120 |
121 |
122 | /**
123 | * When data changes and a re-query occurs, this function swaps the old Cursor
124 | * with a newly updated Cursor (Cursor c) that is passed in.
125 | */
126 | public Cursor swapCursor(Cursor c) {
127 | // check if this cursor is the same as the previous cursor (mCursor)
128 | if (mCursor == c) {
129 | return null; // bc nothing has changed
130 | }
131 | Cursor temp = mCursor;
132 | this.mCursor = c; // new cursor value assigned
133 |
134 | //check if this is a valid cursor, then update the cursor
135 | if (c != null) {
136 | this.notifyDataSetChanged();
137 | }
138 | return temp;
139 | }
140 |
141 |
142 | // Inner class for creating ViewHolders
143 | class TaskViewHolder extends RecyclerView.ViewHolder {
144 |
145 | // Class variables for the task description and priority TextViews
146 | TextView taskDescriptionView;
147 | TextView priorityView;
148 |
149 | /**
150 | * Constructor for the TaskViewHolders.
151 | *
152 | * @param itemView The view inflated in onCreateViewHolder
153 | */
154 | public TaskViewHolder(View itemView) {
155 | super(itemView);
156 |
157 | taskDescriptionView = (TextView) itemView.findViewById(R.id.taskDescription);
158 | priorityView = (TextView) itemView.findViewById(R.id.priorityTextView);
159 | }
160 | }
161 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_add_task.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
16 |
17 |
18 |
25 |
26 |
27 |
36 |
37 |
41 |
42 |
44 |
50 |
51 |
61 |
62 |
72 |
73 |
83 |
84 |
85 |
86 |
87 |
93 |
94 |
101 |
102 |
109 |
110 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist;
2 |
3 | import android.content.Intent;
4 | import android.database.Cursor;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.support.design.widget.FloatingActionButton;
8 | import android.support.v4.app.LoaderManager;
9 | import android.support.v4.content.AsyncTaskLoader;
10 | import android.support.v4.content.Loader;
11 | import android.support.v7.app.AppCompatActivity;
12 | import android.support.v7.widget.LinearLayoutManager;
13 | import android.support.v7.widget.RecyclerView;
14 | import android.support.v7.widget.helper.ItemTouchHelper;
15 | import android.util.Log;
16 | import android.view.View;
17 |
18 | import com.example.udacity.todolist.data.TaskContract;
19 |
20 |
21 | public class MainActivity extends AppCompatActivity implements
22 | LoaderManager.LoaderCallbacks {
23 |
24 |
25 | // Constants for logging and referring to a unique loader
26 | private static final String TAG = MainActivity.class.getSimpleName();
27 | private static final int TASK_LOADER_ID = 0;
28 |
29 | // Member variables for the adapter and RecyclerView
30 | private CustomCursorAdapter mAdapter;
31 | RecyclerView mRecyclerView;
32 |
33 |
34 | @Override
35 | protected void onCreate(Bundle savedInstanceState) {
36 | super.onCreate(savedInstanceState);
37 | setContentView(R.layout.activity_main);
38 |
39 | // Set the RecyclerView to its corresponding view
40 | mRecyclerView = (RecyclerView) findViewById(R.id.recyclerViewTasks);
41 |
42 | // Set the layout for the RecyclerView to be a linear layout, which measures and
43 | // positions items within a RecyclerView into a linear list
44 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
45 |
46 | // Initialize the adapter and attach it to the RecyclerView
47 | mAdapter = new CustomCursorAdapter(this);
48 | mRecyclerView.setAdapter(mAdapter);
49 |
50 | /*
51 | Add a touch helper to the RecyclerView to recognize when a user swipes to delete an item.
52 | An ItemTouchHelper enables touch behavior (like swipe and move) on each ViewHolder,
53 | and uses callbacks to signal when a user is performing these actions.
54 | */
55 | new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
56 | @Override
57 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
58 | return false;
59 | }
60 |
61 | // Called when a user swipes left or right on a ViewHolder
62 | @Override
63 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
64 | // TODO: 9.1. Implement swipe delete for a single item
65 |
66 | // Retrieve the id of the task to delete
67 | int id = (int) viewHolder.itemView.getTag();
68 |
69 | // Build appropriate uri with String row id appended
70 | String stringId = Integer.toString(id);
71 | Uri uri = TaskContract.TaskEntry.CONTENT_URI;
72 | uri = uri.buildUpon().appendPath(stringId).build();
73 |
74 | // Delete via a ContentResolver
75 | getContentResolver().delete(uri, null, null);
76 |
77 | // TODO: 9.2. Re-query for all tasks after a deletion
78 | getSupportLoaderManager().restartLoader(TASK_LOADER_ID, null, MainActivity.this);
79 | }
80 | }).attachToRecyclerView(mRecyclerView);
81 |
82 | /*
83 | Set the Floating Action Button (FAB) to its corresponding View.
84 | Attach an OnClickListener to it, so that when it's clicked, a new intent will be created
85 | to launch the AddTaskActivity.
86 | */
87 | FloatingActionButton fabButton = (FloatingActionButton) findViewById(R.id.fab);
88 |
89 | fabButton.setOnClickListener(new View.OnClickListener() {
90 | @Override
91 | public void onClick(View view) {
92 | // Create a new intent to start an AddTaskActivity
93 | Intent addTaskIntent = new Intent(MainActivity.this, AddTaskActivity.class);
94 | startActivity(addTaskIntent);
95 | }
96 | });
97 |
98 | /*
99 | Ensure a loader is initialized and active. If the loader doesn't already exist, one is
100 | created, otherwise the last created loader is re-used.
101 | */
102 | getSupportLoaderManager().initLoader(TASK_LOADER_ID, null, this);
103 | }
104 |
105 |
106 | /**
107 | * This method is called after this activity has been paused or restarted.
108 | * Often, this is after new data has been inserted through an AddTaskActivity,
109 | * so this restarts the loader to re-query the underlying data for any changes.
110 | */
111 | @Override
112 | protected void onResume() {
113 | super.onResume();
114 |
115 | // re-queries for all tasks
116 | getSupportLoaderManager().restartLoader(TASK_LOADER_ID, null, this);
117 | }
118 |
119 |
120 | /**
121 | * Instantiates and returns a new AsyncTaskLoader with the given ID.
122 | * This loader will return task data as a Cursor or null if an error occurs.
123 | *
124 | * Implements the required callbacks to take care of loading data at all stages of loading.
125 | */
126 | @Override
127 | public Loader onCreateLoader(int id, final Bundle loaderArgs) {
128 |
129 | return new AsyncTaskLoader(this) {
130 |
131 | // Initialize a Cursor, this will hold all the task data
132 | Cursor mTaskData = null;
133 |
134 | // onStartLoading() is called when a loader first starts loading data
135 | @Override
136 | protected void onStartLoading() {
137 | if (mTaskData != null) {
138 | // Delivers any previously loaded data immediately
139 | deliverResult(mTaskData);
140 | } else {
141 | // Force a new load
142 | forceLoad();
143 | }
144 | }
145 |
146 | // loadInBackground() performs asynchronous loading of data
147 | @Override
148 | public Cursor loadInBackground() {
149 | //TODO: 6.5. Query and load all task data in the background; sort by priority
150 | //Hint: use a try/catch block to catch any errors in loading data
151 |
152 | try {
153 | return getContentResolver().query(TaskContract.TaskEntry.CONTENT_URI,
154 | null,
155 | null,
156 | null,
157 | TaskContract.TaskEntry.COLUMN_PRIORITY);
158 |
159 | } catch (Exception e) {
160 | Log.e(TAG, "Failed to asynchronously load data.");
161 | e.printStackTrace();
162 | return null;
163 | }
164 | }
165 |
166 | // deliverResult sends the result of the load, a Cursor, to the registered listener
167 | public void deliverResult(Cursor data) {
168 | mTaskData = data;
169 | super.deliverResult(data);
170 | }
171 | };
172 |
173 | }
174 |
175 |
176 | /**
177 | * Called when a previously created loader has finished its load.
178 | *
179 | * @param loader The Loader that has finished.
180 | * @param data The data generated by the Loader.
181 | */
182 | @Override
183 | public void onLoadFinished(Loader loader, Cursor data) {
184 | // Update the data that the adapter uses to create ViewHolders
185 | mAdapter.swapCursor(data);
186 | }
187 |
188 |
189 | /**
190 | * Called when a previously created loader is being reset, and thus
191 | * making its data unavailable.
192 | * onLoaderReset removes any references this activity had to the loader's data.
193 | *
194 | * @param loader The Loader that is being reset.
195 | */
196 | @Override
197 | public void onLoaderReset(Loader loader) {
198 | mAdapter.swapCursor(null);
199 | }
200 |
201 | }
202 |
203 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/udacity/todolist/data/TaskContentProvider.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.data;
2 |
3 | import android.content.ContentProvider;
4 | import android.content.ContentUris;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.UriMatcher;
8 | import android.database.Cursor;
9 | import android.database.sqlite.SQLiteDatabase;
10 | import android.net.Uri;
11 | import android.support.annotation.NonNull;
12 |
13 | import static com.example.udacity.todolist.data.TaskContract.TaskEntry.TABLE_NAME;
14 |
15 |
16 | // TODO: 1.1 Verify that TaskContentProvider extends from ContentProvider and implements required methods
17 | public class TaskContentProvider extends ContentProvider {
18 |
19 | // Member variable for a TaskDbHelper that's initialized in the onCreate() method
20 | private TaskDbHelper mTaskDbHelper;
21 |
22 | // TODO: 4.1. Define final integer constants for the directory of tasks and a single item.
23 | // It's convention to use 100, 200, 300, etc for directories,
24 | // and related ints (101, 102, ..) for items in that directory.
25 | public static final int TASKS = 100;
26 | public static final int TASK_WITH_ID = 101;
27 |
28 | // TODO: 4.2. Declare a static variable for the Uri matcher that you construct
29 | private static final UriMatcher sUriMatcher = buildUriMatcher();
30 |
31 |
32 | // TODO: 4.3. Define a static buildUriMatcher method that associates URI's with their int match
33 | /**
34 | Initialize a new matcher object without any matches,
35 | then use .addURI(String authority, String path, int match) to add matches
36 | */
37 | public static UriMatcher buildUriMatcher() {
38 |
39 | // Initialize a UriMatcher with no matches by passing in NO_MATCH to the constructor
40 | UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
41 |
42 | /*
43 | All paths added to the UriMatcher have a corresponding int.
44 | For each kind of uri you may want to access, add the corresponding match with addURI.
45 | The two calls below add matches for the task directory and a single item by ID.
46 | */
47 | uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.PATH_TASKS, TASKS);
48 | uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.PATH_TASKS + "/#", TASK_WITH_ID);
49 |
50 | return uriMatcher;
51 | }
52 |
53 |
54 | /* onCreate() is where you should initialize anything you’ll need to setup
55 | your underlying data source.
56 | In this case, you’re working with a SQLite database, so you’ll need to
57 | initialize a DbHelper to gain access to it.
58 | */
59 | @Override
60 | public boolean onCreate() {
61 | // TODO: 1.2. Complete onCreate() and initialize a TaskDbhelper on startup
62 | // [Hint] Declare the DbHelper as a global variable
63 |
64 | Context context = getContext();
65 | mTaskDbHelper = new TaskDbHelper(context);
66 | return true;
67 | //return false;
68 | }
69 |
70 |
71 |
72 | // TODO: 5. Implement insert to handle requests to insert a single new row of data
73 | @Override
74 | public Uri insert(@NonNull Uri uri, ContentValues values) {
75 |
76 | // TODO: 5.1. Get access to the task database (to write new data to)
77 | final SQLiteDatabase db = mTaskDbHelper.getWritableDatabase();
78 |
79 | Uri returnUri; // to be returned
80 |
81 | // TODO: 5.2. Write URI matching code to identify the match for the tasks directory
82 | int match = sUriMatcher.match(uri);
83 |
84 | // TODO: 5.3. Insert new values into the database
85 | // TODO: 5.4. Set the value for the returnedUri and write the default case for unknown URI's
86 | switch (match) {
87 | case TASKS:
88 | //4.3. Insert into tasks table
89 | long id = db.insert(TABLE_NAME, null, values);
90 | if ( id > 0 ) {
91 | returnUri = ContentUris.withAppendedId(TaskContract.TaskEntry.CONTENT_URI, id);
92 | } else {
93 | throw new android.database.SQLException("Failed to insert row into " + uri);
94 | }
95 | break;
96 | // Default case throws an UnsupportedOperationException
97 | default:
98 | throw new UnsupportedOperationException("Unknown uri: " + uri);
99 | }
100 |
101 | // TODO: 5.5. Notify the resolver if the uri has been changed
102 | getContext().getContentResolver().notifyChange(uri, null);
103 |
104 | // Return constructed uri (this points to the newly inserted row of data)
105 | return returnUri;
106 |
107 | }
108 |
109 |
110 | // TODO: 6. Implement query to handle requests for data by URI
111 | @Override
112 | public Cursor query(@NonNull Uri uri, String[] projection, String selection,
113 | String[] selectionArgs, String sortOrder) {
114 |
115 | // TODO: 6.1. Get access to underlying database (read-only for query)
116 | final SQLiteDatabase db = mTaskDbHelper.getReadableDatabase();
117 |
118 | Cursor retCursor;
119 |
120 | // TODO: 6.2. URI match code
121 | // TODO: 6.3. Write a query for the tasks directory and default case
122 | int match = sUriMatcher.match(uri);
123 |
124 | switch (match) {
125 | // Query for the tasks directory
126 | case TASKS:
127 | retCursor = db.query(TABLE_NAME,
128 | projection,
129 | selection,
130 | selectionArgs,
131 | null,
132 | null,
133 | sortOrder);
134 | break;
135 |
136 | // TODO: 7. Query for a single row of data by ID
137 | case TASK_WITH_ID:
138 | // TODO: 7.1. Get the id from the URI
139 | String id = uri.getPathSegments().get(1);
140 |
141 | // TODO: 7.2. Use selections and selectionArgs to filter for that ID
142 | String mSelection = "_id=?";
143 | String[] mSelectionArgs = new String[]{id};
144 |
145 | // Use these variables to construct a query
146 | retCursor = db.query(TABLE_NAME,
147 | projection,
148 | mSelection,
149 | mSelectionArgs,
150 | null,
151 | null,
152 | sortOrder);
153 | break;
154 |
155 | // Default exception
156 | default:
157 | throw new UnsupportedOperationException("Unknown uri: " + uri);
158 | }
159 |
160 | // TODO: 6.4. Set a notification URI on the Cursor
161 | retCursor.setNotificationUri(getContext().getContentResolver(), uri);
162 |
163 | // Return the desired Cursor
164 | return retCursor;
165 | }
166 |
167 |
168 | //TODO: 8. Implement delete to delete a single row of data.
169 |
170 | @Override
171 | public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
172 |
173 | // TODO: 8.1. Get access to the database and write URI matching code to recognize a single item
174 | // Get access to our database
175 | final SQLiteDatabase db = mTaskDbHelper.getWritableDatabase();
176 | // Keep track of the number of deleted tasks
177 | int tasksDeleted; // init as 0
178 |
179 | // Match code
180 | int match = sUriMatcher.match(uri);
181 |
182 | // TODO: 8.2. Write the code to delete a single row of data
183 | switch (match) {
184 | // Handle the single item case, recognized by the ID included in the URI path
185 | case TASK_WITH_ID:
186 | // Get the task ID from the URI path
187 | String id = uri.getPathSegments().get(1);
188 | // Use selections/selectionArgs to filter for this ID
189 | tasksDeleted = db.delete(TABLE_NAME, "_id=?", new String[]{id});
190 | break;
191 | default:
192 | throw new UnsupportedOperationException("Unknown uri: " + uri);
193 | }
194 |
195 | // TODO: 8.3. Notify the resolver of a change
196 | if (tasksDeleted != 0) {
197 | // A task was deleted, set notification
198 | getContext().getContentResolver().notifyChange(uri, null);
199 | }
200 |
201 | // Return the number of tasks deleted
202 | return tasksDeleted;
203 | }
204 |
205 |
206 | //TODO: 10. [Optional] Implement update to handle requests for updating a single row
207 | // This function won't be used in our final app but is included for completeness
208 | @Override
209 | public int update(@NonNull Uri uri, ContentValues values, String selection,
210 | String[] selectionArgs) {
211 |
212 | //Keep track of if an update occurs
213 | int tasksUpdated;
214 |
215 | // match code
216 | int match = sUriMatcher.match(uri);
217 |
218 | switch (match) {
219 | case TASK_WITH_ID:
220 | //update a single task by getting the id
221 | String id = uri.getPathSegments().get(1);
222 | //using selections
223 | tasksUpdated = mTaskDbHelper.getWritableDatabase().update(TABLE_NAME, values, "_id=?", new String[]{id});
224 | break;
225 | default:
226 | throw new UnsupportedOperationException("Unknown uri: " + uri);
227 | }
228 |
229 | if (tasksUpdated != 0) {
230 | //set notifications if a task was updated
231 | getContext().getContentResolver().notifyChange(uri, null);
232 | }
233 |
234 | // return number of tasks updated
235 | return tasksUpdated;
236 | }
237 |
238 |
239 | //TODO: [Optional] 11. Implement getType
240 | /* getType() handles requests for the MIME type of data
241 | We are working with two types of data:
242 | 1) a directory and 2) a single row of data.
243 | This method will not be used in our app, but gives a way to standardize the data formats
244 | that your provider accesses, and this can be useful for data organization.
245 | For now, this method will not be used but will be provided for completeness.
246 | */
247 | @Override
248 | public String getType(@NonNull Uri uri) {
249 |
250 | int match = sUriMatcher.match(uri);
251 |
252 | switch (match) {
253 | case TASKS:
254 | // directory
255 | return TaskContract.TaskEntry.CONTENT_TYPE;
256 | case TASK_WITH_ID:
257 | // single item type
258 | return TaskContract.TaskEntry.CONTENT_ITEM_TYPE;
259 | default:
260 | throw new UnsupportedOperationException("Unknown uri: " + uri);
261 | }
262 | }
263 |
264 | }
265 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/udacity/todolist/data/TestTaskContentProvider.java:
--------------------------------------------------------------------------------
1 | package com.example.udacity.todolist.data;
2 |
3 | import android.content.ComponentName;
4 | import android.content.ContentResolver;
5 | import android.content.ContentUris;
6 | import android.content.ContentValues;
7 | import android.content.Context;
8 | import android.content.UriMatcher;
9 | import android.content.pm.PackageManager;
10 | import android.content.pm.ProviderInfo;
11 | import android.database.Cursor;
12 | import android.database.sqlite.SQLiteDatabase;
13 | import android.net.Uri;
14 | import android.support.test.InstrumentationRegistry;
15 |
16 | import org.junit.Before;
17 | import org.junit.Test;
18 |
19 | import static junit.framework.Assert.assertEquals;
20 | import static junit.framework.Assert.assertTrue;
21 | import static junit.framework.Assert.fail;
22 |
23 |
24 | public class TestTaskContentProvider {
25 |
26 | /* Context used to access various parts of the system */
27 | private final Context mContext = InstrumentationRegistry.getTargetContext();
28 |
29 | /**
30 | * Because we annotate this method with the @Before annotation, this method will be called
31 | * before every single method with an @Test annotation. We want to start each test clean, so we
32 | * delete all entries in the tasks directory to do so.
33 | */
34 | @Before
35 | public void setUp() {
36 | /* Use TaskDbHelper to get access to a writable database */
37 | TaskDbHelper dbHelper = new TaskDbHelper(mContext);
38 | SQLiteDatabase database = dbHelper.getWritableDatabase();
39 | database.delete(TaskContract.TaskEntry.TABLE_NAME, null, null);
40 | }
41 |
42 |
43 | //================================================================================
44 | // Test ContentProvider Registration
45 | //================================================================================
46 |
47 |
48 | /**
49 | * This test checks to make sure that the content provider is registered correctly in the
50 | * AndroidManifest file. If it fails, you should check the AndroidManifest to see if you've
51 | * added a tag and that you've properly specified the android:authorities attribute.
52 | */
53 | @Test
54 | public void testProviderRegistry() {
55 |
56 | /*
57 | * A ComponentName is an identifier for a specific application component, such as an
58 | * Activity, ContentProvider, BroadcastReceiver, or a Service.
59 | *
60 | * Two pieces of information are required to identify a component: the package (a String)
61 | * it exists in, and the class (a String) name inside of that package.
62 | *
63 | * We will use the ComponentName for our ContentProvider class to ask the system
64 | * information about the ContentProvider, specifically, the authority under which it is
65 | * registered.
66 | */
67 | String packageName = mContext.getPackageName();
68 | String taskProviderClassName = TaskContentProvider.class.getName();
69 | ComponentName componentName = new ComponentName(packageName, taskProviderClassName);
70 |
71 | try {
72 |
73 | /*
74 | * Get a reference to the package manager. The package manager allows us to access
75 | * information about packages installed on a particular device. In this case, we're
76 | * going to use it to get some information about our ContentProvider under test.
77 | */
78 | PackageManager pm = mContext.getPackageManager();
79 |
80 | /* The ProviderInfo will contain the authority, which is what we want to test */
81 | ProviderInfo providerInfo = pm.getProviderInfo(componentName, 0);
82 | String actualAuthority = providerInfo.authority;
83 | String expectedAuthority = TaskContract.AUTHORITY;
84 |
85 | /* Make sure that the registered authority matches the authority from the Contract */
86 | String incorrectAuthority =
87 | "Error: TaskContentProvider registered with authority: " + actualAuthority +
88 | " instead of expected authority: " + expectedAuthority;
89 | assertEquals(incorrectAuthority,
90 | actualAuthority,
91 | expectedAuthority);
92 |
93 | } catch (PackageManager.NameNotFoundException e) {
94 | String providerNotRegisteredAtAll =
95 | "Error: TaskContentProvider not registered at " + mContext.getPackageName();
96 | /*
97 | * This exception is thrown if the ContentProvider hasn't been registered with the
98 | * manifest at all. If this is the case, you need to double check your
99 | * AndroidManifest file
100 | */
101 | fail(providerNotRegisteredAtAll);
102 | }
103 | }
104 |
105 |
106 | //================================================================================
107 | // Test UriMatcher
108 | //================================================================================
109 |
110 |
111 | private static final Uri TEST_TASKS = TaskContract.TaskEntry.CONTENT_URI;
112 | // Content URI for a single task with id = 1
113 | private static final Uri TEST_TASK_WITH_ID = TEST_TASKS.buildUpon().appendPath("1").build();
114 |
115 |
116 | /**
117 | * This function tests that the UriMatcher returns the correct integer value for
118 | * each of the Uri types that the ContentProvider can handle. Uncomment this when you are
119 | * ready to test your UriMatcher.
120 | */
121 | @Test
122 | public void testUriMatcher() {
123 |
124 | /* Create a URI matcher that the TaskContentProvider uses */
125 | UriMatcher testMatcher = TaskContentProvider.buildUriMatcher();
126 |
127 | /* Test that the code returned from our matcher matches the expected TASKS int */
128 | String tasksUriDoesNotMatch = "Error: The TASKS URI was matched incorrectly.";
129 | int actualTasksMatchCode = testMatcher.match(TEST_TASKS);
130 | int expectedTasksMatchCode = TaskContentProvider.TASKS;
131 | assertEquals(tasksUriDoesNotMatch,
132 | actualTasksMatchCode,
133 | expectedTasksMatchCode);
134 |
135 | /* Test that the code returned from our matcher matches the expected TASK_WITH_ID */
136 | String taskWithIdDoesNotMatch =
137 | "Error: The TASK_WITH_ID URI was matched incorrectly.";
138 | int actualTaskWithIdCode = testMatcher.match(TEST_TASK_WITH_ID);
139 | int expectedTaskWithIdCode = TaskContentProvider.TASK_WITH_ID;
140 | assertEquals(taskWithIdDoesNotMatch,
141 | actualTaskWithIdCode,
142 | expectedTaskWithIdCode);
143 | }
144 |
145 |
146 | //================================================================================
147 | // Test Insert
148 | //================================================================================
149 |
150 |
151 | /**
152 | * Tests inserting a single row of data via a ContentResolver
153 | */
154 | @Test
155 | public void testInsert() {
156 |
157 | /* Create values to insert */
158 | ContentValues testTaskValues = new ContentValues();
159 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_DESCRIPTION, "Test description");
160 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_PRIORITY, 1);
161 |
162 | /* TestContentObserver allows us to test if notifyChange was called appropriately */
163 | TestUtilities.TestContentObserver taskObserver = TestUtilities.getTestContentObserver();
164 |
165 | ContentResolver contentResolver = mContext.getContentResolver();
166 |
167 | /* Register a content observer to be notified of changes to data at a given URI (tasks) */
168 | contentResolver.registerContentObserver(
169 | /* URI that we would like to observe changes to */
170 | TaskContract.TaskEntry.CONTENT_URI,
171 | /* Whether or not to notify us if descendants of this URI change */
172 | true,
173 | /* The observer to register (that will receive notifyChange callbacks) */
174 | taskObserver);
175 |
176 |
177 | Uri uri = contentResolver.insert(TaskContract.TaskEntry.CONTENT_URI, testTaskValues);
178 |
179 |
180 | Uri expectedUri = ContentUris.withAppendedId(TaskContract.TaskEntry.CONTENT_URI, 1);
181 |
182 | String insertProviderFailed = "Unable to insert item through Provider";
183 | assertEquals(insertProviderFailed, uri, expectedUri);
184 |
185 | /*
186 | * If this fails, it's likely you didn't call notifyChange in your insert method from
187 | * your ContentProvider.
188 | */
189 | taskObserver.waitForNotificationOrFail();
190 |
191 | /*
192 | * waitForNotificationOrFail is synchronous, so after that call, we are done observing
193 | * changes to content and should therefore unregister this observer.
194 | */
195 | contentResolver.unregisterContentObserver(taskObserver);
196 | }
197 |
198 |
199 | //================================================================================
200 | // Test Query (for tasks directory)
201 | //================================================================================
202 |
203 |
204 | /**
205 | * Inserts data, then tests if a query for the tasks directory returns that data as a Cursor
206 | */
207 | @Test
208 | public void testQuery() {
209 |
210 | /* Get access to a writable database */
211 | TaskDbHelper dbHelper = new TaskDbHelper(mContext);
212 | SQLiteDatabase database = dbHelper.getWritableDatabase();
213 |
214 | /* Create values to insert */
215 | ContentValues testTaskValues = new ContentValues();
216 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_DESCRIPTION, "Test description");
217 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_PRIORITY, 1);
218 |
219 | /* Insert ContentValues into database and get a row ID back */
220 | long taskRowId = database.insert(
221 | /* Table to insert values into */
222 | TaskContract.TaskEntry.TABLE_NAME,
223 | null,
224 | /* Values to insert into table */
225 | testTaskValues);
226 |
227 | String insertFailed = "Unable to insert directly into the database";
228 | assertTrue(insertFailed, taskRowId != -1);
229 |
230 | /* We are done with the database, close it now. */
231 | database.close();
232 |
233 | /* Perform the ContentProvider query */
234 | Cursor taskCursor = mContext.getContentResolver().query(
235 | TaskContract.TaskEntry.CONTENT_URI,
236 | /* Columns; leaving this null returns every column in the table */
237 | null,
238 | /* Optional specification for columns in the "where" clause above */
239 | null,
240 | /* Values for "where" clause */
241 | null,
242 | /* Sort order to return in Cursor */
243 | null);
244 |
245 |
246 | String queryFailed = "Query failed to return a valid Cursor";
247 | assertTrue(queryFailed, taskCursor != null);
248 |
249 | /* We are done with the cursor, close it now. */
250 | taskCursor.close();
251 | }
252 |
253 |
254 | //================================================================================
255 | // Test Delete (for a single item)
256 | //================================================================================
257 |
258 |
259 | /**
260 | * Tests deleting a single row of data via a ContentResolver
261 | */
262 | @Test
263 | public void testDelete() {
264 | /* Access writable database */
265 | TaskDbHelper helper = new TaskDbHelper(InstrumentationRegistry.getTargetContext());
266 | SQLiteDatabase database = helper.getWritableDatabase();
267 |
268 | /* Create a new row of task data */
269 | ContentValues testTaskValues = new ContentValues();
270 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_DESCRIPTION, "Test description");
271 | testTaskValues.put(TaskContract.TaskEntry.COLUMN_PRIORITY, 1);
272 |
273 | /* Insert ContentValues into database and get a row ID back */
274 | long taskRowId = database.insert(
275 | /* Table to insert values into */
276 | TaskContract.TaskEntry.TABLE_NAME,
277 | null,
278 | /* Values to insert into table */
279 | testTaskValues);
280 |
281 | /* Always close the database when you're through with it */
282 | database.close();
283 |
284 | String insertFailed = "Unable to insert into the database";
285 | assertTrue(insertFailed, taskRowId != -1);
286 |
287 |
288 | /* TestContentObserver allows us to test if notifyChange was called appropriately */
289 | TestUtilities.TestContentObserver taskObserver = TestUtilities.getTestContentObserver();
290 |
291 | ContentResolver contentResolver = mContext.getContentResolver();
292 |
293 | /* Register a content observer to be notified of changes to data at a given URI (tasks) */
294 | contentResolver.registerContentObserver(
295 | /* URI that we would like to observe changes to */
296 | TaskContract.TaskEntry.CONTENT_URI,
297 | /* Whether or not to notify us if descendants of this URI change */
298 | true,
299 | /* The observer to register (that will receive notifyChange callbacks) */
300 | taskObserver);
301 |
302 |
303 |
304 | /* The delete method deletes the previously inserted row with id = 1 */
305 | Uri uriToDelete = TaskContract.TaskEntry.CONTENT_URI.buildUpon().appendPath("1").build();
306 | int tasksDeleted = contentResolver.delete(uriToDelete, null, null);
307 |
308 | String deleteFailed = "Unable to delete item in the database";
309 | assertTrue(deleteFailed, tasksDeleted != 0);
310 |
311 | /*
312 | * If this fails, it's likely you didn't call notifyChange in your delete method from
313 | * your ContentProvider.
314 | */
315 | taskObserver.waitForNotificationOrFail();
316 |
317 | /*
318 | * waitForNotificationOrFail is synchronous, so after that call, we are done observing
319 | * changes to content and should therefore unregister this observer.
320 | */
321 | contentResolver.unregisterContentObserver(taskObserver);
322 | }
323 |
324 | }
325 |
--------------------------------------------------------------------------------