Browse Source

Merge pull request #874 from nextcloud/contacts-fragment-replace

Contacts backup improvements/bugfixes
Mario Đanić 8 years ago
parent
commit
f07c78221d
21 changed files with 1243 additions and 475 deletions
  1. 2 1
      src/main/AndroidManifest.xml
  2. 234 0
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  3. 9 1
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  4. 61 12
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  5. 9 8
      src/main/java/com/owncloud/android/services/ContactsBackupJob.java
  6. 1 3
      src/main/java/com/owncloud/android/services/ContactsImportJob.java
  7. 32 251
      src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java
  8. 2 1
      src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  9. 4 6
      src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  10. 8 8
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  11. 16 4
      src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java
  12. 28 0
      src/main/java/com/owncloud/android/ui/events/VCardToggleEvent.java
  13. 207 61
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
  14. 463 0
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsBackupFragment.java
  15. 0 32
      src/main/java/com/owncloud/android/utils/PermissionUtil.java
  16. 20 7
      src/main/res/layout/contactlist_fragment.xml
  17. 0 1
      src/main/res/layout/contactlist_list_item.xml
  18. 107 0
      src/main/res/layout/contacts_backup_fragment.xml
  19. 4 78
      src/main/res/layout/contacts_preference.xml
  20. 33 0
      src/main/res/menu/contactlist_menu.xml
  21. 3 1
      src/main/res/values/strings.xml

+ 2 - 1
src/main/AndroidManifest.xml

@@ -88,7 +88,8 @@
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.ExternalSiteWebView"
                   android:configChanges="orientation|screenSize|keyboardHidden" />
-        <activity android:name=".ui.activity.ContactsPreferenceActivity" />
+        <activity android:name=".ui.activity.ContactsPreferenceActivity"
+            android:launchMode="singleInstance"/>
         <activity android:name=".ui.activity.ReceiveExternalFilesActivity"
                   
                   android:taskAffinity=""

+ 234 - 0
src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java

