OSDN Git Service

Revamped CustomLocale for the SDK.
authorRaphael Moll <ralf@android.com>
Mon, 31 Jan 2011 01:05:10 +0000 (17:05 -0800)
committerRaphael Moll <ralf@android.com>
Mon, 31 Jan 2011 23:22:56 +0000 (15:22 -0800)
- Revamped the list UI: replaced the long-press actions by
some stateful buttons. Uses the listview's single choice mode.

- Added a broadcast receiver to be able to change the system
locale using an intent, e.g. sent by adb shell am. This is
convenient for automated testing scripts.

- Changed package namespace to v2. This makes it easier for
an automated testing script to figure that an emulator has the
old version and install the new one side-by-side.

Change-Id: I1849d2b9d55441490b013cfefcca5d8fa48e22bd

apps/CustomLocale/AndroidManifest.xml
apps/CustomLocale/res/layout/list_item.xml [deleted file]
apps/CustomLocale/res/layout/main.xml
apps/CustomLocale/res/values-fr/strings.xml
apps/CustomLocale/res/values/strings.xml
apps/CustomLocale/src/com/android/customlocale/CustomLocaleActivity.java [deleted file]
apps/CustomLocale/src/com/android/customlocale2/ChangeLocale.java [new file with mode: 0755]
apps/CustomLocale/src/com/android/customlocale2/CustomLocaleActivity.java [new file with mode: 0644]
apps/CustomLocale/src/com/android/customlocale2/CustomLocaleReceiver.java [new file with mode: 0755]
apps/CustomLocale/src/com/android/customlocale2/NewLocaleDialog.java [moved from apps/CustomLocale/src/com/android/customlocale/NewLocaleDialog.java with 93% similarity]

index 9dfa896..0a446de 100644 (file)
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
+
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:versionCode="1"
-    android:versionName="1.0" package="com.android.customlocale">
+    android:versionName="1.0"
+    package="com.android.customlocale2"
+    >
+
+    <uses-sdk android:minSdkVersion="3" />
+
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
+
     <application
         android:icon="@drawable/icon"
-        android:label="@string/app_name">
+        android:label="@string/app_name" >
+
         <activity
-            android:label="@string/app_name" android:name="CustomLocaleActivity">
+            android:label="@string/app_name"
+            android:name="CustomLocaleActivity" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
         <activity
-            android:name="NewLocaleDialog"
-            android:theme="@android:style/Theme.Dialog" />
+            android:theme="@android:style/Theme.Dialog"
+            android:name="NewLocaleDialog" />
+
+        <receiver android:name="CustomLocaleReceiver" >
+            <intent-filter>
+                <action android:name="com.android.intent.action.SET_LOCALE" />
+            </intent-filter>
+        </receiver>
     </application>
-    <uses-sdk android:minSdkVersion="3" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
 </manifest>
diff --git a/apps/CustomLocale/res/layout/list_item.xml b/apps/CustomLocale/res/layout/list_item.xml
deleted file mode 100644 (file)
index 0ffb8b0..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 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.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:padding="5dip">
-    <TextView
-        android:id="@+id/locale_code"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?android:textAppearanceLarge"
-        android:layout_weight="1"
-        android:text="@string/locale_default" />
-    <TextView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/locale_name"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?android:textAppearance"
-        android:layout_weight="1" />
-</LinearLayout>
\ No newline at end of file
index 55bd90c..e700a8b 100644 (file)
@@ -17,7 +17,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    >
     <TextView
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/header_current_locale"
         android:textAppearance="@style/TextAppearance.header"
         android:gravity="center_horizontal"
-        android:background="@color/header_background" />
+        android:background="@color/header_background"
+        />
     <TextView
         android:id="@+id/current_locale"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_weight="0"
-        android:padding="5dip" />
+        android:padding="5dip"
+        />
     <TextView
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/header_locale_list"
         android:textAppearance="@style/TextAppearance.header"
         android:gravity="center_horizontal"
-        android:background="@color/header_background" />
-    <ListView
-        android:id="@id/android:list"
+        android:background="@color/header_background"
+        />
+
+    <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_weight="1"
-        android:padding="8dip" />
-    <TextView
-        android:id="@id/android:empty"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:text="@string/no_data_label" />
-    <Button
-        android:id="@+id/new_locale"
+        >
+        <ListView
+            android:id="@id/android:list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:fastScrollEnabled="true"
+            android:focusable="true"
+            android:focusableInTouchMode="true"
+            android:padding="8dip"
+            />
+        <TextView
+            android:id="@id/android:empty"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:text="@string/no_data_label"
+            android:gravity="center"
+            />
+    </FrameLayout>
+
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_weight="0"
-        android:paddingLeft="8dip"
-        android:paddingRight="8dip"
-        android:text="@string/add_new_locale_button" />
+        >
+        <Button
+            android:id="@+id/select_locale_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.9"
+            android:layout_gravity="center_vertical"
+            android:text="@string/select_locale_button"
+            />
+        <Button
+            android:id="@+id/add_new_locale_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:layout_gravity="center_vertical"
+            android:text="@string/add_new_locale_button"
+            />
+        <Button
+            android:id="@+id/remove_locale_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:layout_marginLeft="5dip"
+            android:layout_gravity="center_vertical"
+            android:text="@string/remove_locale_button"
+            />
+    </LinearLayout>
 </LinearLayout>
