I write solutions to the problems I can't find much about elsewhere on the Web, also some code/script snippets that are absolutely awesome and make my life easier. Will be glad if someone finds these posts interesting and helpful!

Friday, April 29, 2011

Android universal Intent to start a "navigation activity"

The mission is what the title says. It was quite hard to define the problem to search for a solution. I ended up asking for help at StackOverflow and as I was suggested, I implemented a way to list some known apps and let the user select a preferred nav app.

So, let's say, somewhere on Activity there's a button, which on click is supposed to call the Navigation application. The way I did it, on tap, the activity shows a custom dialog letting the user select one of the known applications or prompt Android to find suitable activities implicitly.

Here's the source code of the payload of the main method invoked from the OnClickListener - showNavigation():
final int defaultFlag = PackageManager.MATCH_DEFAULT_ONLY;
Intent[] explicitIntents;
//see an example here http://stackoverflow.com/questions/2662531/launching-google-maps-directions-via-an-intent-on-android
private Intent[] getExplicitIntents() {
if(explicitIntents == null) {
PackageManager currentPM = getPackageManager();
explicitIntents = new Intent[]{
new Intent("android.intent.action.navigon.START_PUBLIC"), //navigon with public intent
currentPM.getLaunchIntentForPackage("com.navigon.navigator"), //navigon without public intent
currentPM.getLaunchIntentForPackage("hr.mireo.dp"), //ginius driver dont panic
currentPM.getLaunchIntentForPackage("com.ndrive.android"), //ndrive
currentPM.getLaunchIntentForPackage("com.sygic.aura"), //aura
currentPM.getLaunchIntentForPackage("org.microemu.android.se.appello.lp.Lightpilot") // wisepilot
};
}
return explicitIntents;
}
/**
* Following the suggestion at http://stackoverflow.com/questions/5801684/intent-to-start-a-navigation-activity/5809637#5809637
* Will query up the system for desired applications and then let the user decide which one they would like to launch.
*/
protected void showNavigation() {
//explicit activities (known apps)
//build the list and show it in a dialog
int titleColor = Color.rgb(28, 97, 211);
int headerColor = Color.rgb(98, 151, 251);
HashMap<String, ArrayList<TableRow>> rows = new HashMap<String, ArrayList<TableRow>>();
ArrayList<TableRow> tableRows = new ArrayList<TableRow>();
TableRow row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
row.setBackgroundColor(headerColor);
TextView recyclableTextView = new TextView(this);
recyclableTextView.setText("Explicitly known");
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
int screenWidth = getResources().getDisplayMetrics().widthPixels;
recyclableTextView.setWidth(100 * screenWidth / 100); //take up 100% of the width
recyclableTextView.setPadding(5, 5, 5, 5);
row.addView(recyclableTextView);
tableRows.add(row);
PackageManager currentPM = getPackageManager();
for(int i = 0; i < getExplicitIntents().length; i++) {
Intent navigationAppIntent = explicitIntents[i];
try {
for(ResolveInfo explicitActivityInfo : currentPM.queryIntentActivities(navigationAppIntent, defaultFlag)) {
try {
row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
recyclableTextView = new TextView(this);
recyclableTextView.setText(explicitActivityInfo.loadLabel(currentPM) + " (" + explicitActivityInfo.activityInfo.applicationInfo.packageName + ")");
recyclableTextView.setTypeface(Typeface.DEFAULT_BOLD);
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
recyclableTextView.setWidth(100 * screenWidth / 100);
recyclableTextView.setPadding(5, 5, 5, 5);
row.addView(recyclableTextView);
row.setContentDescription("" + i); //this descriptor will be used in the following listener to find out which row was selected
//set onclick listener to each row (it already has a descriptor pointing the intent index it represents from the array)
row.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String desc = "" + v.getContentDescription();
if(desc != null) {
try {
startActivity(getExplicitIntents()[Integer.parseInt(desc)]);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
});
row.setMinimumHeight(100);
tableRows.add(row);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
catch(Exception e) {
e.printStackTrace();
}
}
//add implicit apps option
row = new TableRow(this);
row.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
row.setGravity(Gravity.CENTER);
row.addView(makePoiTableRowWithText("Find Implicitly", true, 100));
row.setMinimumHeight(150);
//add a listener to this view to let the OS find matching applications
row.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent implicitIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q="); //NOTE: google navigation can't be launched without destination
startActivity(implicitIntent);
}
});
tableRows.add(row);
rows.put("Compatible apps", tableRows);
showTableDialog(rows, "Launch GPS Navigator", titleColor, R.drawable.menu_gps);
}
view raw gistfile1.java hosted with ❤ by GitHub


