Make the authentication service remember selected usernames.

R=ppi@chromium.org, tonyg@chromium.org

Review URL: https://codereview.chromium.org/1163483004
diff --git a/mojo/services/authentication/public/interfaces/authentication.mojom b/mojo/services/authentication/public/interfaces/authentication.mojom
index a69e7de..0bd56d6 100644
--- a/mojo/services/authentication/public/interfaces/authentication.mojom
+++ b/mojo/services/authentication/public/interfaces/authentication.mojom
@@ -11,8 +11,10 @@
 interface AuthenticationService {
   // Requests a Google account to use. In case of success, error will be null.
   // In case of error, username will be null and error will contain a
-  // description of the error.
-  SelectAccount() => (string? username, string? error);
+  // description of the error. If |return_last_selected| is true and the client
+  // application already selected an account, the same account will be returned
+  // without user intervention.
+  SelectAccount(bool return_last_selected) => (string? username, string? error);
 
   // Requests an oauth2 token for the given Google account with the given
   // scopes.  In case of error, username will be null and error will contain a
diff --git a/services/authenticating_url_loader/authenticating_url_loader_factory_impl.cc b/services/authenticating_url_loader/authenticating_url_loader_factory_impl.cc
index 4eba375..795ce0c 100644
--- a/services/authenticating_url_loader/authenticating_url_loader_factory_impl.cc
+++ b/services/authenticating_url_loader/authenticating_url_loader_factory_impl.cc
@@ -53,8 +53,8 @@
       return;
     }
     authentication_service_->SelectAccount(
-        base::Bind(&AuthenticatingURLLoaderFactoryImpl::OnAccountSelected,
-                   base::Unretained(this), origin));
+        true, base::Bind(&AuthenticatingURLLoaderFactoryImpl::OnAccountSelected,
+                         base::Unretained(this), origin));
   }
   pendings_retrieve_token_[origin].push_back(callback);
 }
diff --git a/services/authentication/BUILD.gn b/services/authentication/BUILD.gn
index 72b61bc..88c72a6 100644
--- a/services/authentication/BUILD.gn
+++ b/services/authentication/BUILD.gn
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//mojo/public/tools/bindings/mojom.gni")
+
 if (is_android) {
   import("//services/android/rules.gni")
 
@@ -14,6 +16,7 @@
     mojo_main = "org.chromium.mojo.authentication.AuthenticationApp"
 
     deps = [
+      ":interfaces_java",
       "//mojo/public/interfaces/application:application_java",
       "//mojo/public/java:application",
       "//mojo/services/authentication/public/interfaces:interfaces_java",
@@ -21,4 +24,10 @@
       "//third_party/android_tools:google_play_services_default_java",
     ]
   }
+
+  mojom("interfaces") {
+    sources = [
+      "authentication_impl_db.mojom",
+    ]
+  }
 }
diff --git a/services/authentication/authentication_impl_db.mojom b/services/authentication/authentication_impl_db.mojom
new file mode 100644
index 0000000..72b217d
--- /dev/null
+++ b/services/authentication/authentication_impl_db.mojom
@@ -0,0 +1,16 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[JavaPackage="org.chromium.mojo.authentication"]
+module authentication;
+
+// Database for the authentication implementation.
+// This struct is used to persist state for the authentication service and is
+// not passed between services.
+struct Db {
+  // Version of the database.
+  uint32 version;
+  // Map from application to last selected account.
+  map<string, string> last_selected_accounts;
+};
diff --git a/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java
index 928a8cd..ed2eb74 100644
--- a/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java
+++ b/services/authentication/src/org/chromium/mojo/authentication/AuthenticationServiceImpl.java
@@ -16,21 +16,41 @@
 import com.google.android.gms.common.AccountPicker;
 
 import org.chromium.mojo.application.ShellHelper;
+import org.chromium.mojo.bindings.DeserializationException;
+import org.chromium.mojo.bindings.Message;
 import org.chromium.mojo.bindings.SideEffectFreeCloseable;
 import org.chromium.mojo.intent.IntentReceiver;
 import org.chromium.mojo.intent.IntentReceiverManager;
 import org.chromium.mojo.intent.IntentReceiverManager.RegisterActivityResultReceiverResponse;
 import org.chromium.mojo.system.Core;
+import org.chromium.mojo.system.Handle;
 import org.chromium.mojo.system.MojoException;
 import org.chromium.mojom.mojo.Shell;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.util.ArrayList;