@@ -0,0 +1,234 @@
+/**
+ * Nextcloud Android client application
+ *
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.datamodel;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.owncloud.android.db.ProviderMeta;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.ArrayList;
+
+/**
+ * Database provider for handling the persistence aspects of arbitrary data table.
+ */
+
+public class ArbitraryDataProvider {
+    static private final String TAG = ArbitraryDataProvider.class.getSimpleName();
+
+    private ContentResolver contentResolver;
+
+    public ArbitraryDataProvider(ContentResolver contentResolver) {
+        if (contentResolver == null) {
+            throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver");
+        }
+        this.contentResolver = contentResolver;
+    }
+
+    public void storeOrUpdateKeyValue(Account account, String key, String newValue) {
+
+
+        ArbitraryDataSet data = getArbitraryDataSet(account, key);
+        if (data == null) {
+            Log_OC.v(TAG, "Adding arbitrary data with cloud id: " + account.name + " key: " + key
+                    + " value: " + newValue);
+            ContentValues cv = new ContentValues();
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID, account.name);
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY, key);
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE, newValue);
+
+            Uri result = contentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA, cv);
+
+            if (result == null) {
+                Log_OC.v(TAG, "Failed to store arbitrary data with cloud id: " + account.name + " key: " + key
+                        + " value: " + newValue);
+            }
+        } else {
+            Log_OC.v(TAG, "Updating arbitrary data with cloud id: " + account.name + " key: " + key
+                    + " value: " + newValue);
+            ContentValues cv = new ContentValues();
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID, data.getCloudId());
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY, data.getKey());
+            cv.put(ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE, newValue);
+
+            int result = contentResolver.update(
+                    ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
+                    cv,
+                    ProviderMeta.ProviderTableMeta._ID + "=?",
+                    new String[]{String.valueOf(data.getId())}
+            );
+
+            if (result == 0) {
+                Log_OC.v(TAG, "Failed to update arbitrary data with cloud id: " + account.name + " key: " + key
+                        + " value: " + newValue);
+            }
+        }
+    }
+
+
+    public Long getLongValue(Account account, String key) {
+        String value = getValue(account, key);
+
+        if (value.isEmpty()) {
+            return -1l;
+        } else {
+            return Long.valueOf(value);
+        }
+    }
+
+    public boolean getBooleanValue(Account account, String key) {
+        String value = getValue(account, key);
+
+        return !value.isEmpty() && value.equalsIgnoreCase("true");
+    }
+
+    private ArrayList<String> getValues(Account account, String key) {
+        Cursor cursor = contentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
+                null,
+                ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + " = ?",
+                new String[]{account.name, key},
+                null
+        );
+
+        if (cursor != null) {
+            ArrayList<String> list = new ArrayList<>();
+            if (cursor.moveToFirst()) {
+                do {
+                    String value = cursor.getString(cursor.getColumnIndex(
+                            ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE));
+                    if (value == null) {
+                        Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
+                    } else {
+                        list.add(value);
+                    }
+                } while (cursor.moveToNext());
+            }
+            cursor.close();
+            return list;
+        } else {
+            Log_OC.e(TAG, "DB error restoring arbitrary values.");
+        }
+
+        return new ArrayList<>();
+    }
+
+    public String getValue(Account account, String key) {
+        Cursor cursor = contentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
+                null,
+                ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + " = ?",
+                new String[]{account.name, key},
+                null
+        );
+
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                String value = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE));
+                if (value == null) {
+                    Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
+                } else {
+                    return value;
+                }
+            }
+            cursor.close();
+            return "";
+        } else {
+            Log_OC.e(TAG, "DB error restoring arbitrary values.");
+        }
+
+        return "";
+    }
+
+    private ArbitraryDataSet getArbitraryDataSet(Account account, String key) {
+        Cursor cursor = contentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA,
+                null,
+                ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " = ? and " +
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY + " = ?",
+                new String[]{account.name, key},
+                null
+        );
+
+        ArbitraryDataSet dataSet = null;
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                int id = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta._ID));
+                String dbAccount = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID));
+                String dbKey = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_KEY));
+                String dbValue = cursor.getString(cursor.getColumnIndex(
+                        ProviderMeta.ProviderTableMeta.ARBITRARY_DATA_VALUE));
+
+                if (id == -1) {
+                    Log_OC.e(TAG, "Arbitrary value could not be created from cursor");
+                } else {
+                    dataSet = new ArbitraryDataSet(id, dbAccount, dbKey, dbValue);
+                }
+            }
+            cursor.close();
+        } else {
+            Log_OC.e(TAG, "DB error restoring arbitrary values.");
+        }
+
+        return dataSet;
+    }
+
+
+    public class ArbitraryDataSet {
+        private int id;
+        private String cloudId;
+        private String key;
+        private String value;
+
+        public ArbitraryDataSet(int id, String cloudId, String key, String value) {
+            this.id = id;
+            this.cloudId = cloudId;
+            this.key = key;
+            this.value = value;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public String getCloudId() {
+            return cloudId;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+}

+ 9 - 1
src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -32,7 +32,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {
 
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 19;
+    public static final int DB_VERSION = 20;
 
     private ProviderMeta() {
     }
@@ -44,6 +44,7 @@ public class ProviderMeta {
         public static final String UPLOADS_TABLE_NAME = "list_of_uploads";
         public static final String SYNCED_FOLDERS_TABLE_NAME = "synced_folders";
         public static final String EXTERNAL_LINKS_TABLE_NAME = "external_links";
+        public static final String ARBITRARY_DATA_TABLE_NAME = "arbitrary_data";
 
         private static final String CONTENT_PREFIX = "content://";
 
@@ -63,6 +64,8 @@ public class ProviderMeta {
                 + MainApp.getAuthority() + "/synced_folders");
         public static final Uri CONTENT_URI_EXTERNAL_LINKS = Uri.parse(CONTENT_PREFIX
                 + MainApp.getAuthority() + "/external_links");
+        public static final Uri CONTENT_URI_ARBITRARY_DATA = Uri.parse(CONTENT_PREFIX
+                + MainApp.getAuthority() + "/arbitrary_data");
 
         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file";
         public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.owncloud.file";
@@ -177,5 +180,10 @@ public class ProviderMeta {
         public static final String EXTERNAL_LINKS_TYPE = "type";
         public static final String EXTERNAL_LINKS_NAME = "name";
         public static final String EXTERNAL_LINKS_URL = "url";
+
+        // Columns of arbitrary data table
+        public static final String ARBITRARY_DATA_CLOUD_ID = "cloud_id";
+        public static final String ARBITRARY_DATA_KEY = "key";
+        public static final String ARBITRARY_DATA_VALUE = "value";
     }
 }

+ 61 - 12
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -72,6 +72,7 @@ public class FileContentProvider extends ContentProvider {
     private static final int UPLOADS = 6;
     private static final int SYNCED_FOLDERS = 7;
     private static final int EXTERNAL_LINKS = 8;
+    private static final int ARBITRARY_DATA = 9;
 
     private static final String TAG = FileContentProvider.class.getSimpleName();
 
@@ -201,6 +202,9 @@ public class FileContentProvider extends ContentProvider {
             case EXTERNAL_LINKS:
                 count = db.delete(ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME, where, whereArgs);
                 break;
+            case ARBITRARY_DATA:
+                count = db.delete(ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME, where, whereArgs);
+                break;
             default:
                 //Log_OC.e(TAG, "Unknown uri " + uri);
                 throw new IllegalArgumentException("Unknown uri: " + uri.toString());
@@ -335,6 +339,18 @@ public class FileContentProvider extends ContentProvider {
                 }
                 return insertedExternalLinkUri;
 
+            case ARBITRARY_DATA:
+                Uri insertedArbitraryDataUri = null;
+                long arbitraryDataId = db.insert(ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME, null, values);
+                if (arbitraryDataId > 0) {
+                    insertedArbitraryDataUri =
+                            ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_ARBITRARY_DATA, arbitraryDataId);
+                } else {
+                    throw new SQLException("ERROR " + uri);
+
+                }
+                return insertedArbitraryDataUri;
+
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -385,6 +401,7 @@ public class FileContentProvider extends ContentProvider {
         mUriMatcher.addURI(authority, "uploads/#", UPLOADS);
         mUriMatcher.addURI(authority, "synced_folders", SYNCED_FOLDERS);
         mUriMatcher.addURI(authority, "external_links", EXTERNAL_LINKS);
+        mUriMatcher.addURI(authority, "arbitrary_data", ARBITRARY_DATA);
 
         return true;
     }
@@ -473,6 +490,13 @@ public class FileContentProvider extends ContentProvider {
                             + uri.getPathSegments().get(1));
                 }
                 break;
+            case ARBITRARY_DATA:
+                sqlQuery.setTables(ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME);
+                if (uri.getPathSegments().size() > 1) {
+                    sqlQuery.appendWhere(ProviderTableMeta._ID + "="
+                            + uri.getPathSegments().get(1));
+                }
+                break;
             default:
                 throw new IllegalArgumentException("Unknown uri id: " + uri);
         }
@@ -495,6 +519,9 @@ public class FileContentProvider extends ContentProvider {
                 case EXTERNAL_LINKS:
                     order = ProviderTableMeta.EXTERNAL_LINKS_NAME;
                     break;
+                case ARBITRARY_DATA:
+                    order = ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID;
+                    break;
                 default: // Files
                     order = ProviderTableMeta.FILE_DEFAULT_SORT_ORDER;
                     break;
@@ -538,25 +565,19 @@ public class FileContentProvider extends ContentProvider {
             case DIRECTORY:
                 return 0; //updateFolderSize(db, selectionArgs[0]);
             case SHARES:
-                return db.update(
-                        ProviderTableMeta.OCSHARES_TABLE_NAME, values, selection, selectionArgs
-                );
+                return db.update(ProviderTableMeta.OCSHARES_TABLE_NAME, values, selection, selectionArgs);
             case CAPABILITIES:
-                return db.update(
-                        ProviderTableMeta.CAPABILITIES_TABLE_NAME, values, selection, selectionArgs
-                );
+                return db.update(ProviderTableMeta.CAPABILITIES_TABLE_NAME, values, selection, selectionArgs);
             case UPLOADS:
-                int ret = db.update(
-                        ProviderTableMeta.UPLOADS_TABLE_NAME, values, selection, selectionArgs
-                );
+                int ret = db.update(ProviderTableMeta.UPLOADS_TABLE_NAME, values, selection, selectionArgs);
                 trimSuccessfulUploads(db);
                 return ret;
             case SYNCED_FOLDERS:
                 return db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, values, selection, selectionArgs);
+            case ARBITRARY_DATA:
+                return db.update(ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME, values, selection, selectionArgs);
             default:
-                return db.update(
-                        ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs
-                );
+                return db.update(ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs);
         }
     }
 
@@ -611,6 +632,9 @@ public class FileContentProvider extends ContentProvider {
 
             // Create external links table
             createExternalLinksTable(db);
+
+            // Create arbitrary data table
+            createArbitraryData(db);
         }
 
         @Override
@@ -927,6 +951,22 @@ public class FileContentProvider extends ContentProvider {
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
+            if (oldVersion < 20 && newVersion >= 20) {
+                Log_OC.i(SQL, "Adding arbitrary data table");
+                db.beginTransaction();
+                try {
+                    createArbitraryData(db);
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
         }
     }
 
@@ -1064,6 +1104,15 @@ public class FileContentProvider extends ContentProvider {
         );
     }
 
+    private void createArbitraryData(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME + "("
+                + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "      // id
+                + ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " TEXT, " // cloud id (account name + FQDN)
+                + ProviderTableMeta.ARBITRARY_DATA_KEY + " TEXT, "      // key
+                + ProviderTableMeta.ARBITRARY_DATA_VALUE + " TEXT) "    // value
+        );
+    }
+
     /**
      * Version 10 of database does not modify its scheme. It coincides with the upgrade of the ownCloud account names
      * structure to include in it the path to the server instance. Updating the account names and path to local files

+ 9 - 8
src/main/java/com/owncloud/android/services/ContactsBackupJob.java

@@ -26,7 +26,6 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.IBinder;
@@ -39,9 +38,9 @@ import com.evernote.android.job.util.support.PersistableBundleCompat;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
@@ -76,13 +75,15 @@ public class ContactsBackupJob extends Job {
 
         boolean force = bundle.getBoolean(FORCE, false);
 
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-        Long lastExecution = sharedPreferences.getLong(ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP, -1);
+        final Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString(ACCOUNT, ""));
+
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+        Long lastExecution = arbitraryDataProvider.getLongValue(account,
+                ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP);
 
         if (force || (lastExecution + 24 * 60 * 60 * 1000) < Calendar.getInstance().getTimeInMillis()) {
             Log_OC.d(TAG, "start contacts backup job");
 
-            final Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString(ACCOUNT, ""));
             String backupFolder = getContext().getResources().getString(R.string.contacts_backup_folder) +
                     OCFile.PATH_SEPARATOR;
             Integer daysToExpire = getContext().getResources().getInteger(R.integer.contacts_backup_expire);
@@ -96,9 +97,9 @@ public class ContactsBackupJob extends Job {
                     OperationsService.BIND_AUTO_CREATE);
 
             // store execution date
-            sharedPreferences.edit().putLong(ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP,
-                    Calendar.getInstance().getTimeInMillis()).apply();
-
+            arbitraryDataProvider.storeOrUpdateKeyValue(account,
+                    ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP,
+                    String.valueOf(Calendar.getInstance().getTimeInMillis()));
         } else {
             Log_OC.d(TAG, "last execution less than 24h ago");
         }

+ 1 - 3
src/main/java/com/owncloud/android/services/ContactsImportJob.java

@@ -64,9 +64,7 @@ public class ContactsImportJob extends Job {
             vCards.addAll(Ezvcard.parse(file).all());
 
             for (int i = 0; i < intArray.length; i++ ){
-                if (intArray[i] == 1){
-                    operations.insertContact(vCards.get(i));
-                }
+                operations.insertContact(vCards.get(intArray[i]));
             }
         } catch (Exception e) {
             Log_OC.e(TAG, e.getMessage());

+ 32 - 251
src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java

@@ -21,43 +21,29 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.Manifest;
 import android.accounts.Account;
-import android.app.DatePickerDialog;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.support.annotation.NonNull;
 import android.support.design.widget.BottomNavigationView;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
-import android.support.v7.widget.SwitchCompat;
-import android.view.MenuItem;
 import android.view.View;
-import android.widget.CompoundButton;
-import android.widget.DatePicker;
-import android.widget.TextView;
-import android.widget.Toast;
 
 import com.evernote.android.job.JobManager;
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.support.PersistableBundleCompat;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.services.ContactsBackupJob;
 import com.owncloud.android.ui.fragment.FileFragment;
+import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
+import com.owncloud.android.ui.fragment.contactsbackup.ContactsBackupFragment;
 import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.PermissionUtil;
 
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Comparator;
+import org.parceler.Parcels;
+
 import java.util.Set;
-import java.util.Vector;
 
 /**
  * This activity shows all settings for contact backup/restore
@@ -69,9 +55,6 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
     public static final String PREFERENCE_CONTACTS_AUTOMATIC_BACKUP = "PREFERENCE_CONTACTS_AUTOMATIC_BACKUP";
     public static final String PREFERENCE_CONTACTS_LAST_BACKUP = "PREFERENCE_CONTACTS_LAST_BACKUP";
 
-    private SwitchCompat backupSwitch;
-    private SharedPreferences sharedPreferences;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -84,41 +67,23 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
         // setup drawer
         setupDrawer(R.id.nav_contacts);
 
-        getSupportActionBar().setTitle(R.string.actionbar_contacts);
-        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
-        sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
-
-        backupSwitch = (SwitchCompat) findViewById(R.id.contacts_automatic_backup);
-        backupSwitch.setChecked(sharedPreferences.getBoolean(PREFERENCE_CONTACTS_AUTOMATIC_BACKUP, false));
-
-        backupSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                if (isChecked &&
-                        checkAndAskForContactsReadPermission(PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC)) {
-                    // store value
-                    setAutomaticBackup(backupSwitch, true);
-
-                    // enable daily job
-                    startContactBackupJob(getAccount());
-                } else {
-                    setAutomaticBackup(backupSwitch, false);
-
-                    // cancel pending jobs
-                    cancelContactBackupJob(getBaseContext());
-                }
+        Intent intent = getIntent();
+        if (savedInstanceState == null) {
+            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+            if (intent == null || intent.getParcelableExtra(ContactListFragment.FILE_NAME) == null ||
+                    intent.getParcelableExtra(ContactListFragment.ACCOUNT) == null) {
+                ContactsBackupFragment fragment = new ContactsBackupFragment();
+                Bundle bundle = new Bundle();
+                bundle.putParcelable(ContactListFragment.ACCOUNT, getAccount());
+                fragment.setArguments(bundle);
+                transaction.add(R.id.frame_container, fragment);
+            } else {
+                OCFile file = Parcels.unwrap(intent.getParcelableExtra(ContactListFragment.FILE_NAME));
+                Account account = Parcels.unwrap(intent.getParcelableExtra(ContactListFragment.ACCOUNT));
+                ContactListFragment contactListFragment = ContactListFragment.newInstance(file, account);
+                transaction.add(R.id.frame_container, contactListFragment);
             }
-        });
-
-        // display last backup
-        TextView lastBackup = (TextView) findViewById(R.id.contacts_last_backup_timestamp);
-        Long lastBackupTimestamp = sharedPreferences.getLong(PREFERENCE_CONTACTS_LAST_BACKUP, -1);
-
-        if (lastBackupTimestamp == -1) {
-            lastBackup.setText(R.string.contacts_preference_backup_never);
-        } else {
-            lastBackup.setText(DisplayUtils.getRelativeTimestamp(getBaseContext(), lastBackupTimestamp));
+            transaction.commit();
         }
 
         BottomNavigationView bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottom_navigation_view);
@@ -129,184 +94,6 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
         }
     }
 
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-
-        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) {
-            for (int index = 0; index < permissions.length; index++) {
-                if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
-                    if (grantResults[index] >= 0) {
-                        setAutomaticBackup(backupSwitch, true);
-                    } else {
-                        setAutomaticBackup(backupSwitch, false);
-                    }
-
-                    break;
-                }
-            }
-        }
-
-        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_MANUALLY) {
-            for (int index = 0; index < permissions.length; index++) {
-                if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
-                    if (grantResults[index] >= 0) {
-                        startContactsBackupJob();
-                    }
-
-                    break;
-                }
-            }
-        }
-    }
-
-    public void backupContacts(View v) {
-        if (checkAndAskForContactsReadPermission(PermissionUtil.PERMISSIONS_READ_CONTACTS_MANUALLY)) {
-            startContactsBackupJob();
-        }
-    }
-
-    private void startContactsBackupJob() {
-        PersistableBundleCompat bundle = new PersistableBundleCompat();
-        bundle.putString(ContactsBackupJob.ACCOUNT, getAccount().name);
-        bundle.putBoolean(ContactsBackupJob.FORCE, true);
-
-        new JobRequest.Builder(ContactsBackupJob.TAG)
-                .setExtras(bundle)
-                .setExecutionWindow(3_000L, 10_000L)
-                .setRequiresCharging(false)
-                .setPersisted(false)
-                .setUpdateCurrent(false)
-                .build()
-                .schedule();
-
-        Snackbar.make(findViewById(R.id.contacts_linear_layout), R.string.contacts_preferences_backup_scheduled,
-                Snackbar.LENGTH_LONG).show();
-    }
-
-    private void setAutomaticBackup(SwitchCompat backupSwitch, boolean bool) {
-        backupSwitch.setChecked(bool);
-        SharedPreferences.Editor editor = sharedPreferences.edit();
-        editor.putBoolean(PREFERENCE_CONTACTS_AUTOMATIC_BACKUP, bool);
-        editor.apply();
-    }
-
-    private boolean checkAndAskForContactsReadPermission(final int permission) {
-        // check permissions
-        if ((PermissionUtil.checkSelfPermission(this, Manifest.permission.READ_CONTACTS))) {
-            return true;
-        } else {
-            // Check if we should show an explanation
-            if (PermissionUtil.shouldShowRequestPermissionRationale(ContactsPreferenceActivity.this,
-                    android.Manifest.permission.READ_CONTACTS)) {
-                // Show explanation to the user and then request permission
-                Snackbar snackbar = Snackbar.make(findViewById(R.id.contacts_linear_layout), R.string.contacts_read_permission,
-                        Snackbar.LENGTH_INDEFINITE)
-                        .setAction(R.string.common_ok, new View.OnClickListener() {
-                            @Override
-                            public void onClick(View v) {
-                                PermissionUtil.requestReadContactPermission(ContactsPreferenceActivity.this, permission);
-                            }
-                        });
-
-                DisplayUtils.colorSnackbar(this, snackbar);
-
-                snackbar.show();
-
-                return false;
-            } else {
-                // No explanation needed, request the permission.
-                PermissionUtil.requestReadContactPermission(ContactsPreferenceActivity.this, permission);
-
-                return false;
-            }
-        }
-    }
-
-    public void openDate(View v) {
-        String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
-        OCFile backupFolder = getStorageManager().getFileByPath(backupFolderString);
-
-        Vector<OCFile> backupFiles = getStorageManager().getFolderContent(backupFolder, false);
-
-        Collections.sort(backupFiles, new Comparator<OCFile>() {
-            @Override
-            public int compare(OCFile o1, OCFile o2) {
-                if (o1.getModificationTimestamp() == o2.getModificationTimestamp()) {
-                    return 0;
-                }
-
-                if (o1.getModificationTimestamp() > o2.getModificationTimestamp()) {
-                    return 1;
-                } else {
-                    return -1;
-                }
-            }
-        });
-
-        Calendar cal = Calendar.getInstance();
-        int year = cal.get(Calendar.YEAR);
-        int month = cal.get(Calendar.MONTH) + 1;
-        int day = cal.get(Calendar.DAY_OF_MONTH);
-
-        DatePickerDialog.OnDateSetListener dateSetListener = new DatePickerDialog.OnDateSetListener() {
-            @Override
-            public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
-                String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
-                OCFile backupFolder = getStorageManager().getFileByPath(backupFolderString);
-                Vector<OCFile> backupFiles = getStorageManager().getFolderContent(backupFolder, false);
-
-                // find file with modification with date and time between 00:00 and 23:59
-                // if more than one file exists, take oldest
-                Calendar date = Calendar.getInstance();
-                date.set(year, month, dayOfMonth);
-
-                // start
-                date.set(Calendar.HOUR, 0);
-                date.set(Calendar.MINUTE, 0);
-                date.set(Calendar.SECOND, 1);
-                date.set(Calendar.MILLISECOND, 0);
-                date.set(Calendar.AM_PM, Calendar.AM);
-                Long start = date.getTimeInMillis();
-
-                // end
-                date.set(Calendar.HOUR, 23);
-                date.set(Calendar.MINUTE, 59);
-                date.set(Calendar.SECOND, 59);
-                Long end = date.getTimeInMillis();
-
-                OCFile backupToRestore = null;
-
-                for (OCFile file : backupFiles) {
-                    if (start < file.getModificationTimestamp() && end > file.getModificationTimestamp()) {
-                        if (backupToRestore == null) {
-                            backupToRestore = file;
-                        } else if (backupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) {
-                            backupToRestore = file;
-                        }
-                    }
-                }
-
-                if (backupToRestore != null) {
-                    Fragment contactListFragment = ContactListFragment.newInstance(backupToRestore, getAccount());
-
-                    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-                    transaction.replace(R.id.contacts_linear_layout, contactListFragment);
-                    transaction.commit();
-                } else {
-                    Toast.makeText(ContactsPreferenceActivity.this, R.string.contacts_preferences_no_file_found,
-                            Toast.LENGTH_SHORT).show();
-                }
-            }
-        };
-
-        DatePickerDialog datePickerDialog = new DatePickerDialog(this, dateSetListener, year, month, day);
-        datePickerDialog.getDatePicker().setMaxDate(backupFiles.lastElement().getModificationTimestamp());
-        datePickerDialog.getDatePicker().setMinDate(backupFiles.firstElement().getModificationTimestamp());
-
-        datePickerDialog.show();
-    }
-
     public static void startContactBackupJob(Account account) {
         Log_OC.d(TAG, "start daily contacts backup job");
 
@@ -323,8 +110,8 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
                 .schedule();
     }
 
-    public static void cancelContactBackupJob(Context context) {
-        Log_OC.d(TAG, "disabling contacts backup job");
+    public static void cancelAllContactBackupJobs(Context context) {
+        Log_OC.d(TAG, "disabling all contacts backup job");
 
         JobManager jobManager = JobManager.create(context);
         Set<JobRequest> jobs = jobManager.getAllJobRequestsForTag(ContactsBackupJob.TAG);
@@ -334,27 +121,21 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
         }
     }
 
+    public static void cancelContactBackupJobForAccount(Context context, Account account) {
+        Log_OC.d(TAG, "disabling contacts backup job for account: " + account.name);
 
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        boolean retval;
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                if (isDrawerOpen()) {
-                    closeDrawer();
-                } else {
-                    openDrawer();
-                }
-                retval = true;
-                break;
+        JobManager jobManager = JobManager.create(context);
+        Set<JobRequest> jobs = jobManager.getAllJobRequestsForTag(ContactsBackupJob.TAG);
 
-            default:
-                retval = super.onOptionsItemSelected(item);
-                break;
+        for (JobRequest jobRequest : jobs) {
+            PersistableBundleCompat extras = jobRequest.getExtras();
+            if (extras.getString(ContactsBackupJob.ACCOUNT, "").equalsIgnoreCase(account.name)) {
+                jobManager.cancel(jobRequest.getJobId());
+            }
         }
-        return retval;
     }
 
+
     @Override
     public void showFiles(boolean onDeviceOnly) {
         super.showFiles(onDeviceOnly);

+ 2 - 1
src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -1199,7 +1199,8 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
 
                     Account account = AccountUtils.getCurrentOwnCloudAccount(DrawerActivity.this);
 
-                    if (account != null && getStorageManager().getCapability(account.name) != null &&
+                    if (account != null && getStorageManager() != null &&
+                            getStorageManager().getCapability(account.name) != null &&
                             getStorageManager().getCapability(account.name).getExternalLinks().isTrue()) {
 
                         int count = sharedPreferences.getInt(EXTERNAL_LINKS_COUNT, -1);

+ 4 - 6
src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -28,7 +28,6 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -41,8 +40,8 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.authentication.AuthenticatorActivity;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
@@ -245,12 +244,11 @@ public abstract class FileActivity extends DrawerActivity
     }
 
     private void checkContactsBackupJob() {
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContentResolver());
 
-        if (sharedPreferences.getBoolean(ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP, false)) {
+        if (getAccount() != null && arbitraryDataProvider.getBooleanValue(getAccount(),
+                ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP)) {
             ContactsPreferenceActivity.startContactBackupJob(getAccount());
-        } else {
-            ContactsPreferenceActivity.cancelContactBackupJob(getBaseContext());
         }
     }
 

+ 8 - 8
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -83,6 +83,7 @@ import com.owncloud.android.services.observer.FileObserverService;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
 import com.owncloud.android.ui.events.TokenPushEvent;
+import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
 import com.owncloud.android.ui.fragment.ExtendedListFragment;
 import com.owncloud.android.ui.fragment.FileDetailFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -101,6 +102,7 @@ import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.PermissionUtil;
 
 import org.greenrobot.eventbus.EventBus;
+import org.parceler.Parcels;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -433,7 +435,7 @@ public class FileDisplayActivity extends HookActivity
                 updateActionBarTitleAndHomeButton(file);
             } else {
                 cleanSecondFragment();
-                if (file.isDown() && MimeTypeUtil.isVCard(file.getMimetype())){
+                if (file.isDown() && MimeTypeUtil.isVCard(file.getMimetype())) {
                     startContactListFragment(file);
                 } else if (file.isDown() && PreviewTextFragment.canBePreviewed(file)) {
                     startTextPreview(file);
@@ -602,7 +604,7 @@ public class FileDisplayActivity extends HookActivity
                         if (PreviewMediaFragment.canBePreviewed(mWaitingToPreview)) {
                             startMediaPreview(mWaitingToPreview, 0, true);
                             detailsFragmentChanged = true;
-                        } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimetype())){
+                        } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimetype())) {
                             startContactListFragment(mWaitingToPreview);
                             detailsFragmentChanged = true;
                         } else if (PreviewTextFragment.canBePreviewed(mWaitingToPreview)) {
@@ -1951,12 +1953,10 @@ public class FileDisplayActivity extends HookActivity
     }
 
     public void startContactListFragment(OCFile file) {
-        Fragment contactListFragment = ContactListFragment.newInstance(file, getAccount());
-
-        setSecondFragment(contactListFragment);
-        updateFragmentsVisibility(true);
-        updateActionBarTitleAndHomeButton(file);
-        setFile(file);
+        Intent intent = new Intent(this, ContactsPreferenceActivity.class);
+        intent.putExtra(ContactListFragment.FILE_NAME, Parcels.wrap(file));
+        intent.putExtra(ContactListFragment.ACCOUNT, Parcels.wrap(getAccount()));
+        startActivity(intent);
     }
 
     /**

+ 16 - 4
src/main/java/com/owncloud/android/ui/activity/UserInfoActivity.java

@@ -50,6 +50,7 @@ import android.widget.TextView;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.authentication.AuthenticatorActivity;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.lib.common.UserInfo;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -337,11 +338,22 @@ public class UserInfoActivity extends FileActivity {
                             new DialogInterface.OnClickListener() {
                                 @Override
                                 public void onClick(DialogInterface dialogInterface, int i) {
-                                    Bundle bundle = new Bundle();
-                                    bundle.putParcelable(KEY_ACCOUNT, Parcels.wrap(account));
-                                    Intent intent = new Intent();
-                                    intent.putExtras(bundle);
+                                    // remove contact backup job
+                                    ContactsPreferenceActivity.cancelContactBackupJobForAccount(getActivity(), account);
+
+                                    // disable daily backup
+                                    ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(
+                                            getActivity().getContentResolver());
+
+                                    arbitraryDataProvider.storeOrUpdateKeyValue(account,
+                                            ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
+                                            "false");
+
                                     if (getActivity() != null && !removeDirectly) {
+                                        Bundle bundle = new Bundle();
+                                        bundle.putParcelable(KEY_ACCOUNT, Parcels.wrap(account));
+                                        Intent intent = new Intent();
+                                        intent.putExtras(bundle);
                                         getActivity().setResult(KEY_DELETE_CODE, intent);
                                         getActivity().finish();
                                     } else {

+ 28 - 0
src/main/java/com/owncloud/android/ui/events/VCardToggleEvent.java

@@ -0,0 +1,28 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.events;
+
+public class VCardToggleEvent {
+    public boolean showRestoreButton;
+
+    public VCardToggleEvent(boolean showRestore) {
+        this.showRestoreButton = showRestore;
+    }
+}

+ 207 - 61
src/main/java/com/owncloud/android/ui/activity/ContactListFragment.java → src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java

@@ -19,7 +19,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.owncloud.android.ui.activity;
+package com.owncloud.android.ui.fragment.contactsbackup;
 
 import android.Manifest;
 import android.accounts.Account;
@@ -30,6 +30,7 @@ import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Bundle;
+import android.os.Handler;
 import android.provider.ContactsContract;
 import android.support.annotation.NonNull;
 import android.support.design.widget.Snackbar;
@@ -39,12 +40,15 @@ import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.CheckedTextView;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.support.PersistableBundleCompat;
@@ -54,10 +58,16 @@ import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.services.ContactsImportJob;
 import com.owncloud.android.ui.TextDrawable;
+import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
+import com.owncloud.android.ui.events.VCardToggleEvent;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.PermissionUtil;
 
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -65,6 +75,8 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import butterknife.BindView;
+import butterknife.ButterKnife;
 import ezvcard.Ezvcard;
 import ezvcard.VCard;
 import ezvcard.property.StructuredName;
@@ -72,15 +84,24 @@ import ezvcard.property.StructuredName;
 /**
  * This fragment shows all contacts from a file and allows to import them.
  */