index 8852a9a..8c9bc38 100644 (file)
 -->
 <resources>
     <string name="app_name">Locales Personalisées</string>
-    <string name="add_new_locale_button">Ajouter une nouvelle locale</string>
-    <string name="new_locale_label">Code de la nouvelle locale:</string>
-    <string name="add_button">Ajouter</string>
-    <string name="no_data_label">Aucune locale</string>
+
+    <!-- Top labels in CustomLocaleActivity -->
+
     <string name="header_current_locale">Locale courrante</string>
     <string name="header_locale_list">Liste des locales</string>
-    <string name="add_select_button">Ajouter et sélectionner</string>
+    <string name="no_data_label">Aucune locale</string>
+
+    <!-- Buttons in CustomLocaleActivity -->
+
+    <string name="select_locale_button">Choisir</string>
+    <string name="select_locale_1s_button">Choisir \'%1$s\'</string>
+    <string name="add_new_locale_button">Ajouter\u2026</string>
+    <string name="remove_locale_button">Retirer\u2026</string>
+
+    <!-- Toast status in CustomLocaleActivity -->
+
+    <string name="removed_custom_locale_1s">Locale supprimée: %1$s</string>
+    <string name="select_locale_1s">Locale choisie: %1$s</string>
+    <string name="added_custom_locale_1s">Locale ajoutée: %1$s</string>
+
+    <!-- NewLocaleDialog -->
+
+    <string name="new_locale_label">Code de la nouvelle locale:</string>
+    <string name="add_button">Ajouter</string>
+    <string name="add_select_button">Ajouter et choisir</string>
+
+    <string name="confirm_remove_locale_1s">Voulez-vous supprimer la locale personalisée \'%1$s\' ?</string>
+    <string name="confirm_remove_locale_yes">Oui</string>
+    <string name="confirm_remove_locale_no">Non</string>
+
 </resources>
index 313fb87..955521c 100644 (file)
 -->
 <resources>
     <string name="app_name">Custom Locale</string>
-    <string name="locale_default">ex_EX</string>
-    <string name="add_new_locale_button">Add New Locale</string>
-    <string name="new_locale_label">New locale code:</string>
-    <string name="add_button">Add</string>
-    <string name="no_data_label">No data</string>
+
+    <!-- Top labels in CustomLocaleActivity -->
+
     <string name="header_current_locale">Current Locale</string>
     <string name="header_locale_list">Locale List</string>
+    <string name="no_data_label">No data</string>
+
+    <!-- Buttons in CustomLocaleActivity -->
+
+    <string name="select_locale_button">Select</string>
+    <string name="select_locale_1s_button">Select \'%1$s\'</string>
+    <string name="add_new_locale_button">Add New\u2026</string>
+    <string name="remove_locale_button">Remove\u2026</string>
+
+    <!-- Toast status in CustomLocaleActivity -->
+
+    <string name="removed_custom_locale_1s">Removed custom locale: %1$s</string>
+    <string name="select_locale_1s">Select locale: %1$s</string>
+    <string name="added_custom_locale_1s">Added custom locale: %1$s</string>
+
+    <!-- NewLocaleDialog -->
+
+    <string name="new_locale_label">New locale code:</string>
+    <string name="add_button">Add</string>
     <string name="add_select_button">Add and Select</string>
+
+    <string name="confirm_remove_locale_1s">Do you want to remove the custom locale \'%1$s\'?</string>
+    <string name="confirm_remove_locale_yes">Yes</string>
+    <string name="confirm_remove_locale_no">No</string>
+
+    <!-- Locale hint in textfield. Not translated -->
+    <string name="locale_default">ex_EX</string>
+
 </resources>
