Merge "Deprecating ConfirmAddDetail dialog to use full contact editor."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 74a8a91..b764f17 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,8 +16,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.contacts"
-    android:versionCode="10401"
-    android:versionName="1.4.1">
+    android:versionCode="10402"
+    android:versionName="1.4.2">
 
     <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="23" />
     <original-package android:name="com.android.contacts" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 07bfdc0..31552b6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -678,9 +678,6 @@
          contact filter state. [CHAR LIMIT=64] -->
     <string name="toast_displaying_all_contacts">Displaying all contacts</string>
 
-    <!-- Message in the standard "no account" prompt that encourages the user to add a Google account before continuing to use the People app [CHAR LIMIT=NONE] -->
-    <string name="no_account_prompt">Contacts works better with a Google Account.\n\n\u2022 Access from any web browser.\n\u2022 Back up your contacts securely.</string>
-
     <!-- Message in the standard "no account" prompt that encourages the user to add any account (non Google-specific) before continuing to use the People app [CHAR LIMIT=NONE] -->
     <string name="generic_no_account_prompt">Keep your contacts safe even if you lose your phone: synchronize with an online service.</string>
 
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index c3a7f24..10f8f9c 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -56,6 +56,7 @@
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.compat.PinnedPositionsCompat;
@@ -66,6 +67,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -372,8 +374,12 @@
             String saveModeExtraKey, int saveMode, boolean isProfile,
             Class<? extends Activity> callbackActivity, String callbackAction,
             Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
-        Intent serviceIntent = new Intent(
-                context, ContactSaveService.class);
+        // Don't pass read-only RawContactDeltas in RawContactDeltaList to contact save service,
+        // because 1. read-only RawContactDeltas are not writable anyway; 2. read-only
+        // RawContactDeltas may be problematic, see b/23896510.
+        removeReadOnlyContacts(context, state);
+
+        Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
         serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
         serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
@@ -398,6 +404,26 @@
         return serviceIntent;
     }
 
+    private static void removeReadOnlyContacts(Context context, RawContactDeltaList state) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "Before trimming: " + state.size());
+        }
+        int countReadOnly = 0;
+        final Iterator<RawContactDelta> iterator = state.iterator();
+        while (iterator.hasNext()) {
+            final RawContactDelta rawContactDelta = iterator.next();
+            final AccountType accountType = rawContactDelta.getRawContactAccountType(context);
+            if (accountType != null && !accountType.areContactsWritable()) {
+                countReadOnly++;
+                iterator.remove();
+            }
+        }
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "# of read-only removed: " + countReadOnly);
+            Log.v(TAG, "After trimming: " + state.size());
+        }
+    }
+
     private void saveContact(Intent intent) {
         RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
         boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
diff --git a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
index 617ef0d..9748e46 100644
--- a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
@@ -36,7 +36,7 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.AccountsListAdapter;
 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
-import com.android.contacts.util.AccountPromptUtils;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
 
 import java.util.List;
 
@@ -70,7 +70,7 @@
     private final OnClickListener mAddAccountClickListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            final Intent intent = AccountPromptUtils.getIntentForAddingAccount();
+            final Intent intent = ImplicitIntentsUtil.getIntentForAddingAccount();
             startActivityForResult(intent, SUBACTIVITY_ADD_NEW_ACCOUNT);
         }
     };
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index d887ef9..7643a05 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -59,8 +59,6 @@
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.activity.RequestPermissionsActivity;
 import com.android.contacts.common.dialog.ClearFrequentsDialog;
-import com.android.contacts.common.util.ImplicitIntentsUtil;
-import com.android.contacts.common.widget.FloatingActionButtonController;
 import com.android.contacts.common.interactions.ImportExportDialogFragment;
 import com.android.contacts.common.list.ContactEntryListFragment;
 import com.android.contacts.common.list.ContactListFilter;
@@ -74,7 +72,9 @@
 import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment;
 import com.android.contacts.common.util.AccountFilterUtil;
 import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
 import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.common.widget.FloatingActionButtonController;
 import com.android.contacts.editor.EditorIntents;
 import com.android.contacts.interactions.ContactDeletionInteraction;
 import com.android.contacts.interactions.ContactMultiDeletionInteraction;