-
 public class ContactListFragment extends FileFragment {
     public static final String TAG = ContactListFragment.class.getSimpleName();
 
     public static final String FILE_NAME = "FILE_NAME";
     public static final String ACCOUNT = "ACCOUNT";
 
-    private RecyclerView recyclerView;
-    private Set<Integer> checkedVCards;
+    public static final String CHECKED_ITEMS_ARRAY_KEY = "CHECKED_ITEMS";
+
+    @BindView(R.id.contactlist_recyclerview)
+    public RecyclerView recyclerView;
+
+    @BindView(R.id.contactlist_restore_selected_container)
+    public LinearLayout restoreContactsContainer;
+
+    @BindView(R.id.contactlist_restore_selected)
+    public Button restoreContacts;
+
+    private ContactListAdapter contactListAdapter;
 
     public static ContactListFragment newInstance(OCFile file, Account account) {
         ContactListFragment frag = new ContactListFragment();
@@ -92,14 +113,29 @@ public class ContactListFragment extends FileFragment {
         return frag;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.contactlist_menu, menu);
+    }
+
     @Override
     public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
         View view = inflater.inflate(R.layout.contactlist_fragment, null);
+        ButterKnife.bind(this, view);
+
         setHasOptionsMenu(true);
 
+        ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+        contactsPreferenceActivity.getSupportActionBar().setTitle(R.string.actionbar_contacts_restore);
+        contactsPreferenceActivity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
+
         ArrayList<VCard> vCards = new ArrayList<>();
-        checkedVCards = new HashSet<>();
 
         try {
             OCFile ocFile = getArguments().getParcelable(FILE_NAME);
@@ -116,10 +152,9 @@ public class ContactListFragment extends FileFragment {
                 vCards.addAll(Ezvcard.parse(file).all());
             }
         } catch (IOException e) {
-            e.printStackTrace();
+            Log_OC.e(TAG, "Error processing contacts file!", e);
         }
 
-        final Button restoreContacts = (Button) view.findViewById(R.id.contactlist_restore_selected);
         restoreContacts.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -132,48 +167,95 @@ public class ContactListFragment extends FileFragment {
 
         recyclerView = (RecyclerView) view.findViewById(R.id.contactlist_recyclerview);
 
-
-        ContactListAdapter.OnVCardClickListener vCardClickListener = new ContactListAdapter.OnVCardClickListener() {
-            private void setRestoreButton() {
-                if (checkedVCards.size() > 0) {
-                    restoreContacts.setEnabled(true);
-                    restoreContacts.setBackgroundColor(getResources().getColor(R.color.primary_button_background_color));
-                } else {
-                    restoreContacts.setEnabled(false);
-                    restoreContacts.setBackgroundColor(getResources().getColor(R.color.standard_grey));
-                }
+        if (savedInstanceState == null) {
+            contactListAdapter = new ContactListAdapter(getContext(), vCards);
+        } else {
+            Set<Integer> checkedItems = new HashSet<>();
+            int[] itemsArray = savedInstanceState.getIntArray(CHECKED_ITEMS_ARRAY_KEY);
+            for (int i = 0; i < itemsArray.length; i++) {
+                checkedItems.add(itemsArray[i]);
             }
+            if (checkedItems.size() > 0) {
+                onMessageEvent(new VCardToggleEvent(true));
+            }
+            contactListAdapter = new ContactListAdapter(getContext(), vCards, checkedItems);
+        }
+        recyclerView.setAdapter(contactListAdapter);
+        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
 
-            @Override
-            public void onVCardCheck(int position) {
-                checkedVCards.add(position);
-                Log_OC.d(TAG, position + " checked");
+        return view;
+    }
 
-                setRestoreButton();
-            }
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putIntArray(CHECKED_ITEMS_ARRAY_KEY, contactListAdapter.getCheckedIntArray());
+    }
 
-            @Override
-            public void onVCardUncheck(int position) {
-                checkedVCards.remove(position);
-                Log_OC.d(TAG, position + " unchecked");
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onMessageEvent(VCardToggleEvent event) {
+        if (event.showRestoreButton) {
+            restoreContactsContainer.setVisibility(View.VISIBLE);
+        } else {
+            restoreContactsContainer.setVisibility(View.GONE);
+        }
+    }
 
-                setRestoreButton();
-            }
-        };
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+        contactsPreferenceActivity.setDrawerIndicatorEnabled(true);
+    }
 