diff --git a/apps/CustomLocale/src/com/android/customlocale/CustomLocaleActivity.java b/apps/CustomLocale/src/com/android/customlocale/CustomLocaleActivity.java
deleted file mode 100644 (file)
index 69b9c5b..0000000
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright (C) 2009 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.customlocale;
-
-
-import android.app.ActivityManagerNative;
-import android.app.IActivityManager;
-import android.app.ListActivity;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.widget.Button;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-import android.widget.SimpleAdapter;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-/**
- * Displays the list of system locales as well as maintain a custom list of user
- * locales. The user can select a locale and apply it or it can create or remove
- * a custom locale.
- */
-public class CustomLocaleActivity extends ListActivity {
-
-    private static final String CUSTOM_LOCALES_SEP = " ";
-    private static final String CUSTOM_LOCALES = "custom_locales";
-    private static final String KEY_CUSTOM = "custom";
-    private static final String KEY_NAME = "name";
-    private static final String KEY_CODE = "code";
-
-    private static final String TAG = "LocaleSetup";
-    private static final boolean DEBUG = true;
-
-    /** Request code returned when the NewLocaleDialog activity finishes. */
-    private static final int UPDATE_LIST = 42;
-    /** Menu item id for applying a locale */
-    private static final int MENU_APPLY = 43;
-    /** Menu item id for removing a custom locale */
-    private static final int MENU_REMOVE = 44;
-
-    /** List view displaying system and custom locales. */
-    private ListView mListView;
-    /** Textview used to display current locale */
-    private TextView mCurrentLocaleTextView;
-    /** Private shared preferences of this activity. */
-    private SharedPreferences mPrefs;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.main);
-
-        mPrefs = getPreferences(MODE_PRIVATE);
-
-        Button newLocaleButton = (Button) findViewById(R.id.new_locale);
-
-        newLocaleButton.setOnClickListener(new View.OnClickListener() {
-            public void onClick(View v) {
-                Intent i = new Intent(CustomLocaleActivity.this, NewLocaleDialog.class);
-                startActivityForResult(i, UPDATE_LIST);
-            }
-        });
-
-        mListView = (ListView) findViewById(android.R.id.list);
-        mListView.setFocusable(true);
-        mListView.setFocusableInTouchMode(true);
-        mListView.requestFocus();
-        registerForContextMenu(mListView);
-        setupLocaleList();
-
-        mCurrentLocaleTextView = (TextView) findViewById(R.id.current_locale);
-        displayCurrentLocale();
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-
-        if (requestCode == UPDATE_LIST && resultCode == RESULT_OK && data != null) {
-            String locale = data.getExtras().getString(NewLocaleDialog.INTENT_EXTRA_LOCALE);
-            if (locale != null && locale.length() > 0) {
-                // Get current custom locale list
-                String customLocales = mPrefs.getString(CUSTOM_LOCALES, null);
-
-                // Update
-                if (customLocales == null) {
-                    customLocales = locale;
-                } else {
-                    customLocales += CUSTOM_LOCALES_SEP + locale;
-                }
-
-                // Save prefs
-                if (DEBUG) {
-                    Log.d(TAG, "add/customLocales: " + customLocales);
-                }
-                mPrefs.edit().putString(CUSTOM_LOCALES, customLocales).commit();
-
-                Toast.makeText(this, "Added custom locale: " + locale, Toast.LENGTH_SHORT).show();
-
-                // Update list view
-                setupLocaleList();
-
-                // Find the item to select it in the list view
-                ListAdapter a = mListView.getAdapter();
-                for (int i = 0; i < a.getCount(); i++) {
-                    Object o = a.getItem(i);
-                    if (o instanceof Map<?, ?>) {
-                        String code = ((Map<String, String>) o).get(KEY_CODE);
-                        if (code != null && code.equals(locale)) {
-                            mListView.setSelection(i);
-                            break;
-                        }
-                    }
-                }
-
-                if (data.getExtras().getBoolean(NewLocaleDialog.INTENT_EXTRA_SELECT)) {
-                    selectLocale(locale);
-                }
-            }
-        }
-    }
-
-    private void setupLocaleList() {
-        if (DEBUG) {
-            Log.d(TAG, "Update locate list");
-        }
-
-        ArrayList<Map<String, String>> data = new ArrayList<Map<String, String>>();
-
-        // Insert all system locales
-        String[] locales = getAssets().getLocales();
-        for (String locale : locales) {
-            Locale loc = new Locale(locale);
-
-            Map<String, String> map = new HashMap<String, String>(1);
-            map.put(KEY_CODE, locale);
-            map.put(KEY_NAME, loc.getDisplayName());
-            data.add(map);
-        }
-        locales = null;
-
-        // Insert all custom locales
-        String customLocales = mPrefs.getString(CUSTOM_LOCALES, "");
-        if (DEBUG) {
-            Log.d(TAG, "customLocales: " + customLocales);
-        }
-        for (String locale : customLocales.split(CUSTOM_LOCALES_SEP)) {
-            if (locale != null && locale.length() > 0) {
-                Locale loc = new Locale(locale);
-
-                Map<String, String> map = new HashMap<String, String>(1);
-                map.put(KEY_CODE, locale);
-                map.put(KEY_NAME, loc.getDisplayName() + " [Custom]");
-                // the presence of the "custom" key marks it as custom.
-                map.put(KEY_CUSTOM, "");
-                data.add(map);
-            }
-        }
-
-        // Sort all locales by code
-        Collections.sort(data, new Comparator<Map<String, String>>() {
-            public int compare(Map<String, String> lhs, Map<String, String> rhs) {
-                return lhs.get(KEY_CODE).compareTo(rhs.get(KEY_CODE));
-            }
-        });
-
-        // Update the list view adapter
-        mListView.setAdapter(new SimpleAdapter(this, data, R.layout.list_item, new String[] {
-                KEY_CODE, KEY_NAME}, new int[] {R.id.locale_code, R.id.locale_name}));
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-        super.onCreateContextMenu(menu, v, menuInfo);
-
-        if (menuInfo instanceof AdapterContextMenuInfo) {
-            int position = ((AdapterContextMenuInfo) menuInfo).position;
-            Object o = mListView.getItemAtPosition(position);
-            if (o instanceof Map<?, ?>) {
-                String locale = ((Map<String, String>) o).get(KEY_CODE);
-                String custom = ((Map<String, String>) o).get(KEY_CUSTOM);
-
-                if (custom == null) {
-                    menu.setHeaderTitle("System Locale");
-                    menu.add(0, MENU_APPLY, 0, "Apply");
-                } else {
-                    menu.setHeaderTitle("Custom Locale");
-                    menu.add(0, MENU_APPLY, 0, "Apply");
-                    menu.add(0, MENU_REMOVE, 0, "Remove");
-                }
-            }
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public boolean onContextItemSelected(MenuItem item) {
-
-        String pendingLocale = null;
-        boolean is_custom = false;
-
-        ContextMenuInfo menuInfo = item.getMenuInfo();
-        if (menuInfo instanceof AdapterContextMenuInfo) {
-            int position = ((AdapterContextMenuInfo) menuInfo).position;
-            Object o = mListView.getItemAtPosition(position);
-            if (o instanceof Map<?, ?>) {
-                pendingLocale = ((Map<String, String>) o).get(KEY_CODE);
-                is_custom = ((Map<String, String>) o).get(KEY_CUSTOM) != null;
-            }
-        }
-
-        if (pendingLocale == null) {
-            // should never happen
-            return super.onContextItemSelected(item);
-        }
-
-        if (item.getItemId() == MENU_REMOVE) {
-            // Get current custom locale list
-            String customLocales = mPrefs.getString(CUSTOM_LOCALES, "");
-
-            if (DEBUG) {
-                Log.d(TAG, "Remove " + pendingLocale + " from custom locales: " + customLocales);
-            }
-
-            // Update
-            StringBuilder sb = new StringBuilder();
-            for (String locale : customLocales.split(CUSTOM_LOCALES_SEP)) {
-                if (locale != null && locale.length() > 0 && !locale.equals(pendingLocale)) {
-                    if (sb.length() > 0) {
-                        sb.append(CUSTOM_LOCALES_SEP);
-                    }
-                    sb.append(locale);
-                }
-            }
-            String newLocales = sb.toString();
-            if (!newLocales.equals(customLocales)) {
-                // Save prefs
-                mPrefs.edit().putString(CUSTOM_LOCALES, customLocales).commit();
-
-                Toast.makeText(this, "Removed custom locale: " + pendingLocale, Toast.LENGTH_SHORT)
-                        .show();
-            }
-
-        } else if (item.getItemId() == MENU_APPLY) {
-            selectLocale(pendingLocale);
-        }
-
-        return super.onContextItemSelected(item);
-    }
-
-    private void selectLocale(String locale) {
-        if (DEBUG) {
-            Log.d(TAG, "Select locale " + locale);
-        }
-
-        try {
-            IActivityManager am = ActivityManagerNative.getDefault();
-            Configuration config = am.getConfiguration();
-
-            Locale loc = null;
-
-            String[] langCountry = locale.split("_");
-            if (langCountry.length == 2) {
-                loc = new Locale(langCountry[0], langCountry[1]);
-            } else {
-                loc = new Locale(locale);
-            }
-            
-            config.locale = loc;
-
-            // indicate this isn't some passing default - the user wants this
-            // remembered
-            config.userSetLocale = true;
-
-            am.updateConfiguration(config);
-
-            Toast.makeText(this, "Select locale: " + locale, Toast.LENGTH_SHORT).show();
-        } catch (RemoteException e) {
-            if (DEBUG) {
-                Log.e(TAG, "Select locale failed", e);
-            }
-        }
-    }
-
-    private void displayCurrentLocale() {
-        try {
-            IActivityManager am = ActivityManagerNative.getDefault();
-            Configuration config = am.getConfiguration();
-
-            if (config.locale != null) {
-                String text = String.format("%s - %s",
-                        config.locale.toString(),
-                        config.locale.getDisplayName());
-                mCurrentLocaleTextView.setText(text);
-            }
-        } catch (RemoteException e) {
-            if (DEBUG) {
-                Log.e(TAG, "get current locale failed", e);
-            }
-        }
-    }
-}
diff --git a/apps/CustomLocale/src/com/android/customlocale2/ChangeLocale.java b/apps/CustomLocale/src/com/android/customlocale2/ChangeLocale.java
new file mode 100755 (executable)
index 0000000..8757064
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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.customlocale2;
+
+
+import java.util.Locale;
+
+import android.app.ActivityManagerNative;
+import android.app.IActivityManager;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Helper class to change the system locale.
+ */
+public final class ChangeLocale {
+
+    private static final String TAG = ChangeLocale.class.getSimpleName();
+    private static final boolean DEBUG = true;
+
+    /**
+     * Sets the system locale to the new one specified.
+     *
+     * @param locale A locale name in the form "ab_AB". Must not be null or empty.
+     * @return True if the locale was succesfully changed.
+     */
+    public static boolean changeSystemLocale(String locale) {
+        if (DEBUG) {
+            Log.d(TAG, "Change locale to: " + locale);
+        }
+
+        try {
+            IActivityManager am = ActivityManagerNative.getDefault();
+            Configuration config = am.getConfiguration();
+
+            Locale loc = null;
+
+            String[] langCountry = locale.split("_");
+            if (langCountry.length == 2) {
+                loc = new Locale(langCountry[0], langCountry[1]);
+            } else {
+                loc = new Locale(locale);
+            }
+
+            config.locale = loc;
+
+            // indicate this isn't some passing default - the user wants this
+            // remembered
+            config.userSetLocale = true;
+
+            am.updateConfiguration(config);
+
+            return true;
+
+        } catch (RemoteException e) {
+            if (DEBUG) {
+                Log.e(TAG, "Change locale failed", e);
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/apps/CustomLocale/src/com/android/customlocale2/CustomLocaleActivity.java b/apps/CustomLocale/src/com/android/customlocale2/CustomLocaleActivity.java
new file mode 100644 (file)
index 0000000..3bf7993
--- /dev/null
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2009 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.customlocale2;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Locale;
+
+import android.app.ActivityManagerNative;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.IActivityManager;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+/**
+ * Displays the list of system locales as well as maintain a custom list of user
+ * locales. The user can select a locale and apply it or it can create or remove
+ * a custom locale.
+ */
+public class CustomLocaleActivity extends ListActivity {
+
+    private static final String TAG = "CustomLocale";
+    private static final boolean DEBUG = true;
+
+    private static final int DLG_REMOVE_ID = 0;
+
+    private static final String CUSTOM_LOCALES_SEP = " ";
+    private static final String CUSTOM_LOCALES = "custom_locales";
+
+    /** Request code returned when the NewLocaleDialog activity finishes. */
+    private static final int UPDATE_LIST = 42;
+    /** Menu item id for applying a locale */
+    private static final int MENU_APPLY = 43;
+    /** Menu item id for removing a custom locale */
+    private static final int MENU_REMOVE = 44;
+
+    /** List view displaying system and custom locales. */
+    private ListView mListView;
+    /** Textview used to display current locale */
+    private TextView mCurrentLocaleTextView;
+    /** Private shared preferences of this activity. */
+    private SharedPreferences mPrefs;
+
+    private Button mRemoveLocaleButton;
+    private Button mSelectLocaleButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+
+        mPrefs = getPreferences(MODE_PRIVATE);
+
+        Button addLocaleButton = (Button) findViewById(R.id.add_new_locale_button);
+        mRemoveLocaleButton = (Button) findViewById(R.id.remove_locale_button);
+        mSelectLocaleButton = (Button) findViewById(R.id.select_locale_button);
+
+        addLocaleButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                onAddNewLocale();
+            }
+        });
+
+        mRemoveLocaleButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showDialog(DLG_REMOVE_ID);
+            }
+        });
+
+        mSelectLocaleButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                onSelectLocale();
+            }
+        });
+
+        mListView = (ListView) findViewById(android.R.id.list);
+        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mListView.setItemsCanFocus(false);
+        mListView.setFocusable(true);
+        mListView.setFocusableInTouchMode(true);
+        mListView.requestFocus();
+        setupLocaleList();
+
+        mCurrentLocaleTextView = (TextView) findViewById(R.id.current_locale);
+        displayCurrentLocale();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        updateLocaleButtons();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        if (requestCode == UPDATE_LIST && resultCode == RESULT_OK && data != null) {
+            String locale = data.getExtras().getString(NewLocaleDialog.INTENT_EXTRA_LOCALE);
+            if (locale != null && locale.length() > 0) {
+                // Get current custom locale list
+                String customLocales = mPrefs.getString(CUSTOM_LOCALES, null);
+
+                // Update
+                if (customLocales == null) {
+                    customLocales = locale;
+                } else {
+                    customLocales += CUSTOM_LOCALES_SEP + locale;
+                }
+
+                // Save prefs
+                if (DEBUG) {
+                    Log.d(TAG, "add/customLocales: " + customLocales);
+                }
+                mPrefs.edit().putString(CUSTOM_LOCALES, customLocales).commit();
+
+                Toast.makeText(this,
+                               getString(R.string.added_custom_locale_1s, locale),
+                               Toast.LENGTH_SHORT)
+                     .show();
+
+                // Update list view
+                setupLocaleList();
+
+                // Find the item to select it in the list view
+                checkLocaleInList(locale);
+
+                if (data.getExtras().getBoolean(NewLocaleDialog.INTENT_EXTRA_SELECT)) {
+                    changeSystemLocale(locale);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        super.onListItemClick(l, v, position, id);
+        updateLocaleButtons();
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+
+        if (menuInfo instanceof AdapterContextMenuInfo) {
+            int position = ((AdapterContextMenuInfo) menuInfo).position;
+            Object o = mListView.getItemAtPosition(position);
+            if (o instanceof LocaleInfo) {
+                if (((LocaleInfo) o).isCustom()) {
+                    menu.setHeaderTitle("System Locale");
+                    menu.add(0, MENU_APPLY, 0, "Apply");
+                } else {
+                    menu.setHeaderTitle("Custom Locale");
+                    menu.add(0, MENU_APPLY, 0, "Apply");
+                    menu.add(0, MENU_REMOVE, 0, "Remove");
+                }
+            }
+        }
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        if (id == DLG_REMOVE_ID) {
+            return createRemoveLocaleDialog();
+        }
+        return super.onCreateDialog(id);
+    }
+
+    //--- private parts ---
+
+    private void setupLocaleList() {
+        if (DEBUG) {
+            Log.d(TAG, "Update locate list");
+        }
+
+        ArrayList<LocaleInfo> data = new ArrayList<LocaleInfo>();
+
+        // Insert all system locales
+        String[] locales = getAssets().getLocales();
+        for (String locale : locales) {
+            if (locale != null && locale.length() > 0) {
+                Locale loc = new Locale(locale);
+                data.add(new LocaleInfo(locale, loc.getDisplayName()));
+            }
+        }
+        locales = null;
+
+        // Insert all custom locales
+        String customLocales = mPrefs.getString(CUSTOM_LOCALES, "");
+        if (DEBUG) {
+            Log.d(TAG, "customLocales: " + customLocales);
+        }
+        for (String locale : customLocales.split(CUSTOM_LOCALES_SEP)) {
+            if (locale != null && locale.length() > 0) {
+                Locale loc = new Locale(locale);
+                data.add(new LocaleInfo(
+                                locale,
+                                loc.getDisplayName(),
+                                true /*custom*/));
+            }
+        }
+
+        // Sort all locales by code
+        Collections.sort(data, new Comparator<LocaleInfo>() {
+            public int compare(LocaleInfo lhs, LocaleInfo rhs) {
+                return lhs.getLocale().compareTo(rhs.getLocale());
+            }
+        });
+
+        // Update the list view adapter
+        mListView.setAdapter(new ArrayAdapter<LocaleInfo>(
+                        this,
+                        android.R.layout.simple_list_item_single_choice,
+                        data));
+        updateLocaleButtons();
+    }
+
+    private void changeSystemLocale(String locale) {
+        if (ChangeLocale.changeSystemLocale(locale)) {
+            Toast.makeText(this,
+                            getString(R.string.select_locale_1s, locale),
+                            Toast.LENGTH_SHORT)
+                 .show();
+        }
+    }
+
+    private void displayCurrentLocale() {
+        try {
+            IActivityManager am = ActivityManagerNative.getDefault();
+            Configuration config = am.getConfiguration();
+
+            if (config.locale != null) {
+                String text = String.format("%1$s - %2$s",
+                        config.locale.toString(),
+                        config.locale.getDisplayName());
+                mCurrentLocaleTextView.setText(text);
+
+                checkLocaleInList(config.locale.toString());
+            }
+        } catch (RemoteException e) {
+            if (DEBUG) {
+                Log.e(TAG, "get current locale failed", e);
+            }
+        }
+    }
+
+    /** Find the locale by code to select it in the list view */
+    private void checkLocaleInList(String locale) {
+        ListAdapter a = mListView.getAdapter();
+        for (int i = 0; i < a.getCount(); i++) {
+            Object o = a.getItem(i);
+            if (o instanceof LocaleInfo) {
+                String code = ((LocaleInfo) o).getLocale();
+                if (code != null && code.equals(locale)) {
+                    mListView.setSelection(i);
+                    mListView.clearChoices();
+                    mListView.setItemChecked(i, true);
+                    updateLocaleButtons();
+                    break;
+                }
+            }
+        }
+    }
+
+    private LocaleInfo getCheckedLocale() {
+        int pos = mListView.getCheckedItemPosition();
+        ListAdapter a = mListView.getAdapter();
+        int n = a.getCount();
+        if (pos >= 0 && pos < n) {
+            Object o = a.getItem(pos);
+            if (o instanceof LocaleInfo) {
+                return (LocaleInfo) o;
+            }
+        }
+
+        return null;
+    }
+
+    /** Update the Select/Remove buttons based on the currently checked locale. */
+    private void updateLocaleButtons() {
+        LocaleInfo info = getCheckedLocale();
+        if (info != null) {
+            // Enable it
+            mSelectLocaleButton.setEnabled(true);
+            mSelectLocaleButton.setText(
+                getString(R.string.select_locale_1s_button, info.getLocale()));
+
+            // Enable the remove button only for custom locales and set the tag to the locale
+            mRemoveLocaleButton.setEnabled(info.isCustom());
+        } else {
+            // If nothing is selected, disable the buttons
+            mSelectLocaleButton.setEnabled(false);
+            mSelectLocaleButton.setText(R.string.select_locale_button);
+
+            mRemoveLocaleButton.setEnabled(false);
+        }
+    }
+
+    /** Invoked by the button "Add new..." */
+    private void onAddNewLocale() {
+        Intent i = new Intent(CustomLocaleActivity.this, NewLocaleDialog.class);
+        startActivityForResult(i, UPDATE_LIST);
+    }
+
+    /** Invoked by the button Select / mSelectLocaleButton */
+    private void onSelectLocale() {
+        LocaleInfo info = getCheckedLocale();
+        if (info != null) {
+            changeSystemLocale(info.getLocale());
+        }
+    }
+
+    /**
+     * Invoked by the button Remove / mRemoveLocaleButton.
+     * Creates a dialog to ask for confirmation before actually remove the custom locale.
+     */
+    private Dialog createRemoveLocaleDialog() {
+
+        LocaleInfo info = getCheckedLocale();
+        final String localeToRemove = info == null ? "<error>" : info.getLocale();
+
+        AlertDialog.Builder b = new AlertDialog.Builder(this);
+        b.setMessage(getString(R.string.confirm_remove_locale_1s, localeToRemove));
+        b.setCancelable(false);
+        b.setPositiveButton(R.string.confirm_remove_locale_yes,
+            new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    removeCustomLocale(localeToRemove);
+                    dismissDialog(DLG_REMOVE_ID);
+                }
+        });
+        b.setNegativeButton(R.string.confirm_remove_locale_no,
+            new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    dismissDialog(DLG_REMOVE_ID);
+                }
+        });
+
+        return b.create();
+    }
+
+    private void removeCustomLocale(String localeToRemove) {
+        // Get current custom locale list
+        String oldLocales = mPrefs.getString(CUSTOM_LOCALES, "");
+
+        if (DEBUG) {
+            Log.d(TAG, "Remove " + localeToRemove + " from custom locales: " + oldLocales);
+        }
+
+        // Update
+        StringBuilder sb = new StringBuilder();
+        for (String locale : oldLocales.split(CUSTOM_LOCALES_SEP)) {
+            if (locale != null && locale.length() > 0 && !locale.equals(localeToRemove)) {
+                if (sb.length() > 0) {
+                    sb.append(CUSTOM_LOCALES_SEP);
+                }
+                sb.append(locale);
+            }
+        }
+
+        String newLocales = sb.toString();
+        if (!newLocales.equals(oldLocales)) {
+            // Save prefs
+            boolean ok = mPrefs.edit().putString(CUSTOM_LOCALES, newLocales).commit();
+            if (DEBUG) {
+                Log.d(TAG, "Prefs commit:" + Boolean.toString(ok) + ". Saved: " + newLocales);
+            }
+
+            Toast.makeText(this,
+                            getString(R.string.removed_custom_locale_1s, localeToRemove),
+                            Toast.LENGTH_SHORT)
+                 .show();
+
+            // Update list view
+            setupLocaleList();
+        }
+    }
+
+    /**
+     * Immutable structure that holds the information displayed by a list view item.
+     */
+    private static class LocaleInfo {
+        private final String mLocale;
+        private final String mDisplayName;
+        private final boolean mIsCustom;
+
+        public LocaleInfo(String locale, String displayName, boolean isCustom) {
+            mLocale = locale;
+            mDisplayName = displayName;
+            mIsCustom = isCustom;
+        }
+
+        public LocaleInfo(String locale, String displayName) {
+            this(locale, displayName, false /*custom*/);
+        }
+
+        public String getLocale() {
+            return mLocale;
+        }
+
+        public boolean isCustom() {
+            return mIsCustom;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(mLocale)
+              .append(" - ")
+              .append(mDisplayName);
+            if (mIsCustom) {
+                sb.append(" [Custom]");
+            }
+            return sb.toString();
+        }
+    }
+}
diff --git a/apps/CustomLocale/src/com/android/customlocale2/CustomLocaleReceiver.java b/apps/CustomLocale/src/com/android/customlocale2/CustomLocaleReceiver.java
new file mode 100755 (executable)
index 0000000..30d886e
--- /dev/null
@@ -0,0 +1,82 @@
+/*\r
+ * Copyright (C) 2011 The Android Open Source Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.android.customlocale2;\r
+\r
+import android.app.Activity;\r
+import android.content.BroadcastReceiver;\r
+import android.content.Context;\r
+import android.content.Intent;\r
+import android.util.Log;\r
+\r
+//-----------------------------------------------\r
+\r
+/**\r
+ * Broadcast receiver that can change the system's locale.\r
+ * <p/>\r
+ * This allows an external script such as an automated testing framework\r
+ * to easily trigger a locale change on an emulator as such:\r
+ * <pre>\r
+ * $ adb shell am broadcast -a com.android.intent.action.SET_LOCALE \\r
+ *                          --es com.android.intent.extra.LOCALE en_US\r
+ * </pre>\r
+ */\r
+public class CustomLocaleReceiver extends BroadcastReceiver {\r
+\r
+    private static final String TAG = CustomLocaleReceiver.class.getSimpleName();\r
+    private static final boolean DEBUG = true;\r
+\r
+    /** Intent action that triggers this receiver. */\r
+    public static final String ACTION_SET_LOCALE = "com.android.intent.action.SET_LOCALE";\r
+    /** An extra String that specifies the locale to set, in the form "en_US". */\r
+    public static final String EXTRA_LOCALE = "com.android.intent.extra.LOCALE";\r
+\r
+    @Override\r
+    public void onReceive(Context context, Intent intent) {\r
+        setResult(Activity.RESULT_CANCELED, null, null);\r
+        if (intent == null || ! ACTION_SET_LOCALE.equals(intent.getAction())) {\r
+            if (DEBUG) {\r
+                Log.d(TAG, "Invalid intent: " + (intent == null ? "null" : intent.toString()));\r
+            }\r
+            return;\r
+        }\r
+\r
+        String locale = intent.getStringExtra(EXTRA_LOCALE);\r
+\r
+        // Enforce the locale string is either in the form "ab" or "ab_cd"\r
+        boolean is_ok = locale != null;\r
+        is_ok = is_ok && (locale.length() == 2 || locale.length() == 5);\r
+        if (is_ok && locale.length() >= 2) {\r
+            is_ok = Character.isLetter(locale.charAt(0)) &&\r
+                    Character.isLetter(locale.charAt(1));\r
+        }\r
+        if (is_ok && locale.length() == 5) {\r
+            is_ok = locale.charAt(2) == '_' &&\r
+                    Character.isLetter(locale.charAt(3)) &&\r
+                    Character.isLetter(locale.charAt(4));\r
+        }\r
+\r
+        if (!is_ok && DEBUG) {\r
+            Log.e(TAG, "Invalid locale: expected ab_CD but got " + locale);\r
+        } else if (is_ok) {\r
+            ChangeLocale.changeSystemLocale(locale);\r
+            setResult(Activity.RESULT_OK, locale, null);\r
+        }\r
+    }\r
+\r
+}\r
+\r
+\r
  * limitations under the License.
  */
 
-package com.android.customlocale;
+package com.android.customlocale2;
 
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
-import android.text.TextUtils;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.View;
 import android.widget.Button;
 import android.widget.EditText;
@@ -41,7 +39,6 @@ public class NewLocaleDialog extends Activity implements View.OnClickListener {
     private Button mButtonAdd;
     private Button mButtonAddSelect;
     private EditText mEditText;
-    private boolean mWasEmpty;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -50,7 +47,6 @@ public class NewLocaleDialog extends Activity implements View.OnClickListener {
         setContentView(R.layout.new_locale);
 
         mEditText = (EditText) findViewById(R.id.value);
-        mWasEmpty = true;
 
         mButtonAdd = (Button) findViewById(R.id.add);
         mButtonAdd.setOnClickListener(this);
@@ -62,7 +58,7 @@ public class NewLocaleDialog extends Activity implements View.OnClickListener {
     public void onClick(View v) {
         String locale = mEditText.getText().toString();
         boolean select = v == mButtonAddSelect;
-        
+
         if (DEBUG) {
             Log.d(TAG, "New Locale: " + locale + (select ? " + select" : ""));
         }