Hotseats: better strategy for finding the default browser.

To do this, we invoke resolveActivity to see what activity
would result from viewing an arbitrary (but valid) URL. If
there's just one installed, or there are multiple and the
user has chosen a default handler for http:, we take that
component and launch it with CATEGORY_HOME (so as not to
upset the URL in the frontmost window/tab/what-have-you).
We also use this information to extract the localized name
of the app, which is then installed into the hotseat as the
contentDescription (for accessibility).

If there's no default and multiple options are availble,
we'll get the activity chooser instead. In this case, we
just fire off that chooser and let the user pick an app
(possibly setting a default along the way). Because the
default may change, we reload all this hotseat information
every time one of the hotseats is tapped.

Another side-effect of this approach is that until there
exists a default browser, the original URL will be sent to
the activity the user chooses from the ResolveActivity. So
we need a sensible default URL here; one can be found in
R.string.default_browser_url (similar to Browser's
R.string.homepage_base).

This change also moves the hotseat intents and icons into
arrays.xml for easier configuration.

Change-Id: I06268df8b59e0f41f1f8b0e47f823db4c44ec761
diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java
index 5a2a7d3..132f0e9 100644
--- a/src/com/android/launcher2/Launcher.java
+++ b/src/com/android/launcher2/Launcher.java
@@ -35,8 +35,10 @@
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
@@ -74,6 +76,7 @@
 import android.appwidget.AppWidgetProviderInfo;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.HashMap;
 import java.io.DataOutputStream;
 import java.io.FileNotFoundException;
@@ -203,13 +206,10 @@
     // Hotseats (quick-launch icons next to AllApps)
     // TODO: move these intial intents out to Uris in an XML resource
     private static final int NUM_HOTSEATS = 2;
-    private Intent[] mHotseats = new Intent[] {
-        new Intent(Intent.ACTION_MAIN)
-            .setComponent(ComponentName.unflattenFromString(
-                "com.android.contacts/.ContactsLaunchActivity")),
-        new Intent(Intent.ACTION_WEB_SEARCH, Uri.EMPTY),
-    };
-    private CharSequence[] mHotseatLabels = new CharSequence[2];
+    private String[] mHotseatConfig = null;
+    private Intent[] mHotseats = null;
+    private Drawable[] mHotseatIcons = null;
+    private CharSequence[] mHotseatLabels = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -368,37 +368,144 @@
         wpm.suggestDesiredDimensions(width * WALLPAPER_SCREENS_SPAN, height);
     }
 