-        ContactListAdapter contactListAdapter = new ContactListAdapter(getContext(), vCards, vCardClickListener);
-        recyclerView.setAdapter(contactListAdapter);
-        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+    public void onResume() {
+        super.onResume();
+        ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+        contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
+    }
 
-        return view;
+    @Override
+    public void onStart() {
+        super.onStart();
+        EventBus.getDefault().register(this);
     }
 
     @Override
-    public void onPrepareOptionsMenu(Menu menu) {
-        menu.findItem(R.id.action_search).setVisible(false);
-        menu.findItem(R.id.action_sync_account).setVisible(false);
-        menu.findItem(R.id.action_sort).setVisible(false);
-        menu.findItem(R.id.action_switch_view).setVisible(false);
+    public void onStop() {
+        EventBus.getDefault().unregister(this);
+        super.onStop();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        boolean retval;
+        ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                contactsPreferenceActivity.onBackPressed();
+                retval = true;
+                break;
+            case R.id.action_select_all:
+                item.setChecked(!item.isChecked());
+                setSelectAllMenuItem(item, item.isChecked());
+                contactListAdapter.selectAllFiles(item.isChecked());
+                retval = true;
+                break;
+            default:
+                retval = super.onOptionsItemSelected(item);
+                break;
+        }
+        return retval;
+    }
+
+    private void setSelectAllMenuItem(MenuItem selectAll, boolean checked) {
+        selectAll.setChecked(checked);
+        if(checked) {
+            selectAll.setIcon(R.drawable.ic_select_none);
+        } else {
+            selectAll.setIcon(R.drawable.ic_select_all);
+        }
     }
 
     static class ContactItemViewHolder extends RecyclerView.ViewHolder {
@@ -211,19 +293,11 @@ public class ContactListFragment extends FileFragment {
     }
 
     private void importContacts(ContactAccount account) {
-        int[] intArray = new int[checkedVCards.size()];
-
-        int i = 0;
-        for (Integer checkedVCard : checkedVCards) {
-            intArray[i] = checkedVCard;
-            i++;
-        }
-
         PersistableBundleCompat bundle = new PersistableBundleCompat();
         bundle.putString(ContactsImportJob.ACCOUNT_NAME, account.name);
         bundle.putString(ContactsImportJob.ACCOUNT_TYPE, account.type);
         bundle.putString(ContactsImportJob.VCARD_FILE_PATH, getFile().getStoragePath());
-        bundle.putIntArray(ContactsImportJob.CHECKED_ITEMS_ARRAY, intArray);
+        bundle.putIntArray(ContactsImportJob.CHECKED_ITEMS_ARRAY, contactListAdapter.getCheckedIntArray());
 
         new JobRequest.Builder(ContactsImportJob.TAG)
                 .setExtras(bundle)
@@ -234,8 +308,19 @@ public class ContactListFragment extends FileFragment {
                 .build()
                 .schedule();
 
-
         Snackbar.make(recyclerView, R.string.contacts_preferences_import_scheduled, Snackbar.LENGTH_LONG).show();
+
+        Handler handler = new Handler();
+        handler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                if (getFragmentManager().getBackStackEntryCount() > 0) {
+                    getFragmentManager().popBackStack();
+                } else {
+                    getActivity().finish();
+                }
+            }
+        }, 1750);
     }
 
     private void getAccountForImport() {
@@ -269,13 +354,14 @@ public class ContactListFragment extends FileFragment {
         } catch (Exception e) {
             Log_OC.d(TAG, e.getMessage());
         } finally {
-            cursor.close();
+            if (cursor != null) {
+                cursor.close();
+            }
         }
 
         if (accounts.size() == 1) {
             importContacts(accounts.get(0));
         } else {
-
             ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, accounts);
             AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
             builder.setTitle(R.string.contactlist_account_chooser_title)
@@ -291,7 +377,8 @@ public class ContactListFragment extends FileFragment {
     private boolean checkAndAskForContactsWritePermission() {
         // check permissions
         if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) {
-            PermissionUtil.requestWriteContactPermission(this);
+            requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
+                    PermissionUtil.PERMISSIONS_WRITE_CONTACTS);
             return false;
         } else {
             return true;
@@ -346,15 +433,46 @@ public class ContactListFragment extends FileFragment {
 
 class ContactListAdapter extends RecyclerView.Adapter<ContactListFragment.ContactItemViewHolder> {
     private List<VCard> vCards;
+    private Set<Integer> checkedVCards;
+
     private Context context;
-    private OnVCardClickListener vCardClickListener;
 
-    ContactListAdapter(Context context, List<VCard> vCards, OnVCardClickListener vCardClickListener) {
+    ContactListAdapter(Context context, List<VCard> vCards) {
         this.vCards = vCards;
         this.context = context;
-        this.vCardClickListener = vCardClickListener;
+        this.checkedVCards = new HashSet<>();
     }
 
+    ContactListAdapter(Context context, List<VCard> vCards,
+                       Set<Integer> checkedVCards) {
+        this.vCards = vCards;
+        this.context = context;
+        this.checkedVCards = checkedVCards;
+    }
+
+    public int getCheckedCount() {
+        if (checkedVCards != null) {
+            return checkedVCards.size();
+        } else {
+            return 0;
+        }
+    }
+
+    public int[] getCheckedIntArray() {
+        int[] intArray;
+        if (checkedVCards != null && checkedVCards.size() > 0) {
+            intArray = new int[checkedVCards.size()];
+            int i = 0;
+            for (int position: checkedVCards) {
+                intArray[i] = position;
+                i++;
+            }
+            return intArray;
+        } else {
+            intArray = new int[0];
+            return intArray;
+        }
+    }
 
     @Override
     public ContactListFragment.ContactItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
@@ -365,9 +483,15 @@ class ContactListAdapter extends RecyclerView.Adapter<ContactListFragment.Contac
 
     @Override
     public void onBindViewHolder(final ContactListFragment.ContactItemViewHolder holder, final int position) {
-        final VCard vcard = vCards.get(holder.getAdapterPosition());
+        final VCard vcard = vCards.get(position);
 
         if (vcard != null) {
+
+            if (checkedVCards.contains(position)) {
+                holder.getName().setChecked(true);
+            } else {
+                holder.getName().setChecked(false);
+            }
             // name
             StructuredName name = vcard.getStructuredName();
             if (name != null) {
@@ -407,9 +531,20 @@ class ContactListAdapter extends RecyclerView.Adapter<ContactListFragment.Contac
                     holder.getName().setChecked(!holder.getName().isChecked());
 
                     if (holder.getName().isChecked()) {
-                        vCardClickListener.onVCardCheck(holder.getAdapterPosition());
+                        if (!checkedVCards.contains(position)) {
+                            checkedVCards.add(position);
+                        }
+                        if (checkedVCards.size() == 1) {
+                            EventBus.getDefault().post(new VCardToggleEvent(true));
+                        }
                     } else {
-                        vCardClickListener.onVCardUncheck(holder.getAdapterPosition());
+                        if (checkedVCards.contains(position)) {
+                            checkedVCards.remove(position);
+                        }
+
+                        if (checkedVCards.size() == 0) {
+                            EventBus.getDefault().post(new VCardToggleEvent(false));
+                        }
                     }
                 }
             });
@@ -421,9 +556,20 @@ class ContactListAdapter extends RecyclerView.Adapter<ContactListFragment.Contac
         return vCards.size();
     }
 
-    interface OnVCardClickListener {
-        void onVCardCheck(int position);
+    public void selectAllFiles(boolean select) {
+        checkedVCards = new HashSet<>();
+        if (select) {
+            for (int i = 0; i < vCards.size(); i++) {
+                checkedVCards.add(i);
+            }
+        }
+
+        if (checkedVCards.size() > 0) {
+            EventBus.getDefault().post(new VCardToggleEvent(true));
+        } else {
+            EventBus.getDefault().post(new VCardToggleEvent(false));
+        }
 
-        void onVCardUncheck(int position);
+        notifyDataSetChanged();
     }
 }

+ 463 - 0
src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsBackupFragment.java

@@ -0,0 +1,463 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import android.Manifest;
+import android.accounts.Account;
+import android.app.DatePickerDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.SwitchCompat;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.services.ContactsBackupJob;
+import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
+import com.owncloud.android.ui.fragment.FileFragment;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.PermissionUtil;
+
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Vector;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+
+import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP;
+import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP;
+
+public class ContactsBackupFragment extends FileFragment implements DatePickerDialog.OnDateSetListener {
+    public static final String TAG = ContactsBackupFragment.class.getSimpleName();
+
+    @BindView(R.id.contacts_automatic_backup)
+    public SwitchCompat backupSwitch;
+
+    @BindView(R.id.contacts_header_restore)
+    public TextView contactsRestoreHeader;
+
+    @BindView(R.id.contacts_datepicker)
+    public AppCompatButton contactsDatePickerBtn;
+
+    @BindView(R.id.contacts_last_backup_timestamp)
+    public TextView lastBackup;
+
+    private Date selectedDate = null;
+    private boolean calendarPickerOpen;
+
+    private DatePickerDialog datePickerDialog;
+
+    private CompoundButton.OnCheckedChangeListener onCheckedChangeListener;
+
+    private static final String KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN";
+    private static final String KEY_CALENDAR_DAY = "CALENDAR_DAY";
+    private static final String KEY_CALENDAR_MONTH = "CALENDAR_MONTH";
+    private static final String KEY_CALENDAR_YEAR = "CALENDAR_YEAR";
+    private ArbitraryDataProvider arbitraryDataProvider;
+    private Account account;
+
+
+    @Override
+    public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+        View view = inflater.inflate(R.layout.contacts_backup_fragment, null);
+        ButterKnife.bind(this, view);
+
+        setHasOptionsMenu(true);
+
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        account = (Account) getArguments().get(ContactListFragment.ACCOUNT);
+
+        contactsPreferenceActivity.getSupportActionBar().setTitle(R.string.actionbar_contacts);
+        contactsPreferenceActivity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+        arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+        backupSwitch.setChecked(arbitraryDataProvider.getBooleanValue(account, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP));
+
+        onCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                if (checkAndAskForContactsReadPermission()) {
+                    if (isChecked) {
+                        setAutomaticBackup(true);
+                    } else {
+                        setAutomaticBackup(false);
+                    }
+                }
+            }
+        };
+
+        backupSwitch.setOnCheckedChangeListener(onCheckedChangeListener);
+
+        // display last backup
+        Long lastBackupTimestamp = arbitraryDataProvider.getLongValue(account, PREFERENCE_CONTACTS_LAST_BACKUP);
+
+        if (lastBackupTimestamp == -1) {
+            lastBackup.setText(R.string.contacts_preference_backup_never);
+        } else {
+            lastBackup.setText(DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp));
+        }
+
+        if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) {
+            if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 &&
+                    savedInstanceState.getInt(KEY_CALENDAR_MONTH, -1) != -1 &&
+                    savedInstanceState.getInt(KEY_CALENDAR_DAY, -1) != -1) {
+                selectedDate = new Date(savedInstanceState.getInt(KEY_CALENDAR_YEAR),
+                        savedInstanceState.getInt(KEY_CALENDAR_MONTH), savedInstanceState.getInt(KEY_CALENDAR_DAY));
+            }
+            calendarPickerOpen = true;
+        }
+
+        return view;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        if (calendarPickerOpen) {
+            if (selectedDate != null) {
+                openDate(selectedDate);
+            } else {
+                openDate(null);
+            }
+        }
+
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
+        OCFile backupFolder = contactsPreferenceActivity.getStorageManager().getFileByPath(backupFolderString);
+
+        Vector<OCFile> backupFiles = contactsPreferenceActivity.getStorageManager().getFolderContent(backupFolder,
+                false);
+
+        if (backupFiles == null || backupFiles.size() == 0) {
+            contactsRestoreHeader.setVisibility(View.GONE);
+            contactsDatePickerBtn.setVisibility(View.GONE);
+        } else {
+            contactsRestoreHeader.setVisibility(View.VISIBLE);
+            contactsDatePickerBtn.setVisibility(View.VISIBLE);
+        }
+    }
+
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        boolean retval;
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                if (contactsPreferenceActivity.isDrawerOpen()) {
+                    contactsPreferenceActivity.closeDrawer();
+                } else {
+                    contactsPreferenceActivity.openDrawer();
+                }
+                retval = true;
+                break;
+
+            default:
+                retval = super.onOptionsItemSelected(item);
+                break;
+        }
+        return retval;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) {
+            for (int index = 0; index < permissions.length; index++) {
+                if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
+                    if (grantResults[index] >= 0) {
+                        setAutomaticBackup(true);
+                    } else {
+                        backupSwitch.setOnCheckedChangeListener(null);
+                        backupSwitch.setChecked(false);
+                        backupSwitch.setOnCheckedChangeListener(onCheckedChangeListener);
+                    }
+
+                    break;
+                }
+            }
+        }
+
+        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_MANUALLY) {
+            for (int index = 0; index < permissions.length; index++) {
+                if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
+                    if (grantResults[index] >= 0) {
+                        startContactsBackupJob();
+                    }
+
+                    break;
+                }
+            }
+        }
+    }
+
+    @OnClick(R.id.contacts_backup_now)
+    public void backupContacts() {
+        if (checkAndAskForContactsReadPermission()) {
+            startContactsBackupJob();
+        }
+    }
+
+    private void startContactsBackupJob() {
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        PersistableBundleCompat bundle = new PersistableBundleCompat();
+        bundle.putString(ContactsBackupJob.ACCOUNT, contactsPreferenceActivity.getAccount().name);
+        bundle.putBoolean(ContactsBackupJob.FORCE, true);
+
+        new JobRequest.Builder(ContactsBackupJob.TAG)
+                .setExtras(bundle)
+                .setExecutionWindow(3_000L, 10_000L)
+                .setRequiresCharging(false)
+                .setPersisted(false)
+                .setUpdateCurrent(false)
+                .build()
+                .schedule();
+
+        Snackbar.make(getView().findViewById(R.id.contacts_linear_layout),
+                R.string.contacts_preferences_backup_scheduled,
+                Snackbar.LENGTH_LONG).show();
+    }
+
+    private void setAutomaticBackup(final boolean bool) {
+
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        if (bool) {
+            ContactsPreferenceActivity.startContactBackupJob(contactsPreferenceActivity.getAccount());
+        } else {
+            ContactsPreferenceActivity.cancelContactBackupJobForAccount(contactsPreferenceActivity,
+                    contactsPreferenceActivity.getAccount());
+        }
+
+        arbitraryDataProvider.storeOrUpdateKeyValue(account, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
+                String.valueOf(bool));
+    }
+
+    private boolean checkAndAskForContactsReadPermission() {
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        // check permissions
+        if ((PermissionUtil.checkSelfPermission(contactsPreferenceActivity, Manifest.permission.READ_CONTACTS))) {
+            return true;
+        } else {
+            // Check if we should show an explanation
+            if (PermissionUtil.shouldShowRequestPermissionRationale(contactsPreferenceActivity,
+                    android.Manifest.permission.READ_CONTACTS)) {
+                // Show explanation to the user and then request permission
+                Snackbar snackbar = Snackbar.make(getView().findViewById(R.id.contacts_linear_layout),
+                        R.string.contacts_read_permission,
+                        Snackbar.LENGTH_INDEFINITE)
+                        .setAction(R.string.common_ok, new View.OnClickListener() {
+                            @Override
+                            public void onClick(View v) {
+                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
+                                        PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC);
+                            }
+                        });
+
+                DisplayUtils.colorSnackbar(contactsPreferenceActivity, snackbar);
+
+                snackbar.show();
+
+                return false;
+            } else {
+                // No explanation needed, request the permission.
+                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
+                        PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC);
+                return false;
+            }
+        }
+    }
+
+    @OnClick(R.id.contacts_datepicker)
+    public void openCleanDate() {
+        openDate(null);
+    }
+
+    public void openDate(@Nullable Date savedDate) {
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+        String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
+        OCFile backupFolder = contactsPreferenceActivity.getStorageManager().getFileByPath(backupFolderString);
+
+        Vector<OCFile> backupFiles = contactsPreferenceActivity.getStorageManager().getFolderContent(backupFolder,
+                false);
+
+        Collections.sort(backupFiles, new Comparator<OCFile>() {
+            @Override
+            public int compare(OCFile o1, OCFile o2) {
+                if (o1.getModificationTimestamp() == o2.getModificationTimestamp()) {
+                    return 0;
+                }
+
+                if (o1.getModificationTimestamp() > o2.getModificationTimestamp()) {
+                    return 1;
+                } else {
+                    return -1;
+                }
+            }
+        });
+
+        Calendar cal = Calendar.getInstance();
+        int year;
+        int month;
+        int day;
+
+        if (savedDate == null) {
+            year = cal.get(Calendar.YEAR);
+            month = cal.get(Calendar.MONTH) + 1;
+            day = cal.get(Calendar.DAY_OF_MONTH);
+        } else {
+            year = savedDate.getYear();
+            month = savedDate.getMonth();
+            day = savedDate.getDay();
+        }
+
+        if (backupFiles.size() > 0 && backupFiles.lastElement() != null) {
+            datePickerDialog = new DatePickerDialog(contactsPreferenceActivity, this, year, month, day);
+            datePickerDialog.getDatePicker().setMaxDate(backupFiles.lastElement().getModificationTimestamp());
+            datePickerDialog.getDatePicker().setMinDate(backupFiles.firstElement().getModificationTimestamp());
+
+            datePickerDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+                @Override
+                public void onDismiss(DialogInterface dialog) {
+                    selectedDate = null;
+                }
+            });
+
+            datePickerDialog.show();
+        } else {
+            Toast.makeText(contactsPreferenceActivity, R.string.contacts_preferences_something_strange_happened,
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (datePickerDialog != null) {
+            datePickerDialog.dismiss();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (datePickerDialog != null) {
+            outState.putBoolean(KEY_CALENDAR_PICKER_OPEN, datePickerDialog.isShowing());
+
+            if (datePickerDialog.isShowing()) {
+                outState.putInt(KEY_CALENDAR_DAY, datePickerDialog.getDatePicker().getDayOfMonth());
+                outState.putInt(KEY_CALENDAR_MONTH, datePickerDialog.getDatePicker().getMonth());
+                outState.putInt(KEY_CALENDAR_YEAR, datePickerDialog.getDatePicker().getYear());
+            }
+        }
+    }
+
+    @Override
+    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
+        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+        selectedDate = new Date(year, month, dayOfMonth);
+
+        String backupFolderString = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
+        OCFile backupFolder = contactsPreferenceActivity.getStorageManager().getFileByPath(backupFolderString);
+        Vector<OCFile> backupFiles = contactsPreferenceActivity.getStorageManager().getFolderContent(
+                backupFolder, false);
+
+        // find file with modification with date and time between 00:00 and 23:59
+        // if more than one file exists, take oldest
+        Calendar date = Calendar.getInstance();
+        date.set(year, month, dayOfMonth);
+
+        // start
+        date.set(Calendar.HOUR, 0);
+        date.set(Calendar.MINUTE, 0);
+        date.set(Calendar.SECOND, 1);
+        date.set(Calendar.MILLISECOND, 0);
+        date.set(Calendar.AM_PM, Calendar.AM);
+        Long start = date.getTimeInMillis();
+
+        // end
+        date.set(Calendar.HOUR, 23);
+        date.set(Calendar.MINUTE, 59);
+        date.set(Calendar.SECOND, 59);
+        Long end = date.getTimeInMillis();
+
+        OCFile backupToRestore = null;
+
+        for (OCFile file : backupFiles) {
+            if (start < file.getModificationTimestamp() && end > file.getModificationTimestamp()) {
+                if (backupToRestore == null) {
+                    backupToRestore = file;
+                } else if (backupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) {
+                    backupToRestore = file;
+                }
+            }
+        }
+
+        if (backupToRestore != null) {
+            Fragment contactListFragment = ContactListFragment.newInstance(backupToRestore,
+                    contactsPreferenceActivity.getAccount());
+
+            FragmentTransaction transaction = contactsPreferenceActivity.getSupportFragmentManager().
+                    beginTransaction();
+            transaction.replace(R.id.frame_container, contactListFragment);
+            transaction.addToBackStack(null);
+            transaction.commit();
+        } else {
+            Toast.makeText(contactsPreferenceActivity, R.string.contacts_preferences_no_file_found,
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+}

+ 0 - 32
src/main/java/com/owncloud/android/utils/PermissionUtil.java

@@ -52,36 +52,4 @@ public class PermissionUtil {
                 new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                 PERMISSIONS_WRITE_EXTERNAL_STORAGE);
     }
-
-    /**
-     * request the read permission for contacts
-     *
-     * @param activity The target activity.
-     */
-    public static void requestReadContactPermission(Activity activity, int permission) {
-        ActivityCompat.requestPermissions(activity,
-                new String[]{Manifest.permission.READ_CONTACTS},
-                permission);
-    }
-
-    /**
-     * request the write permission for contacts
-     *
-     * @param activity The target activity.
-     */
-    public static void requestWriteContactPermission(Activity activity) {
-        ActivityCompat.requestPermissions(activity,
-                new String[]{Manifest.permission.WRITE_CONTACTS},
-                PERMISSIONS_WRITE_CONTACTS);
-    }
-
-    /**
-     * request the write permission for contacts
-     *
-     * @param fragment The target fragment.
-     */
-    public static void requestWriteContactPermission(android.support.v4.app.Fragment fragment) {
-        fragment.requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
-                PERMISSIONS_WRITE_CONTACTS);
-    }
 }