@@ -92,7 +92,6 @@
 import com.android.contacts.list.ProviderStatusWatcher;
 import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
 import com.android.contacts.quickcontact.QuickContactActivity;
-import com.android.contacts.util.AccountPromptUtils;
 import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contactsbind.HelpUtils;
@@ -196,10 +195,6 @@
         return (mProviderStatus != null) && mProviderStatus.equals(ProviderStatus.STATUS_NORMAL);
     }
 
-    private boolean areContactWritableAccountsAvailable() {
-        return ContactsUtils.areContactWritableAccountsAvailable(this);
-    }
-
     private boolean areGroupWritableAccountsAvailable() {
         return ContactsUtils.areGroupWritableAccountsAvailable(this);
     }
@@ -899,24 +894,8 @@
                 mAllFragment.setEnabled(true);
             }
         } else {
-            // If there are no accounts on the device and we should show the "no account" prompt
-            // (based on {@link SharedPreferences}), then launch the account setup activity so the
-            // user can sign-in or create an account.
-            //
-            // Also check for ability to modify accounts.  In limited user mode, you can't modify
-            // accounts so there is no point sending users to account setup activity.
-            final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
-            final boolean disallowModifyAccounts = userManager.getUserRestrictions().getBoolean(
-                    UserManager.DISALLOW_MODIFY_ACCOUNTS);
-            if (!disallowModifyAccounts && !areContactWritableAccountsAvailable() &&
-                    AccountPromptUtils.shouldShowAccountPrompt(this)) {
-                AccountPromptUtils.neverShowAccountPromptAgain(this);
-                AccountPromptUtils.launchAccountPrompt(this);
-                return;
-            }
-
-            // Otherwise, continue setting up the page so that the user can still use the app
-            // without an account.
+            // Setting up the page so that the user can still use the app
+            // even without an account.
             if (mAllFragment != null) {
                 mAllFragment.setEnabled(false);
             }
@@ -1024,7 +1003,7 @@
 
         @Override
         public void onAddAccountAction() {
-            final Intent intent = AccountPromptUtils.getIntentForAddingAccount();
+            final Intent intent = ImplicitIntentsUtil.getIntentForAddingAccount();
             ImplicitIntentsUtil.startActivityOutsideApp(PeopleActivity.this, intent);
         }
 
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 181bc44..1d7ace6 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -979,8 +979,7 @@
     abstract protected boolean doSaveAction(int saveMode, Long joinContactId);
 
     protected boolean startSaveService(Context context, Intent intent, int saveMode) {
-        final boolean result = ContactSaveService.startService(
-                context, intent, saveMode);
+        final boolean result = ContactSaveService.startService(context, intent, saveMode);
         if (!result) {
             onCancelEditConfirmed();
         }
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index ad41f2c..90cd350 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -1151,6 +1151,12 @@
             destroyInteractionLoaders();
             mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
                     LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
+            // mContactLoader may not be in the state of "started". If not, onContentChanged() will
+            // not call forceLoad(), so QuickContact will not get the newly updated hi-res
+            // photo. If this is the case, we call forceLoad explicitly. See b/25204200.
+            if (!mContactLoader.isStarted()) {
+                mContactLoader.forceLoad();
+            }
             mCachedCp2DataCardModel = null;
         }
 
diff --git a/src/com/android/contacts/util/AccountPromptUtils.java b/src/com/android/contacts/util/AccountPromptUtils.java
deleted file mode 100644
index 4fc95a5..0000000
--- a/src/com/android/contacts/util/AccountPromptUtils.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts.util;
-
-import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorDescription;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.provider.ContactsContract;
-import android.provider.Settings;
-import android.util.Log;
-
-import com.android.contacts.R;
-import com.android.contacts.common.model.account.GoogleAccountType;
-
-import java.io.IOException;
-
-/**
- * Utility class for controlling whether the standard "no account" prompt on launch is shown.
- */
-public class AccountPromptUtils {
-
-    private static final String TAG = AccountPromptUtils.class.getSimpleName();
-
-    /** {@link SharedPreferences} key for whether or not the "no account" prompt should be shown. */
-    private static final String KEY_SHOW_ACCOUNT_PROMPT = "settings.showAccountPrompt";
-
-    /**
-     * The following intent keys are understood by the {@link AccountManager} and should not be
-     * changed unless the API changes.
-     */
-    private static final String KEY_INTRO_MESSAGE = "introMessage";
-    private static final String KEY_ALLOW_SKIP_ACCOUNT_SETUP = "allowSkip";
-    private static final String KEY_USER_SKIPPED_ACCOUNT_SETUP = "setupSkipped";
-
-    private static SharedPreferences getSharedPreferences(Context context) {
-        return PreferenceManager.getDefaultSharedPreferences(context);
-    }
-
-    /**
-     * Returns true if the "no account" prompt should be shown
-     * (according to {@link SharedPreferences}), otherwise return false. Since this prompt is
-     * Google-specific for the time being, this method will also return false if the Google
-     * account type is not available from the {@link AccountManager}.
-     */
-    public static boolean shouldShowAccountPrompt(Context context) {
-        // TODO: Remove the filtering of account types once there is an API in
-        // {@link AccountManager} to show a similar account prompt
-        // (see {@link AccountManager#addAccount()} in {@link #launchAccountPrompt()}
-        // for any type of account. Bug: 5375902
-        AuthenticatorDescription[] allTypes =
-                AccountManager.get(context).getAuthenticatorTypes();
-        for (AuthenticatorDescription authenticatorType : allTypes) {
-            if (GoogleAccountType.ACCOUNT_TYPE.equals(authenticatorType.type)) {
-                return getSharedPreferences(context).getBoolean(KEY_SHOW_ACCOUNT_PROMPT, true);
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Remember to never show the "no account" prompt again by saving this to
-     * {@link SharedPreferences}.
-     */
-    public static void neverShowAccountPromptAgain(Context context) {
-        getSharedPreferences(context).edit()
-                .putBoolean(KEY_SHOW_ACCOUNT_PROMPT, false)
-                .apply();
-    }
-
-    /**
-     * Launch the "no account" prompt. (We assume the caller has already verified that the prompt
-     * can be shown, so checking the {@link #KEY_SHOW_ACCOUNT_PROMPT} value in
-     * {@link SharedPreferences} will not be done in this method).
-     */
-    public static void launchAccountPrompt(Activity activity) {
-        Bundle options = new Bundle();
-        options.putCharSequence(KEY_INTRO_MESSAGE, activity.getString(R.string.no_account_prompt));
-        options.putBoolean(KEY_ALLOW_SKIP_ACCOUNT_SETUP, true);
-        AccountManager.get(activity).addAccount(GoogleAccountType.ACCOUNT_TYPE, null, null, options,
-                activity, getAccountManagerCallback(activity), null);
-    }
-
-    /**
-     * When adding account
-     * open the same UI screen for user to choose account
-     */
-    public static Intent getIntentForAddingAccount() {
-         final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
-         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
-         intent.putExtra(Settings.EXTRA_AUTHORITIES,
-                 new String[]{ContactsContract.AUTHORITY});
-         return intent;
-    }
-
-    private static AccountManagerCallback<Bundle> getAccountManagerCallback(
-            final Activity activity) {
-        return new AccountManagerCallback<Bundle>() {
-            @Override
-            public void run(AccountManagerFuture<Bundle> future) {
-                if (future.isCancelled()) {
-                    // The account creation process was canceled
-                    return;
-                }
-                try {
-                    Bundle result = future.getResult();
-                    if (result.getBoolean(KEY_USER_SKIPPED_ACCOUNT_SETUP)) {
-                        AccountPromptUtils.neverShowAccountPromptAgain(activity);
-                    }
-                } catch (OperationCanceledException ignore) {
-                    Log.e(TAG, "Account setup error: account creation process canceled");
-                } catch (IOException ignore) {
-                    Log.e(TAG, "Account setup error: No authenticator was registered for this"
-                            + "account type or the authenticator failed to respond");
-                } catch (AuthenticatorException ignore) {
-                    Log.e(TAG, "Account setup error: Authenticator experienced an I/O problem");
-                }
-            }
-        };
-    }
-}