Here are few utils needed only to follow up the last call in the showNavigation() method - showTableDialog():
static final int TABLE_DIALOG = 0;
ArrayList<TableRow> rows;
String tableDialogTime;
int tableDialogTableHeaderColor;
int tableDialogTableHeaderIcon;
Dialog dialog;
/**
* Pops up a dialog to show a table.
* @param rows
*/
public void showTableDialog(ArrayList<TableRow> rows, String time, int headerColor, int headerIcon) {
this.rows = rows;
this.tableDialogTableHeaderColor = headerColor;
this.tableDialogTableHeaderIcon = headerIcon;
activityHandler.post(new Runnable() {
@Override
public void run() {
showDialog(TABLE_DIALOG);
}
});
}
public Dialog onCreateDialog(int id) {
switch(id) {
case TABLE_DIALOG:
dialog = new Dialog(this, R.style.TableDialog);
dialog.setContentView(R.layout.table_dialog);
break;
default:
dialog = null;
}
return dialog;
}
public void onPrepareDialog(int id, Dialog dialog) {
if(id == TABLE_DIALOG) {
//set icon
((ImageView) dialog.findViewById(R.id.table_dialog_icon)).setBackgroundDrawable(getResources().getDrawable(tableDialogTableHeaderIcon));
//set background for the whole header
dialog.findViewById(R.id.table_dialog_header).setBackgroundColor(tableDialogTableHeaderColor);
//set title
TextView titleField = (TextView) dialog.findViewById(R.id.table_dialog_title);
titleField.setText(title);
//set up the header for the table on a separate TableLayout (it's a separate TableLayout, so that it doesn't scroll along with the rest of the scrollable area)
TableLayout tableHeader = (TableLayout) dialog.findViewById(R.id.table_header);
tableHeader.removeAllViews();
tableHeader.addView(rows.get(0));
//set up the table
TableLayout table = (TableLayout) dialog.findViewById(R.id.table_dialog_layout);
//clean up old entries (on re-invoke)
table.removeAllViews();
//populate table rows: add rows with separator inbetween
for(int i = 1; i < rows.size(); i++) {
int colorAmount = i % 2 == 0? 230 : 200;
TableRow row = rows.get(i);
row.setBackgroundColor(Color.rgb(colorAmount, colorAmount, colorAmount));
table.addView(row);
//add a separator after each line
View separator = new View(dialog.getContext());
separator.setBackgroundColor(Color.rgb(77, 77, 77));
separator.setMinimumHeight(2);
table.addView(separator);
}
}
}
private TextView recyclableTextView;
public TextView makePoiTableRowWithText(CharSequence text, boolean bold, int widthInPercentOfScreenWidth) {
recyclableTextView = new TextView(this);
recyclableTextView.setText(text);
recyclableTextView.setTypeface(bold? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
recyclableTextView.setTextColor(Color.BLACK);
recyclableTextView.setTextSize(20);
recyclableTextView.setWidth(widthInPercentOfScreenWidth * screenWidth / 100);
recyclableTextView.setPadding(5, 5, 5, 5);
return recyclableTextView;
}
view raw gistfile1.java hosted with ❤ by GitHub


These are the resource files:
* this goes to /res/values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TableDialog" parent="android:style/Theme.Dialog">
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>
view raw themes.xml hosted with ❤ by GitHub

* this goes to /res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialogText">
<item name="android:textColor">#FF231f20</item>
<item name="android:textSize">25sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="DialogText.Title">
<item name="android:textColor">#FF364945</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
</style>
</resources>
view raw styles.xml hosted with ❤ by GitHub

* this goes to /res/layout/table_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:gravity="center_horizontal"
android:id="@+id/table_dialog_header">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/table_dialog_icon"/>
<TextView
android:id="@+id/table_dialog_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textSize="30sp"
android:textColor="#fff"
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
android:id="@+id/fillable_area">
<TableLayout
android:id="@+id/table_header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<ScrollView
android:id="@+id/scrollable_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TableLayout
android:id="@+id/table_dialog_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</ScrollView>
</LinearLayout>
</LinearLayout>

* this goes to /res/drawable as menu_gps.png







And here is the result dialog you are supposed to see:

On click on each row it will start up the respective activity.
That's it, I hope it helps!

2 comments:

  1. Very nice!

    But your code will serve to start a turn by turn Navigator withour any destination set, correct?

    Do you know any way to start turn-by-turn Navigation to a specified lat/long coordinates? I wonder if this is possible with NDrive for example.

    ReplyDelete
  2. @Fede, that's right, without destination. The only navigator I know of which supports destination (it supports a public Intent and accepts parameters) is Navigon. That's the reason why it's the only one with 'new Intent' statement in the list of navigators.

    ReplyDelete