+ 20 - 7
src/main/res/layout/contactlist_fragment.xml

@@ -21,6 +21,7 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
+              android:animateLayoutChanges="true"
               android:orientation="vertical">
 
     <android.support.v7.widget.RecyclerView
@@ -30,14 +31,26 @@
         android:layout_weight="1"
         android:choiceMode="multipleChoice"/>
 
-    <android.support.v7.widget.AppCompatButton
-        android:id="@+id/contactlist_restore_selected"
+    <LinearLayout
+        android:id="@+id/contactlist_restore_selected_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_margin="@dimen/standard_margin"
-        android:enabled="false"
-        android:text="@string/contaclist_restore_selected"
-        android:background="@color/standard_grey"
-        android:theme="@style/Button.Primary"/>
+        android:background="@color/white"
+        android:orientation="vertical"
+        android:visibility="gone">
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:src="@drawable/uploader_list_separator"/>
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/contactlist_restore_selected"
+            style="@style/Button.Borderless"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/contaclist_restore_selected"/>
+
+    </LinearLayout>
 
 </LinearLayout>

+ 0 - 1
src/main/res/layout/contactlist_list_item.xml

@@ -18,7 +18,6 @@
   You should have received a copy of the GNU Affero General Public
   License along with this program. If not, see <http://www.gnu.org/licenses/>.
 -->