+import java.util.HashMap;
 
 /**
  * Implementation of AuthenticationService from services/authentication/authentication.mojom
  */
 public class AuthenticationServiceImpl
         extends SideEffectFreeCloseable implements AuthenticationService {
+    // The current version of the database. This must be incremented each time Db definition in
+    // authentication_impl_db.mojom is changed in a non backward-compatible way.
+    private static final int VERSION = 0;
+
+    // Type of google accounts.
+    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+
+    // Type of the accounts that this service allows the user to pick.
+    private static final String[] ACCOUNT_TYPES = new String[] {GOOGLE_ACCOUNT_TYPE};
+
     /**
      * An callback that takes a serialized intent, add the intent the shell needs to send and start
      * the container intent.
@@ -38,8 +58,7 @@
     private final class RegisterActivityResultReceiverCallback
             implements RegisterActivityResultReceiverResponse {
         /**
-         * The intent that the requesting application needs to be run by shell on its
-behalf.
+         * The intent that the requesting application needs to be run by shell on its behalf.
          */
         private final Intent mIntent;
 
@@ -61,6 +80,8 @@
     private final Activity mContext;
     private final String mConsumerURL;
     private final IntentReceiverManager mIntentReceiverManager;
+    private Db mDb = null;
+    private File mDbFile = null;
 
     public AuthenticationServiceImpl(Context context, Core core, String consumerURL, Shell shell) {
         mContext = (Activity) context;
@@ -136,11 +157,24 @@
     }
 
     /**
-     * @see AuthenticationService#selectAccount(AuthenticationService.SelectAccountResponse)
+     * @see AuthenticationService#selectAccount(boolean,
+     *      AuthenticationService.SelectAccountResponse)
      */
     @Override
-    public void selectAccount(final SelectAccountResponse callback) {
-        String[] accountTypes = new String[] {"com.google"};
+    public void selectAccount(boolean returnLastSelected, final SelectAccountResponse callback) {
+        if (returnLastSelected) {
+            Db db = getDb();
+            String username = db.lastSelectedAccounts.get(mConsumerURL);
+            if (username != null) {
+                try {
+                    GoogleAuthUtil.getAccountId(mContext, username);
+                    callback.call(username, null);
+                    return;
+                } catch (final GoogleAuthException | IOException e) {
+                }
+            }
+        }
+
         String message = null;
         if (mConsumerURL.equals("")) {
             message = "Select an account to use with mojo shell";
@@ -148,7 +182,7 @@
             message = "Select an account to use with application: " + mConsumerURL;
         }
         Intent accountPickerIntent = AccountPicker.newChooseAccountIntent(
-                null, null, accountTypes, false, message, null, null, null);
+                null, null, ACCOUNT_TYPES, false, message, null, null, null);
 
         mIntentReceiverManager.registerActivityResultReceiver(new IntentReceiver() {
             SelectAccountResponse mPendingCallback = callback;
@@ -175,6 +209,7 @@
                 }
                 mPendingCallback.call(username, error);
                 mPendingCallback = null;
+                updateDb(username);
             }
         }, new RegisterActivityResultReceiverCallback(accountPickerIntent));
     }
@@ -197,4 +232,64 @@
         p.setDataPosition(0);
         return Intent.CREATOR.createFromParcel(p);
     }
+
+    private File getDbFile() {
+        if (mDbFile != null) {
+            return mDbFile;
+        }
+        File home = new File(System.getenv("HOME"));
+        File configDir = new File(home, ".mojo_authentication");
+        configDir.mkdirs();
+        mDbFile = new File(configDir, "db");
+        return mDbFile;
+    }
+
+    private Db getDb() {
+        if (mDb != null) {
+            return mDb;
+        }
+        File dbFile = getDbFile();
+        if (dbFile.exists()) {
+            try {
+                int size = (int) dbFile.length();
+                try (FileInputStream stream = new FileInputStream(dbFile);
+                        FileChannel channel = stream.getChannel()) {
+                    // Use mojo serialization to read the database.
+                    Db db = Db.deserialize(new Message(
+                            channel.map(MapMode.READ_ONLY, 0, size), new ArrayList<Handle>()));
+                    if (db.version == VERSION) {
+                        mDb = db;
+                        return mDb;
+                    }
+                } catch (DeserializationException e) {
+                }
+                dbFile.delete();
+            } catch (IOException e) {
+            }
+        }
+        mDb = new Db();
+        mDb.version = VERSION;
+        mDb.lastSelectedAccounts = new HashMap<>();
+        return mDb;
+    }
+
+    private void updateDb(String username) {
+        try {
+            Db db = getDb();
+            if (username == null) {
+                db.lastSelectedAccounts.remove(mConsumerURL);
+            } else {
+                db.lastSelectedAccounts.put(mConsumerURL, username);
+            }
+            // Use mojo serialization to persist the database.
+            Message m = db.serialize(null);
+            File dbFile = getDbFile();
+            dbFile.delete();
+            try (FileOutputStream stream = new FileOutputStream(dbFile);
+                    FileChannel channel = stream.getChannel()) {
+                channel.write(m.getData());
+            }
+        } catch (IOException e) {
+        }
+    }
 }