+    // Note: This doesn't do all the client-id magic that BrowserProvider does
+    // in Browser. (http://b/2425179)
+    private Uri getDefaultBrowserUri() {
+        String url = getString(R.string.default_browser_url);
+        if (url.indexOf("{CID}") != -1) {
+            url = url.replace("{CID}", "android-google");
+        }
+        return Uri.parse(url);
+    }
+
+    // Load the Intent templates from arrays.xml to populate the hotseats. For
+    // each Intent, if it resolves to a single app, use that as the launch
+    // intent & use that app's label as the contentDescription. Otherwise,
+    // retain the ResolveActivity so the user can pick an app.
     private void loadHotseats() {
+        if (mHotseatConfig == null) {
+            mHotseatConfig = getResources().getStringArray(R.array.hotseats);
+            if (mHotseatConfig.length > 0) {
+                mHotseats = new Intent[mHotseatConfig.length];
+                mHotseatLabels = new CharSequence[mHotseatConfig.length];
+                mHotseatIcons = new Drawable[mHotseatConfig.length];
+            } else {
+                mHotseats = null;
+                mHotseatIcons = null;
+                mHotseatLabels = null;
+            }
+
+            TypedArray hotseatIconDrawables = getResources().obtainTypedArray(R.array.hotseat_icons);
+            for (int i=0; i<mHotseatConfig.length; i++) {
+                // load icon for this slot; currently unrelated to the actual activity
+                try {
+                    mHotseatIcons[i] = hotseatIconDrawables.getDrawable(i);
+                } catch (ArrayIndexOutOfBoundsException ex) {
+                    Log.w(TAG, "Missing hotseat_icons array item #" + i);
+                    mHotseatIcons[i] = null;
+                }
+            }
+            hotseatIconDrawables.recycle();
+        }
+
         PackageManager pm = getPackageManager();
-        for (int i=0; i<mHotseats.length; i++) {
-            Intent intent = mHotseats[i];
+        for (int i=0; i<mHotseatConfig.length; i++) {
+            Intent intent = null;
+            if (mHotseatConfig[i].equals("*BROWSER*")) {
+                // magic value meaning "launch user's default web browser"
+                // replace it with a generic web request so we can see if there is indeed a default
+                String defaultUri = getString(R.string.default_browser_url);
+                intent = new Intent(
+                        Intent.ACTION_VIEW,
+                        ((defaultUri != null)
+                            ? Uri.parse(defaultUri)
+                            : getDefaultBrowserUri())
+                    ).addCategory(Intent.CATEGORY_BROWSABLE);
+                // note: if the user launches this without a default set, she
+                // will always be taken to the default URL above; this is
+                // unavoidable as we must specify a valid URL in order for the
+                // chooser to appear, and once the user selects something, that 
+                // URL is unavoidably sent to the chosen app.
+            } else {
+                try {
+                    intent = Intent.parseUri(mHotseatConfig[i], 0);
+                } catch (java.net.URISyntaxException ex) {
+                    Log.w(TAG, "Invalid hotseat intent: " + mHotseatConfig[i]);
+                    // bogus; leave intent=null
+                }
+            }
+            
+            if (intent == null) {
+                mHotseats[i] = null;
+                mHotseatLabels[i] = getText(R.string.activity_not_found);
+                continue;
+            }
 
             if (LOGD) {
                 Log.d(TAG, "loadHotseats: hotseat " + i 
-                    + " initial intent=[" + intent.toUri(Intent.URI_INTENT_SCHEME)
+                    + " initial intent=[" 
+                    + intent.toUri(Intent.URI_INTENT_SCHEME)
                     + "]");
             }
 
-            // fix up the default intents
-            if (intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
-                    && intent.getData().equals(Uri.EMPTY)) {
-                // use this to represent "default web browser"
-                intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com/"));
+            ResolveInfo bestMatch = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+            List<ResolveInfo> allMatches = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+            if (LOGD) { 
+                Log.d(TAG, "Best match for intent: " + bestMatch);
+                Log.d(TAG, "All matches: ");
+                for (ResolveInfo ri : allMatches) {
+                    Log.d(TAG, "  --> " + ri);
+                }
             }
-            ComponentName com = intent.resolveActivity(pm);
-            mHotseats[i] = new Intent(Intent.ACTION_MAIN).setComponent(com);
+            // did this resolve to a single app, or the resolver?
+            if (allMatches.size() == 0 || bestMatch == null) {
+                // can't find any activity to handle this. let's leave the 
+                // intent as-is and let Launcher show a toast when it fails 
+                // to launch.
+                mHotseats[i] = intent;
 
-            // load the labels for accessibility
-            try {
-                ActivityInfo ai = pm.getActivityInfo(com, 0);
-                mHotseatLabels[i] = ai.loadLabel(pm);
-            } catch (PackageManager.NameNotFoundException ex) {
-                mHotseatLabels[i] = "";
+                // set accessibility text to "Not installed"
+                mHotseatLabels[i] = getText(R.string.activity_not_found);
+            } else {
+                boolean found = false;
+                for (ResolveInfo ri : allMatches) {
+                    if (bestMatch.activityInfo.name.equals(ri.activityInfo.name)
+                        && bestMatch.activityInfo.applicationInfo.packageName
+                            .equals(ri.activityInfo.applicationInfo.packageName)) {
+                        found = true;
+                        break;
+                    }
+                }
+                
+                if (!found) {
+                    if (LOGD) Log.d(TAG, "Multiple options, no default yet");
+                    // the bestMatch is probably the ResolveActivity, meaning the
+                    // user has not yet selected a default
+                    // so: we'll keep the original intent for now
+                    mHotseats[i] = intent;
+
+                    // set the accessibility text to "Select shortcut"
+                    mHotseatLabels[i] = getText(R.string.title_select_shortcut);
+                } else {
+                    // we have an app!
+                    // now reconstruct the intent to launch it through the front
+                    // door
+                    ComponentName com = new ComponentName(
+                        bestMatch.activityInfo.applicationInfo.packageName,
+                        bestMatch.activityInfo.name);
+                    mHotseats[i] = new Intent(Intent.ACTION_MAIN).setComponent(com);
+
+                    // load the app label for accessibility
+                    mHotseatLabels[i] = bestMatch.activityInfo.loadLabel(pm);
+                }
             }
 
             if (LOGD) {
                 Log.d(TAG, "loadHotseats: hotseat " + i 
-                    + " intent=[" + mHotseats[i].toUri(Intent.URI_INTENT_SCHEME)
+                    + " final intent=[" 
+                    + ((mHotseats[i] == null)
+                        ? "null"
+                        : mHotseats[i].toUri(Intent.URI_INTENT_SCHEME))
                     + "] label=[" + mHotseatLabels[i]
                     + "]"
                     );
@@ -622,8 +729,12 @@
         mHandleView.setOnClickListener(this);
         mHandleView.setOnLongClickListener(this);
 
-        findViewById(R.id.hotseat_left).setContentDescription(mHotseatLabels[0]);
-        findViewById(R.id.hotseat_right).setContentDescription(mHotseatLabels[1]);
+        ImageView hotseatLeft = (ImageView) findViewById(R.id.hotseat_left);
+        hotseatLeft.setContentDescription(mHotseatLabels[0]);
+        hotseatLeft.setImageDrawable(mHotseatIcons[0]);
+        ImageView hotseatRight = (ImageView) findViewById(R.id.hotseat_right);
+        hotseatRight.setContentDescription(mHotseatLabels[1]);
+        hotseatRight.setImageDrawable(mHotseatIcons[1]);
 
         mPreviousView = (ImageView) dragLayer.findViewById(R.id.previous_screen);
         mNextView = (ImageView) dragLayer.findViewById(R.id.next_screen);
@@ -678,7 +789,10 @@
             index = 1;
         }
 
-        if (index >= 0 && mHotseats[index] != null) {
+        // reload these every tap; you never know when they might change
+        loadHotseats();
+        if (index >= 0 && index < mHotseats.length && mHotseats[index] != null) {
+            Intent intent = mHotseats[index];
             startActivitySafely(
                 mHotseats[index],
                 "hotseat"