Add PhoneAccount registration.

Writes/Reads PhoneAccount registration as they are added and
removed through the TelecommManager APIs.
Ultimately, we may want to use a proper Telecomm-provider DB instead of
string-based serialized setting.

Bug: 16292368
Change-Id: I1214fcdd8728cddc949945a590b20e328de5ee7f
diff --git a/src/com/android/telecomm/PhoneAccountRegistrar.java b/src/com/android/telecomm/PhoneAccountRegistrar.java
new file mode 100644
index 0000000..e8abb8a
--- /dev/null
+++ b/src/com/android/telecomm/PhoneAccountRegistrar.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 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.telecomm;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.provider.Settings;
+import android.telecomm.PhoneAccount;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Handles writing and reading PhoneAccount registration entries.
+ * TODO(santoscordon): Replace this implementation with a proper database stored in a Telecomm
+ * provider.
+ */
+final class PhoneAccountRegistrar {
+    private static final int VERSION = 1;
+    private static final String TELECOMM_PREFERENCES = "telecomm_prefs";
+    private static final String PREFERENCE_PHONE_ACCOUNTS = "phone_accounts";
+
+    private final Context mContext;
+
+    private final class DeserializationToken {
+        int currentIndex = 0;
+        final String source;
+
+        DeserializationToken(String source) {
+            this.source = source;
+        }
+    }
+
+    PhoneAccountRegistrar(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Adds a new phone account entry or updates an existing one.
+     */
+    boolean addAccount(PhoneAccount account) {
+        List<PhoneAccount> allAccounts = getAllAccounts();
+        // Should we implement an artificial limit for # of accounts associated with a single
+        // ComponentName?
+        allAccounts.add(account);
+
+        // Search for duplicates and remove any that are found.
+        for (int i = 0; i < allAccounts.size() - 1; i++) {
+            if (account.equalsComponentAndId(allAccounts.get(i))) {
+                // replace existing entry.
+                allAccounts.remove(i);
+                break;
+            }
+        }
+
+        return writeAllAccounts(allAccounts);
+    }
+
+    /**
+     * Removes an existing phone account entry.
+     */
+    boolean removeAccount(PhoneAccount account) {
+        List<PhoneAccount> allAccounts = getAllAccounts();
+
+        for (int i = 0; i < allAccounts.size(); i++) {
+            if (account.equalsComponentAndId(allAccounts.get(i))) {
+                allAccounts.remove(i);
+                return writeAllAccounts(allAccounts);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a list of all accounts which the user has enabled.
+     */
+    List<PhoneAccount> getEnabledAccounts() {
+        List<PhoneAccount> allAccounts = getAllAccounts();
+        // TODO: filter list
+        return allAccounts;
+    }
+
+    /**
+     * Returns the list of all accounts registered with the system, whether or not the user
+     * has explicitly enabled them.
+     */
+    List<PhoneAccount> getAllAccounts() {
+        String value = getPreferences().getString(PREFERENCE_PHONE_ACCOUNTS, null);
+        return deserializeAllAccounts(value);
+    }
+
+    /**
+     * Returns the registered version of the account matching the component name and ID of the
+     * specified account.
+     */
+    PhoneAccount getRegisteredAccount(PhoneAccount account) {
+        for (PhoneAccount registeredAccount : getAllAccounts()) {
+            if (registeredAccount.equalsComponentAndId(account)) {
+                return registeredAccount;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Replaces the contents of our list of accounts with this new list.
+     */
+    private boolean writeAllAccounts(List<PhoneAccount> allAccounts) {
+        Editor editor = getPreferences().edit();
+        editor.putString(PREFERENCE_PHONE_ACCOUNTS, serializeAllAccounts(allAccounts));
+        return editor.commit();
+    }
+
+    // Serialization implementation
+    // Serializes all strings into the format "len:string-value"
+    // Example, we will serialize the following PhoneAccount.
+    //   PhoneAccount
+    //     ComponentName: "abc"
+    //     Id:            "def"
+    //     Handle:        "555"
+    //     Capabilities:  1
+    //
+    //  Each value serializes into (spaces added for readability)
+    //    3:abc 3:def 3:555 1:1
+    //
+    //  Two identical accounts would likewise be serialized as a list of strings with a prepended
+    //  size of 2.
+    //    1:2 3:abc 3:def 3:555 1:1 3:abc 3:def 3:555 1:1
+    //
+    //  The final result with a prepended version ("1:1") would be:
+    //    "1:11:23:abc3:def3:5551:13:abc3:def3:5551:1"
+
+    private String serializeAllAccounts(List<PhoneAccount> allAccounts) {
+        StringBuilder buffer = new StringBuilder();
+
+        // Version
+        serializeIntValue(VERSION, buffer);
+
+        // Number of accounts
+        serializeIntValue(allAccounts.size(), buffer);
+
+        // The actual accounts
+        for (int i = 0; i < allAccounts.size(); i++) {
+            PhoneAccount account = allAccounts.get(i);
+            serializeStringValue(account.getComponentName().flattenToShortString(), buffer);
+            serializeStringValue(account.getId(), buffer);
+            serializeStringValue(account.getHandle().toString(), buffer);
+            serializeIntValue(account.getCapabilities(), buffer);
+        }
+
+        return buffer.toString();
+    }
+
+    private List<PhoneAccount> deserializeAllAccounts(String source) {
+        List<PhoneAccount> accounts = new ArrayList<PhoneAccount>();
+
+        if (source != null) {
+            DeserializationToken token = new DeserializationToken(source);
+            int version = deserializeIntValue(token);
+            if (version == 1) {
+                int size = deserializeIntValue(token);
+
+                for (int i = 0; i < size; i++) {
+                    String strComponentName = deserializeStringValue(token);
+                    String strId = deserializeStringValue(token);
+                    String strHandle = deserializeStringValue(token);
+                    int capabilities = deserializeIntValue(token);
+
+                    accounts.add(new PhoneAccount(
+                            ComponentName.unflattenFromString(strComponentName),
+                            strId,
+                            Uri.parse(strHandle),
+                            capabilities));
+                }
+            }
+        }
+
+        return accounts;
+    }
+
+    private void serializeIntValue(int value, StringBuilder buffer) {
+        serializeStringValue(String.valueOf(value), buffer);
+    }
+
+    private void serializeStringValue(String value, StringBuilder buffer) {
+        buffer.append(value.length()).append(":").append(value);
+    }
+
+    private int deserializeIntValue(DeserializationToken token) {
+        return Integer.parseInt(deserializeStringValue(token));
+    }
+
+    private String deserializeStringValue(DeserializationToken token) {
+        int colonIndex = token.source.indexOf(':', token.currentIndex);
+        int valueLength = Integer.parseInt(token.source.substring(token.currentIndex, colonIndex));
+        int endIndex = colonIndex + 1 + valueLength;
+        token.currentIndex = endIndex;
+        return token.source.substring(colonIndex + 1, endIndex);
+    }
+
+    private SharedPreferences getPreferences() {
+        return mContext.getSharedPreferences(TELECOMM_PREFERENCES, Context.MODE_PRIVATE);
+    }
+}
diff --git a/src/com/android/telecomm/TelecommApp.java b/src/com/android/telecomm/TelecommApp.java
index 37c7aa0..2b2f160 100644
--- a/src/com/android/telecomm/TelecommApp.java
+++ b/src/com/android/telecomm/TelecommApp.java
@@ -33,14 +33,21 @@
      */
     private MissedCallNotifier mMissedCallNotifier;
 
+    /**
+     * Maintains the list of registered {@link PhoneAccount}s.
+     */
+    private PhoneAccountRegistrar mPhoneAccountRegistrar;
+
     /** {@inheritDoc} */
     @Override public void onCreate() {
         super.onCreate();
         sInstance = this;
 
         mMissedCallNotifier = new MissedCallNotifier(this);
+        mPhoneAccountRegistrar = new PhoneAccountRegistrar(this);
+
         if (UserHandle.myUserId() == UserHandle.USER_OWNER) {
-            TelecommServiceImpl.init(mMissedCallNotifier);
+            TelecommServiceImpl.init(mMissedCallNotifier, mPhoneAccountRegistrar);
         }
     }
 
diff --git a/src/com/android/telecomm/TelecommServiceImpl.java b/src/com/android/telecomm/TelecommServiceImpl.java
index 509307b..d848029 100644
--- a/src/com/android/telecomm/TelecommServiceImpl.java
+++ b/src/com/android/telecomm/TelecommServiceImpl.java
@@ -20,7 +20,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
-import android.net.Uri;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
@@ -35,10 +34,7 @@
 
 import com.android.internal.telecomm.ITelecommService;
 
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Implementation of the ITelecomm interface.
@@ -117,10 +113,13 @@
     private final MainThreadHandler mMainThreadHandler = new MainThreadHandler();
     private final CallsManager mCallsManager = CallsManager.getInstance();
     private final MissedCallNotifier mMissedCallNotifier;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final AppOpsManager mAppOpsManager;
 
-    private TelecommServiceImpl(MissedCallNotifier missedCallNotifier) {
+    private TelecommServiceImpl(
+            MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
         mMissedCallNotifier = missedCallNotifier;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
         mAppOpsManager =
                 (AppOpsManager) TelecommApp.getInstance().getSystemService(Context.APP_OPS_SERVICE);
 
@@ -131,10 +130,11 @@
      * Initialize the singleton TelecommServiceImpl instance.
      * This is only done once, at startup, from TelecommApp.onCreate().
      */
-    static TelecommServiceImpl init(MissedCallNotifier missedCallNotifier) {
+    static TelecommServiceImpl init(
+            MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
         synchronized (TelecommServiceImpl.class) {
             if (sInstance == null) {
-                sInstance = new TelecommServiceImpl(missedCallNotifier);
+                sInstance = new TelecommServiceImpl(missedCallNotifier, phoneAccountRegistrar);
             } else {
                 Log.wtf(TAG, "init() called multiple times!  sInstance %s", sInstance);
             }
@@ -146,98 +146,43 @@
     // Implementation of the ITelecommService interface.
     //
 
-    private static Map<PhoneAccount, PhoneAccountMetadata> sMetadataByAccount = new HashMap<>();
-
-    static {
-        // TODO (STOPSHIP): Static list of Accounts for testing and UX work only.
-        ComponentName componentName = new ComponentName(
-                "com.android.telecomm",
-                TelecommServiceImpl.class.getName());  // This field is a no-op
-        Context app = TelecommApp.getInstance();
-
-        PhoneAccount[] accounts = new PhoneAccount[] {
-                new PhoneAccount(
-                        componentName,
-                        "account0",
-                        Uri.parse("tel:999-555-1212"),
-                        0),
-                new PhoneAccount(
-                        componentName,
-                        "account1",
-                        Uri.parse("tel:333-111-2222"),
-                        0),
-                new PhoneAccount(
-                        componentName,
-                        "account2",
-                        Uri.parse("mailto:two@example.com"),
-                        0),
-                new PhoneAccount(
-                        componentName,
-                        "account3",
-                        Uri.parse("mailto:three@example.com"),
-                        0)
-        };
-
-        sMetadataByAccount.put(
-                accounts[0],
-                new PhoneAccountMetadata(
-                        accounts[0],
-                        0,
-                        app.getString(R.string.test_account_0_label),
-                        app.getString(R.string.test_account_0_short_description)));
-        sMetadataByAccount.put(
-                accounts[1],
-                new PhoneAccountMetadata(
-                        accounts[1],
-                        0,
-                        app.getString(R.string.test_account_1_label),
-                        app.getString(R.string.test_account_1_short_description)));
-        sMetadataByAccount.put(
-                accounts[2],
-                new PhoneAccountMetadata(
-                        accounts[2],
-                        0,
-                        app.getString(R.string.test_account_2_label),
-                        app.getString(R.string.test_account_2_short_description)));
-        sMetadataByAccount.put(
-                accounts[3],
-                new PhoneAccountMetadata(
-                        accounts[3],
-                        0,
-                        app.getString(R.string.test_account_3_label),
-                        app.getString(R.string.test_account_3_short_description)));
-    }
-
     @Override
     public List<PhoneAccount> getEnabledPhoneAccounts() {
-        return new ArrayList<>(sMetadataByAccount.keySet());
+        return mPhoneAccountRegistrar.getEnabledAccounts();
     }
 
     @Override
     public PhoneAccountMetadata getPhoneAccountMetadata(PhoneAccount account) {
-        return sMetadataByAccount.get(account);
+        PhoneAccount registeredAccount = mPhoneAccountRegistrar.getRegisteredAccount(account);
+        if (registeredAccount != null) {
+            return new PhoneAccountMetadata(
+                    registeredAccount, 0, account.getComponentName().getPackageName(), null);
+        }
+        return null;
     }
 
     @Override
     public void registerPhoneAccount(PhoneAccount account, PhoneAccountMetadata metadata) {
         enforceModifyPermissionOrCallingPackage(account.getComponentName().getPackageName());
-        // TODO(santoscordon) -- IMPLEMENT ...
+        mPhoneAccountRegistrar.addAccount(account);
+        // TODO(santoscordon): Implement metadata
     }
 
     @Override
     public void unregisterPhoneAccount(PhoneAccount account) {
         enforceModifyPermissionOrCallingPackage(account.getComponentName().getPackageName());
-        // TODO(santoscordon) -- IMPLEMENT ...
+        mPhoneAccountRegistrar.removeAccount(account);
     }
 
     @Override
     public void clearAccounts(String packageName) {
         enforceModifyPermissionOrCallingPackage(packageName);
-        // TODO(santoscordon) -- IMPLEMENT ...
+        // TODO(santoscordon): Is this needed?
+        Log.e(TAG, null, "Unexpected method call: clearAccounts()");
     }
 
     /**
-     * @see TelecommManager#silenceringer
+     * @see TelecommManager#silenceRinger
      */
     @Override
     public void silenceRinger() {