-
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="@dimen/standard_list_item_size">

+ 107 - 0
src/main/res/layout/contacts_backup_fragment.xml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2017 Tobias Kaminsky
+  Copyright (C) 2017 Nextcloud.
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@+id/contacts_linear_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/contacts_header_backup"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/standard_margin"
+            android:layout_marginRight="@dimen/standard_margin"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:text="@string/contacts_header_backup"
+            android:textColor="@color/primary"
+            android:textStyle="bold"/>
+
+        <android.support.v7.widget.SwitchCompat
+            android:id="@+id/contacts_automatic_backup"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_margin"
+            android:text="@string/contacts_automatic_backup"
+            android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/contacts_last_backup"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/standard_margin"
+                android:layout_weight="1"
+                android:text="@string/contacts_last_backup"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:textColor="@color/black"/>
+
+            <TextView
+                android:id="@+id/contacts_last_backup_timestamp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/standard_margin"
+                android:layout_weight="1"
+                android:gravity="right"
+                android:text="@string/contacts_preference_backup_never"
+                android:textAppearance="?android:attr/textAppearanceMedium"/>
+        </LinearLayout>
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/contacts_backup_now"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_margin"
+            android:onClick="backupContacts"
+            android:text="@string/contacts_backup_button"
+            android:theme="@style/Button.Primary"/>
+
+        <TextView
+            android:id="@+id/contacts_header_restore"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/standard_margin"
+            android:layout_marginRight="@dimen/standard_margin"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:text="@string/contacts_header_restore"
+            android:textColor="@color/primary"
+            android:textStyle="bold"/>
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/contacts_datepicker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_margin"
+            android:onClick="openDate"
+            android:text="@string/contacts_preference_choose_date"
+            android:theme="@style/Button.Primary"/>
+
+    </LinearLayout>
+
+</ScrollView>

