├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── couchbase │ │ └── todolite │ │ ├── Application.java │ │ ├── ImageActivity.java │ │ ├── ListActivity.java │ │ ├── LoginActivity.java │ │ ├── ShareActivity.java │ │ ├── TaskActivity.java │ │ ├── UserProfile.java │ │ └── util │ │ ├── ImageUtil.java │ │ ├── LiveQueryAdapter.java │ │ ├── RoundImageView.java │ │ └── StringUtil.java │ └── res │ ├── drawable-xhdpi │ ├── ic_camera.png │ └── ic_camera_light.png │ ├── layout │ ├── activity_image.xml │ ├── activity_list.xml │ ├── activity_login.xml │ ├── activity_share.xml │ ├── activity_task.xml │ ├── view_dialog_input.xml │ ├── view_list.xml │ ├── view_task.xml │ ├── view_task_create.xml │ └── view_user.xml │ ├── menu │ ├── list.xml │ ├── list_item.xml │ ├── task.xml │ └── task_item.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── sync-gateway-config.json /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | build/ 15 | out/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # Gradle cache files 21 | .gradle/ 22 | 23 | # Android Studio files 24 | *.iml 25 | .idea/ 26 | *.ipr 27 | *.iws 28 | 29 | # Eclipse project files 30 | .classpath 31 | .project 32 | 33 | # Proguard folder generated by Eclipse 34 | proguard/ 35 | 36 | # Other 37 | .DS_Store 38 | _sandbox -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/ToDoLite-Android/eeda0e7fe246baa366a2646e824bc2635fb939ef/.gitmodules -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We hate bugs, but we love bug reports! And we're grateful to our developers who exercise Couchbase Lite and the Couchbase Sync Gateway in new and unexpected ways and help us work out the kinks. 2 | 3 | We also want to hear about your ideas for new features and improvements. You can file those in the issue trackers too. 4 | 5 | And while we welcome questions, **we prefer to answer questions on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/mobile-couchbase)** rather than in Github issues. 6 | 7 | # 1. Is This A Duplicate? 8 | 9 | It's great if you can scan the open issues to see if your problem/idea has been reported already. If so, feel free to add any new details or just a note that you hit this too. But if you're in a hurry, go ahead and skip this step -- we'd rather get duplicate reports than miss an issue! 10 | 11 | # 2. Describe The Bug 12 | 13 | ## Version 14 | 15 | Please indicate **what version of the software** you're using. If you compiled it yourself from source, it helps if you give the Git commit ID, or at least the branch name and date ("I last pulled from master on 6/30.") 16 | 17 | If the bug involves replication, also indicate what software and version is on the other end of the line, i.e. "Couchbase Lite Android 1.0" or "Sync Gateway 1.0" or "Sync Gateway commit f3d3229c" or "CouchDB 1.6". 18 | 19 | ## Include Steps To Reproduce 20 | 21 | The most **important information** to provide with a bug report is a clear set of steps to reproduce the problem. Include as much information as possible that you think may be related to the bug. An example would be: 22 | 23 | * Install & run Sync Gateway 1.0.3 on Mac running OS X 10.10.1 24 | * Install app on iPhone 6 running iOS 8.1.1 25 | * Login with Facebook 26 | * Turn off WiFi 27 | * Add a new document 28 | * Turn on WiFi 29 | * Saw no new documents on Sync Gateway (expected: there should have been some documents) 30 | 31 | ## Include Actual vs. Expected 32 | 33 | As mentioned above, the last thing in your steps to reproduce is the "actual vs expected" behavior. The reason this is important is because you may have misunderstood what is supposed to happen. If you give a clear description of what actually happened as well as what you were expecting to happen, it will make the bug a lot easier to figure out. 34 | 35 | ## General Formatting 36 | 37 | Please **format source code or program output (including logs or backtraces) as code**. This makes it easier to read and prevents Github from interpreting any of it as Markdown formatting or bug numbers. To do this, put a line of just three back-quotes ("```") before and after it. (For inline code snippets, just put a single back-quote before and after.) 38 | 39 | **If you need to post a block of code/output that is longer than 1/2 a page, please don't paste it into the bug report** -- it's annoying to scroll past. Instead, create a [gist](https://gist.github.com) (or something similar) and just post a link to it. 40 | 41 | ## Crashes / Exceptions 42 | 43 | If the bug causes a crash or an uncaught exception, include a crash log or backtrace. **Please don't add this as a screenshot of the IDE** if you have any alternative. (In Xcode, use the `bt` command in the debugger console to dump a backtrace that you can copy.) 44 | 45 | If the log/backtrace is long, don't paste it in directly (see the previous section.) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repository is in the process of being deprecated. See the [Android TODO app](https://developer.couchbase.com/documentation/mobile/1.3/training/develop/create-database/index.html) in the training documentation for a more up-to-date sample. 2 | 3 | ## ToDo Lite for Android 4 | 5 | [![Join the chat at https://gitter.im/couchbase/mobile](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/couchbase/mobile?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | A shared todo app that shows how to use the [Couchbase Lite Android](https://github.com/couchbase/couchbase-lite-android) framework to embed a nonrelational ("NoSQL") document-oriented database in an Android app and sync it with [Couchbase Server](http://www.couchbase.com/nosql-databases/couchbase-server) in a public or private cloud. 8 | 9 | ![screenshot](http://f.cl.ly/items/1K2e200t2D3s1l0i473e/ToDoLite.gif) 10 | 11 | ## Get the code 12 | 13 | ``` 14 | $ git clone https://github.com/couchbaselabs/ToDoLite-Android.git 15 | $ cd ToDoLite-Android 16 | ``` 17 | 18 | ## Build and run the app 19 | 20 | * Import the project into Android Studio by selecting `build.gradle` or `settings.gradle` from the root of the project. 21 | * Run the app using the "play" or "debug" button. 22 | 23 | ## Point to your own Sync Gateway 24 | 25 | 1. [Download Sync Gateway](http://www.couchbase.com/nosql-databases/downloads#couchbase-mobile). 26 | 2. Start Sync Gateway with the configuration file in the root of this project. 27 | 28 | ```bash 29 | ~/Downloads/couchbase-sync-gateway/bin/sync_gateway sync-gateway-config.json 30 | ``` 31 | 32 | 3. Open **Application.java** and update the `SYNC_URL_HTTP` constant to point to your Sync Gateway instance. 33 | 34 | ```java 35 | private static final String SYNC_URL_HTTP = "http://localhost:4984/todolite"; 36 | ``` 37 | 38 | You can use the `adb reverse tcp:4984 tcp:4984` command to open the port access from the host to the Android emulator. This command is only available on devices running android 5.0+ (API 21). 39 | 40 | 4. Log in with your Facebook account. 41 | 5. Add lists and tasks and they should be visible on the Sync Gateway Admin UI on [http://localhost:4985/_admin/](http://localhost:4985/_admin/). 42 | 43 | ## Community 44 | 45 | If you have any comments or suggestions, please join [our forum](https://forums.couchbase.com/c/mobile) and let us know. 46 | 47 | ## License 48 | 49 | Released under the Apache license, 2.0. 50 | 51 | Copyright 2011-2014, Couchbase, Inc. 52 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.couchbase.todolite" 9 | minSdkVersion 16 10 | targetSdkVersion 24 11 | versionCode 130 12 | versionName "1.3.1" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | // workaround for "duplicate files during packaging of APK" issue: 21 | packagingOptions { 22 | exclude 'META-INF/ASL2.0' 23 | exclude 'META-INF/LICENSE' 24 | exclude 'META-INF/NOTICE' 25 | } 26 | } 27 | 28 | dependencies { 29 | compile fileTree(dir: 'libs', include: ['*.jar']) 30 | testCompile 'junit:junit:4.12' 31 | compile 'com.android.support:appcompat-v7:24.2.1' 32 | compile 'com.couchbase.lite:couchbase-lite-android:1.3.1' 33 | // compile 'com.couchbase.lite:couchbase-lite-android-forestdb:1.3.1' 34 | // compile 'com.couchbase.lite:couchbase-lite-android-sqlcipher:1.3.1' 35 | compile 'com.facebook.android:facebook-android-sdk:4.+' 36 | } 37 | -------------------------------------------------------------------------------- /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/pasin/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/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/Application.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Handler; 7 | import android.widget.Toast; 8 | 9 | import com.couchbase.lite.CouchbaseLiteException; 10 | import com.couchbase.lite.Database; 11 | import com.couchbase.lite.DatabaseOptions; 12 | import com.couchbase.lite.Document; 13 | import com.couchbase.lite.Emitter; 14 | import com.couchbase.lite.Manager; 15 | import com.couchbase.lite.Mapper; 16 | import com.couchbase.lite.View; 17 | import com.couchbase.lite.android.AndroidContext; 18 | import com.couchbase.lite.auth.Authenticator; 19 | import com.couchbase.lite.auth.AuthenticatorFactory; 20 | import com.couchbase.lite.replicator.Replication; 21 | import com.couchbase.lite.util.Log; 22 | import com.couchbase.todolite.util.StringUtil; 23 | 24 | import java.net.MalformedURLException; 25 | import java.net.URL; 26 | import java.util.ArrayList; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | public class Application extends android.app.Application implements Replication.ChangeListener { 32 | public static final String TAG = "ToDoLite"; 33 | 34 | private static final String SYNC_URL_HTTP = "http://us-east.testfest.couchbasemobile.com:4984/todolite"; 35 | 36 | // Storage Type: .SQLITE_STORAGE or .FORESTDB_STORAGE 37 | private static final String STORAGE_TYPE = Manager.SQLITE_STORAGE; 38 | 39 | // Encryption (Don't store encryption key in the source code. We are doing it here just as an example): 40 | private static final boolean ENCRYPTION_ENABLED = false; 41 | private static final String ENCRYPTION_KEY = "seekrit"; 42 | 43 | // Logging: 44 | private static final boolean LOGGING_ENABLED = true; 45 | 46 | // Guest database: 47 | private static final String GUEST_DATABASE_NAME = "guest"; 48 | 49 | private Manager mManager; 50 | private Database mDatabase; 51 | private Replication mPull; 52 | private Replication mPush; 53 | private Throwable mReplError; 54 | private String mCurrentUserId; 55 | 56 | @Override 57 | public void onCreate() { 58 | super.onCreate(); 59 | enableLogging(); 60 | } 61 | 62 | private void enableLogging() { 63 | if (LOGGING_ENABLED) { 64 | Manager.enableLogging(TAG, Log.VERBOSE); 65 | Manager.enableLogging(Log.TAG, Log.VERBOSE); 66 | Manager.enableLogging(Log.TAG_SYNC_ASYNC_TASK, Log.VERBOSE); 67 | Manager.enableLogging(Log.TAG_SYNC, Log.VERBOSE); 68 | Manager.enableLogging(Log.TAG_QUERY, Log.VERBOSE); 69 | Manager.enableLogging(Log.TAG_VIEW, Log.VERBOSE); 70 | Manager.enableLogging(Log.TAG_DATABASE, Log.VERBOSE); 71 | } 72 | } 73 | 74 | private Manager getManager() { 75 | if (mManager == null) { 76 | try { 77 | AndroidContext context = new AndroidContext(getApplicationContext()); 78 | mManager = new Manager(context, Manager.DEFAULT_OPTIONS); 79 | } catch (Exception e) { 80 | Log.e(TAG, "Cannot create Manager object", e); 81 | } 82 | } 83 | return mManager; 84 | } 85 | 86 | public Database getDatabase() { 87 | return mDatabase; 88 | } 89 | 90 | private void setDatabase(Database database) { 91 | this.mDatabase = database; 92 | } 93 | 94 | private Database getUserDatabase(String name) { 95 | try { 96 | String dbName = "db" + StringUtil.MD5(name); 97 | DatabaseOptions options = new DatabaseOptions(); 98 | options.setCreate(true); 99 | options.setStorageType(STORAGE_TYPE); 100 | options.setEncryptionKey(ENCRYPTION_ENABLED ? ENCRYPTION_KEY : null); 101 | return getManager().openDatabase(dbName, options); 102 | } catch (CouchbaseLiteException e) { 103 | Log.e(TAG, "Cannot create database for name: " + name, e); 104 | } 105 | return null; 106 | } 107 | 108 | public void loginAsFacebookUser(Activity activity, String token, String userId, String name) { 109 | setCurrentUserId(userId); 110 | setDatabase(getUserDatabase(userId)); 111 | 112 | String profileDocID = "p:" + userId; 113 | Document profile = mDatabase.getExistingDocument(profileDocID); 114 | if (profile == null) { 115 | try { 116 | Map properties = new HashMap(); 117 | properties.put("type", "profile"); 118 | properties.put("user_id", userId); 119 | properties.put("name", name); 120 | 121 | profile = mDatabase.getDocument(profileDocID); 122 | profile.putProperties(properties); 123 | 124 | // Migrate guest data to user: 125 | UserProfile.migrateGuestData(getUserDatabase(GUEST_DATABASE_NAME), profile); 126 | } catch (CouchbaseLiteException e) { 127 | Log.e(TAG, "Cannot create a new user profile", e); 128 | } 129 | } 130 | 131 | startReplication(AuthenticatorFactory.createFacebookAuthenticator(token)); 132 | login(activity); 133 | } 134 | 135 | public void loginAsGuest(Activity activity) { 136 | setDatabase(getUserDatabase(GUEST_DATABASE_NAME)); 137 | setCurrentUserId(null); 138 | login(activity); 139 | } 140 | 141 | private void login(Activity activity) { 142 | Intent intent = new Intent(activity, ListActivity.class); 143 | intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 144 | activity.startActivity(intent); 145 | activity.finish(); 146 | } 147 | 148 | public void logout() { 149 | setCurrentUserId(null); 150 | stopReplication(); 151 | setDatabase(null); 152 | 153 | Intent intent = new Intent(getApplicationContext(), LoginActivity.class); 154 | intent.setAction(LoginActivity.ACTION_LOGOUT); 155 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 156 | startActivity(intent); 157 | } 158 | 159 | private void setCurrentUserId(String userId) { 160 | this.mCurrentUserId = userId; 161 | } 162 | 163 | public String getCurrentUserId() { 164 | return this.mCurrentUserId; 165 | } 166 | 167 | /** Replicator */ 168 | 169 | private URL getSyncUrl() { 170 | URL url = null; 171 | try { 172 | url = new URL(SYNC_URL_HTTP); 173 | } catch (MalformedURLException e) { 174 | Log.e(TAG, "Invalid sync url", e); 175 | } 176 | return url; 177 | } 178 | 179 | private void startReplication(Authenticator auth) { 180 | if (mPull == null) { 181 | mPull = mDatabase.createPullReplication(getSyncUrl()); 182 | mPull.setContinuous(true); 183 | mPull.setAuthenticator(auth); 184 | mPull.addChangeListener(this); 185 | } 186 | 187 | if (mPush == null) { 188 | mPush = mDatabase.createPushReplication(getSyncUrl()); 189 | mPush.setContinuous(true); 190 | mPush.setAuthenticator(auth); 191 | mPush.addChangeListener(this); 192 | } 193 | 194 | mPull.stop(); 195 | mPull.start(); 196 | 197 | mPush.stop(); 198 | mPush.start(); 199 | } 200 | 201 | private void stopReplication() { 202 | if (mPull != null) { 203 | mPull.removeChangeListener(this); 204 | mPull.stop(); 205 | mPull = null; 206 | } 207 | 208 | if (mPush != null) { 209 | mPush.removeChangeListener(this); 210 | mPush.stop(); 211 | mPush = null; 212 | } 213 | } 214 | 215 | @Override 216 | public void changed(Replication.ChangeEvent event) { 217 | Throwable error = null; 218 | if (mPull != null) { 219 | if (error == null) 220 | error = mPull.getLastError(); 221 | } 222 | 223 | if (error == null || error == mReplError) 224 | error = mPush.getLastError(); 225 | 226 | if (error != mReplError) { 227 | mReplError = error; 228 | if (mReplError != null) 229 | showErrorMessage(mReplError.getMessage(), null); 230 | } 231 | } 232 | 233 | /** Database View */ 234 | public View getListsView() { 235 | View view = mDatabase.getView("lists"); 236 | if (view.getMap() == null) { 237 | Mapper mapper = new Mapper() { 238 | public void map(Map document, Emitter emitter) { 239 | String type = (String)document.get("type"); 240 | if ("list".equals(type)) 241 | emitter.emit(document.get("title"), null); 242 | } 243 | }; 244 | view.setMap(mapper, "1.0"); 245 | } 246 | return view; 247 | } 248 | 249 | public View getTasksView() { 250 | View view = mDatabase.getView("tasks"); 251 | if (view.getMap() == null) { 252 | Mapper map = new Mapper() { 253 | @Override 254 | public void map(Map document, Emitter emitter) { 255 | if ("task".equals(document.get("type"))) { 256 | List keys = new ArrayList(); 257 | keys.add(document.get("list_id")); 258 | keys.add(document.get("created_at")); 259 | emitter.emit(keys, document); 260 | } 261 | } 262 | }; 263 | view.setMap(map, "1.0"); 264 | } 265 | return view; 266 | } 267 | 268 | public View getUserProfilesView() { 269 | View view = mDatabase.getView("profiles"); 270 | if (view.getMap() == null) { 271 | Mapper map = new Mapper() { 272 | @Override 273 | public void map(Map document, Emitter emitter) { 274 | if ("profile".equals(document.get("type"))) 275 | emitter.emit(document.get("name"), null); 276 | } 277 | }; 278 | view.setMap(map, "1.0"); 279 | } 280 | return view; 281 | } 282 | 283 | /** Display error message */ 284 | 285 | public void showErrorMessage(final String errorMessage, final Throwable throwable) { 286 | runOnUiThread(new Runnable() { 287 | @Override 288 | public void run() { 289 | android.util.Log.e(TAG, errorMessage, throwable); 290 | String msg = String.format("%s: %s", 291 | errorMessage, throwable != null ? throwable : ""); 292 | Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); 293 | } 294 | }); 295 | } 296 | 297 | private void runOnUiThread(Runnable runnable) { 298 | Handler mainHandler = new Handler(getApplicationContext().getMainLooper()); 299 | mainHandler.post(runnable); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/ImageActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.widget.ImageView; 11 | 12 | import com.couchbase.lite.Attachment; 13 | import com.couchbase.lite.Database; 14 | import com.couchbase.lite.Document; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | 19 | public class ImageActivity extends AppCompatActivity { 20 | public static final String INTENT_TASK_DOC_ID = "image"; 21 | 22 | private String mTaskDocId; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_image); 28 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 29 | 30 | if (savedInstanceState != null) 31 | mTaskDocId = savedInstanceState.getString(INTENT_TASK_DOC_ID); 32 | else 33 | mTaskDocId = getIntent().getStringExtra(INTENT_TASK_DOC_ID); 34 | 35 | Application application = (Application) getApplication(); 36 | Database database = application.getDatabase(); 37 | Document document = database.getDocument(mTaskDocId); 38 | Attachment attachment = document.getCurrentRevision().getAttachment("image"); 39 | if (attachment == null) 40 | return; 41 | 42 | Bitmap image = null; 43 | InputStream is = null; 44 | try { 45 | is = attachment.getContent(); 46 | image = BitmapFactory.decodeStream(is); 47 | } catch (Exception e) { 48 | Log.e(Application.TAG, "Cannot display the attached image", e); 49 | } finally { 50 | if (is != null) try { is.close(); } catch (IOException e) { } 51 | } 52 | 53 | ImageView imageView = (ImageView) findViewById(R.id.image); 54 | imageView.setImageBitmap(image); 55 | imageView.setOnClickListener(new View.OnClickListener() { 56 | @Override 57 | public void onClick(View v) { 58 | finish(); 59 | } 60 | }); 61 | } 62 | 63 | public void onSaveInstanceState(Bundle savedInstanceState) { 64 | savedInstanceState.putString(INTENT_TASK_DOC_ID, mTaskDocId); 65 | super.onSaveInstanceState(savedInstanceState); 66 | } 67 | 68 | @Override 69 | public boolean onOptionsItemSelected(MenuItem item) { 70 | switch (item.getItemId()) { 71 | case android.R.id.home: 72 | finish(); 73 | return true; 74 | } 75 | return super.onOptionsItemSelected(item); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/ListActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.os.Bundle; 9 | import android.view.LayoutInflater; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.AdapterView; 15 | import android.widget.EditText; 16 | import android.widget.ListView; 17 | import android.widget.PopupMenu; 18 | import android.widget.TextView; 19 | 20 | import com.couchbase.lite.CouchbaseLiteException; 21 | import com.couchbase.lite.Database; 22 | import com.couchbase.lite.Document; 23 | import com.couchbase.lite.Emitter; 24 | import com.couchbase.lite.LiveQuery; 25 | import com.couchbase.lite.Mapper; 26 | import com.couchbase.lite.Query; 27 | import com.couchbase.lite.QueryEnumerator; 28 | import com.couchbase.lite.QueryRow; 29 | import com.couchbase.lite.TransactionalTask; 30 | import com.couchbase.lite.util.Log; 31 | import com.couchbase.todolite.util.LiveQueryAdapter; 32 | 33 | import java.text.SimpleDateFormat; 34 | import java.util.ArrayList; 35 | import java.util.Date; 36 | import java.util.HashMap; 37 | import java.util.List; 38 | import java.util.Map; 39 | import java.util.TimeZone; 40 | 41 | public class ListActivity extends AppCompatActivity { 42 | private Database mDatabase = null; 43 | private ListAdapter mAdapter = null; 44 | 45 | private static SimpleDateFormat mDateFormatter = 46 | new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_list); 52 | 53 | Application application = (Application) getApplication(); 54 | mDatabase = application.getDatabase(); 55 | 56 | Query query = getQuery(); 57 | mAdapter = new ListAdapter(this, query.toLiveQuery()); 58 | 59 | ListView listView = (ListView) findViewById(R.id.list); 60 | listView.setAdapter(mAdapter); 61 | listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 62 | @Override 63 | public void onItemClick(AdapterView adapterView, View view, int i, long l) { 64 | Document list = (Document) mAdapter.getItem(i); 65 | showTasks(list); 66 | } 67 | }); 68 | 69 | listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 70 | @Override 71 | public boolean onItemLongClick(AdapterView parent, View view, final int pos, long id) { 72 | PopupMenu popup = new PopupMenu(ListActivity.this, view); 73 | popup.inflate(R.menu.list_item); 74 | popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 75 | @Override 76 | public boolean onMenuItemClick(MenuItem item) { 77 | Document list = (Document) mAdapter.getItem(pos); 78 | String owner = (String) list.getProperties().get("owner"); 79 | Application application = (Application) getApplication(); 80 | if (owner == null || owner.equals("p:" + application.getCurrentUserId())) 81 | deleteList(list); 82 | else 83 | application.showErrorMessage("Only owner can delete the list", null); 84 | return true; 85 | } 86 | }); 87 | popup.show(); 88 | return true; 89 | } 90 | }); 91 | 92 | mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 93 | } 94 | 95 | @Override 96 | public boolean onCreateOptionsMenu(Menu menu) { 97 | getMenuInflater().inflate(R.menu.list, menu); 98 | return true; 99 | } 100 | 101 | @Override 102 | public boolean onOptionsItemSelected(MenuItem item) { 103 | switch (item.getItemId()) { 104 | case android.R.id.home: 105 | finish(); 106 | return true; 107 | case R.id.create: 108 | displayCreateDialog(); 109 | return true; 110 | case R.id.logout: 111 | logout(); 112 | return true; 113 | } 114 | return super.onOptionsItemSelected(item); 115 | } 116 | 117 | @Override 118 | protected void onDestroy() { 119 | mAdapter.invalidate(); 120 | super.onDestroy(); 121 | } 122 | 123 | private void displayCreateDialog() { 124 | AlertDialog.Builder alert = new AlertDialog.Builder(this); 125 | alert.setTitle(getResources().getString(R.string.title_dialog_new_list)); 126 | 127 | LayoutInflater inflater = this.getLayoutInflater(); 128 | final View view = inflater.inflate(R.layout.view_dialog_input, null); 129 | final EditText input = (EditText) view.findViewById(R.id.text); 130 | alert.setView(view); 131 | 132 | alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { 133 | public void onClick(DialogInterface dialog, int whichButton) { 134 | try { 135 | String title = input.getText().toString(); 136 | if (title.length() == 0) 137 | return; 138 | create(title); 139 | } catch (CouchbaseLiteException e) { 140 | Log.e(Application.TAG, "Cannot create a new list", e); 141 | } 142 | } 143 | }); 144 | 145 | alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { 146 | public void onClick(DialogInterface dialog, int whichButton) { } 147 | }); 148 | 149 | alert.show(); 150 | } 151 | 152 | private Query getQuery() { 153 | Application application = (Application) getApplication(); 154 | return application.getListsView().createQuery(); 155 | } 156 | 157 | private Document create(String title) throws CouchbaseLiteException { 158 | String currentTimeString = mDateFormatter.format(new Date()); 159 | 160 | Map properties = new HashMap(); 161 | properties.put("type", "list"); 162 | properties.put("title", title); 163 | properties.put("created_at", currentTimeString); 164 | properties.put("members", new ArrayList()); 165 | 166 | Application application = (Application) getApplication(); 167 | String userId = application.getCurrentUserId(); 168 | if (userId != null) 169 | properties.put("owner", "p:" + userId); 170 | 171 | Document document = mDatabase.createDocument(); 172 | document.putProperties(properties); 173 | 174 | return document; 175 | } 176 | 177 | private void deleteList(final Document list) { 178 | Application application = (Application) getApplication(); 179 | final Query query = application.getTasksView().createQuery(); 180 | query.setDescending(true); 181 | 182 | List startKeys = new ArrayList(); 183 | startKeys.add(list.getId()); 184 | startKeys.add(new HashMap()); 185 | 186 | List endKeys = new ArrayList(); 187 | endKeys.add(list.getId()); 188 | 189 | query.setStartKey(startKeys); 190 | query.setEndKey(endKeys); 191 | 192 | mDatabase.runInTransaction(new TransactionalTask() { 193 | @Override 194 | public boolean run() { 195 | try { 196 | QueryEnumerator tasks = query.run(); 197 | while(tasks.hasNext()) { 198 | QueryRow task = tasks.next(); 199 | task.getDocument().getCurrentRevision().deleteDocument(); 200 | } 201 | list.delete(); 202 | } catch (CouchbaseLiteException e) { 203 | Log.e(Application.TAG, "Cannot delete list", e); 204 | return false; 205 | } 206 | return true; 207 | } 208 | }); 209 | } 210 | 211 | private void showTasks(Document list) { 212 | Intent intent = new Intent(this, TaskActivity.class); 213 | intent.putExtra(TaskActivity.INTENT_LIST_ID, list.getId()); 214 | startActivity(intent); 215 | } 216 | 217 | private void logout() { 218 | Application application = (Application) getApplication(); 219 | application.logout(); 220 | } 221 | 222 | private class ListAdapter extends LiveQueryAdapter { 223 | public ListAdapter(Context context, LiveQuery query) { 224 | super(context, query); 225 | } 226 | 227 | @Override 228 | public View getView(int position, View convertView, ViewGroup parent) { 229 | if (convertView == null) { 230 | LayoutInflater inflater = (LayoutInflater) parent.getContext(). 231 | getSystemService(Context.LAYOUT_INFLATER_SERVICE); 232 | convertView = inflater.inflate(R.layout.view_list, null); 233 | } 234 | 235 | final Document list = (Document) getItem(position); 236 | TextView text = (TextView) convertView.findViewById(R.id.text); 237 | text.setText((String) list.getProperty("title")); 238 | return convertView; 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.widget.Button; 11 | 12 | import com.facebook.*; 13 | import com.facebook.login.LoginManager; 14 | import com.facebook.login.LoginResult; 15 | import com.facebook.login.widget.LoginButton; 16 | 17 | import org.json.JSONException; 18 | import org.json.JSONObject; 19 | 20 | public class LoginActivity extends AppCompatActivity { 21 | public static final String ACTION_LOGOUT = "logout"; 22 | 23 | private CallbackManager mCallbackManager; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | 29 | FacebookSdk.sdkInitialize(getApplicationContext()); 30 | setContentView(R.layout.activity_login); 31 | 32 | if (ACTION_LOGOUT.equals(getIntent().getAction())) { 33 | logout(); 34 | } else { 35 | AccessToken accessToken = AccessToken.getCurrentAccessToken(); 36 | if (accessToken != null && !accessToken.isExpired()) { 37 | loginAsFacebookUser(accessToken.getToken(), accessToken.getUserId(), null); 38 | return; 39 | } 40 | 41 | if (isLoggedAsGuest()) { 42 | loginAsGuest(); 43 | return; 44 | } 45 | } 46 | 47 | LoginButton facebookLoginButton = (LoginButton) findViewById(R.id.facebook_login_button); 48 | facebookLoginButton.setReadPermissions("public_profile"); 49 | 50 | mCallbackManager = CallbackManager.Factory.create(); 51 | facebookLoginButton.registerCallback(mCallbackManager, new FacebookCallback() { 52 | @Override 53 | public void onSuccess(LoginResult loginResult) { 54 | continueFacebookLogin(loginResult); 55 | } 56 | 57 | @Override 58 | public void onError(FacebookException error) { 59 | Log.e(Application.TAG, "Facebook login error", error); 60 | } 61 | 62 | @Override 63 | public void onCancel() { } 64 | }); 65 | 66 | Button guestLoginButton = (Button) findViewById(R.id.guest_login_button); 67 | guestLoginButton.setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View view) { 70 | loginAsGuest(); 71 | } 72 | }); 73 | } 74 | 75 | @Override 76 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 77 | super.onActivityResult(requestCode, resultCode, data); 78 | mCallbackManager.onActivityResult(requestCode, resultCode, data); 79 | } 80 | 81 | private void continueFacebookLogin(final LoginResult loginResult) { 82 | GraphRequest request = GraphRequest.newMeRequest( 83 | loginResult.getAccessToken(), new GraphRequest.GraphJSONObjectCallback() { 84 | @Override 85 | public void onCompleted(JSONObject object, GraphResponse response) { 86 | if (object == null) { 87 | Log.e(Application.TAG, "Cannot get facebook user info after login"); 88 | return; 89 | } 90 | 91 | try { 92 | AccessToken accessToken = loginResult.getAccessToken(); 93 | String token = accessToken.getToken(); 94 | String userId = accessToken.getUserId(); 95 | String name = object.getString("name"); 96 | loginAsFacebookUser(token, userId, name); 97 | } catch (JSONException e) { 98 | Log.e(Application.TAG, "Cannot get facebook user info after login", e); 99 | return; 100 | } 101 | } 102 | } 103 | ); 104 | 105 | Bundle parameters = new Bundle(); 106 | parameters.putString("fields", "name"); 107 | request.setParameters(parameters); 108 | request.executeAsync(); 109 | } 110 | 111 | private void loginAsFacebookUser(String token, String userId, String name) { 112 | Application application = (Application) getApplication(); 113 | application.loginAsFacebookUser(this, token, userId, name); 114 | } 115 | 116 | private void loginAsGuest() { 117 | SharedPreferences pref = getPreferences(Context.MODE_PRIVATE); 118 | SharedPreferences.Editor editor = pref.edit(); 119 | editor.putBoolean("guest", true); 120 | editor.commit(); 121 | Application application = (Application) getApplication(); 122 | application.loginAsGuest(this); 123 | } 124 | 125 | private boolean isLoggedAsGuest() { 126 | SharedPreferences pref = getPreferences(Context.MODE_PRIVATE); 127 | return pref.getBoolean("guest", false); 128 | } 129 | 130 | private void logout() { 131 | SharedPreferences pref = getPreferences(Context.MODE_PRIVATE); 132 | SharedPreferences.Editor editor = pref.edit(); 133 | editor.remove("guest"); 134 | editor.commit(); 135 | LoginManager.getInstance().logOut(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/ShareActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.content.Context; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.CheckBox; 11 | import android.widget.ListView; 12 | import android.widget.TextView; 13 | 14 | import com.couchbase.lite.CouchbaseLiteException; 15 | import com.couchbase.lite.Database; 16 | import com.couchbase.lite.Document; 17 | import com.couchbase.lite.Emitter; 18 | import com.couchbase.lite.LiveQuery; 19 | import com.couchbase.lite.Mapper; 20 | import com.couchbase.lite.Query; 21 | import com.couchbase.lite.util.Log; 22 | import com.couchbase.todolite.util.LiveQueryAdapter; 23 | 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | public class ShareActivity extends AppCompatActivity { 30 | public static final String INTENT_LIST_ID = "list_id"; 31 | 32 | private Database mDatabase = null; 33 | private UserAdapter mAdapter = null; 34 | private Document mList = null; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_share); 40 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 41 | 42 | String listId; 43 | if (savedInstanceState != null) 44 | listId = savedInstanceState.getString(INTENT_LIST_ID); 45 | else 46 | listId = getIntent().getStringExtra(INTENT_LIST_ID); 47 | 48 | Application application = (Application) getApplication(); 49 | mDatabase = application.getDatabase(); 50 | mList = mDatabase.getDocument(listId); 51 | 52 | mAdapter = new UserAdapter(this, getQuery().toLiveQuery()); 53 | ListView listView = (ListView) findViewById(R.id.list); 54 | listView.setAdapter(mAdapter); 55 | } 56 | 57 | public void onSaveInstanceState(Bundle savedInstanceState) { 58 | savedInstanceState.putString(INTENT_LIST_ID, mList.getId()); 59 | super.onSaveInstanceState(savedInstanceState); 60 | } 61 | 62 | @Override 63 | public boolean onOptionsItemSelected(MenuItem item) { 64 | switch (item.getItemId()) { 65 | case android.R.id.home: 66 | finish(); 67 | return true; 68 | } 69 | return super.onOptionsItemSelected(item); 70 | } 71 | 72 | @Override 73 | protected void onDestroy() { 74 | mAdapter.invalidate(); 75 | super.onDestroy(); 76 | } 77 | 78 | private Query getQuery() { 79 | Application application = (Application) getApplication(); 80 | Query query = application.getUserProfilesView().createQuery(); 81 | return query; 82 | } 83 | 84 | private void addMember(Document user) { 85 | Map properties = new HashMap<>(); 86 | properties.putAll(mList.getProperties()); 87 | 88 | List members = (List) properties.get("members"); 89 | if (members == null) { 90 | members = new ArrayList(); 91 | properties.put("members", members); 92 | } 93 | members.add(user.getId()); 94 | 95 | try { 96 | mList.putProperties(properties); 97 | } catch (CouchbaseLiteException e) { 98 | Log.e(Application.TAG, "Cannot add member to the list", e); 99 | } 100 | } 101 | 102 | private void removeMember(Document user) { 103 | Map properties = new HashMap(); 104 | properties.putAll(mList.getProperties()); 105 | 106 | List members = (List) properties.get("members"); 107 | if (members != null) { 108 | members.remove(user.getId()); 109 | properties.put("members", members); 110 | try { 111 | mList.putProperties(properties); 112 | } catch (CouchbaseLiteException e) { 113 | Log.e(Application.TAG, "Cannot add member to the list", e); 114 | } 115 | } 116 | } 117 | 118 | private class UserAdapter extends LiveQueryAdapter { 119 | public UserAdapter(Context context, LiveQuery query) { 120 | super(context, query); 121 | } 122 | 123 | private boolean isListOwner(Document user) { 124 | Application application = (Application) getApplication(); 125 | return user.getId().equals("p:" + application.getCurrentUserId()); 126 | } 127 | 128 | private boolean isMemberOfTheCurrentList(Document user) { 129 | if (isListOwner(user)) 130 | return true; 131 | 132 | List members = (List) mList.getProperty("members"); 133 | return members != null ? members.contains(user.getId()) : false; 134 | } 135 | 136 | @Override 137 | public View getView(int position, View convertView, ViewGroup parent) { 138 | if (convertView == null) { 139 | LayoutInflater inflater = (LayoutInflater) parent.getContext(). 140 | getSystemService(Context.LAYOUT_INFLATER_SERVICE); 141 | convertView = inflater.inflate(R.layout.view_user, null); 142 | } 143 | 144 | final Document task = (Document) getItem(position); 145 | 146 | TextView text = (TextView) convertView.findViewById(R.id.text); 147 | text.setText((String) task.getProperty("name")); 148 | 149 | final Document user = (Document) getItem(position); 150 | final CheckBox checkBox = (CheckBox) convertView.findViewById(R.id.checked); 151 | boolean checked = isMemberOfTheCurrentList(user); 152 | checkBox.setChecked(checked); 153 | checkBox.setEnabled(!isListOwner(user)); 154 | checkBox.setOnClickListener(new View.OnClickListener() { 155 | @Override 156 | public void onClick(View view) { 157 | Application application = (Application) getApplication(); 158 | 159 | if (checkBox.isChecked()) 160 | addMember(user); 161 | else 162 | removeMember(user); 163 | } 164 | }); 165 | return convertView; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/TaskActivity.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.ContentResolver; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.res.AssetFileDescriptor; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.media.ThumbnailUtils; 12 | import android.net.Uri; 13 | import android.os.Environment; 14 | import android.provider.MediaStore; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.os.Bundle; 17 | import android.view.KeyEvent; 18 | import android.view.LayoutInflater; 19 | import android.view.Menu; 20 | import android.view.MenuItem; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.AdapterView; 24 | import android.widget.CheckBox; 25 | import android.widget.EditText; 26 | import android.widget.ImageView; 27 | import android.widget.ListView; 28 | import android.widget.PopupMenu; 29 | import android.widget.TextView; 30 | 31 | import com.couchbase.lite.Attachment; 32 | import com.couchbase.lite.CouchbaseLiteException; 33 | import com.couchbase.lite.Database; 34 | import com.couchbase.lite.Document; 35 | import com.couchbase.lite.Emitter; 36 | import com.couchbase.lite.LiveQuery; 37 | import com.couchbase.lite.Mapper; 38 | import com.couchbase.lite.Query; 39 | import com.couchbase.lite.UnsavedRevision; 40 | import com.couchbase.lite.util.Log; 41 | import com.couchbase.todolite.util.ImageUtil; 42 | import com.couchbase.todolite.util.LiveQueryAdapter; 43 | 44 | import java.io.ByteArrayInputStream; 45 | import java.io.ByteArrayOutputStream; 46 | import java.io.File; 47 | import java.io.IOException; 48 | import java.io.InputStream; 49 | import java.text.SimpleDateFormat; 50 | import java.util.ArrayList; 51 | import java.util.Date; 52 | import java.util.HashMap; 53 | import java.util.List; 54 | import java.util.Map; 55 | import java.util.TimeZone; 56 | 57 | public class TaskActivity extends AppCompatActivity { 58 | public static final String INTENT_LIST_ID = "list_id"; 59 | 60 | private static final int REQUEST_TAKE_PHOTO = 1; 61 | private static final int REQUEST_CHOOSE_PHOTO = 2; 62 | private static final int THUMBNAIL_SIZE = 150; 63 | 64 | private static SimpleDateFormat mDateFormatter = 65 | new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 66 | 67 | private String mListId; 68 | private Database mDatabase; 69 | private TaskAdapter mAdapter; 70 | private String mImagePathToBeAttached; 71 | private Document mCurrentTaskToAttachImage; 72 | private Bitmap mImageToBeAttached; 73 | 74 | @Override 75 | protected void onCreate(Bundle savedInstanceState) { 76 | super.onCreate(savedInstanceState); 77 | setContentView(R.layout.activity_task); 78 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 79 | 80 | Application application = (Application) getApplication(); 81 | mDatabase = application.getDatabase(); 82 | 83 | if (savedInstanceState != null) 84 | mListId = savedInstanceState.getString(INTENT_LIST_ID); 85 | else 86 | mListId = getIntent().getStringExtra(INTENT_LIST_ID); 87 | 88 | Query query = getQuery(); 89 | mAdapter = new TaskAdapter(this, query.toLiveQuery()); 90 | 91 | ListView listView = (ListView) findViewById(R.id.list); 92 | listView.setAdapter(mAdapter); 93 | setListHeader(listView); 94 | setListItemLongClick(listView); 95 | 96 | mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 97 | } 98 | 99 | public void onSaveInstanceState(Bundle savedInstanceState) { 100 | savedInstanceState.putString(INTENT_LIST_ID, mListId); 101 | super.onSaveInstanceState(savedInstanceState); 102 | } 103 | 104 | @Override 105 | public boolean onCreateOptionsMenu(Menu menu) { 106 | getMenuInflater().inflate(R.menu.task, menu); 107 | 108 | // Hide share menu if the current user is not the owner of the list: 109 | Application application = (Application) getApplication(); 110 | String owner = (String) application.getDatabase(). 111 | getDocument(mListId).getProperties().get("owner"); 112 | 113 | boolean sharable = owner != null && owner.equals("p:" + application.getCurrentUserId()); 114 | menu.findItem(R.id.share).setVisible(sharable); 115 | 116 | return true; 117 | } 118 | 119 | @Override 120 | public boolean onOptionsItemSelected(MenuItem item) { 121 | switch (item.getItemId()) { 122 | case android.R.id.home: 123 | finish(); 124 | return true; 125 | case R.id.share: 126 | displayShareActivity(); 127 | return true; 128 | } 129 | return super.onOptionsItemSelected(item); 130 | } 131 | 132 | private void setListHeader(ListView listView) { 133 | ViewGroup header = (ViewGroup) getLayoutInflater().inflate( 134 | R.layout.view_task_create, listView, false); 135 | 136 | final ImageView imageView = (ImageView) header.findViewById(R.id.image); 137 | imageView.setOnClickListener(new View.OnClickListener() { 138 | @Override 139 | public void onClick(View v) { 140 | displayAttachImageDialog(null); 141 | } 142 | }); 143 | 144 | final EditText text = (EditText) header.findViewById(R.id.text); 145 | text.setOnKeyListener(new View.OnKeyListener() { 146 | @Override 147 | public boolean onKey(View view, int i, KeyEvent keyEvent) { 148 | if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) && 149 | (keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { 150 | String inputText = text.getText().toString(); 151 | if (inputText.length() > 0) 152 | createTask(inputText, mImageToBeAttached, mListId); 153 | 154 | text.setText(""); 155 | deleteCurrentPhoto(); 156 | 157 | return true; 158 | } 159 | return false; 160 | } 161 | }); 162 | 163 | listView.addHeaderView(header); 164 | } 165 | 166 | private void setListItemLongClick(ListView listView) { 167 | listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 168 | @Override 169 | public boolean onItemLongClick(AdapterView parent, View view, final int pos, long id) { 170 | PopupMenu popup = new PopupMenu(TaskActivity.this, view); 171 | popup.inflate(R.menu.task_item); 172 | popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 173 | @Override 174 | public boolean onMenuItemClick(MenuItem item) { 175 | Document task = (Document) mAdapter.getItem(pos - 1); 176 | handleTaskPopupAction(item, task); 177 | return true; 178 | } 179 | }); 180 | popup.show(); 181 | return true; 182 | } 183 | }); 184 | } 185 | 186 | private void handleTaskPopupAction(MenuItem item, Document task) { 187 | switch (item.getItemId()) { 188 | case R.id.update: 189 | updateTask(task); 190 | return; 191 | case R.id.delete: 192 | deleteTask(task); 193 | return; 194 | } 195 | } 196 | 197 | private void displayShareActivity() { 198 | Intent intent = new Intent(this, ShareActivity.class); 199 | intent.putExtra(ShareActivity.INTENT_LIST_ID, mListId); 200 | startActivity(intent); 201 | } 202 | 203 | private Query getQuery() { 204 | Application application = (Application) getApplication(); 205 | Query query = application.getTasksView().createQuery(); 206 | query.setDescending(true); 207 | 208 | List startKeys = new ArrayList(); 209 | startKeys.add(mListId); 210 | startKeys.add(new HashMap()); 211 | 212 | List endKeys = new ArrayList(); 213 | endKeys.add(mListId); 214 | 215 | query.setStartKey(startKeys); 216 | query.setEndKey(endKeys); 217 | 218 | return query; 219 | } 220 | 221 | private void createTask(String title, Bitmap image, String listId) { 222 | String currentTimeString = mDateFormatter.format(new Date()); 223 | 224 | Map properties = new HashMap(); 225 | properties.put("type", "task"); 226 | properties.put("title", title); 227 | properties.put("checked", Boolean.FALSE); 228 | properties.put("created_at", currentTimeString); 229 | properties.put("list_id", listId); 230 | 231 | Document document = mDatabase.createDocument(); 232 | UnsavedRevision revision = document.createRevision(); 233 | revision.setUserProperties(properties); 234 | 235 | if (image != null) { 236 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 237 | image.compress(Bitmap.CompressFormat.JPEG, 50, out); 238 | ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); 239 | revision.setAttachment("image", "image/jpg", in); 240 | } 241 | 242 | try { 243 | revision.save(); 244 | } catch (CouchbaseLiteException e) { 245 | Log.e(Application.TAG, "Cannot create new task", e); 246 | } 247 | } 248 | 249 | private void updateTask(final Document task) { 250 | AlertDialog.Builder alert = new AlertDialog.Builder(this); 251 | alert.setTitle(getResources().getString(R.string.title_dialog_update)); 252 | 253 | final EditText input = new EditText(this); 254 | input.setMaxLines(1); 255 | input.setSingleLine(true); 256 | String text = (String) task.getProperty("title"); 257 | input.setText(text); 258 | alert.setView(input); 259 | alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { 260 | @Override 261 | public void onClick(DialogInterface dialogInterface, int i) { 262 | String currentTimeString = mDateFormatter.format(new Date()); 263 | 264 | Map updatedProperties = new HashMap(); 265 | updatedProperties.putAll(task.getProperties()); 266 | updatedProperties.put("title", input.getText().toString()); 267 | updatedProperties.put("updated_at", currentTimeString); 268 | 269 | try { 270 | task.putProperties(updatedProperties); 271 | } catch (CouchbaseLiteException e) { 272 | e.printStackTrace(); 273 | } 274 | } 275 | }); 276 | alert.show(); 277 | } 278 | 279 | private void deleteTask(final Document task) { 280 | try { 281 | task.delete(); 282 | } catch (CouchbaseLiteException e) { 283 | Log.e(Application.TAG, "Cannot delete a task", e); 284 | } 285 | } 286 | 287 | private void attachImage(Document task, Bitmap image) { 288 | if (task == null || image == null) return; 289 | 290 | UnsavedRevision revision = task.createRevision(); 291 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 292 | image.compress(Bitmap.CompressFormat.JPEG, 50, out); 293 | ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); 294 | revision.setAttachment("image", "image/jpg", in); 295 | 296 | try { 297 | revision.save(); 298 | } catch (CouchbaseLiteException e) { 299 | Log.e(Application.TAG, "Cannot attach image", e); 300 | } 301 | } 302 | 303 | private void updateCheckedStatus(Document task, boolean checked) { 304 | Map properties = new HashMap(); 305 | properties.putAll(task.getProperties()); 306 | properties.put("checked", checked); 307 | 308 | try { 309 | task.putProperties(properties); 310 | } catch (CouchbaseLiteException e) { 311 | Log.e(Application.TAG, "Cannot update checked status", e); 312 | } 313 | } 314 | 315 | private void dispatchTakePhotoIntent() { 316 | Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 317 | if (takePictureIntent.resolveActivity(this.getPackageManager()) != null) { 318 | File photoFile = null; 319 | try { 320 | photoFile = createImageFile(); 321 | } catch (IOException e) { 322 | Log.e(Application.TAG, "Cannot create a temp image file", e); 323 | } 324 | 325 | if (photoFile != null) { 326 | takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); 327 | startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); 328 | } 329 | } 330 | } 331 | 332 | private File createImageFile() throws IOException { 333 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 334 | String fileName = "TODO_LITE-" + timeStamp + "_"; 335 | File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); 336 | File image = File.createTempFile(fileName, ".jpg", storageDir); 337 | mImagePathToBeAttached = image.getAbsolutePath(); 338 | return image; 339 | } 340 | 341 | private void dispatchChoosePhotoIntent() { 342 | Intent intent = new Intent(Intent.ACTION_PICK, 343 | android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 344 | intent.setType("image/*"); 345 | startActivityForResult(Intent.createChooser(intent, "Select File"), REQUEST_CHOOSE_PHOTO); 346 | } 347 | 348 | private void deleteCurrentPhoto() { 349 | if (mImageToBeAttached != null) { 350 | mImageToBeAttached.recycle(); 351 | mImageToBeAttached = null; 352 | } 353 | 354 | ViewGroup view = (ViewGroup) findViewById(R.id.create_task); 355 | ImageView imageView = (ImageView) view.findViewById(R.id.image); 356 | imageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_camera)); 357 | } 358 | 359 | private void displayAttachImageDialog(final Document task) { 360 | CharSequence[] items; 361 | if (mImageToBeAttached != null) 362 | items = new CharSequence[] { "Take photo", "Choose photo", "Delete photo" }; 363 | else 364 | items = new CharSequence[] { "Take photo", "Choose photo" }; 365 | 366 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 367 | builder.setTitle("Add picture"); 368 | builder.setItems(items, new DialogInterface.OnClickListener() { 369 | @Override 370 | public void onClick(DialogInterface dialog, int item) { 371 | if (item == 0) { 372 | mCurrentTaskToAttachImage = task; 373 | dispatchTakePhotoIntent(); 374 | } else if (item == 1) { 375 | mCurrentTaskToAttachImage = task; 376 | dispatchChoosePhotoIntent(); 377 | } else { 378 | deleteCurrentPhoto(); 379 | } 380 | } 381 | }); 382 | builder.show(); 383 | } 384 | 385 | @Override 386 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 387 | super.onActivityResult(requestCode, resultCode, data); 388 | 389 | if (resultCode != RESULT_OK) { 390 | if (mCurrentTaskToAttachImage != null) 391 | mCurrentTaskToAttachImage = null; 392 | return; 393 | } 394 | 395 | final int size = THUMBNAIL_SIZE; 396 | Bitmap thumbnail = null; 397 | if (requestCode == REQUEST_TAKE_PHOTO) { 398 | File file = new File(mImagePathToBeAttached); 399 | if (file.exists()) { 400 | final BitmapFactory.Options options = new BitmapFactory.Options(); 401 | options.inJustDecodeBounds = true; 402 | BitmapFactory.decodeFile(mImagePathToBeAttached, options); 403 | options.inJustDecodeBounds = false; 404 | mImageToBeAttached = BitmapFactory.decodeFile(mImagePathToBeAttached, options); 405 | if (mCurrentTaskToAttachImage == null) { 406 | thumbnail = ThumbnailUtils.extractThumbnail(mImageToBeAttached, size, size); 407 | } 408 | 409 | // Delete the temporary image file 410 | file.delete(); 411 | } 412 | mImagePathToBeAttached = null; 413 | } else if (requestCode == REQUEST_CHOOSE_PHOTO) { 414 | try { 415 | Uri uri = data.getData(); 416 | ContentResolver resolver = getContentResolver(); 417 | mImageToBeAttached = MediaStore.Images.Media.getBitmap(resolver, uri); 418 | if (mCurrentTaskToAttachImage == null) { 419 | AssetFileDescriptor asset = resolver.openAssetFileDescriptor(uri, "r"); 420 | thumbnail = ImageUtil.thumbnailFromDescriptor(asset.getFileDescriptor(), size, size); 421 | } 422 | } catch (IOException e) { 423 | Log.e(Application.TAG, "Cannot get a selected photo from the gallery.", e); 424 | } 425 | } 426 | 427 | if (mImageToBeAttached != null) { 428 | if (mCurrentTaskToAttachImage != null) { 429 | attachImage(mCurrentTaskToAttachImage, mImageToBeAttached); 430 | mImageToBeAttached = null; 431 | } 432 | } 433 | 434 | if (thumbnail != null) { 435 | ImageView imageView = (ImageView) findViewById(R.id.image); 436 | imageView.setImageBitmap(thumbnail); 437 | } 438 | 439 | // Ensure resetting the task to attach an image 440 | if (mCurrentTaskToAttachImage != null) 441 | mCurrentTaskToAttachImage = null; 442 | } 443 | 444 | 445 | private class TaskAdapter extends LiveQueryAdapter { 446 | public TaskAdapter(Context context, LiveQuery query) { 447 | super(context, query); 448 | } 449 | 450 | @Override 451 | public View getView(int position, View convertView, ViewGroup parent) { 452 | if (convertView == null) { 453 | LayoutInflater inflater = (LayoutInflater) parent.getContext(). 454 | getSystemService(Context.LAYOUT_INFLATER_SERVICE); 455 | convertView = inflater.inflate(R.layout.view_task, null); 456 | } 457 | 458 | final Document task = (Document) getItem(position); 459 | if (task == null || task.getCurrentRevision() == null) { 460 | return convertView; 461 | } 462 | 463 | Bitmap thumbnail = getTaskThumbnail(task); 464 | ImageView imageView = (ImageView) convertView.findViewById(R.id.image); 465 | if (thumbnail != null) 466 | imageView.setImageBitmap(thumbnail); 467 | else 468 | imageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_camera_light)); 469 | 470 | imageView.setOnClickListener(new View.OnClickListener() { 471 | @Override 472 | public void onClick(View v) { 473 | if (task.getCurrentRevision().getAttachment("image") != null) { 474 | Intent intent = new Intent(TaskActivity.this, ImageActivity.class); 475 | intent.putExtra(ImageActivity.INTENT_TASK_DOC_ID, task.getId()); 476 | startActivity(intent); 477 | } else 478 | displayAttachImageDialog(task); 479 | } 480 | }); 481 | 482 | TextView text = (TextView) convertView.findViewById(R.id.text); 483 | text.setText((String) task.getProperty("title")); 484 | 485 | final CheckBox checkBox = (CheckBox) convertView.findViewById(R.id.checked); 486 | Boolean checkedProperty = (Boolean) task.getProperty("checked"); 487 | boolean checked = checkedProperty != null ? checkedProperty.booleanValue() : false; 488 | checkBox.setChecked(checked); 489 | checkBox.setOnClickListener(new View.OnClickListener() { 490 | @Override 491 | public void onClick(View view) { 492 | updateCheckedStatus(task, checkBox.isChecked()); 493 | } 494 | }); 495 | 496 | return convertView; 497 | } 498 | 499 | private Bitmap getTaskThumbnail(Document task) { 500 | List attachments = task.getCurrentRevision().getAttachments(); 501 | if (attachments.size() == 0) 502 | return null; 503 | 504 | Bitmap bitmap = null; 505 | InputStream is = null; 506 | final int size = THUMBNAIL_SIZE; 507 | try { 508 | BitmapFactory.Options options = new BitmapFactory.Options(); 509 | options.inJustDecodeBounds = true; 510 | is = attachments.get(0).getContent(); 511 | BitmapFactory.decodeStream(is, null, options); 512 | options.inSampleSize = ImageUtil.calculateInSampleSize(options, size, size); 513 | is.close(); 514 | 515 | options.inJustDecodeBounds = false; 516 | is = task.getCurrentRevision().getAttachments().get(0).getContent(); 517 | bitmap = BitmapFactory.decodeStream(is, null, options); 518 | bitmap = ThumbnailUtils.extractThumbnail(bitmap, size, size); 519 | } catch (Exception e) { 520 | Log.e(Application.TAG, "Cannot decode the attached image", e); 521 | } finally { 522 | try { if (is != null) is.close(); } catch (IOException e) { } 523 | } 524 | return bitmap; 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/UserProfile.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite; 2 | 3 | import android.util.Log; 4 | 5 | import com.couchbase.lite.Attachment; 6 | import com.couchbase.lite.CouchbaseLiteException; 7 | import com.couchbase.lite.Database; 8 | import com.couchbase.lite.Document; 9 | import com.couchbase.lite.QueryEnumerator; 10 | import com.couchbase.lite.QueryRow; 11 | import com.couchbase.lite.TransactionalTask; 12 | import com.couchbase.lite.UnsavedRevision; 13 | 14 | import java.util.List; 15 | 16 | public class UserProfile { 17 | public static boolean migrateGuestData(final Database guestDb, final Document profile) { 18 | boolean success = true; 19 | final Database userDB = profile.getDatabase(); 20 | if (guestDb.getLastSequenceNumber() > 0 && userDB.getLastSequenceNumber() == 0) { 21 | success = userDB.runInTransaction(new TransactionalTask() { 22 | @Override 23 | public boolean run() { 24 | try { 25 | QueryEnumerator rows = guestDb.createAllDocumentsQuery().run(); 26 | for (QueryRow row : rows) { 27 | Document doc = row.getDocument(); 28 | Document newDoc = userDB.getDocument(doc.getId()); 29 | newDoc.putProperties(doc.getUserProperties()); 30 | 31 | List attachments = doc.getCurrentRevision().getAttachments(); 32 | if (attachments.size() > 0) { 33 | UnsavedRevision rev = newDoc.getCurrentRevision().createRevision(); 34 | for (Attachment attachment : attachments) { 35 | rev.setAttachment( 36 | attachment.getName(), 37 | attachment.getContentType(), 38 | attachment.getContent()); 39 | } 40 | rev.save(); 41 | } 42 | } 43 | // Delete guest database: 44 | guestDb.delete(); 45 | } catch (CouchbaseLiteException e) { 46 | Log.e(Application.TAG, "Error when migrating guest data to user", e); 47 | return false; 48 | } 49 | return true; 50 | } 51 | }); 52 | } 53 | return success; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/util/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite.util; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.media.ThumbnailUtils; 6 | 7 | import com.couchbase.lite.Attachment; 8 | import com.couchbase.lite.CouchbaseLiteException; 9 | import com.couchbase.lite.Document; 10 | 11 | import java.io.FileDescriptor; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | 15 | public class ImageUtil { 16 | /** 17 | * Create a thumbnail bitmap from an input stream. This method is not 18 | * currently work with a FileInputStream as the FileInputStream doesn't 19 | * support mark and reset. 20 | * 21 | * @param is The input stream to read from 22 | * @param width width 23 | * @param height height 24 | * @return a result thumbnail bitmap 25 | */ 26 | public static Bitmap thumbmailFromInputStream(InputStream is, int width, int height) { 27 | Bitmap bitmap = decodeBitmapFromInputStream(is, width, height); 28 | return ThumbnailUtils.extractThumbnail(bitmap, width, height); 29 | } 30 | 31 | /** 32 | * Create a thumbnail bitmap from a filename 33 | * @param filename The full path of the file 34 | * @param width width 35 | * @param height height 36 | * @return a result thumbnail bitmap 37 | */ 38 | public static Bitmap thumbnailFromFile(String filename, int width, int height) { 39 | Bitmap bitmap = decodeBitmapFromFile(filename, width, height); 40 | return ThumbnailUtils.extractThumbnail(bitmap, width, height); 41 | } 42 | 43 | /** 44 | * Create a thumbnail bitmap from a descriptor 45 | * @param descriptor The file descriptor to read from 46 | * @param width width 47 | * @param height height 48 | * @return a result thumbnail bitmap 49 | */ 50 | public static Bitmap thumbnailFromDescriptor(FileDescriptor descriptor, int width, int height) { 51 | Bitmap bitmap = decodeBitmapFromDescriptor(descriptor, width, height); 52 | return ThumbnailUtils.extractThumbnail(bitmap, width, height); 53 | } 54 | 55 | /** 56 | * Decode and sample down a bitmap from an input stream to the requested width and height. 57 | * 58 | * @param is The input stream to read from 59 | * @param reqWidth The requested width of the resulting bitmap 60 | * @param reqHeight The requested height of the resulting bitmap 61 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 62 | * that are equal to or greater than the requested width and height 63 | */ 64 | public static Bitmap decodeBitmapFromInputStream(InputStream is, int reqWidth, int reqHeight) { 65 | // Bitmap m = BitmapFactory.decodeStream(is); 66 | // First decode with inJustDecodeBounds=true to check dimensions 67 | final BitmapFactory.Options options = new BitmapFactory.Options(); 68 | options.inJustDecodeBounds = true; 69 | BitmapFactory.decodeStream(is, null, options); 70 | 71 | // Calculate inSampleSize 72 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 73 | 74 | // Decode bitmap with inSampleSize set 75 | options.inJustDecodeBounds = false; 76 | 77 | Bitmap mm = BitmapFactory.decodeStream(is, null, options); 78 | 79 | return mm; 80 | } 81 | 82 | /** 83 | * Decode and sample down a bitmap from a file to the requested width and height. 84 | * 85 | * @param filename The full path of the file to decode 86 | * @param reqWidth The requested width of the resulting bitmap 87 | * @param reqHeight The requested height of the resulting bitmap 88 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 89 | * that are equal to or greater than the requested width and height 90 | */ 91 | public static Bitmap decodeBitmapFromFile(String filename, int reqWidth, int reqHeight) { 92 | // First decode with inJustDecodeBounds=true to check dimensions 93 | final BitmapFactory.Options options = new BitmapFactory.Options(); 94 | options.inJustDecodeBounds = true; 95 | BitmapFactory.decodeFile(filename, options); 96 | 97 | // Calculate inSampleSize 98 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 99 | 100 | // Decode bitmap with inSampleSize set 101 | options.inJustDecodeBounds = false; 102 | return BitmapFactory.decodeFile(filename, options); 103 | } 104 | 105 | /** 106 | * Decode and sample down a bitmap from a file input stream to the requested width and height. 107 | * 108 | * @param fileDescriptor The file descriptor to read from 109 | * @param reqWidth The requested width of the resulting bitmap 110 | * @param reqHeight The requested height of the resulting bitmap 111 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 112 | * that are equal to or greater than the requested width and height 113 | */ 114 | public static Bitmap decodeBitmapFromDescriptor( 115 | FileDescriptor fileDescriptor, int reqWidth, int reqHeight) { 116 | // First decode with inJustDecodeBounds=true to check dimensions 117 | final BitmapFactory.Options options = new BitmapFactory.Options(); 118 | options.inJustDecodeBounds = true; 119 | BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); 120 | 121 | // Calculate inSampleSize 122 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 123 | 124 | // Decode bitmap with inSampleSize set 125 | options.inJustDecodeBounds = false; 126 | 127 | return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); 128 | } 129 | 130 | /** 131 | * 132 | * Calculate an inSampleSize for use in a {@link android.graphics.BitmapFactory.Options} object when decoding 133 | * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This implementation calculates 134 | * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap 135 | * having a width and height equal to or larger than the requested width and height. 136 | * 137 | * @param options An options object with out* params already populated (run through a decode* 138 | * method with inJustDecodeBounds==true 139 | * @param reqWidth The requested width of the resulting bitmap 140 | * @param reqHeight The requested height of the resulting bitmap 141 | * @return The value to be used for inSampleSize 142 | */ 143 | public static int calculateInSampleSize(BitmapFactory.Options options, 144 | int reqWidth, int reqHeight) { 145 | /// Raw height and width of image 146 | final int height = options.outHeight; 147 | final int width = options.outWidth; 148 | int inSampleSize = 1; 149 | 150 | if (height > reqHeight || width > reqWidth) { 151 | 152 | final int halfHeight = height / 2; 153 | final int halfWidth = width / 2; 154 | 155 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both 156 | // height and width larger than the requested height and width. 157 | while ((halfHeight / inSampleSize) > reqHeight 158 | && (halfWidth / inSampleSize) > reqWidth) { 159 | inSampleSize *= 2; 160 | } 161 | } 162 | 163 | return inSampleSize; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/util/LiveQueryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | 9 | import com.couchbase.lite.LiveQuery; 10 | import com.couchbase.lite.QueryEnumerator; 11 | 12 | public class LiveQueryAdapter extends BaseAdapter { 13 | private LiveQuery query; 14 | private QueryEnumerator enumerator; 15 | private Context context; 16 | 17 | public LiveQueryAdapter(Context context, LiveQuery query) { 18 | this.context = context; 19 | this.query = query; 20 | 21 | query.addChangeListener(new LiveQuery.ChangeListener() { 22 | @Override 23 | public void changed(final LiveQuery.ChangeEvent event) { 24 | ((Activity) LiveQueryAdapter.this.context).runOnUiThread(new Runnable() { 25 | @Override 26 | public void run() { 27 | enumerator = event.getRows(); 28 | notifyDataSetChanged(); 29 | } 30 | }); 31 | } 32 | }); 33 | query.start(); 34 | } 35 | 36 | @Override 37 | public int getCount() { 38 | return enumerator != null ? enumerator.getCount() : 0; 39 | } 40 | 41 | @Override 42 | public Object getItem(int i) { 43 | return enumerator != null ? enumerator.getRow(i).getDocument() : null; 44 | } 45 | 46 | @Override 47 | public long getItemId(int i) { 48 | return enumerator.getRow(i).getSequenceNumber(); 49 | } 50 | 51 | @Override 52 | public View getView(int position, View convertView, ViewGroup parent) { 53 | return null; 54 | } 55 | 56 | public void invalidate() { 57 | if (query != null) 58 | query.stop(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/util/RoundImageView.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite.util; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.PorterDuff; 7 | import android.graphics.PorterDuffXfermode; 8 | import android.graphics.RectF; 9 | import android.graphics.Xfermode; 10 | import android.graphics.drawable.BitmapDrawable; 11 | import android.graphics.drawable.Drawable; 12 | import android.util.AttributeSet; 13 | import android.widget.ImageView; 14 | 15 | public class RoundImageView extends ImageView { 16 | private static final float RADIUS = 90; 17 | 18 | public RoundImageView(Context context) { 19 | super(context); 20 | } 21 | 22 | public RoundImageView(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | public RoundImageView(Context context, AttributeSet attrs, int defStyle) { 27 | super(context, attrs, defStyle); 28 | } 29 | 30 | @Override 31 | protected void onDraw(Canvas canvas) { 32 | Drawable drawable = getDrawable(); 33 | if (drawable instanceof BitmapDrawable) { 34 | RectF rectF = new RectF(drawable.getBounds()); 35 | int restoreCount = canvas.saveLayer(rectF, null, Canvas.ALL_SAVE_FLAG); 36 | getImageMatrix().mapRect(rectF); 37 | 38 | Paint paint = ((BitmapDrawable) drawable).getPaint(); 39 | paint.setAntiAlias(true); 40 | paint.setColor(0xff000000); 41 | 42 | canvas.drawARGB(0, 0, 0, 0); 43 | canvas.drawRoundRect(rectF, RADIUS, RADIUS, paint); 44 | 45 | Xfermode restoreMode = paint.getXfermode(); 46 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 47 | super.onDraw(canvas); 48 | 49 | // Restore paint and canvas 50 | paint.setXfermode(restoreMode); 51 | canvas.restoreToCount(restoreCount); 52 | } else { 53 | super.onDraw(canvas); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/couchbase/todolite/util/StringUtil.java: -------------------------------------------------------------------------------- 1 | package com.couchbase.todolite.util; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | public class StringUtil { 7 | public static String MD5(String string) { 8 | if (string == null) 9 | return null; 10 | 11 | try { 12 | MessageDigest digest = MessageDigest.getInstance("MD5"); 13 | byte[] inputBytes = string.getBytes(); 14 | byte[] hashBytes = digest.digest(inputBytes); 15 | return byteArrayToHex(hashBytes); 16 | } catch (NoSuchAlgorithmException e) { } 17 | 18 | return null; 19 | } 20 | 21 | private static String byteArrayToHex(byte[] a) { 22 | StringBuilder sb = new StringBuilder(a.length * 2); 23 | for(byte b: a) 24 | sb.append(String.format("%02x", b & 0xff)); 25 | return sb.toString(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/ToDoLite-Android/eeda0e7fe246baa366a2646e824bc2635fb939ef/app/src/main/res/drawable-xhdpi/ic_camera.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_camera_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/ToDoLite-Android/eeda0e7fe246baa366a2646e824bc2635fb939ef/app/src/main/res/drawable-xhdpi/ic_camera_light.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_image.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | 26 | 27 |