+ 4 - 78
src/main/res/layout/contacts_preference.xml

@@ -28,7 +28,7 @@
 
     <!-- The main content view -->
     <RelativeLayout
-        android:id="@+id/contacts_linear_layout"
+        android:id="@+id/contacts_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical">
@@ -36,88 +36,14 @@
         <include
             layout="@layout/toolbar_standard"/>
 
-        <LinearLayout
+        <FrameLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_above="@+id/bottom_navigation_view"
             android:layout_below="@+id/appbar"
-            android:orientation="vertical">
+            android:id="@+id/frame_container">
 
-        <TextView
-            android:id="@+id/contacts_header_backup"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="@dimen/standard_margin"
-            android:layout_marginRight="@dimen/standard_margin"
-            android:layout_marginTop="@dimen/standard_margin"
-            android:text="@string/contacts_header_backup"
-            android:textColor="@color/primary"
-            android:textStyle="bold"/>
-
-        <android.support.v7.widget.SwitchCompat
-            android:id="@+id/contacts_automatic_backup"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="@dimen/standard_margin"
-            android:text="@string/contacts_automatic_backup"
-            android:textAppearance="?android:attr/textAppearanceMedium"/>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal">
-
-            <TextView
-                android:id="@+id/contacts_last_backup"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_margin="@dimen/standard_margin"
-                android:layout_weight="1"
-                android:text="@string/contacts_last_backup"
-                android:textAppearance="?android:attr/textAppearanceMedium"
-                android:textColor="@color/black"/>
-
-            <TextView
-                android:id="@+id/contacts_last_backup_timestamp"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_margin="@dimen/standard_margin"
-                android:layout_weight="1"
-                android:gravity="right"
-                android:text="@string/contacts_preference_backup_never"
-                android:textAppearance="?android:attr/textAppearanceMedium"/>
-        </LinearLayout>
-
-        <android.support.v7.widget.AppCompatButton
-            android:id="@+id/contacts_backup_now"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_margin="@dimen/standard_margin"
-            android:onClick="backupContacts"
-            android:text="@string/contacts_backup_button"
-            android:theme="@style/Button.Primary"/>
-
-        <TextView
-            android:id="@+id/contacts_header_restore"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="@dimen/standard_margin"
-            android:layout_marginRight="@dimen/standard_margin"
-            android:layout_marginTop="@dimen/standard_margin"
-            android:text="@string/contacts_header_restore"
-            android:textColor="@color/primary"
-            android:textStyle="bold"/>
-
-        <android.support.v7.widget.AppCompatButton
-            android:id="@+id/contacts_datepacker"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_margin="@dimen/standard_margin"
-            android:onClick="openDate"
-            android:text="@string/contacts_preference_choose_date"
-            android:theme="@style/Button.Primary"/>
-
-        </LinearLayout>
+        </FrameLayout>
 
         <android.support.design.widget.BottomNavigationView
             android:id="@+id/bottom_navigation_view"

+ 33 - 0
src/main/res/menu/contactlist_menu.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Nextcloud Android client application
+
+ @author Tobias Kaminsky
+ Copyright (C) 2017 Tobias Kaminsky
+ Copyright (C) 2017 Nextcloud GmbH.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_select_all"
+        android:checkable="true"
+        android:contentDescription="@string/select_all"
+        android:title="@string/select_all"
+        android:icon="@drawable/ic_select_all"
+        app:showAsAction="always"/>
+
+</menu>

+ 3 - 1
src/main/res/values/strings.xml

@@ -633,7 +633,8 @@
     <string name="prefs_category_about">About</string>
 
     <string name="actionbar_contacts">Backup contacts</string>
-    <string name="contacts_backup_button">Now</string>
+    <string name="actionbar_contacts_restore">Restore contacts</string>
+    <string name="contacts_backup_button">Backup now</string>
     <string name="contacts_restore_button">Restore last backup</string>
     <string name="contacts_header_restore">Restore</string>
     <string name="contacts_header_backup">Backup</string>
@@ -648,6 +649,7 @@
     <string name="contacts_preference_choose_date">Choose date</string>
     <string name="contacts_preference_backup_never">never</string>
     <string name="contacts_preferences_no_file_found">No file found</string>
+    <string name="contacts_preferences_something_strange_happened">We can\'t find your last backup!</string>
     <string name="contacts_preferences_backup_scheduled">Backup scheduled and will start shortly</string>
     <string name="contacts_preferences_import_scheduled">Import scheduled and will start shortly</string>