diff --git a/apps/CustomLocale/Android.mk b/apps/CustomLocale/Android.mk new file mode 100644 index 000000000..275207f29 --- /dev/null +++ b/apps/CustomLocale/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := CustomLocale +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/apps/CustomLocale/AndroidManifest.xml b/apps/CustomLocale/AndroidManifest.xml new file mode 100644 index 000000000..9dfa89630 --- /dev/null +++ b/apps/CustomLocale/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/apps/CustomLocale/res/drawable/icon.png b/apps/CustomLocale/res/drawable/icon.png new file mode 100644 index 000000000..cb40a1988 Binary files /dev/null and b/apps/CustomLocale/res/drawable/icon.png differ diff --git a/apps/CustomLocale/res/layout/list_item.xml b/apps/CustomLocale/res/layout/list_item.xml new file mode 100644 index 000000000..8c59f92c7 --- /dev/null +++ b/apps/CustomLocale/res/layout/list_item.xml @@ -0,0 +1,35 @@ + + + + + + \ No newline at end of file diff --git a/apps/CustomLocale/res/layout/main.xml b/apps/CustomLocale/res/layout/main.xml new file mode 100644 index 000000000..b1eaa51ba --- /dev/null +++ b/apps/CustomLocale/res/layout/main.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + diff --git a/samples/ApiDemos/res/layout/reorder_on_launch.xml b/samples/ApiDemos/res/layout/reorder_on_launch.xml new file mode 100644 index 000000000..850a2f58c --- /dev/null +++ b/samples/ApiDemos/res/layout/reorder_on_launch.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/samples/ApiDemos/res/layout/reorder_three.xml b/samples/ApiDemos/res/layout/reorder_three.xml new file mode 100644 index 000000000..30ef41b55 --- /dev/null +++ b/samples/ApiDemos/res/layout/reorder_three.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/samples/ApiDemos/res/layout/reorder_two.xml b/samples/ApiDemos/res/layout/reorder_two.xml new file mode 100644 index 000000000..713256194 --- /dev/null +++ b/samples/ApiDemos/res/layout/reorder_two.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/samples/ApiDemos/res/values/strings.xml b/samples/ApiDemos/res/values/strings.xml index 7db3e1b7c..0cafe1428 100644 --- a/samples/ApiDemos/res/values/strings.xml +++ b/samples/ApiDemos/res/values/strings.xml @@ -185,11 +185,21 @@ Change Left Change Right + App/Activity/Reorder Activities + This is the first of a sequence of four Activities. A button on the fourth will use the Intent.FLAG_ACTIVITY_REORDER_TO_FRONT flag to bring the second of the activities to the front of the history stack. After that, proceeding back through the history should begin with the newly-frontmost second reorder activity, then the fourth, the third, and finally the first. + Go to the second + This is the second in a sequence of four Activities. + Go to the third + This is the third of a sequence of four Activities. + Go to the fourth + This is the last in a sequence of four Activities. + Bring the second in front + App/Menu/Inflate from XML Select a menu resource and press the menu key. If you want to choose another menu resource, go back and re-run this activity. - App/Voice Recognition + App/Voice Recognition @@ -803,5 +813,12 @@ textColorTertiary listSeparatorTextViewStyle + + + + + This text will be shown before the date in our example widget. + Oh hai + %1$s: %2$s diff --git a/samples/ApiDemos/res/xml/appwidget_provider.xml b/samples/ApiDemos/res/xml/appwidget_provider.xml new file mode 100644 index 000000000..5f5c7351e --- /dev/null +++ b/samples/ApiDemos/res/xml/appwidget_provider.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/samples/ApiDemos/res/xml/searchable.xml b/samples/ApiDemos/res/xml/searchable.xml index df0924891..df4ef7acc 100644 --- a/samples/ApiDemos/res/xml/searchable.xml +++ b/samples/ApiDemos/res/xml/searchable.xml @@ -22,6 +22,10 @@ android:hint="@string/search_hint" android:searchMode="showSearchLabelAsBadge" + android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" + android:voiceLanguageModel="free_form" + android:voicePromptText="@string/search_invoke" + android:searchSuggestAuthority="com.example.android.apis.SuggestionProvider" android:searchSuggestSelection=" ? " /> diff --git a/samples/ApiDemos/src/com/example/android/apis/app/ReorderFour.java b/samples/ApiDemos/src/com/example/android/apis/app/ReorderFour.java new file mode 100644 index 000000000..cdff538f3 --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/ReorderFour.java @@ -0,0 +1,46 @@ +/* + * 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.example.android.apis.app; + +import com.example.android.apis.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +public class ReorderFour extends Activity { + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + setContentView(R.layout.reorder_four); + + Button twoButton = (Button) findViewById(R.id.reorder_second_to_front); + twoButton.setOnClickListener(mClickListener); + } + + private final OnClickListener mClickListener = new OnClickListener() { + public void onClick(View v) { + Intent intent = new Intent(ReorderFour.this, ReorderTwo.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(intent); + } + }; +} \ No newline at end of file diff --git a/samples/ApiDemos/src/com/example/android/apis/app/ReorderOnLaunch.java b/samples/ApiDemos/src/com/example/android/apis/app/ReorderOnLaunch.java new file mode 100644 index 000000000..58561848e --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/ReorderOnLaunch.java @@ -0,0 +1,44 @@ +/* + * 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.example.android.apis.app; + +import com.example.android.apis.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +public class ReorderOnLaunch extends Activity { + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + setContentView(R.layout.reorder_on_launch); + + Button twoButton = (Button) findViewById(R.id.reorder_launch_two); + twoButton.setOnClickListener(mClickListener); + } + + private final OnClickListener mClickListener = new OnClickListener() { + public void onClick(View v) { + startActivity(new Intent(ReorderOnLaunch.this, ReorderTwo.class)); + } + }; +} \ No newline at end of file diff --git a/samples/ApiDemos/src/com/example/android/apis/app/ReorderThree.java b/samples/ApiDemos/src/com/example/android/apis/app/ReorderThree.java new file mode 100644 index 000000000..7f725a67b --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/ReorderThree.java @@ -0,0 +1,44 @@ +/* + * 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.example.android.apis.app; + +import com.example.android.apis.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +public class ReorderThree extends Activity { + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + setContentView(R.layout.reorder_three); + + Button twoButton = (Button) findViewById(R.id.reorder_launch_four); + twoButton.setOnClickListener(mClickListener); + } + + private final OnClickListener mClickListener = new OnClickListener() { + public void onClick(View v) { + startActivity(new Intent(ReorderThree.this, ReorderFour.class)); + } + }; +} \ No newline at end of file diff --git a/samples/ApiDemos/src/com/example/android/apis/app/ReorderTwo.java b/samples/ApiDemos/src/com/example/android/apis/app/ReorderTwo.java new file mode 100644 index 000000000..a1521d0ad --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/ReorderTwo.java @@ -0,0 +1,44 @@ +/* + * 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.example.android.apis.app; + +import com.example.android.apis.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +public class ReorderTwo extends Activity { + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + setContentView(R.layout.reorder_two); + + Button twoButton = (Button) findViewById(R.id.reorder_launch_three); + twoButton.setOnClickListener(mClickListener); + } + + private final OnClickListener mClickListener = new OnClickListener() { + public void onClick(View v) { + startActivity(new Intent(ReorderTwo.this, ReorderThree.class)); + } + }; +} \ No newline at end of file diff --git a/samples/ApiDemos/src/com/android/samples/app/VoiceRecognition.java b/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java similarity index 98% rename from samples/ApiDemos/src/com/android/samples/app/VoiceRecognition.java rename to samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java index 35d94307d..a784e1587 100644 --- a/samples/ApiDemos/src/com/android/samples/app/VoiceRecognition.java +++ b/samples/ApiDemos/src/com/example/android/apis/app/VoiceRecognition.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.samples.app; +package com.example.android.apis.app; import android.app.Activity; import android.content.Intent; diff --git a/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetConfigure.java b/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetConfigure.java new file mode 100644 index 000000000..e0a4c76b0 --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetConfigure.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2008 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.example.android.apis.appwidget; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.EditText; + +import java.util.ArrayList; + +// Need the following import to get access to the app resources, since this +// class is in a sub-package. +import com.example.android.apis.R; + +/** + * The configuration screen for the ExampleAppWidgetProvider widget sample. + */ +public class ExampleAppWidgetConfigure extends Activity { + static final String TAG = "ExampleAppWidgetConfigure"; + + private static final String PREFS_NAME + = "com.example.android.apis.appwidget.ExampleAppWidgetProvider"; + private static final String PREF_PREFIX_KEY = "prefix_"; + + int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + EditText mAppWidgetPrefix; + + public ExampleAppWidgetConfigure() { + super(); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if they press the back button. + setResult(RESULT_CANCELED); + + // Set the view layout resource to use. + setContentView(R.layout.appwidget_configure); + + // Find the EditText + mAppWidgetPrefix = (EditText)findViewById(R.id.appwidget_prefix); + + // Bind the action for the save button. + findViewById(R.id.save_button).setOnClickListener(mOnClickListener); + + // Find the widget id from the intent. + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (extras != null) { + mAppWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + } + + // If they gave us an intent without the widget id, just bail. + if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish(); + } + + mAppWidgetPrefix.setText(loadTitlePref(ExampleAppWidgetConfigure.this, mAppWidgetId)); + } + + View.OnClickListener mOnClickListener = new View.OnClickListener() { + public void onClick(View v) { + // When the button is clicked, save the string in our prefs and return that they + // clicked OK. + saveTitlePref(ExampleAppWidgetConfigure.this, mAppWidgetId, + mAppWidgetPrefix.getText().toString()); + + setResult(RESULT_OK); + finish(); + } + }; + + // Write the prefix to the SharedPreferences object for this widget + static void saveTitlePref(Context context, int appWidgetId, String text) { + SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit(); + prefs.putString(PREF_PREFIX_KEY + appWidgetId, text); + prefs.commit(); + } + + // Read the prefix from the SharedPreferences object for this widget. + // If there is no preference saved, get the default from a resource + static String loadTitlePref(Context context, int appWidgetId) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); + String prefix = prefs.getString(PREF_PREFIX_KEY, null); + if (prefix != null) { + return prefix; + } else { + return context.getString(R.string.appwidget_prefix_default); + } + } + + static void deleteTitlePref(Context context, int appWidgetId) { + } + + static void loadAllTitlePrefs(Context context, ArrayList appWidgetIds, + ArrayList texts) { + } +} + + + diff --git a/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetProvider.java b/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetProvider.java new file mode 100644 index 000000000..6977d3e6e --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/appwidget/ExampleAppWidgetProvider.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2008 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.example.android.apis.appwidget; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.SystemClock; +import android.util.Log; +import android.widget.RemoteViews; + +import java.util.ArrayList; + +// Need the following import to get access to the app resources, since this +// class is in a sub-package. +import com.example.android.apis.R; + +/** + * A widget provider. We have a string that we pull from a preference in order to show + * the configuration settings and the current time when the widget was updated. We also + * register a BroadcastReceiver for time-changed and timezone-changed broadcasts, and + * update then too. + * + *

See also the following files: + *

+ */ +public class ExampleAppWidgetProvider extends AppWidgetProvider { + // log tag + private static final String TAG = "ExampleAppWidgetProvider"; + + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + Log.d(TAG, "onUpdate"); + // For each widget that needs an update, get the text that we should display: + // - Create a RemoteViews object for it + // - Set the text in the RemoteViews object + // - Tell the AppWidgetManager to show that views object for the widget. + final int N = appWidgetIds.length; + for (int i=0; i mApplications; private static LinkedList mFavorites; - private Handler mHandler = new Handler(); - private final BroadcastReceiver mWallpaperReceiver = new WallpaperIntentReceiver(); private final BroadcastReceiver mApplicationsReceiver = new ApplicationsIntentReceiver(); - private final ContentObserver mObserver = new FavoritesChangeObserver(); private GridView mGrid; @@ -120,7 +127,6 @@ public class Home extends Activity { setContentView(R.layout.home); registerIntentReceivers(); - registerContentObservers(); setDefaultWallpaper(); @@ -156,7 +162,6 @@ public class Home extends Activity { mApplications.get(i).icon.setCallback(null); } - getContentResolver().unregisterContentObserver(mObserver); unregisterReceiver(mWallpaperReceiver); unregisterReceiver(mApplicationsReceiver); } @@ -198,16 +203,6 @@ public class Home extends Activity { registerReceiver(mApplicationsReceiver, filter); } - /** - * Registers various content observers. The current implementation registers - * only a favorites observer to keep track of the favorites applications. - */ - private void registerContentObservers() { - ContentResolver resolver = getContentResolver(); - resolver.registerContentObserver(Uri.parse("content://" + - android.provider.Settings.AUTHORITY + "/favorites?notify=true"), true, mObserver); - } - /** * Creates a new appplications adapter for the grid view and registers it. */ @@ -247,7 +242,7 @@ public class Home extends Activity { Log.e(LOG_TAG, "Failed to clear wallpaper " + e); } } else { - getWindow().setBackgroundDrawable(wallpaper); + getWindow().setBackgroundDrawable(new ClippedDrawable(wallpaper)); } mWallpaperChecked = true; } @@ -259,18 +254,17 @@ public class Home extends Activity { */ private void bindFavorites(boolean isLaunching) { if (!isLaunching || mFavorites == null) { - final Cursor c = getContentResolver().query(Uri.parse("content://" + - android.provider.Settings.AUTHORITY + "/favorites?notify=true"), - null, null, null, "cellX"); - final int intentIndex = c.getColumnIndexOrThrow("intent"); - final int titleIndex = c.getColumnIndexOrThrow("title"); - final int typeIndex = c.getColumnIndexOrThrow("itemType"); + FileReader favReader; - final PackageManager manager = getPackageManager(); - - ApplicationInfo info; - String intentDescription; + // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system". + final File favFile = new File(Environment.getRootDirectory(), DEFAULT_FAVORITES_PATH); + try { + favReader = new FileReader(favFile); + } catch (FileNotFoundException e) { + Log.e(LOG_TAG, "Couldn't find or open favorites file " + favFile); + return; + } if (mFavorites == null) { mFavorites = new LinkedList(); @@ -278,38 +272,77 @@ public class Home extends Activity { mFavorites.clear(); } - while (c.moveToNext()) { - final int itemType = c.getInt(typeIndex); + final Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); - if (itemType == 0 || // 0 == application - itemType == 1) { // 1 == shortcut + final PackageManager packageManager = getPackageManager(); - intentDescription = c.getString(intentIndex); - if (intentDescription == null) { - continue; + try { + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(favReader); + + beginDocument(parser, TAG_FAVORITES); + + ApplicationInfo info; + + while (true) { + nextElement(parser); + + String name = parser.getName(); + if (!TAG_FAVORITE.equals(name)) { + break; } - Intent intent; - try { - intent = Intent.getIntent(intentDescription); - } catch (java.net.URISyntaxException e) { - continue; - } - info = getApplicationInfo(manager, intent); + final String favoritePackage = parser.getAttributeValue(null, TAG_PACKAGE); + final String favoriteClass = parser.getAttributeValue(null, TAG_CLASS); + + final ComponentName cn = new ComponentName(favoritePackage, favoriteClass); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + info = getApplicationInfo(packageManager, intent); if (info != null) { - info.title = c.getString(titleIndex); info.intent = intent; mFavorites.addFirst(info); } } + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Got exception parsing favorites.", e); + } catch (IOException e) { + Log.w(LOG_TAG, "Got exception parsing favorites.", e); } - - c.close(); } mApplicationsStack.setFavorites(mFavorites); } + private static void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + private static void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty + } + } + /** * Refreshes the recently launched applications stacked over the favorites. The number * of recents depends on how many favorites are present. @@ -361,14 +394,6 @@ public class Home extends Activity { return info; } - /** - * When the notification that favorites have changed is received, requests - * a favorites list refresh. - */ - private void onFavoritesChanged() { - bindFavorites(false); - } - @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -530,7 +555,7 @@ public class Home extends Activity { private class WallpaperIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - getWindow().setBackgroundDrawable(getWallpaper()); + getWindow().setBackgroundDrawable(new ClippedDrawable(getWallpaper())); } } @@ -547,20 +572,6 @@ public class Home extends Activity { } } - /** - * Receives notifications whenever the user favorites have changed. - */ - private class FavoritesChangeObserver extends ContentObserver { - public FavoritesChangeObserver() { - super(mHandler); - } - - @Override - public void onChange(boolean selfChange) { - onFavoritesChanged(); - } - } - /** * GridView adapter to show the list of all installed applications. */ @@ -580,13 +591,12 @@ public class Home extends Activity { convertView = inflater.inflate(R.layout.application, parent, false); } - //final ImageView imageView = (ImageView) convertView.findViewById(R.id.icon); Drawable icon = info.icon; if (!info.filtered) { - final Resources resources = getContext().getResources(); - int width = (int) resources.getDimension(android.R.dimen.app_icon_size); - int height = (int) resources.getDimension(android.R.dimen.app_icon_size); + //final Resources resources = getContext().getResources(); + int width = 42;//(int) resources.getDimension(android.R.dimen.app_icon_size); + int height = 42;//(int) resources.getDimension(android.R.dimen.app_icon_size); final int iconWidth = icon.getIntrinsicWidth(); final int iconHeight = icon.getIntrinsicHeight(); @@ -687,4 +697,47 @@ public class Home extends Activity { startActivity(app.intent); } } + + /** + * When a drawable is attached to a View, the View gives the Drawable its dimensions + * by calling Drawable.setBounds(). In this application, the View that draws the + * wallpaper has the same size as the screen. However, the wallpaper might be larger + * that the screen which means it will be automatically stretched. Because stretching + * a bitmap while drawing it is very expensive, we use a ClippedDrawable instead. + * This drawable simply draws another wallpaper but makes sure it is not stretched + * by always giving it its intrinsic dimensions. If the wallpaper is larger than the + * screen, it will simply get clipped but it won't impact performance. + */ + private class ClippedDrawable extends Drawable { + private final Drawable mWallpaper; + + public ClippedDrawable(Drawable wallpaper) { + mWallpaper = wallpaper; + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + // Ensure the wallpaper is as large as it really is, to avoid stretching it + // at drawing time + mWallpaper.setBounds(left, top, left + mWallpaper.getIntrinsicWidth(), + top + mWallpaper.getIntrinsicHeight()); + } + + public void draw(Canvas canvas) { + mWallpaper.draw(canvas); + } + + public void setAlpha(int alpha) { + mWallpaper.setAlpha(alpha); + } + + public void setColorFilter(ColorFilter cf) { + mWallpaper.setColorFilter(cf); + } + + public int getOpacity() { + return mWallpaper.getOpacity(); + } + } } diff --git a/samples/NotePad/Android.mk b/samples/NotePad/Android.mk index 490a31d50..793921270 100644 --- a/samples/NotePad/Android.mk +++ b/samples/NotePad/Android.mk @@ -1,7 +1,7 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := eng samples +LOCAL_MODULE_TAGS := samples # Only compile source java files in this apk. LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/samples/Snake/Android.mk b/samples/Snake/Android.mk index e0cfdfd2b..56b642e1e 100644 --- a/samples/Snake/Android.mk +++ b/samples/Snake/Android.mk @@ -1,7 +1,7 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := eng samples +LOCAL_MODULE_TAGS := samples # Only compile source java files in this apk. LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/samples/SoftKeyboard/Android.mk b/samples/SoftKeyboard/Android.mk index dfd546eaf..883bf2fd7 100755 --- a/samples/SoftKeyboard/Android.mk +++ b/samples/SoftKeyboard/Android.mk @@ -1,7 +1,7 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := eng samples +LOCAL_MODULE_TAGS := samples LOCAL_SRC_FILES := $(call all-subdir-java-files) diff --git a/samples/SoftKeyboard/res/drawable/sym_keyboard_search.png b/samples/SoftKeyboard/res/drawable/sym_keyboard_search.png new file mode 100755 index 000000000..127755d6b Binary files /dev/null and b/samples/SoftKeyboard/res/drawable/sym_keyboard_search.png differ diff --git a/samples/SoftKeyboard/res/values-land/dimens.xml b/samples/SoftKeyboard/res/values-land/dimens.xml index 1929ae08d..b5f3bc128 100644 --- a/samples/SoftKeyboard/res/values-land/dimens.xml +++ b/samples/SoftKeyboard/res/values-land/dimens.xml @@ -19,5 +19,5 @@ --> - 46px - \ No newline at end of file + 46dip + diff --git a/samples/SoftKeyboard/res/values/dimens.xml b/samples/SoftKeyboard/res/values/dimens.xml index cdaf27a2d..caf615cfa 100644 --- a/samples/SoftKeyboard/res/values/dimens.xml +++ b/samples/SoftKeyboard/res/values/dimens.xml @@ -19,7 +19,7 @@ --> - 50px + 50dip 16sp 6sp \ No newline at end of file diff --git a/samples/SoftKeyboard/res/values/strings.xml b/samples/SoftKeyboard/res/values/strings.xml index c1e306d5d..bc645b2c0 100644 --- a/samples/SoftKeyboard/res/values/strings.xml +++ b/samples/SoftKeyboard/res/values/strings.xml @@ -25,9 +25,7 @@ \u0020.,;:!?\n()[]*&@{}/<>_+=|" - Done - Search - Enter - Next - Previous + Go + Next + Send diff --git a/samples/SoftKeyboard/src/com/example/android/softkeyboard/LatinKeyboard.java b/samples/SoftKeyboard/src/com/example/android/softkeyboard/LatinKeyboard.java index 944cefb3b..179844234 100644 --- a/samples/SoftKeyboard/src/com/example/android/softkeyboard/LatinKeyboard.java +++ b/samples/SoftKeyboard/src/com/example/android/softkeyboard/LatinKeyboard.java @@ -20,9 +20,14 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.inputmethodservice.Keyboard.Row; +import android.view.inputmethod.EditorInfo; public class LatinKeyboard extends Keyboard { + private Key mEnterKey; + public LatinKeyboard(Context context, int xmlLayoutResId) { super(context, xmlLayoutResId); } @@ -35,7 +40,49 @@ public class LatinKeyboard extends Keyboard { @Override protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { - return new LatinKey(res, parent, x, y, parser); + Key key = new LatinKey(res, parent, x, y, parser); + if (key.codes[0] == 10) { + mEnterKey = key; + } + return key; + } + + /** + * This looks at the ime options given by the current editor, to set the + * appropriate label on the keyboard's enter key (if it has one). + */ + void setImeOptions(Resources res, int options) { + if (mEnterKey == null) { + return; + } + + switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + case EditorInfo.IME_ACTION_GO: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_go_key); + break; + case EditorInfo.IME_ACTION_NEXT: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_next_key); + break; + case EditorInfo.IME_ACTION_SEARCH: + mEnterKey.icon = res.getDrawable( + R.drawable.sym_keyboard_search); + mEnterKey.label = null; + break; + case EditorInfo.IME_ACTION_SEND: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_send_key); + break; + default: + mEnterKey.icon = res.getDrawable( + R.drawable.sym_keyboard_return); + mEnterKey.label = null; + break; + } } static class LatinKey extends Keyboard.Key { diff --git a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java index d26b1730d..9aeb2b54e 100644 --- a/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java +++ b/samples/SoftKeyboard/src/com/example/android/softkeyboard/SoftKeyboard.java @@ -27,6 +27,7 @@ import android.view.View; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; import java.util.ArrayList; import java.util.List; @@ -64,20 +65,28 @@ public class SoftKeyboard extends InputMethodService private long mLastShiftTime; private long mMetaState; - private Keyboard mSymbolsKeyboard; - private Keyboard mSymbolsShiftedKeyboard; - private Keyboard mQwertyKeyboard; + private LatinKeyboard mSymbolsKeyboard; + private LatinKeyboard mSymbolsShiftedKeyboard; + private LatinKeyboard mQwertyKeyboard; - private Keyboard mCurKeyboard; + private LatinKeyboard mCurKeyboard; private String mWordSeparators; /** - * Helper function to generate the various keyboard layouts used by the - * input method. Takes care of regenerating the layouts if the width - * of the input method changes. + * Main initialization of the input method component. Be sure to call + * to super class. */ - private void makeKeyboards() { + @Override public void onCreate() { + super.onCreate(); + mWordSeparators = getResources().getString(R.string.word_separators); + } + + /** + * This is the point where you can do all of your UI initialization. It + * is called after creation and any configuration change. + */ + @Override public void onInitializeInterface() { if (mQwertyKeyboard != null) { // Configuration changes can happen after the keyboard gets recreated, // so we need to be able to re-build the keyboards if the available @@ -91,16 +100,6 @@ public class SoftKeyboard extends InputMethodService mSymbolsShiftedKeyboard = new LatinKeyboard(this, R.xml.symbols_shift); } - /** - * Main initialization of the input method component. Be sure to call - * to super class. - */ - @Override public void onCreate() { - super.onCreate(); - makeKeyboards(); - mWordSeparators = getResources().getString(R.string.word_separators); - } - /** * Called by the framework when your view for creating input needs to * be generated. This will be called the first time your input method @@ -108,9 +107,6 @@ public class SoftKeyboard extends InputMethodService * a configuration change. */ @Override public View onCreateInputView() { - // We call makeKeyboards() here to regenerate them if needed due to - // a configuration change. - makeKeyboards(); mInputView = (KeyboardView) getLayoutInflater().inflate( R.layout.input, null); mInputView.setOnKeyboardActionListener(this); @@ -178,14 +174,16 @@ public class SoftKeyboard extends InputMethodService // We now look for a few special variations of text that will // modify our behavior. int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; - if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD) { + if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || + variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { // Do not display predictions / what the user is typing // when they are entering a password. mPredictionOn = false; } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - || variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { + || variation == EditorInfo.TYPE_TEXT_VARIATION_URI + || variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { // Our predictions are not useful for e-mail addresses // or URIs. mPredictionOn = false; @@ -211,7 +209,12 @@ public class SoftKeyboard extends InputMethodService // For all unknown input types, default to the alphabetic // keyboard with no special features. mCurKeyboard = mQwertyKeyboard; + updateShiftKeyState(attribute); } + + // Update the label on the enter key, depending on what the application + // says it will do. + mCurKeyboard.setImeOptions(getResources(), attribute.imeOptions); } /** @@ -250,6 +253,8 @@ public class SoftKeyboard extends InputMethodService @Override public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { + super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, + candidatesStart, candidatesEnd); // If the current selection in the text view changes, we should // clear whatever candidate text we have. @@ -362,6 +367,26 @@ public class SoftKeyboard extends InputMethodService // text being entered with a hard keyboard, we need to process // it and do the appropriate action. if (PROCESS_HARD_KEYS) { + if (keyCode == KeyEvent.KEYCODE_SPACE + && (event.getMetaState()&KeyEvent.META_ALT_ON) != 0) { + // A silly example: in our input method, Alt+Space + // is a shortcut for 'android' in lower case. + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + // First, tell the editor that it is no longer in the + // shift state, since we are consuming this. + ic.clearMetaKeyStates(KeyEvent.META_ALT_ON); + keyDownUp(KeyEvent.KEYCODE_A); + keyDownUp(KeyEvent.KEYCODE_N); + keyDownUp(KeyEvent.KEYCODE_D); + keyDownUp(KeyEvent.KEYCODE_R); + keyDownUp(KeyEvent.KEYCODE_O); + keyDownUp(KeyEvent.KEYCODE_I); + keyDownUp(KeyEvent.KEYCODE_D); + // And we consume this event. + return true; + } + } if (mPredictionOn && translateKeyDown(keyCode, event)) { return true; } @@ -408,7 +433,11 @@ public class SoftKeyboard extends InputMethodService private void updateShiftKeyState(EditorInfo attr) { if (attr != null && mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) { - int caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); + int caps = 0; + EditorInfo ei = getCurrentInputEditorInfo(); + if (ei != null && ei.inputType != EditorInfo.TYPE_NULL) { + caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); + } mInputView.setShifted(mCapsLock || caps != 0); } } @@ -488,6 +517,18 @@ public class SoftKeyboard extends InputMethodService } } + public void onText(CharSequence text) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + ic.beginBatchEdit(); + if (mComposing.length() > 0) { + commitTyped(ic); + } + ic.commitText(text, 0); + ic.endBatchEdit(); + updateShiftKeyState(getCurrentInputEditorInfo()); + } + /** * Update the list of available candidates from the current composing * text. This will need to be filled in by however you are determining @@ -521,7 +562,7 @@ public class SoftKeyboard extends InputMethodService final int length = mComposing.length(); if (length > 1) { mComposing.delete(length - 1, length); - getCurrentInputConnection().setComposingText(mComposing, mComposing.length()); + getCurrentInputConnection().setComposingText(mComposing, 1); updateCandidates(); } else if (length > 0) { mComposing.setLength(0); @@ -562,7 +603,7 @@ public class SoftKeyboard extends InputMethodService } if (isAlphabet(primaryCode) && mPredictionOn) { mComposing.append((char) primaryCode); - getCurrentInputConnection().setComposingText(mComposing, mComposing.length()); + getCurrentInputConnection().setComposingText(mComposing, 1); updateShiftKeyState(getCurrentInputEditorInfo()); updateCandidates(); } else { @@ -573,7 +614,7 @@ public class SoftKeyboard extends InputMethodService private void handleClose() { commitTyped(getCurrentInputConnection()); - dismissSoftInput(0); + requestHideSelf(0); mInputView.closing(); } diff --git a/simulator/app/PropertyServer.cpp b/simulator/app/PropertyServer.cpp index 565cc0991..4b1aedd68 100644 --- a/simulator/app/PropertyServer.cpp +++ b/simulator/app/PropertyServer.cpp @@ -108,6 +108,8 @@ void PropertyServer::SetDefaultProperties(void) { "ro.SECONDARY_SERVER_MEM", "4096" }, { "ro.HIDDEN_APP_MEM", "8192" }, { "ro.EMPTY_APP_MEM", "16384" }, + { "ro.HOME_APP_ADJ", "4" }, + { "ro.HOME_APP_MEM", "4096" }, //{ "init.svc.adbd", "running" }, // causes ADB-JDWP { "init.svc.usbd", "running" }, { "init.svc.debuggerd", "running" }, @@ -132,7 +134,7 @@ void PropertyServer::SetDefaultProperties(void) { "dalvik.vm.stack-trace-file", "/data/anr/traces.txt" }, //{ "dalvik.vm.execution-mode", "int:portable" }, { "dalvik.vm.enableassertions", "all" }, // -ea - { "dalvik.vm.verify-bytecode", "false" }, // -Xverify + { "dalvik.vm.dexopt-flags", "" }, // e.g. "v=a,o=v,m=n" { "dalvik.vm.deadlock-predict", "off" }, // -Xdeadlockpredict //{ "dalvik.vm.jniopts", "forcecopy" }, // -Xjniopts { "log.redirect-stdio", "false" }, // -Xlog-stdio diff --git a/simulator/wrapsim/DevPower.c b/simulator/wrapsim/DevPower.c index 2d25704b3..b44231bd3 100644 --- a/simulator/wrapsim/DevPower.c +++ b/simulator/wrapsim/DevPower.c @@ -1,7 +1,7 @@ /* * Copyright 2007 The Android Open Source Project * - * Magic entries in /sys/android_power/. + * Magic entries in /sys/class/power_supply/. */ #include "Common.h" @@ -12,30 +12,6 @@ #include #include -#if 0 -/* - * Set of entries found in /sys/android_power. - */ -typedef enum DeviceIndex { - kPowerUnknown = 0, - - kPowerAutoOffTimeout, - kPowerBatteryLevel, - kPowerBatteryLevelLow, - kPowerBatteryLevelRaw, - kPowerBatteryLevelScale, - kPowerBatteryLowLevel, - kPowerBatteryShutdownLevel, - kPowerChargingState, - kPowerRequestState, - kPowerState, - - kPowerAcquireFullWakeLock, - kPowerAcquirePartialWakeLock, - kPowerReleaseWakeLock, -} DeviceIndex; -#endif - /* * Map filename to device index. * @@ -47,38 +23,26 @@ static const struct { //DeviceIndex idx; const char* data; } gDeviceMap[] = { - { "auto_off_timeout", //kPowerAutoOffTimeout, - "\n" }, - { "battery_level", //kPowerBatteryLevel, - "9\n" }, - { "battery_level_low", //kPowerBatteryLevelLow, + { "ac/online", "0\n" }, - { "battery_level_raw", //kPowerBatteryLevelRaw, + + { "battery/batt_temp", + "281\n", }, + { "battery/batt_vol", + "4170\n" }, + { "battery/capacity", "100\n" }, - { "battery_level_scale", //kPowerBatteryLevelScale, - "9\n" }, - { "battery_low_level", //kPowerBatteryLowLevel, - "10\n" }, - { "battery_shutdown_level", //kPowerBatteryShutdownLevel, - "5\n", }, - { "charging_state", //kPowerChargingState, - "Maintaining\n" }, - { "request_state", //kPowerRequestState, - "wake\n" }, - { "state", //kPowerState, - "0-1-0\n" }, - - { "acquire_full_wake_lock", //kPowerAcquireFullWakeLock, - "\n" }, - { "acquire_partial_wake_lock", //kPowerAcquirePartialWakeLock, - "\n" }, - { "release_wake_lock", //kPowerReleaseWakeLock, - "radio-interface PowerManagerService KeyEvents\n" }, - { "wait_for_fb_sleep", //kSleepFileName, - "" }, // this means "block forever on read" - { "wait_for_fb_wake", //kWakeFileName, - "0" }, + { "battery/health", + "Good\n" }, + { "battery/present", + "0\n" }, + { "battery/status", + "Full" }, + { "battery/technology", + "Li-ion\n" }, + { "usb/online", + "1\n" }, }; /* @@ -96,7 +60,7 @@ typedef struct PowerState { */ static void configureInitialState(const char* pathName, PowerState* powerState) { - const char* cp = pathName + strlen("/sys/android_power/"); + const char* cp = pathName + strlen("/sys/class/power_supply/"); int i; powerState->which = -1; @@ -134,8 +98,11 @@ static ssize_t readPower(FakeDev* dev, int fd, void* buf, size_t count) wsLog("%s: read %d\n", dev->debugName, count); - if (state->which < 0 || state->which >= sizeof(gDeviceMap)/sizeof(gDeviceMap[0])) + if (state->which < 0 || + state->which >= (int) (sizeof(gDeviceMap)/sizeof(gDeviceMap[0]))) + { return 0; + } const char* data = gDeviceMap[state->which].data; size_t strLen = strlen(data); diff --git a/simulator/wrapsim/FakeDev.c b/simulator/wrapsim/FakeDev.c index 75aee1648..3e223d363 100644 --- a/simulator/wrapsim/FakeDev.c +++ b/simulator/wrapsim/FakeDev.c @@ -47,10 +47,6 @@ resources.) * Devices we intercept. * * Needed: - * /sys/android_power/battery_level_scale - * /sys/android_power/battery_level - * /sys/android_power/battery_level_raw - * /sys/android_power/charging_state * /dev/alarm * radio */ @@ -70,7 +66,7 @@ FakedPath fakedpaths[] = { "/dev/input/event0", wsOpenDevEvent }, { "/dev/input/*", NULL }, { "/dev/log/*", wsOpenDevLog }, - { "/sys/android_power/*", wsOpenDevPower }, + { "/sys/class/power_supply/*", wsOpenDevPower }, { "/sys/devices/platform/android-vibrator/enable", wsOpenDevVibrator }, { "/sys/qemu_trace/*", NULL }, { NULL, NULL } diff --git a/testrunner/adb_interface.py b/testrunner/adb_interface.py new file mode 100755 index 000000000..fb304df9d --- /dev/null +++ b/testrunner/adb_interface.py @@ -0,0 +1,347 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. + +"""Provides an interface to communicate with the device via the adb command. + +Assumes adb binary is currently on system path. +""" +# Python imports +import os +import string +import time + +# local imports +import am_instrument_parser +import errors +import logger +import run_command + + +class AdbInterface: + """Helper class for communicating with Android device via adb.""" + + # argument to pass to adb, to direct command to specific device + _target_arg = "" + + DEVICE_TRACE_DIR = "/data/test_results/" + + def SetEmulatorTarget(self): + """Direct all future commands to the only running emulator.""" + self._target_arg = "-e" + + def SetDeviceTarget(self): + """Direct all future commands to the only connected USB device.""" + self._target_arg = "-d" + + def SetTargetSerial(self, serial): + """Direct all future commands to Android target with the given serial.""" + self._target_arg = "-s %s" % serial + + def SendCommand(self, command_string, timeout_time=20, retry_count=3): + """Send a command via adb. + + Args: + command_string: adb command to run + timeout_time: number of seconds to wait for command to respond before + retrying + retry_count: number of times to retry command before raising + WaitForResponseTimedOutError + Returns: + string output of command + + Raises: + WaitForResponseTimedOutError if device does not respond to command + """ + adb_cmd = "adb %s %s" % (self._target_arg, command_string) + logger.SilentLog("about to run %s" % adb_cmd) + return run_command.RunCommand(adb_cmd, timeout_time=timeout_time, + retry_count=retry_count) + + def SendShellCommand(self, cmd, timeout_time=20, retry_count=3): + """Send a adb shell command. + + Args: + cmd: adb shell command to run + timeout_time: number of seconds to wait for command to respond before + retrying + retry_count: number of times to retry command before raising + WaitForResponseTimedOutError + + Returns: + string output of command + + Raises: + WaitForResponseTimedOutError: if device does not respond to command + """ + return self.SendCommand("shell %s" % cmd, timeout_time=timeout_time, + retry_count=retry_count) + + def BugReport(self, path): + """Dumps adb bugreport to the file specified by the path. + + Args: + path: Path of the file where adb bugreport is dumped to. + """ + bug_output = self.SendShellCommand("bugreport", timeout_time=60) + bugreport_file = open(path, "w") + bugreport_file.write(bug_output) + bugreport_file.close() + + def Push(self, src, dest): + """Pushes the file src onto the device at dest. + + Args: + src: file path of host file to push + dest: destination absolute file path on device + """ + self.SendCommand("push %s %s" % (src, dest), timeout_time=60) + + def Pull(self, src, dest): + """Pulls the file src on the device onto dest on the host. + + Args: + src: absolute file path of file on device to pull + dest: destination file path on host + + Returns: + True if success and False otherwise. + """ + # Create the base dir if it doesn't exist already + if not os.path.exists(os.path.dirname(dest)): + os.makedirs(os.path.dirname(dest)) + + if self.DoesFileExist(src): + self.SendCommand("pull %s %s" % (src, dest), timeout_time=60) + return True + else: + logger.Log("ADB Pull Failed: Source file %s does not exist." % src) + return False + + def DoesFileExist(self, src): + """Checks if the given path exists on device target. + + Args: + src: file path to be checked. + + Returns: + True if file exists + """ + + output = self.SendShellCommand("ls %s" % src) + error = "No such file or directory" + + if error in output: + return False + return True + + def StartInstrumentationForPackage( + self, package_name, runner_name, timeout_time=60*10, + no_window_animation=False, instrumentation_args={}): + """Run instrumentation test for given package and runner. + + Equivalent to StartInstrumentation, except instrumentation path is + separated into its package and runner components. + """ + instrumentation_path = "%s/%s" % (package_name, runner_name) + return self.StartInstrumentation(self, instrumentation_path, timeout_time, + no_window_animation, instrumentation_args) + + def StartInstrumentation( + self, instrumentation_path, timeout_time=60*10, no_window_animation=False, + profile=False, instrumentation_args={}): + + """Runs an instrumentation class on the target. + + Returns a dictionary containing the key value pairs from the + instrumentations result bundle and a list of TestResults. Also handles the + interpreting of error output from the device and raises the necessary + exceptions. + + Args: + instrumentation_path: string. It should be the fully classified package + name, and instrumentation test runner, separated by "/" + e.g. com.android.globaltimelaunch/.GlobalTimeLaunch + timeout_time: Timeout value for the am command. + no_window_animation: boolean, Whether you want window animations enabled + or disabled + profile: If True, profiling will be turned on for the instrumentation. + instrumentation_args: Dictionary of key value bundle arguments to pass to + instrumentation. + + Returns: + (test_results, inst_finished_bundle) + + test_results: a list of TestResults + inst_finished_bundle (dict): Key/value pairs contained in the bundle that + is passed into ActivityManager.finishInstrumentation(). Included in this + bundle is the return code of the Instrumentation process, any error + codes reported by the activity manager, and any results explicitly added + by the instrumentation code. + + Raises: + WaitForResponseTimedOutError: if timeout occurred while waiting for + response to adb instrument command + DeviceUnresponsiveError: if device system process is not responding + InstrumentationError: if instrumentation failed to run + """ + + command_string = self._BuildInstrumentationCommandPath( + instrumentation_path, no_window_animation=no_window_animation, + profile=profile, raw_mode=True, + instrumentation_args=instrumentation_args) + + (test_results, inst_finished_bundle) = ( + am_instrument_parser.ParseAmInstrumentOutput( + self.SendShellCommand(command_string, timeout_time=timeout_time, + retry_count=2))) + + if "code" not in inst_finished_bundle: + raise errors.InstrumentationError("no test results... device setup " + "correctly?") + + if inst_finished_bundle["code"] == "0": + short_msg_result = "no error message" + if "shortMsg" in inst_finished_bundle: + short_msg_result = inst_finished_bundle["shortMsg"] + logger.Log(short_msg_result) + raise errors.InstrumentationError(short_msg_result) + + if "INSTRUMENTATION_ABORTED" in inst_finished_bundle: + logger.Log("INSTRUMENTATION ABORTED!") + raise errors.DeviceUnresponsiveError + + return (test_results, inst_finished_bundle) + + def StartInstrumentationNoResults( + self, package_name, runner_name, no_window_animation=False, + raw_mode=False, instrumentation_args={}): + """Runs instrumentation and dumps output to stdout. + + Equivalent to StartInstrumentation, but will dump instrumentation + 'normal' output to stdout, instead of parsing return results. Command will + never timeout. + """ + adb_command_string = self.PreviewInstrumentationCommand( + package_name, runner_name, no_window_animation=no_window_animation, + raw_mode=raw_mode, instrumentation_args=instrumentation_args) + logger.Log(adb_command_string) + run_command.RunCommand(adb_command_string, return_output=False) + + def PreviewInstrumentationCommand( + self, package_name, runner_name, no_window_animation=False, + raw_mode=False, instrumentation_args={}): + """Returns a string of adb command that will be executed.""" + inst_command_string = self._BuildInstrumentationCommand( + package_name, runner_name, no_window_animation=no_window_animation, + raw_mode=raw_mode, instrumentation_args=instrumentation_args) + command_string = "adb %s shell %s" % (self._target_arg, inst_command_string) + return command_string + + def _BuildInstrumentationCommand( + self, package, runner_name, no_window_animation=False, profile=False, + raw_mode=True, instrumentation_args={}): + instrumentation_path = "%s/%s" % (package, runner_name) + + return self._BuildInstrumentationCommandPath( + instrumentation_path, no_window_animation=no_window_animation, + profile=profile, raw_mode=raw_mode, + instrumentation_args=instrumentation_args) + + def _BuildInstrumentationCommandPath( + self, instrumentation_path, no_window_animation=False, profile=False, + raw_mode=True, instrumentation_args={}): + command_string = "am instrument" + if no_window_animation: + command_string += " --no_window_animation" + if profile: + self._CreateTraceDir() + command_string += ( + " -p %s/%s.dmtrace" % + (self.DEVICE_TRACE_DIR, instrumentation_path.split(".")[-1])) + + for key, value in instrumentation_args.items(): + command_string += " -e %s %s" % (key, value) + if raw_mode: + command_string += " -r" + command_string += " -w %s" % instrumentation_path + return command_string + + def _CreateTraceDir(self): + ls_response = self.SendShellCommand("ls /data/trace") + if ls_response.strip("#").strip(string.whitespace) != "": + self.SendShellCommand("create /data/trace", "mkdir /data/trace") + self.SendShellCommand("make /data/trace world writeable", + "chmod 777 /data/trace") + + def WaitForDevicePm(self, wait_time=120): + """Waits for targeted device's package manager to be up. + + Args: + wait_time: time in seconds to wait + + Raises: + WaitForResponseTimedOutError if wait_time elapses and pm still does not + respond. + """ + logger.Log("Waiting for device package manager for %s seconds..." + % wait_time) + self.SendCommand("wait-for-device") + # Now the device is there, but may not be running. + # Query the package manager with a basic command + pm_found = False + attempts = 0 + wait_period = 5 + while not pm_found and (attempts*wait_period) < wait_time: + # assume the 'adb shell pm path android' command will always + # return 'package: something' in the success case + output = self.SendShellCommand("pm path android", retry_count=1) + if "package:" in output: + pm_found = True + else: + time.sleep(wait_period) + attempts += 1 + if not pm_found: + raise errors.WaitForResponseTimedOutError + + def Sync(self, retry_count=3): + """Perform a adb sync. + + Blocks until device package manager is responding. + + Args: + retry_count: number of times to retry sync before failing + + Raises: + WaitForResponseTimedOutError if package manager does not respond + """ + output = self.SendCommand("sync", retry_count=retry_count) + if "Read-only file system" in output: + logger.SilentLog(output) + logger.Log("adb sync failed due to read only fs, retrying") + self.SendCommand("remount") + output = self.SendCommand("sync", retry_count=retry_count) + if "No space left on device" in output: + logger.SilentLog(output) + logger.Log("adb sync failed due to no space on device, trying shell" + + " start/stop") + self.SendShellCommand("stop", retry_count=retry_count) + output = self.SendCommand("sync", retry_count=retry_count) + self.SendShellCommand("start", retry_count=retry_count) + + logger.SilentLog(output) + self.WaitForDevicePm() + return output diff --git a/testrunner/am_instrument_parser.py b/testrunner/am_instrument_parser.py new file mode 100755 index 000000000..cad87c047 --- /dev/null +++ b/testrunner/am_instrument_parser.py @@ -0,0 +1,178 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. + +"""Module that assists in parsing the output of "am instrument" commands run on +the device.""" + +import re +import string + + +def ParseAmInstrumentOutput(result): + """Given the raw output of an "am instrument" command that targets and + InstrumentationTestRunner, return structured data. + + Args: + result (string): Raw output of "am instrument" + + Return + (test_results, inst_finished_bundle) + + test_results (list of am_output_parser.TestResult) + inst_finished_bundle (dict): Key/value pairs contained in the bundle that is + passed into ActivityManager.finishInstrumentation(). Included in this bundle is the return + code of the Instrumentation process, any error codes reported by the + activity manager, and any results explicity added by the instrumentation + code. + """ + + re_status_code = re.compile(r'INSTRUMENTATION_STATUS_CODE: (?P-?\d)$') + test_results = [] + inst_finished_bundle = {} + + result_block_string = "" + for line in result.splitlines(): + result_block_string += line + '\n' + + if "INSTRUMENTATION_STATUS_CODE:" in line: + test_result = TestResult(result_block_string) + if test_result.GetStatusCode() == 1: # The test started + pass + elif test_result.GetStatusCode() in [0, -1, -2]: + test_results.append(test_result) + else: + pass + result_block_string = "" + if "INSTRUMENTATION_CODE:" in line: + inst_finished_bundle = _ParseInstrumentationFinishedBundle(result_block_string) + result_block_string = "" + + return (test_results, inst_finished_bundle) + + +def _ParseInstrumentationFinishedBundle(result): + """Given the raw output of "am instrument" returns a dictionary of the + key/value pairs from the bundle passed into + ActivityManager.finishInstrumentation(). + + Args: + result (string): Raw output of "am instrument" + + Return: + inst_finished_bundle (dict): Key/value pairs contained in the bundle that is + passed into ActivityManager.finishInstrumentation(). Included in this bundle is the return + code of the Instrumentation process, any error codes reported by the + activity manager, and any results explicity added by the instrumentation + code. + """ + + re_result = re.compile(r'INSTRUMENTATION_RESULT: ([^=]+)=(.+)$') + re_code = re.compile(r'INSTRUMENTATION_CODE: (\-?\d)$') + result_dict = {} + key = '' + val = '' + last_tag = '' + + for line in result.split('\n'): + line = line.strip(string.whitespace) + if re_result.match(line): + last_tag = 'INSTRUMENTATION_RESULT' + key = re_result.search(line).group(1).strip(string.whitespace) + if key.startswith('performance.'): + key = key[len('performance.'):] + val = re_result.search(line).group(2).strip(string.whitespace) + try: + result_dict[key] = float(val) + except ValueError: + result_dict[key] = val + except TypeError: + result_dict[key] = val + elif re_code.match(line): + last_tag = 'INSTRUMENTATION_CODE' + key = 'code' + val = re_code.search(line).group(1).strip(string.whitespace) + result_dict[key] = val + elif 'INSTRUMENTATION_ABORTED:' in line: + last_tag = 'INSTRUMENTATION_ABORTED' + key = 'INSTRUMENTATION_ABORTED' + val = '' + result_dict[key] = val + elif last_tag == 'INSTRUMENTATION_RESULT': + result_dict[key] += '\n' + line + + if not result_dict.has_key('code'): + result_dict['code'] = '0' + result_dict['shortMsg'] = "No result returned from instrumentation" + + return result_dict + + +class TestResult(object): + """A class that contains information about a single test result.""" + + def __init__(self, result_block_string): + """ + Args: + result_block_string (string): Is a single "block" of output. A single + "block" would be either a "test started" status report, or a "test + finished" status report. + """ + + self._test_name = None + self._status_code = None + self._failure_reason = None + + re_start_block = re.compile( + r'\s*INSTRUMENTATION_STATUS: stream=(?P.*)' + 'INSTRUMENTATION_STATUS: test=(?P\w+)\s+' + 'INSTRUMENTATION_STATUS: class=(?P[\w\.]+)\s+' + 'INSTRUMENTATION_STATUS: current=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: numtests=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: id=.*\s+' + 'INSTRUMENTATION_STATUS_CODE: 1\s*', re.DOTALL) + + re_end_block = re.compile( + r'\s*INSTRUMENTATION_STATUS: stream=(?P.*)' + 'INSTRUMENTATION_STATUS: test=(?P\w+)\s+' + '(INSTRUMENTATION_STATUS: stack=(?P.*))?' + 'INSTRUMENTATION_STATUS: class=(?P[\w\.]+)\s+' + 'INSTRUMENTATION_STATUS: current=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: numtests=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: id=.*\s+' + 'INSTRUMENTATION_STATUS_CODE: (?P0|-1|-2)\s*', re.DOTALL) + + start_block_match = re_start_block.match(result_block_string) + end_block_match = re_end_block.match(result_block_string) + + if start_block_match: + self._test_name = "%s:%s" % (start_block_match.group('class'), + start_block_match.group('test')) + self._status_code = 1 + elif end_block_match: + self._test_name = "%s:%s" % (end_block_match.group('class'), + end_block_match.group('test')) + self._status_code = int(end_block_match.group('status_code')) + self._failure_reason = end_block_match.group('stack') + + def GetTestName(self): + return self._test_name + + def GetStatusCode(self): + return self._status_code + + def GetFailureReason(self): + return self._failure_reason diff --git a/testrunner/coverage.py b/testrunner/coverage.py new file mode 100755 index 000000000..507c5c79d --- /dev/null +++ b/testrunner/coverage.py @@ -0,0 +1,312 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. + +"""Utilities for generating code coverage reports for Android tests.""" + +# Python imports +import glob +import optparse +import os + +# local imports +import android_build +import coverage_targets +import errors +import logger +import run_command + + +class CoverageGenerator(object): + """Helper utility for obtaining code coverage results on Android. + + Intended to simplify the process of building,running, and generating code + coverage results for a pre-defined set of tests and targets + """ + + # environment variable to enable emma builds in Android build system + _EMMA_BUILD_FLAG = "EMMA_INSTRUMENT" + # build path to Emma target Makefile + _EMMA_BUILD_PATH = os.path.join("external", "emma") + # path to EMMA host jar, relative to Android build root + _EMMA_JAR = os.path.join(_EMMA_BUILD_PATH, "lib", "emma.jar") + _TEST_COVERAGE_EXT = "ec" + # default device-side path to code coverage results file + _DEVICE_COVERAGE_PATH = "/sdcard/coverage.ec" + # root path of generated coverage report files, relative to Android build root + _COVERAGE_REPORT_PATH = os.path.join("out", "emma") + _CORE_TARGET_PATH = os.path.join("development", "testrunner", + "coverage_targets.xml") + # vendor glob file path patterns to tests, relative to android + # build root + _VENDOR_TARGET_PATH = os.path.join("vendor", "*", "tests", "testinfo", + "coverage_targets.xml") + + # path to root of target build intermediates + _TARGET_INTERMEDIATES_BASE_PATH = os.path.join("out", "target", "common", + "obj") + + def __init__(self, android_root_path, adb_interface): + self._root_path = android_root_path + self._output_root_path = os.path.join(self._root_path, + self._COVERAGE_REPORT_PATH) + self._emma_jar_path = os.path.join(self._root_path, self._EMMA_JAR) + self._adb = adb_interface + self._targets_manifest = self._ReadTargets() + + def EnableCoverageBuild(self): + """Enable building an Android target with code coverage instrumentation.""" + os.environ[self._EMMA_BUILD_FLAG] = "true" + + def ExtractReport(self, test_suite, + device_coverage_path=_DEVICE_COVERAGE_PATH, + output_path=None): + """Extract runtime coverage data and generate code coverage report. + + Assumes test has just been executed. + Args: + test_suite: TestSuite to generate coverage data for + device_coverage_path: location of coverage file on device + output_path: path to place output files in. If None will use + /<_COVERAGE_REPORT_PATH>// + + Returns: + absolute file path string of generated html report file. + """ + if output_path is None: + output_path = os.path.join(self._root_path, + self._COVERAGE_REPORT_PATH, + test_suite.GetTargetName(), + test_suite.GetName()) + + coverage_local_name = "%s.%s" % (test_suite.GetName(), + self._TEST_COVERAGE_EXT) + coverage_local_path = os.path.join(output_path, + coverage_local_name) + if self._adb.Pull(device_coverage_path, coverage_local_path): + + report_path = os.path.join(output_path, + test_suite.GetName()) + target = self._targets_manifest.GetTarget(test_suite.GetTargetName()) + return self._GenerateReport(report_path, coverage_local_path, [target], + do_src=True) + return None + + def _GenerateReport(self, report_path, coverage_file_path, targets, + do_src=True): + """Generate the code coverage report. + + Args: + report_path: absolute file path of output file, without extension + coverage_file_path: absolute file path of code coverage result file + targets: list of CoverageTargets to use as base for code coverage + measurement. + do_src: True if generate coverage report with source linked in. + Note this will increase size of generated report. + + Returns: + absolute file path to generated report file. + """ + input_metadatas = self._GatherMetadatas(targets) + + if do_src: + src_arg = self._GatherSrcs(targets) + else: + src_arg = "" + + report_file = "%s.html" % report_path + cmd1 = ("java -cp %s emma report -r html -in %s %s %s " % + (self._emma_jar_path, coverage_file_path, input_metadatas, src_arg)) + cmd2 = "-Dreport.html.out.file=%s" % report_file + self._RunCmd(cmd1 + cmd2) + return report_file + + def _GatherMetadatas(self, targets): + """Builds the emma input metadata argument from provided targets. + + Args: + targets: list of CoverageTargets + + Returns: + input metadata argument string + """ + input_metadatas = "" + for target in targets: + input_metadata = os.path.join(self._GetBuildIntermediatePath(target), + "coverage.em") + input_metadatas += " -in %s" % input_metadata + return input_metadatas + + def _GetBuildIntermediatePath(self, target): + return os.path.join( + self._root_path, self._TARGET_INTERMEDIATES_BASE_PATH, target.GetType(), + "%s_intermediates" % target.GetName()) + + def _GatherSrcs(self, targets): + """Builds the emma input source path arguments from provided targets. + + Args: + targets: list of CoverageTargets + Returns: + source path arguments string + """ + src_list = [] + for target in targets: + target_srcs = target.GetPaths() + for path in target_srcs: + src_list.append("-sp %s" % os.path.join(self._root_path, path)) + return " ".join(src_list) + + def _MergeFiles(self, input_paths, dest_path): + """Merges a set of emma coverage files into a consolidated file. + + Args: + input_paths: list of string absolute coverage file paths to merge + dest_path: absolute file path of destination file + """ + input_list = [] + for input_path in input_paths: + input_list.append("-in %s" % input_path) + input_args = " ".join(input_list) + self._RunCmd("java -cp %s emma merge %s -out %s" % (self._emma_jar_path, + input_args, dest_path)) + + def _RunCmd(self, cmd): + """Runs and logs the given os command.""" + run_command.RunCommand(cmd, return_output=False) + + def _CombineTargetCoverage(self): + """Combines all target mode code coverage results. + + Will find all code coverage data files in direct sub-directories of + self._output_root_path, and combine them into a single coverage report. + Generated report is placed at self._output_root_path/android.html + """ + coverage_files = self._FindCoverageFiles(self._output_root_path) + combined_coverage = os.path.join(self._output_root_path, + "android.%s" % self._TEST_COVERAGE_EXT) + self._MergeFiles(coverage_files, combined_coverage) + report_path = os.path.join(self._output_root_path, "android") + # don't link to source, to limit file size + self._GenerateReport(report_path, combined_coverage, + self._targets_manifest.GetTargets(), do_src=False) + + def _CombineTestCoverage(self): + """Consolidates code coverage results for all target result directories.""" + target_dirs = os.listdir(self._output_root_path) + for target_name in target_dirs: + output_path = os.path.join(self._output_root_path, target_name) + target = self._targets_manifest.GetTarget(target_name) + if os.path.isdir(output_path) and target is not None: + coverage_files = self._FindCoverageFiles(output_path) + combined_coverage = os.path.join(output_path, "%s.%s" % + (target_name, self._TEST_COVERAGE_EXT)) + self._MergeFiles(coverage_files, combined_coverage) + report_path = os.path.join(output_path, target_name) + self._GenerateReport(report_path, combined_coverage, [target]) + else: + logger.Log("%s is not a valid target directory, skipping" % output_path) + + def _FindCoverageFiles(self, root_path): + """Finds all files in /*/*.<_TEST_COVERAGE_EXT>. + + Args: + root_path: absolute file path string to search from + Returns: + list of absolute file path strings of coverage files + """ + file_pattern = os.path.join(root_path, "*", "*.%s" % + self._TEST_COVERAGE_EXT) + coverage_files = glob.glob(file_pattern) + return coverage_files + + def GetEmmaBuildPath(self): + return self._EMMA_BUILD_PATH + + def _ReadTargets(self): + """Parses the set of coverage target data. + + Returns: + a CoverageTargets object that contains set of parsed targets. + Raises: + AbortError if a fatal error occurred when parsing the target files. + """ + core_target_path = os.path.join(self._root_path, self._CORE_TARGET_PATH) + try: + targets = coverage_targets.CoverageTargets() + targets.Parse(core_target_path) + vendor_targets_pattern = os.path.join(self._root_path, + self._VENDOR_TARGET_PATH) + target_file_paths = glob.glob(vendor_targets_pattern) + for target_file_path in target_file_paths: + targets.Parse(target_file_path) + return targets + except errors.ParseError: + raise errors.AbortError + + def TidyOutput(self): + """Runs tidy on all generated html files. + + This is needed to the html files can be displayed cleanly on a web server. + Assumes tidy is on current PATH. + """ + logger.Log("Tidying output files") + self._TidyDir(self._output_root_path) + + def _TidyDir(self, dir_path): + """Recursively tidy all html files in given dir_path.""" + html_file_pattern = os.path.join(dir_path, "*.html") + html_files_iter = glob.glob(html_file_pattern) + for html_file_path in html_files_iter: + os.system("tidy -m -errors -quiet %s" % html_file_path) + sub_dirs = os.listdir(dir_path) + for sub_dir_name in sub_dirs: + sub_dir_path = os.path.join(dir_path, sub_dir_name) + if os.path.isdir(sub_dir_path): + self._TidyDir(sub_dir_path) + + def CombineCoverage(self): + """Create combined coverage reports for all targets and tests.""" + self._CombineTestCoverage() + self._CombineTargetCoverage() + + +def Run(): + """Does coverage operations based on command line args.""" + # TODO: do we want to support combining coverage for a single target + + try: + parser = optparse.OptionParser(usage="usage: %prog --combine-coverage") + parser.add_option( + "-c", "--combine-coverage", dest="combine_coverage", default=False, + action="store_true", help="Combine coverage results stored given " + "android root path") + parser.add_option( + "-t", "--tidy", dest="tidy", default=False, action="store_true", + help="Run tidy on all generated html files") + + options, args = parser.parse_args() + + coverage = CoverageGenerator(android_build.GetTop(), None) + if options.combine_coverage: + coverage.CombineCoverage() + if options.tidy: + coverage.TidyOutput() + except errors.AbortError: + logger.SilentLog("Exiting due to AbortError") + +if __name__ == "__main__": + Run() diff --git a/testrunner/coverage_targets.py b/testrunner/coverage_targets.py new file mode 100644 index 000000000..8847bca04 --- /dev/null +++ b/testrunner/coverage_targets.py @@ -0,0 +1,131 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. +import xml.dom.minidom +import xml.parsers +import os + +import logger +import errors + +class CoverageTargets: + """Accessor for the code coverage target xml file + Expects the following format: + + ] (0..*) - These are relative to build_path. If missing, + assumes 'src' + >/target> + + TODO: add more format checking + """ + + _TARGET_TAG_NAME = 'coverage_target' + + def __init__(self, ): + self._target_map= {} + + def __iter__(self): + return iter(self._target_map.values()) + + def Parse(self, file_path): + """Parse the coverage target data from from given file path, and add it to + the current object + Args: + file_path: absolute file path to parse + Raises: + errors.ParseError if file_path cannot be parsed + """ + try: + doc = xml.dom.minidom.parse(file_path) + except IOError: + # Error: The results file does not exist + logger.Log('Results file %s does not exist' % file_path) + raise errors.ParseError + except xml.parsers.expat.ExpatError: + logger.Log('Error Parsing xml file: %s ' % file_path) + raise errors.ParseError + + target_elements = doc.getElementsByTagName(self._TARGET_TAG_NAME) + + for target_element in target_elements: + target = CoverageTarget(target_element) + self._AddTarget(target) + + def _AddTarget(self, target): + self._target_map[target.GetName()] = target + + def GetBuildTargets(self): + """ returns list of target names """ + build_targets = [] + for target in self: + build_targets.append(target.GetName()) + return build_targets + + def GetTargets(self): + """ returns list of CoverageTarget""" + return self._target_map.values() + + def GetTarget(self, name): + """ returns CoverageTarget for given name. None if not found """ + try: + return self._target_map[name] + except KeyError: + return None + +class CoverageTarget: + """ Represents one coverage target definition parsed from xml """ + + _NAME_ATTR = 'name' + _TYPE_ATTR = 'type' + _BUILD_ATTR = 'build_path' + _SRC_TAG = 'src' + _PATH_ATTR = 'path' + + def __init__(self, target_element): + self._name = target_element.getAttribute(self._NAME_ATTR) + self._type = target_element.getAttribute(self._TYPE_ATTR) + self._build_path = target_element.getAttribute(self._BUILD_ATTR) + self._paths = [] + self._ParsePaths(target_element) + + def GetName(self): + return self._name + + def GetPaths(self): + return self._paths + + def GetType(self): + return self._type + + def GetBuildPath(self): + return self._build_path + + def _ParsePaths(self, target_element): + src_elements = target_element.getElementsByTagName(self._SRC_TAG) + for src_element in src_elements: + rel_path = src_element.getAttribute(self._PATH_ATTR) + self._paths.append(os.path.join(self.GetBuildPath(), rel_path)) + +def Parse(xml_file_path): + """parses out a file_path class from given path to xml""" + targets = CoverageTargets() + targets.Parse(xml_file_path) + return targets diff --git a/testrunner/coverage_targets.xml b/testrunner/coverage_targets.xml new file mode 100644 index 000000000..d7700f63a --- /dev/null +++ b/testrunner/coverage_targets.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testrunner/errors.py b/testrunner/errors.py new file mode 100755 index 000000000..6d606ecf5 --- /dev/null +++ b/testrunner/errors.py @@ -0,0 +1,40 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. + +"""Defines common exception classes for this package.""" + + +class WaitForResponseTimedOutError(Exception): + """We sent a command and had to wait too long for response.""" + + +class DeviceUnresponsiveError(Exception): + """Device is unresponsive to command.""" + + +class InstrumentationError(Exception): + """Failed to run instrumentation.""" + + +class AbortError(Exception): + """Generic exception that indicates a fatal error has occurred and program + execution should be aborted.""" + + +class ParseError(Exception): + """Raised when xml data to parse has unrecognized format.""" + diff --git a/testrunner/logger.py b/testrunner/logger.py new file mode 100755 index 000000000..762c89311 --- /dev/null +++ b/testrunner/logger.py @@ -0,0 +1,85 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2007, 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. + +"""Simple logging utility. Dumps log messages to stdout, and optionally, to a +log file. + +Init(path) must be called to enable logging to a file +""" + +import datetime + +_LOG_FILE = None +_verbose = False + +def Init(log_file_path): + """Set the path to the log file""" + global _LOG_FILE + _LOG_FILE = log_file_path + print "Using log file: %s" % _LOG_FILE + +def GetLogFilePath(): + """Returns the path and name of the Log file""" + global _LOG_FILE + return _LOG_FILE + +def Log(new_str): + """Appends new_str to the end of _LOG_FILE and prints it to stdout. + + Args: + # new_str is a string. + new_str: 'some message to log' + """ + msg = _PrependTimeStamp(new_str) + print msg + _WriteLog(msg) + +def _WriteLog(msg): + global _LOG_FILE + if _LOG_FILE is not None: + file_handle = file(_LOG_FILE, 'a') + file_handle.write('\n' + str(msg)) + file_handle.close() + +def _PrependTimeStamp(log_string): + """Returns the log_string prepended with current timestamp """ + return "# %s: %s" % (datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"), + log_string) + +def SilentLog(new_str): + """Silently log new_str. Unless verbose mode is enabled, will log new_str + only to the log file + Args: + # new_str is a string. + new_str: 'some message to log' + """ + global _verbose + msg = _PrependTimeStamp(new_str) + if _verbose: + print msg + _WriteLog(msg) + +def SetVerbose(new_verbose=True): + """ Enable or disable verbose logging""" + global _verbose + _verbose = new_verbose + +def main(): + pass + +if __name__ == '__main__': + main() diff --git a/testrunner/run_command.py b/testrunner/run_command.py new file mode 100755 index 000000000..6b72b77b3 --- /dev/null +++ b/testrunner/run_command.py @@ -0,0 +1,117 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2007, 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. + +# System imports +import os +import signal +import subprocess +import time +import threading + +# local imports +import logger +import errors + +_abort_on_error = False + +def SetAbortOnError(abort=True): + """Sets behavior of RunCommand to throw AbortError if command process returns + a negative error code""" + global _abort_on_error + _abort_on_error = abort + +def RunCommand(cmd, timeout_time=None, retry_count=3, return_output=True): + """Spawns a subprocess to run the given shell command, and checks for + timeout_time. If return_output is True, the output of the command is returned + as a string. Otherwise, output of command directed to stdout """ + + result = None + while True: + try: + result = RunOnce(cmd, timeout_time=timeout_time, + return_output=return_output) + except errors.WaitForResponseTimedOutError: + if retry_count == 0: + raise + retry_count -= 1 + logger.Log("No response for %s, retrying" % cmd) + else: + # Success + return result + +def RunOnce(cmd, timeout_time=None, return_output=True): + start_time = time.time() + so = [] + pid = [] + global _abort_on_error + error_occurred = False + + def Run(): + if return_output: + output_dest = subprocess.PIPE + else: + # None means direct to stdout + output_dest = None + pipe = subprocess.Popen( + cmd, + executable='/bin/bash', + stdout=output_dest, + stderr=subprocess.STDOUT, + shell=True) + pid.append(pipe.pid) + try: + output = pipe.communicate()[0] + if output is not None and len(output) > 0: + so.append(output) + except OSError, e: + logger.SilentLog("failed to retrieve stdout from: %s" % cmd) + logger.Log(e) + so.append("ERROR") + error_occurred = True + if pipe.returncode < 0: + logger.SilentLog("Error: %s was terminated by signal %d" %(cmd, + pipe.returncode)) + error_occurred = True + + t = threading.Thread(target=Run) + t.start() + + break_loop = False + while not break_loop: + if not t.isAlive(): + break_loop = True + + # Check the timeout + if (not break_loop and timeout_time is not None + and time.time() > start_time + timeout_time): + try: + os.kill(pid[0], signal.SIGKILL) + except OSError: + # process already dead. No action required. + pass + + logger.SilentLog("about to raise a timeout for: %s" % cmd) + raise errors.WaitForResponseTimedOutError + if not break_loop: + time.sleep(0.1) + + t.join() + + if _abort_on_error and error_occurred: + raise errors.AbortError + + return "".join(so) diff --git a/testrunner/test_defs.py b/testrunner/test_defs.py new file mode 100644 index 000000000..949ad6ef8 --- /dev/null +++ b/testrunner/test_defs.py @@ -0,0 +1,199 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2008, 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. + +"""Parser for test definition xml files.""" + +# Python imports +import xml.dom.minidom +import xml.parsers + +# local imports +import errors +import logger + + +class TestDefinitions(object): + """Accessor for a test definitions xml file data. + + Expected format is: + + + + + TODO: add format checking. + """ + + # tag/attribute constants + _TEST_TAG_NAME = "test" + + def __init__(self): + # dictionary of test name to tests + self._testname_map = {} + + def __iter__(self): + return iter(self._testname_map.values()) + + def Parse(self, file_path): + """Parse the test suite data from from given file path. + + Args: + file_path: absolute file path to parse + Raises: + ParseError if file_path cannot be parsed + """ + try: + doc = xml.dom.minidom.parse(file_path) + except IOError: + logger.Log("test file %s does not exist" % file_path) + raise errors.ParseError + except xml.parsers.expat.ExpatError: + logger.Log("Error Parsing xml file: %s " % file_path) + raise errors.ParseError + self._ParseDoc(doc) + + def ParseString(self, xml_string): + """Alternate parse method that accepts a string of the xml data.""" + doc = xml.dom.minidom.parseString(xml_string) + # TODO: catch exceptions and raise ParseError + return self._ParseDoc(doc) + + def _ParseDoc(self, doc): + suite_elements = doc.getElementsByTagName(self._TEST_TAG_NAME) + + for suite_element in suite_elements: + test = self._ParseTestSuite(suite_element) + self._AddTest(test) + + def _ParseTestSuite(self, suite_element): + """Parse the suite element. + + Returns: + a TestSuite object, populated with parsed data + """ + test = TestSuite(suite_element) + return test + + def _AddTest(self, test): + """Adds a test to this TestManifest. + + If a test already exists with the same name, it overrides it. + + Args: + test: TestSuite to add + """ + self._testname_map[test.GetName()] = test + + def GetTests(self): + return self._testname_map.values() + + def GetContinuousTests(self): + con_tests = [] + for test in self.GetTests(): + if test.IsContinuous(): + con_tests.append(test) + return con_tests + + def GetTest(self, name): + return self._testname_map.get(name, None) + +class TestSuite(object): + """Represents one test suite definition parsed from xml.""" + + _NAME_ATTR = "name" + _PKG_ATTR = "package" + _RUNNER_ATTR = "runner" + _CLASS_ATTR = "class" + _TARGET_ATTR = "coverage_target" + _BUILD_ATTR = "build_path" + _CONTINUOUS_ATTR = "continuous" + + _DEFAULT_RUNNER = "android.test.InstrumentationTestRunner" + + def __init__(self, suite_element): + """Populates this instance's data from given suite xml element.""" + self._name = suite_element.getAttribute(self._NAME_ATTR) + self._package = suite_element.getAttribute(self._PKG_ATTR) + if suite_element.hasAttribute(self._RUNNER_ATTR): + self._runner = suite_element.getAttribute(self._RUNNER_ATTR) + else: + self._runner = self._DEFAULT_RUNNER + if suite_element.hasAttribute(self._CLASS_ATTR): + self._class = suite_element.getAttribute(self._CLASS_ATTR) + else: + self._class = None + if suite_element.hasAttribute(self._TARGET_ATTR): + self._target_name = suite_element.getAttribute(self._TARGET_ATTR) + else: + self._target_name = None + if suite_element.hasAttribute(self._BUILD_ATTR): + self._build_path = suite_element.getAttribute(self._BUILD_ATTR) + else: + self._build_path = None + if suite_element.hasAttribute(self._CONTINUOUS_ATTR): + self._continuous = suite_element.getAttribute(self._CONTINUOUS_ATTR) + else: + self._continuous = False + + def GetName(self): + return self._name + + def GetPackageName(self): + return self._package + + def GetRunnerName(self): + return self._runner + + def GetClassName(self): + return self._class + + def GetTargetName(self): + """Retrieve module that this test is targeting. + + Used for generating code coverage metrics. + """ + return self._target_name + + def GetBuildPath(self): + """Returns the build path of this test, relative to source tree root.""" + return self._build_path + + def IsContinuous(self): + """Returns true if test is flagged as being part of the continuous tests""" + return self._continuous + +def Parse(file_path): + """Parses out a TestDefinitions from given path to xml file. + + Args: + file_path: string absolute file path + Returns: + a TestDefinitions object containing data parsed from file_path + Raises: + ParseError if xml format is not recognized + """ + tests_result = TestDefinitions() + tests_result.Parse(file_path) + return tests_result diff --git a/testrunner/tests.xml b/testrunner/tests.xml new file mode 100644 index 000000000..8d9c0ab29 --- /dev/null +++ b/testrunner/tests.xml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/androidprefs/src/com/android/prefs/AndroidLocation.java b/tools/androidprefs/src/com/android/prefs/AndroidLocation.java index a7ceb76a4..cfd9f53b2 100644 --- a/tools/androidprefs/src/com/android/prefs/AndroidLocation.java +++ b/tools/androidprefs/src/com/android/prefs/AndroidLocation.java @@ -22,19 +22,10 @@ import java.io.File; * Manages the location of the android files (including emulator files, ddms config, debug keystore) */ public final class AndroidLocation { - /** - * Used to know where to store the user data image. - *

- * This must match the constant ANDROID_SDK_VERSION used by the emulator - * to find its own emulator images. It is defined in tools/qemu/android.h + * Virtual Device folder inside the path returned by {@link #getFolder()} */ - private static final String ANDROID_SDK_VERSION = "SDK-1.0"; - - /** - * VM folder inside the path returned by {@link #getFolder()} - */ - public static final String FOLDER_VMS = "vm"; + public static final String FOLDER_AVD = "avd"; /** * Throw when the location of the android folder couldn't be found. @@ -56,7 +47,7 @@ public final class AndroidLocation { */ public final static String getFolder() throws AndroidLocationException { if (sPrefsLocation == null) { - String home = findValidPath("user.home", "HOME"); + String home = findValidPath("ANDROID_SDK_HOME", "user.home", "HOME"); // if the above failed, we throw an exception. if (home == null) { @@ -79,25 +70,6 @@ public final class AndroidLocation { return sPrefsLocation; } - /** - * Returns the folder where the emulator is going to find its android related files. - * @return an OS specific path, terminated by a separator. - * @throws AndroidLocationException - */ - public final static String getEmulatorFolder() throws AndroidLocationException { - String path = getFolder() + ANDROID_SDK_VERSION + File.separator; - - File f = new File(path); - if (f.exists() == false) { - f.mkdir(); - } else if (f.isFile()) { - throw new AndroidLocationException(path + - " is not a directory! This is required to run Android tools."); - } - - return path; - } - /** * Checks a list of system properties and/or system environment variables for validity, and * existing director, and returns the first one. diff --git a/tools/anttasks/.classpath b/tools/anttasks/.classpath index 08ced2120..d6ce15a37 100644 --- a/tools/anttasks/.classpath +++ b/tools/anttasks/.classpath @@ -4,5 +4,6 @@ + diff --git a/tools/anttasks/src/Android.mk b/tools/anttasks/src/Android.mk index dbaf2bc29..94d6d3f10 100644 --- a/tools/anttasks/src/Android.mk +++ b/tools/anttasks/src/Android.mk @@ -20,6 +20,7 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_JAVA_LIBRARIES := \ sdklib \ + apkbuilder \ ant LOCAL_MODULE := anttasks diff --git a/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java b/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java new file mode 100644 index 000000000..d2c71624d --- /dev/null +++ b/tools/anttasks/src/com/android/ant/AaptExecLoopTask.java @@ -0,0 +1,218 @@ +/* + * 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.ant; + +import com.android.sdklib.project.ApkConfigurationHelper; +import com.android.sdklib.project.ProjectProperties; +import com.android.sdklib.project.ProjectProperties.PropertyType; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.ExecTask; +import org.apache.tools.ant.types.Path; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * Task able to run an Exec task on aapt several times. + * It does not follow the exec task format, instead it has its own parameters, which maps + * directly to aapt. + * + */ +public final class AaptExecLoopTask extends Task { + + private String mExecutable; + private String mCommand; + private String mManifest; + private String mResources; + private String mAssets; + private String mAndroidJar; + private String mOutFolder; + private String mBaseName; + + /** + * Sets the value of the "executable" attribute. + * @param executable the value. + */ + public void setExecutable(String executable) { + mExecutable = executable; + } + + /** + * Sets the value of the "command" attribute. + * @param command the value. + */ + public void setCommand(String command) { + mCommand = command; + } + + /** + * Sets the value of the "manifest" attribute. + * @param manifest the value. + */ + public void setManifest(Path manifest) { + mManifest = manifest.toString(); + } + + /** + * Sets the value of the "resources" attribute. + * @param resources the value. + */ + public void setResources(Path resources) { + mResources = resources.toString(); + } + + /** + * Sets the value of the "assets" attribute. + * @param assets the value. + */ + public void setAssets(Path assets) { + mAssets = assets.toString(); + } + + /** + * Sets the value of the "androidjar" attribute. + * @param androidJar the value. + */ + public void setAndroidjar(Path androidJar) { + mAndroidJar = androidJar.toString(); + } + + /** + * Sets the value of the "outfolder" attribute. + * @param outFolder the value. + */ + public void setOutfolder(Path outFolder) { + mOutFolder = outFolder.toString(); + } + + /** + * Sets the value of the "basename" attribute. + * @param baseName the value. + */ + public void setBasename(String baseName) { + mBaseName = baseName; + } + + /* + * (non-Javadoc) + * + * Executes the loop. Based on the values inside default.properties, this will + * create alternate temporary ap_ files. + * + * @see org.apache.tools.ant.Task#execute() + */ + @Override + public void execute() throws BuildException { + Project taskProject = getProject(); + + // first do a full resource package + createPackage(null /*configName*/, null /*resourceFilter*/); + + // now see if we need to create file with filtered resources. + // Get the project base directory. + File baseDir = taskProject.getBaseDir(); + ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), + PropertyType.DEFAULT); + + Map apkConfigs = ApkConfigurationHelper.getConfigs(properties); + if (apkConfigs.size() > 0) { + Set> entrySet = apkConfigs.entrySet(); + for (Entry entry : entrySet) { + createPackage(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Creates a resource package. + * @param configName the name of the filter config. Can be null in which case a full resource + * package will be generated. + * @param resourceFilter the resource configuration filter to pass to aapt (if configName is + * non null) + */ + private void createPackage(String configName, String resourceFilter) { + Project taskProject = getProject(); + + if (configName == null || resourceFilter == null) { + System.out.println("Creating full resource package..."); + } else { + System.out.println(String.format( + "Creating resource package for config '%1$s' (%2$s)...", + configName, resourceFilter)); + } + + // create a task for the default apk. + ExecTask task = new ExecTask(); + task.setExecutable(mExecutable); + task.setFailonerror(true); + + // aapt command. Only "package" is supported at this time really. + task.createArg().setValue(mCommand); + + // filters if needed + if (configName != null && resourceFilter != null) { + task.createArg().setValue("-c"); + task.createArg().setValue(resourceFilter); + } + + // force flag + task.createArg().setValue("-f"); + + // manifest location + task.createArg().setValue("-M"); + task.createArg().setValue(mManifest); + + // resources location + task.createArg().setValue("-S"); + task.createArg().setValue(mResources); + + // assets location. this may not exists, and aapt doesn't like it, so we check first. + File assets = new File(mAssets); + if (assets.isDirectory()) { + task.createArg().setValue("-A"); + task.createArg().setValue(mAssets); + } + + // android.jar + task.createArg().setValue("-I"); + task.createArg().setValue(mAndroidJar); + + // out file. This is based on the outFolder, baseName, and the configName (if applicable) + String filename; + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName + ".ap_"; + } else { + filename = mBaseName + ".ap_"; + } + + File file = new File(mOutFolder, filename); + task.createArg().setValue("-F"); + task.createArg().setValue(file.getAbsolutePath()); + + // final setup of the task + task.setProject(taskProject); + task.setOwningTarget(getOwningTarget()); + + // execute it. + task.execute(); + } +} diff --git a/tools/anttasks/src/com/android/ant/ApkBuilderTask.java b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java new file mode 100644 index 000000000..22729ec13 --- /dev/null +++ b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java @@ -0,0 +1,293 @@ +/* + * 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.ant; + +import com.android.apkbuilder.ApkBuilder; +import com.android.apkbuilder.ApkBuilder.ApkFile; +import com.android.sdklib.project.ApkConfigurationHelper; +import com.android.sdklib.project.ProjectProperties; +import com.android.sdklib.project.ProjectProperties.PropertyType; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.ProjectComponent; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.Path; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +public class ApkBuilderTask extends Task { + + /** + * Class to represent nested elements. Since they all have only one attribute ('path'), the + * same class can be used for all the nested elements (zip, file, sourcefolder, jarfolder, + * nativefolder). + */ + public final static class Value extends ProjectComponent { + String mPath; + + /** + * Sets the value of the "path" attribute. + * @param path the value. + */ + public void setPath(Path path) { + mPath = path.toString(); + } + } + + private String mOutFolder; + private String mBaseName; + private boolean mVerbose = false; + private boolean mSigned = true; + + private final ArrayList mZipList = new ArrayList(); + private final ArrayList mFileList = new ArrayList(); + private final ArrayList mSourceList = new ArrayList(); + private final ArrayList mJarList = new ArrayList(); + private final ArrayList mNativeList = new ArrayList(); + + private final ArrayList mZipArchives = new ArrayList(); + private final ArrayList mArchiveFiles = new ArrayList(); + private final ArrayList mJavaResources = new ArrayList(); + private final ArrayList mResourcesJars = new ArrayList(); + private final ArrayList mNativeLibraries = new ArrayList(); + + /** + * Sets the value of the "outfolder" attribute. + * @param outFolder the value. + */ + public void setOutfolder(Path outFolder) { + mOutFolder = outFolder.toString(); + } + + /** + * Sets the value of the "basename" attribute. + * @param baseName the value. + */ + public void setBasename(String baseName) { + mBaseName = baseName; + } + + /** + * Sets the value of the "verbose" attribute. + * @param verbose the value. + */ + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + /** + * Sets the value of the "signed" attribute. + * @param signed the value. + */ + public void setSigned(boolean signed) { + mSigned = signed; + } + + /** + * Returns an object representing a nested zip element. + */ + public Object createZip() { + Value zip = new Value(); + mZipList.add(zip); + return zip; + } + + /** + * Returns an object representing a nested file element. + */ + public Object createFile() { + Value file = new Value(); + mFileList.add(file); + return file; + } + + /** + * Returns an object representing a nested sourcefolder element. + */ + public Object createSourcefolder() { + Value file = new Value(); + mSourceList.add(file); + return file; + } + + /** + * Returns an object representing a nested jarfolder element. + */ + public Object createJarfolder() { + Value file = new Value(); + mJarList.add(file); + return file; + } + + /** + * Returns an object representing a nested nativefolder element. + */ + public Object createNativefolder() { + Value file = new Value(); + mNativeList.add(file); + return file; + } + + @Override + public void execute() throws BuildException { + Project taskProject = getProject(); + + ApkBuilder apkBuilder = new ApkBuilder(); + apkBuilder.setVerbose(mVerbose); + apkBuilder.setSignedPackage(mSigned); + + try { + // setup the list of everything that needs to go in the archive. + + // go through the list of zip files to add. This will not include + // the resource package, which is handled separaly for each apk to create. + for (Value v : mZipList) { + FileInputStream input = new FileInputStream(v.mPath); + mZipArchives.add(input); + } + + // now go through the list of file to directly add the to the list. + for (Value v : mFileList) { + mArchiveFiles.add(ApkBuilder.getInputFile(v.mPath)); + } + + // now go through the list of file to directly add the to the list. + for (Value v : mSourceList) { + ApkBuilder.processSourceFolderForResource(v.mPath, mJavaResources); + } + + // now go through the list of jar folders. + for (Value v : mJarList) { + ApkBuilder.processJarFolder(v.mPath, mResourcesJars); + } + + // now the native lib folder. + for (Value v : mNativeList) { + String parameter = v.mPath; + File f = new File(parameter); + + // compute the offset to get the relative path + int offset = parameter.length(); + if (parameter.endsWith(File.separator) == false) { + offset++; + } + + ApkBuilder.processNativeFolder(offset, f, mNativeLibraries); + } + + + // first do a full resource package + createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/); + + // now see if we need to create file with filtered resources. + // Get the project base directory. + File baseDir = taskProject.getBaseDir(); + ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), + PropertyType.DEFAULT); + + Map apkConfigs = ApkConfigurationHelper.getConfigs(properties); + if (apkConfigs.size() > 0) { + Set> entrySet = apkConfigs.entrySet(); + for (Entry entry : entrySet) { + createApk(apkBuilder, entry.getKey(), entry.getValue()); + } + } + } catch (FileNotFoundException e) { + throw new BuildException(e); + } catch (IllegalArgumentException e) { + throw new BuildException(e); + } + } + + /** + * Creates an application package. + * @param apkBuilder + * @param configName the name of the filter config. Can be null in which case a full resource + * package will be generated. + * @param resourceFilter the resource configuration filter to pass to aapt (if configName is + * non null) + * @throws FileNotFoundException + */ + private void createApk(ApkBuilder apkBuilder, String configName, String resourceFilter) + throws FileNotFoundException { + // All the files to be included in the archive have already been prep'ed up, except + // the resource package. + // figure out its name. + String filename; + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName + ".ap_"; + } else { + filename = mBaseName + ".ap_"; + } + + // now we add it to the list of zip archive (it's just a zip file). + + // it's used as a zip archive input + FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename)); + mZipArchives.add(resoucePackageZipFile); + + // prepare the filename to generate. Same thing as the resource file. + if (configName != null && resourceFilter != null) { + filename = mBaseName + "-" + configName; + } else { + filename = mBaseName; + } + + if (mSigned) { + filename = filename + "-debug.apk"; + } else { + filename = filename + "-unsigned.apk"; + } + + if (configName == null || resourceFilter == null) { + if (mSigned) { + System.out.println(String.format( + "Creating %s and signing it with a debug key...", filename)); + } else { + System.out.println(String.format( + "Creating %s for release...", filename)); + } + } else { + if (mSigned) { + System.out.println(String.format( + "Creating %1$s (with %2$s) and signing it with a debug key...", + filename, resourceFilter)); + } else { + System.out.println(String.format( + "Creating %1$s (with %2$s) for release...", + filename, resourceFilter)); + } + } + + File f = new File(mOutFolder, filename); + + // and generate the apk + apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives, + mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries); + + // we are done. We need to remove the resource package from the list of zip archives + // in case we have another apk to generate. + mZipArchives.remove(resoucePackageZipFile); + } +} diff --git a/tools/anttasks/src/com/android/ant/AndroidInitTask.java b/tools/anttasks/src/com/android/ant/SetupTask.java similarity index 72% rename from tools/anttasks/src/com/android/ant/AndroidInitTask.java rename to tools/anttasks/src/com/android/ant/SetupTask.java index 30779c5da..d425a2fae 100644 --- a/tools/anttasks/src/com/android/ant/AndroidInitTask.java +++ b/tools/anttasks/src/com/android/ant/SetupTask.java @@ -1,11 +1,11 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -33,7 +33,7 @@ import java.util.ArrayList; import java.util.HashSet; /** - * Import Target Ant task. This task accomplishes: + * Setup/Import Ant task. This task accomplishes: *

    *
  • Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET}, * and resolves it to get the project's {@link IAndroidTarget}.
  • @@ -42,21 +42,29 @@ import java.util.HashSet; * the libraries. This includes the default android.jar from the resolved target but also optional * libraries provided by the target (if any, when the target is an add-on). *
  • Imports the build rules located in the resolved target so that the build actually does - * something.
  • - *
+ * something. This can be disabled with the attribute import set to false + * * * This is used in build.xml/template. * */ -public class AndroidInitTask extends ImportTask { +public final class SetupTask extends ImportTask { private final static String ANDROID_RULES = "android_rules.xml"; // ant property with the path to the android.jar private final static String PROPERTY_ANDROID_JAR = "android-jar"; // ant property with the path to the framework.jar private final static String PROPERTY_ANDROID_AIDL = "android-aidl"; + // ant property with the path to the aapt tool + private final static String PROPERTY_AAPT = "aapt"; + // ant property with the path to the aidl tool + private final static String PROPERTY_AIDL = "aidl"; + // ant property with the path to the dx tool + private final static String PROPERTY_DX = "dx"; // ref id to the object containing all the boot classpaths. private final static String REF_CLASSPATH = "android.target.classpath"; + + private boolean mDoImport = true; @Override public void execute() throws BuildException { @@ -122,15 +130,19 @@ public class AndroidInitTask extends ImportTask { System.out.println("Project Target: " + androidTarget.getName()); if (androidTarget.isPlatform() == false) { System.out.println("Vendor: " + androidTarget.getVendor()); + System.out.println("Platform Version: " + androidTarget.getApiVersionName()); } - System.out.println("Platform Version: " + androidTarget.getApiVersionName()); System.out.println("API level: " + androidTarget.getApiVersionNumber()); - // sets up the properties to find android.jar/framework.aidl + // sets up the properties to find android.jar/framework.aidl/target tools String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR); - String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL); antProject.setProperty(PROPERTY_ANDROID_JAR, androidJar); - antProject.setProperty(PROPERTY_ANDROID_AIDL, androidAidl); + + antProject.setProperty(PROPERTY_ANDROID_AIDL, + androidTarget.getPath(IAndroidTarget.ANDROID_AIDL)); + antProject.setProperty(PROPERTY_AAPT, androidTarget.getPath(IAndroidTarget.AAPT)); + antProject.setProperty(PROPERTY_AIDL, androidTarget.getPath(IAndroidTarget.AIDL)); + antProject.setProperty(PROPERTY_DX, androidTarget.getPath(IAndroidTarget.DX)); // sets up the boot classpath @@ -162,24 +174,35 @@ public class AndroidInitTask extends ImportTask { // find the file to import, and import it. String templateFolder = androidTarget.getPath(IAndroidTarget.TEMPLATES); - // make sure the file exists. - File templates = new File(templateFolder); - if (templates.isDirectory() == false) { - throw new BuildException(String.format("Template directory '%s' is missing.", - templateFolder)); + // Now the import section. This is only executed if the task actually has to import a file. + if (mDoImport) { + // make sure the file exists. + File templates = new File(templateFolder); + if (templates.isDirectory() == false) { + throw new BuildException(String.format("Template directory '%s' is missing.", + templateFolder)); + } + + // now check the rules file exists. + File rules = new File(templateFolder, ANDROID_RULES); + if (rules.isFile() == false) { + throw new BuildException(String.format("Build rules file '%s' is missing.", + templateFolder)); + } + + // set the file location to import + setFile(rules.getAbsolutePath()); + + // and import + super.execute(); } - - // now check the rules file exists. - File rules = new File(templateFolder, ANDROID_RULES); - if (rules.isFile() == false) { - throw new BuildException(String.format("Build rules file '%s' is missing.", - templateFolder)); - } - - // set the file location to import - setFile(rules.getAbsolutePath()); - - // and import it - super.execute(); + } + + /** + * Sets the value of the "import" attribute. + * @param value the value. + */ + public void setImport(boolean value) { + mDoImport = value; } } diff --git a/tools/apkbuilder/etc/apkbuilder.bat b/tools/apkbuilder/etc/apkbuilder.bat index 7ab3e6c43..c4689c62a 100755 --- a/tools/apkbuilder/etc/apkbuilder.bat +++ b/tools/apkbuilder/etc/apkbuilder.bat @@ -20,9 +20,9 @@ rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 -rem Change current directory to where ddms is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=apkbuilder.jar set frameworkdir= diff --git a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java index 9a4d24b54..40abff1ad 100644 --- a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java +++ b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java @@ -51,7 +51,7 @@ public final class ApkBuilder { * A File to be added to the APK archive. *

This includes the {@link File} representing the file and its path in the archive. */ - private final static class ApkFile { + public final static class ApkFile { String archivePath; File file; @@ -64,6 +64,8 @@ public final class ApkBuilder { private JavaResourceFilter mResourceFilter = new JavaResourceFilter(); private boolean mVerbose = false; private boolean mSignedPackage = true; + /** the optional type of the debug keystore. If null, the default */ + private String mStoreType = null; /** * @param args @@ -71,110 +73,97 @@ public final class ApkBuilder { public static void main(String[] args) { new ApkBuilder().run(args); } + + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + public void setSignedPackage(boolean signedPackage) { + mSignedPackage = signedPackage; + } private void run(String[] args) { if (args.length < 1) { printUsageAndQuit(); } - - // read the first args that should be a file path - File outFile = getOutFile(args[0]); - ArrayList zipArchives = new ArrayList(); - ArrayList archiveFiles = new ArrayList(); - ArrayList javaResources = new ArrayList(); - ArrayList resourcesJars = new ArrayList(); - ArrayList nativeLibraries = new ArrayList(); - - // optional store type. - String storeType = null; - - int index = 1; - do { - String argument = args[index++]; - - if ("-v".equals(argument)) { - mVerbose = true; - } else if ("-u".equals(argument)) { - mSignedPackage = false; - } else if ("-z".equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - try { - FileInputStream input = new FileInputStream(args[index++]); - zipArchives.add(input); - } catch (FileNotFoundException e) { - printAndExit(e.getMessage()); - } - } else if ("-f". equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - archiveFiles.add(getInputFile(args[index++])); - } else if ("-rf". equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - processSourceFolderForResource(args[index++], javaResources); - } else if ("-rj". equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - String parameter = args[index++]; - File f = new File(parameter); - if (f.isDirectory()) { - String[] files = f.list(new FilenameFilter() { - public boolean accept(File dir, String name) { - return PATTERN_JAR_EXT.matcher(name).matches(); - } - }); - - for (String file : files) { - try { - String path = f.getAbsolutePath() + File.separator + file; - FileInputStream input = new FileInputStream(path); - resourcesJars.add(input); - } catch (FileNotFoundException e) { - printAndExit(e.getMessage()); - } - } - } else { + try { + // read the first args that should be a file path + File outFile = getOutFile(args[0]); + + ArrayList zipArchives = new ArrayList(); + ArrayList archiveFiles = new ArrayList(); + ArrayList javaResources = new ArrayList(); + ArrayList resourcesJars = new ArrayList(); + ArrayList nativeLibraries = new ArrayList(); + + int index = 1; + do { + String argument = args[index++]; + + if ("-v".equals(argument)) { + mVerbose = true; + } else if ("-u".equals(argument)) { + mSignedPackage = false; + } else if ("-z".equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + try { - FileInputStream input = new FileInputStream(parameter); - resourcesJars.add(input); + FileInputStream input = new FileInputStream(args[index++]); + zipArchives.add(input); } catch (FileNotFoundException e) { printAndExit(e.getMessage()); } + } else if ("-f". equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + + archiveFiles.add(getInputFile(args[index++])); + } else if ("-rf". equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + + processSourceFolderForResource(args[index++], javaResources); + } else if ("-rj". equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + + processJarFolder(args[index++], resourcesJars); + } else if ("-nf".equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + + String parameter = args[index++]; + File f = new File(parameter); + + // compute the offset to get the relative path + int offset = parameter.length(); + if (parameter.endsWith(File.separator) == false) { + offset++; + } + + processNativeFolder(offset, f, nativeLibraries); + } else if ("-storetype".equals(argument)) { + // quick check on the next argument. + if (index == args.length) printUsageAndQuit(); + + mStoreType = args[index++]; + } else { + printAndExit("Unknown argument: " + argument); } - } else if ("-nf".equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - String parameter = args[index++]; - File f = new File(parameter); - - // compute the offset to get the relative path - int offset = parameter.length(); - if (parameter.endsWith(File.separator) == false) { - offset++; - } - - processNativeFolder(offset, f, nativeLibraries); - } else if ("-storetype".equals(argument)) { - // quick check on the next argument. - if (index == args.length) printUsageAndQuit(); - - storeType = args[index++]; - } else { - printAndExit("Unknown argument: " + argument); - } - } while (index < args.length); - - createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars, - nativeLibraries, storeType); + } while (index < args.length); + + createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars, + nativeLibraries); + } catch (IllegalArgumentException e) { + printAndExit(e.getMessage()); + } catch (FileNotFoundException e) { + printAndExit(e.getMessage()); + } } + private File getOutFile(String filepath) { File f = new File(filepath); @@ -199,31 +188,30 @@ public final class ApkBuilder { return f; } - private File getInputFile(String filepath) { + public static File getInputFile(String filepath) throws IllegalArgumentException { File f = new File(filepath); if (f.isDirectory()) { - printAndExit(filepath + " is a directory!"); + throw new IllegalArgumentException(filepath + " is a directory!"); } if (f.exists()) { if (f.canRead() == false) { - printAndExit("Cannot read " + filepath); + throw new IllegalArgumentException("Cannot read " + filepath); } } else { - printAndExit(filepath + " does not exists!"); + throw new IllegalArgumentException(filepath + " does not exists!"); } return f; } /** - * Processes a source folder and add its java resources to the list of {@link ApkFile} to - * write into the {@link SignedJarBuilder}. + * Processes a source folder and adds its java resources to a given list of {@link ApkFile}. * @param folderPath the path to the source folder. * @param javaResources the list of {@link ApkFile} to fill. */ - private void processSourceFolderForResource(String folderPath, + public static void processSourceFolderForResource(String folderPath, ArrayList javaResources) { File folder = new File(folderPath); @@ -237,13 +225,35 @@ public final class ApkBuilder { } else { // not a directory? output error and quit. if (folder.exists()) { - printAndExit(folderPath + " is not a folder!"); + throw new IllegalArgumentException(folderPath + " is not a folder!"); } else { - printAndExit(folderPath + " does not exist!"); + throw new IllegalArgumentException(folderPath + " does not exist!"); } } } + public static void processJarFolder(String parameter, ArrayList resourcesJars) + throws FileNotFoundException { + File f = new File(parameter); + if (f.isDirectory()) { + String[] files = f.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return PATTERN_JAR_EXT.matcher(name).matches(); + } + }); + + for (String file : files) { + String path = f.getAbsolutePath() + File.separator + file; + FileInputStream input = new FileInputStream(path); + resourcesJars.add(input); + } + } else { + FileInputStream input = new FileInputStream(parameter); + resourcesJars.add(input); + } + } + + /** * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing * java resources. @@ -252,7 +262,7 @@ public final class ApkBuilder { * identify a root file. * @param javaResources the list of {@link ApkFile} object to fill. */ - private void processFileForResource(File file, String path, + private static void processFileForResource(File file, String path, ArrayList javaResources) { if (file.isDirectory()) { // a directory? we check it @@ -292,7 +302,7 @@ public final class ApkBuilder { * @param f the {@link File} to process * @param nativeLibraries the array to add native libraries. */ - private void processNativeFolder(int offset, File f, ArrayList nativeLibraries) { + public static void processNativeFolder(int offset, File f, ArrayList nativeLibraries) { if (f.isDirectory()) { File[] children = f.listFiles(); @@ -318,13 +328,11 @@ public final class ApkBuilder { * @param resourcesJars * @param files * @param javaResources - * @param storeType the optional type of the debug keystore. If null, the default - * keystore type of the VM is used. + * keystore type of the Java VM is used. */ - private void createPackage(File outFile, ArrayList zipArchives, + public void createPackage(File outFile, ArrayList zipArchives, ArrayList files, ArrayList javaResources, - ArrayList resourcesJars, ArrayList nativeLibraries, - String storeType) { + ArrayList resourcesJars, ArrayList nativeLibraries) { // get the debug key try { @@ -337,18 +345,18 @@ public final class ApkBuilder { DebugKeyProvider keyProvider = new DebugKeyProvider( null /* osKeyPath: use default */, - storeType, null /* IKeyGenOutput */); + mStoreType, null /* IKeyGenOutput */); PrivateKey key = keyProvider.getDebugKey(); X509Certificate certificate = (X509Certificate)keyProvider.getCertificate(); if (key == null) { - printAndExit("Unable to get debug signature key"); + throw new IllegalArgumentException("Unable to get debug signature key"); } // compare the certificate expiration date if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { // TODO, regenerate a new one. - printAndExit("Debug Certificate expired on " + + throw new IllegalArgumentException("Debug Certificate expired on " + DateFormat.getInstance().format(certificate.getNotAfter())); } @@ -403,21 +411,20 @@ public final class ApkBuilder { builder.close(); } catch (KeytoolException e) { if (e.getJavaHome() == null) { - printAndExit(e.getMessage(), - "JAVA_HOME seems undefined, setting it will help locating keytool automatically", - "You can also manually execute the following command:", + throw new IllegalArgumentException(e.getMessage() + + "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" + + "You can also manually execute the following command\n:" + e.getCommandLine()); } else { - printAndExit(e.getMessage(), - "JAVA_HOME is set to: " + e.getJavaHome(), - "Update it if necessary, or manually execute the following command:", + throw new IllegalArgumentException(e.getMessage() + + "\nJAVA_HOME is set to: " + e.getJavaHome() + + "\nUpdate it if necessary, or manually execute the following command:\n" + e.getCommandLine()); } - System.err.println(e.getMessage()); } catch (AndroidLocationException e) { - printAndExit(e.getMessage()); + throw new IllegalArgumentException(e); } catch (Exception e) { - printAndExit(e.getMessage()); + throw new IllegalArgumentException(e); } } diff --git a/tools/ddms/app/etc/ddms.bat b/tools/ddms/app/etc/ddms.bat index 8d941b9c2..5da9fb589 100755 --- a/tools/ddms/app/etc/ddms.bat +++ b/tools/ddms/app/etc/ddms.bat @@ -20,9 +20,9 @@ rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 -rem Change current directory to where ddms is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=ddms.jar set frameworkdir= diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java index 154bfa182..42022fe7b 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java @@ -268,56 +268,61 @@ final class AdbHelper { }; byte[] reply; - SocketChannel adbChan = SocketChannel.open(adbSockAddr); - adbChan.configureBlocking(false); - - // if the device is not -1, then we first tell adb we're looking to talk - // to a specific device - setDevice(adbChan, device); - - if (write(adbChan, request) == false) - throw new IOException("failed asking for frame buffer"); - - AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); - if (!resp.ioSuccess || !resp.okay) { - Log.w("ddms", "Got timeout or unhappy response from ADB fb req: " - + resp.message); - adbChan.close(); - return null; + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + if (write(adbChan, request) == false) + throw new IOException("failed asking for frame buffer"); + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.w("ddms", "Got timeout or unhappy response from ADB fb req: " + + resp.message); + adbChan.close(); + return null; + } + + reply = new byte[16]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got partial reply from ADB fb:"); + Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length); + adbChan.close(); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(reply); + buf.order(ByteOrder.LITTLE_ENDIAN); + + imageParams.bpp = buf.getInt(); + imageParams.size = buf.getInt(); + imageParams.width = buf.getInt(); + imageParams.height = buf.getInt(); + + Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" + + imageParams.size + ", width=" + imageParams.width + + ", height=" + imageParams.height); + + if (write(adbChan, nudge) == false) + throw new IOException("failed nudging"); + + reply = new byte[imageParams.size]; + if (read(adbChan, reply) == false) { + Log.w("ddms", "got truncated reply from ADB fb data"); + adbChan.close(); + return null; + } + imageParams.data = reply; + } finally { + if (adbChan != null) { + adbChan.close(); + } } - reply = new byte[16]; - if (read(adbChan, reply) == false) { - Log.w("ddms", "got partial reply from ADB fb:"); - Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length); - adbChan.close(); - return null; - } - ByteBuffer buf = ByteBuffer.wrap(reply); - buf.order(ByteOrder.LITTLE_ENDIAN); - - imageParams.bpp = buf.getInt(); - imageParams.size = buf.getInt(); - imageParams.width = buf.getInt(); - imageParams.height = buf.getInt(); - - Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" - + imageParams.size + ", width=" + imageParams.width - + ", height=" + imageParams.height); - - if (write(adbChan, nudge) == false) - throw new IOException("failed nudging"); - - reply = new byte[imageParams.size]; - if (read(adbChan, reply) == false) { - Log.w("ddms", "got truncated reply from ADB fb data"); - adbChan.close(); - return null; - } - imageParams.data = reply; - - adbChan.close(); - return imageParams; } @@ -330,58 +335,61 @@ final class AdbHelper { throws IOException { Log.v("ddms", "execute: running " + command); - SocketChannel adbChan = SocketChannel.open(adbSockAddr); - adbChan.configureBlocking(false); + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); - // if the device is not -1, then we first tell adb we're looking to talk - // to a specific device - setDevice(adbChan, device); + // if the device is not -1, then we first tell adb we're looking to + // talk + // to a specific device + setDevice(adbChan, device); - byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$ - if (write(adbChan, request) == false) - throw new IOException("failed submitting shell command"); + byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$ + if (write(adbChan, request) == false) + throw new IOException("failed submitting shell command"); - AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); - if (!resp.ioSuccess || !resp.okay) { - Log.e("ddms", "ADB rejected shell command (" + command + "): " - + resp.message); - throw new IOException("sad result from adb: " + resp.message); - } - - byte[] data = new byte[16384]; - ByteBuffer buf = ByteBuffer.wrap(data); - while (true) { - int count; - - if (rcvr != null && rcvr.isCancelled()) { - Log.v("ddms", "execute: cancelled"); - break; + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message); + throw new IOException("sad result from adb: " + resp.message); } - count = adbChan.read(buf); - if (count < 0) { - // we're at the end, we flush the output - rcvr.flush(); - Log.v("ddms", - "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + count); - break; - } else if (count == 0) { - try { - Thread.sleep(WAIT_TIME * 5); - } catch (InterruptedException ie) { + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + Log.v("ddms", "execute: cancelled"); + break; } - } else { - if (rcvr != null) { - rcvr.addOutput(buf.array(), buf.arrayOffset(), buf - .position()); + + count = adbChan.read(buf); + if (count < 0) { + // we're at the end, we flush the output + rcvr.flush(); + Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + + count); + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); } - buf.rewind(); } + } finally { + if (adbChan != null) { + adbChan.close(); + } + Log.v("ddms", "execute: returning"); } - - adbChan.close(); - - Log.v("ddms", "execute: returning"); } /** @@ -407,49 +415,55 @@ final class AdbHelper { */ public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName, LogReceiver rcvr) throws IOException { - SocketChannel adbChan = SocketChannel.open(adbSockAddr); - adbChan.configureBlocking(false); - - // if the device is not -1, then we first tell adb we're looking to talk - // to a specific device - setDevice(adbChan, device); - - byte[] request = formAdbRequest("log:" + logName); - if (write(adbChan, request) == false) { - throw new IOException("failed to submit the log command"); - } - - AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); - if (!resp.ioSuccess || !resp.okay) { - throw new IOException("Device rejected log command: " + resp.message); - } - - byte[] data = new byte[16384]; - ByteBuffer buf = ByteBuffer.wrap(data); - while (true) { - int count; - - if (rcvr != null && rcvr.isCancelled()) { - break; + SocketChannel adbChan = null; + + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + // if the device is not -1, then we first tell adb we're looking to talk + // to a specific device + setDevice(adbChan, device); + + byte[] request = formAdbRequest("log:" + logName); + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the log command"); } - - count = adbChan.read(buf); - if (count < 0) { - break; - } else if (count == 0) { - try { - Thread.sleep(WAIT_TIME * 5); - } catch (InterruptedException ie) { + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected log command: " + resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + break; } - } else { - if (rcvr != null) { - rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + + count = adbChan.read(buf); + if (count < 0) { + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); } - buf.rewind(); + } + } finally { + if (adbChan != null) { + adbChan.close(); } } - - adbChan.close(); } /** @@ -464,24 +478,29 @@ final class AdbHelper { public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort, int remotePort) throws IOException { - SocketChannel adbChan = SocketChannel.open(adbSockAddr); - adbChan.configureBlocking(false); - - byte[] request = formAdbRequest(String.format( - "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ - device.serialNumber, localPort, remotePort)); - - if (write(adbChan, request) == false) { - throw new IOException("failed to submit the forward command."); + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.serialNumber, localPort, remotePort)); + + if (write(adbChan, request) == false) { + throw new IOException("failed to submit the forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } } - AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); - if (!resp.ioSuccess || !resp.okay) { - throw new IOException("Device rejected command: " + resp.message); - } - - adbChan.close(); - return true; } @@ -497,24 +516,29 @@ final class AdbHelper { public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort, int remotePort) throws IOException { - SocketChannel adbChan = SocketChannel.open(adbSockAddr); - adbChan.configureBlocking(false); - - byte[] request = formAdbRequest(String.format( - "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ - device.serialNumber, localPort, remotePort)); - - if (!write(adbChan, request)) { - throw new IOException("failed to submit the remove forward command."); + SocketChannel adbChan = null; + try { + adbChan = SocketChannel.open(adbSockAddr); + adbChan.configureBlocking(false); + + byte[] request = formAdbRequest(String.format( + "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.serialNumber, localPort, remotePort)); + + if (!write(adbChan, request)) { + throw new IOException("failed to submit the remove forward command."); + } + + AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); + if (!resp.ioSuccess || !resp.okay) { + throw new IOException("Device rejected command: " + resp.message); + } + } finally { + if (adbChan != null) { + adbChan.close(); + } } - AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */); - if (!resp.ioSuccess || !resp.okay) { - throw new IOException("Device rejected command: " + resp.message); - } - - adbChan.close(); - return true; } diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java index a4576aa91..866d57882 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java @@ -94,7 +94,7 @@ public class Client { * is only used for data generated within Client. */ private static final int INITIAL_BUF_SIZE = 2*1024; - private static final int MAX_BUF_SIZE = 2*1024*1024; + private static final int MAX_BUF_SIZE = 200*1024*1024; private ByteBuffer mReadBuffer; private static final int WRITE_BUF_SIZE = 256; diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java index 8291f591e..0e7f0bbd0 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java @@ -30,7 +30,7 @@ import java.util.Map; /** * A Device. It can be a physical device or an emulator. - * + * * TODO: make this class package-protected, and shift all callers to use IDevice */ public final class Device implements IDevice { @@ -62,19 +62,19 @@ public final class Device implements IDevice { return null; } } - + /** Emulator Serial Number regexp. */ final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ - + /** Serial number of the device */ String serialNumber = null; - /** Name of the vm */ - String mVmName = null; + /** Name of the AVD */ + String mAvdName = null; /** State of the device. */ DeviceState state = null; - + /** Device properties. */ private final Map mProperties = new HashMap(); @@ -85,29 +85,29 @@ public final class Device implements IDevice { * Socket for the connection monitoring client connection/disconnection. */ private SocketChannel mSocketChannel; - - /* + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getSerialNumber() */ public String getSerialNumber() { return serialNumber; } - - public String getVmName() { - return mVmName; + + public String getAvdName() { + return mAvdName; } - - /* + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getState() */ public DeviceState getState() { return state; } - - /* + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getProperties() */ @@ -115,7 +115,7 @@ public final class Device implements IDevice { return Collections.unmodifiableMap(mProperties); } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getPropertyCount() */ @@ -123,21 +123,21 @@ public final class Device implements IDevice { return mProperties.size(); } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getProperty(java.lang.String) */ public String getProperty(String name) { return mProperties.get(name); } - + @Override public String toString() { return serialNumber; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isOnline() */ @@ -145,7 +145,7 @@ public final class Device implements IDevice { return state == DeviceState.ONLINE; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isEmulator() */ @@ -153,7 +153,7 @@ public final class Device implements IDevice { return serialNumber.matches(RE_EMULATOR_SN); } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isOffline() */ @@ -161,7 +161,7 @@ public final class Device implements IDevice { return state == DeviceState.OFFLINE; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isBootLoader() */ @@ -169,7 +169,7 @@ public final class Device implements IDevice { return state == DeviceState.BOOTLOADER; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#hasClients() */ @@ -177,7 +177,7 @@ public final class Device implements IDevice { return mClients.size() > 0; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getClients() */ @@ -186,8 +186,8 @@ public final class Device implements IDevice { return mClients.toArray(new Client[mClients.size()]); } } - - /* + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getClient(java.lang.String) */ @@ -204,7 +204,7 @@ public final class Device implements IDevice { return null; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getSyncService() */ @@ -217,7 +217,7 @@ public final class Device implements IDevice { return null; } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getFileListingService() */ @@ -225,7 +225,7 @@ public final class Device implements IDevice { return new FileListingService(this); } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getScreenshot() */ @@ -233,7 +233,7 @@ public final class Device implements IDevice { return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this); } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver) */ @@ -242,16 +242,25 @@ public final class Device implements IDevice { AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this, receiver); } - - /* + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver) */ public void runEventLogService(LogReceiver receiver) throws IOException { AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver); } - - /* + + /* + * (non-Javadoc) + * @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver) + */ + public void runLogService(String logname, + LogReceiver receiver) throws IOException { + AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver); + } + + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#createForward(int, int) */ @@ -265,7 +274,7 @@ public final class Device implements IDevice { } } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#removeForward(int, int) */ @@ -279,7 +288,7 @@ public final class Device implements IDevice { } } - /* + /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getClientName(int) */ @@ -325,7 +334,7 @@ public final class Device implements IDevice { return false; } - + void clearClientList() { synchronized (mClients) { mClients.clear(); diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java index 8547ac1f4..f9d0fa070 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java @@ -420,11 +420,11 @@ final class DeviceMonitor { device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND, new GetPropReceiver(device)); - // now get the emulator VM name (if applicable). + // now get the emulator Virtual Device name (if applicable). if (device.isEmulator()) { EmulatorConsole console = EmulatorConsole.getConsole(device); if (console != null) { - device.mVmName = console.getVmName(); + device.mAvdName = console.getAvdName(); } } } catch (IOException e) { @@ -470,7 +470,7 @@ final class DeviceMonitor { } catch (IOException e1) { // we can ignore that one. It may already have been closed. } - Log.e("DeviceMonitor", + Log.d("DeviceMonitor", "Connection Failure when starting to monitor device '" + device + "' : " + e.getMessage()); } @@ -558,7 +558,7 @@ final class DeviceMonitor { processIncomingJdwpData(device, socket, length); } catch (IOException ioe) { - Log.e("DeviceMonitor", + Log.d("DeviceMonitor", "Error reading jdwp list: " + ioe.getMessage()); socket.close(); diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java index e00073c15..f3986ed4e 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java @@ -54,7 +54,7 @@ public final class EmulatorConsole { private final static String HOST = "127.0.0.1"; //$NON-NLS-1$ private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$ - private final static String COMMAND_VM_NAME = "vm name\r\n"; //$NON-NLS-1$ + private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$ private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$ private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$ private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$ @@ -309,8 +309,8 @@ public final class EmulatorConsole { } } - public synchronized String getVmName() { - if (sendCommand(COMMAND_VM_NAME)) { + public synchronized String getAvdName() { + if (sendCommand(COMMAND_AVD_NAME)) { String[] result = readLines(); if (result != null && result.length == 2) { // this should be the name on first line, // and ok on 2nd line diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java index 61d1ca462..5dbce9292 100755 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java @@ -44,15 +44,15 @@ public interface IDevice { * Returns the serial number of the device. */ public String getSerialNumber(); - + /** - * Returns the name of the VM the emulator is running. + * Returns the name of the AVD the emulator is running. *

This is only valid if {@link #isEmulator()} returns true. - *

If the emulator is not running any VM (for instance it's running from an Android source + *

If the emulator is not running any AVD (for instance it's running from an Android source * tree build), this method will return "<build>". - * @return the name of the VM or null if there isn't any. + * @return the name of the AVD or null if there isn't any. */ - public String getVmName(); + public String getAvdName(); /** * Returns the state of the device. @@ -151,6 +151,14 @@ public interface IDevice { */ public void runEventLogService(LogReceiver receiver) throws IOException; + /** + * Runs the log service for the given log and outputs the log to the {@link LogReceiver}. + * @param logname the logname of the log to read from. + * @param receiver the receiver to receive the event log entries. + * @throws IOException + */ + public void runLogService(String logname, LogReceiver receiver) throws IOException; + /** * Creates a port forwarding between a local and a remote port. * @param localPort the local port to forward diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java index 4c0d9debd..b61a69861 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java @@ -17,56 +17,69 @@ package com.android.ddmlib.testrunner; /** - * Listener for instrumentation test runs - * - * Modeled after junit.runner.TestRunListener + * Receives event notifications during instrumentation test runs. + * Patterned after {@link junit.runner.TestRunListener}. */ public interface ITestRunListener { - public static final int STATUS_ERROR = 1; - public static final int STATUS_FAILURE = 2; + + /** + * Types of test failures. + */ + enum TestFailure { + /** Test failed due to unanticipated uncaught exception. */ + ERROR, + /** Test failed due to a false assertion. */ + FAILURE + } /** - * Reports the start of a test run - * @param testCount - total number of tests in test run - * */ + * Reports the start of a test run. + * + * @param testCount total number of tests in test run + */ public void testRunStarted(int testCount); /** - * Reports end of test run - * @param elapsedTime - device reported elapsed time, in milliseconds + * Reports end of test run. + * + * @param elapsedTime device reported elapsed time, in milliseconds */ public void testRunEnded(long elapsedTime); /** - * Reports test run stopped before completion - * @param elapsedTime - device reported elapsed time, in milliseconds + * Reports test run stopped before completion. + * + * @param elapsedTime device reported elapsed time, in milliseconds */ public void testRunStopped(long elapsedTime); /** - * Reports the start of an individual test case - */ - public void testStarted(String className, String testName); - - /** - * Reports the execution end of an individual test case - * If no testFailed has been reported, this is a passed test - */ - public void testEnded(String className, String testName); - - /** - * Reports the failure of a individual test case - * Will be called between testStarted and testEnded + * Reports the start of an individual test case. * - * @param status - one of STATUS_ERROR, STATUS_FAILURE - * @param className - name of test class - * @param testName - name of test method - * @param trace - stack trace of failure + * @param test identifies the test */ - public void testFailed(int status, String className, String testName, String trace); + public void testStarted(TestIdentifier test); + + /** + * Reports the execution end of an individual test case. + * If {@link #testFailed} was not invoked, this test passed. + * + * @param test identifies the test + */ + public void testEnded(TestIdentifier test); + + /** + * Reports the failure of a individual test case. + * Will be called between testStarted and testEnded. + * + * @param status failure type + * @param test identifies the test + * @param trace stack trace of failure + */ + public void testFailed(TestFailure status, TestIdentifier test, String trace); /** - * Reports test run failed to execute due to a fatal error + * Reports test run failed to execute due to a fatal error. */ public void testRunFailed(String errorMessage); } diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java index d47bd5652..bc1834f61 100755 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -20,24 +20,21 @@ import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.Log; import com.android.ddmlib.MultiLineReceiver; -import java.util.Hashtable; -import java.util.Map; - /** - * Parses the 'raw output mode' results of an instrument test run from shell, and informs a - * ITestRunListener of the results + * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a + * ITestRunListener of the results. * - * Expects the following output: + *

Expects the following output: * - * If fatal error occurred when attempted to run the tests: - * INSTRUMENTATION_FAILED: + *

If fatal error occurred when attempted to run the tests: + *

 INSTRUMENTATION_FAILED: 
* - * Otherwise, expect a series of test results, each one containing a set of status key/value + *

Otherwise, expect a series of test results, each one containing a set of status key/value * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test * run, expects that the elapsed test time in seconds will be displayed * - * i.e. - * + *

For example: + *

  * INSTRUMENTATION_STATUS_CODE: 1
  * INSTRUMENTATION_STATUS: class=com.foo.FooTest
  * INSTRUMENTATION_STATUS: test=testFoo
@@ -48,64 +45,85 @@ import java.util.Map;
  * ... 
  * 
  * Time: X
- * 
- * 
- * Note that the "value" portion of the key-value pair may wrap over several text lines
+ * 
+ *

Note that the "value" portion of the key-value pair may wrap over several text lines */ public class InstrumentationResultParser extends MultiLineReceiver { - // relevant test status keys - private static final String CODE_KEY = "code"; - private static final String TEST_KEY = "test"; - private static final String CLASS_KEY = "class"; - private static final String STACK_KEY = "stack"; - private static final String NUMTESTS_KEY = "numtests"; + /** Relevant test status keys. */ + private static class StatusKeys { + private static final String TEST = "test"; + private static final String CLASS = "class"; + private static final String STACK = "stack"; + private static final String NUMTESTS = "numtests"; + } - // test result status codes - private static final int FAILURE_STATUS_CODE = -2; - private static final int START_STATUS_CODE = 1; - private static final int ERROR_STATUS_CODE = -1; - private static final int OK_STATUS_CODE = 0; + /** Test result status codes. */ + private static class StatusCodes { + private static final int FAILURE = -2; + private static final int START = 1; + private static final int ERROR = -1; + private static final int OK = 0; + } - // recognized output patterns - private static final String STATUS_PREFIX = "INSTRUMENTATION_STATUS: "; - private static final String STATUS_PREFIX_CODE = "INSTRUMENTATION_STATUS_CODE: "; - private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; - private static final String TIME_REPORT = "Time: "; + /** Prefixes used to identify output. */ + private static class Prefixes { + private static final String STATUS = "INSTRUMENTATION_STATUS: "; + private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: "; + private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; + private static final String TIME_REPORT = "Time: "; + } private final ITestRunListener mTestListener; - /** key-value map for current test */ - private Map mStatusValues; - /** stores the current "key" portion of the status key-value being parsed */ - private String mCurrentKey; - /** stores the current "value" portion of the status key-value being parsed */ - private StringBuilder mCurrentValue; - /** true if start of test has already been reported to listener */ - private boolean mTestStartReported; - /** the elapsed time of the test run, in ms */ - private long mTestTime; - /** true if current test run has been canceled by user */ - private boolean mIsCancelled; + + /** + * Test result data + */ + private static class TestResult { + private Integer mCode = null; + private String mTestName = null; + private String mTestClass = null; + private String mStackTrace = null; + private Integer mNumTests = null; + + /** Returns true if all expected values have been parsed */ + boolean isComplete() { + return mCode != null && mTestName != null && mTestClass != null; + } + } + + /** Stores the status values for the test result currently being parsed */ + private TestResult mCurrentTestResult = null; + + /** Stores the current "key" portion of the status key-value being parsed. */ + private String mCurrentKey = null; + + /** Stores the current "value" portion of the status key-value being parsed. */ + private StringBuilder mCurrentValue = null; + + /** True if start of test has already been reported to listener. */ + private boolean mTestStartReported = false; + + /** The elapsed time of the test run, in milliseconds. */ + private long mTestTime = 0; + + /** True if current test run has been canceled by user. */ + private boolean mIsCancelled = false; private static final String LOG_TAG = "InstrumentationResultParser"; /** - * Creates the InstrumentationResultParser - * @param listener - listener to report results to. will be informed of test results as the - * tests are executing + * Creates the InstrumentationResultParser. + * + * @param listener informed of test results as the tests are executing */ public InstrumentationResultParser(ITestRunListener listener) { - mStatusValues = new Hashtable(); - mCurrentKey = null; - setTrimLine(false); mTestListener = listener; - mTestStartReported = false; - mTestTime = 0; - mIsCancelled = false; } /** - * Processes the instrumentation test output from shell + * Processes the instrumentation test output from shell. + * * @see MultiLineReceiver#processNewLines */ @Override @@ -116,31 +134,37 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Parse an individual output line. Expects a line that either is: - * a) the start of a new status line (ie. starts with STATUS_PREFIX or STATUS_PREFIX_CODE), - * and thus there is a new key=value pair to parse, and the previous key-value pair is - * finished - * b) a continuation of the previous status (ie the "value" portion of the key has wrapped - * to the next line. - * c) a line reporting a fatal error in the test run (STATUS_FAILED) - * d) a line reporting the total elapsed time of the test run. + * Parse an individual output line. Expects a line that is one of: + *

    + *
  • + * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE), + * and thus there is a new key=value pair to parse, and the previous key-value pair is + * finished. + *
  • + *
  • + * A continuation of the previous status (the "value" portion of the key has wrapped + * to the next line). + *
  • + *
  • A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED)
  • + *
  • A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT)
  • + *
* - * @param line - text output line + * @param line Text output line */ private void parse(String line) { - if (line.startsWith(STATUS_PREFIX_CODE)) { + if (line.startsWith(Prefixes.STATUS_CODE)) { // Previous status key-value has been collected. Store it. submitCurrentKeyValue(); parseStatusCode(line); - } else if (line.startsWith(STATUS_PREFIX)) { + } else if (line.startsWith(Prefixes.STATUS)) { // Previous status key-value has been collected. Store it. submitCurrentKeyValue(); - parseKey(line, STATUS_PREFIX.length()); - } else if (line.startsWith(STATUS_FAILED)) { + parseKey(line, Prefixes.STATUS.length()); + } else if (line.startsWith(Prefixes.STATUS_FAILED)) { Log.e(LOG_TAG, "test run failed " + line); mTestListener.testRunFailed(line); - } else if (line.startsWith(TIME_REPORT)) { - parseTime(line, TIME_REPORT.length()); + } else if (line.startsWith(Prefixes.TIME_REPORT)) { + parseTime(line, Prefixes.TIME_REPORT.length()); } else { if (mCurrentValue != null) { // this is a value that has wrapped to next line. @@ -153,21 +177,53 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Stores the currently parsed key-value pair in the status map + * Stores the currently parsed key-value pair into mCurrentTestInfo. */ private void submitCurrentKeyValue() { if (mCurrentKey != null && mCurrentValue != null) { - mStatusValues.put(mCurrentKey, mCurrentValue.toString()); + TestResult testInfo = getCurrentTestInfo(); + String statusValue = mCurrentValue.toString(); + + if (mCurrentKey.equals(StatusKeys.CLASS)) { + testInfo.mTestClass = statusValue.trim(); + } + else if (mCurrentKey.equals(StatusKeys.TEST)) { + testInfo.mTestName = statusValue.trim(); + } + else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) { + try { + testInfo.mNumTests = Integer.parseInt(statusValue); + } + catch (NumberFormatException e) { + Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue); + } + } + else if (mCurrentKey.equals(StatusKeys.STACK)) { + testInfo.mStackTrace = statusValue; + } + mCurrentKey = null; mCurrentValue = null; } } + private TestResult getCurrentTestInfo() { + if (mCurrentTestResult == null) { + mCurrentTestResult = new TestResult(); + } + return mCurrentTestResult; + } + + private void clearCurrentTestInfo() { + mCurrentTestResult = null; + } + /** - * Parses the key from the current line - * Expects format of "key=value", - * @param line - full line of text to parse - * @param keyStartPos - the starting position of the key in the given line + * Parses the key from the current line. + * Expects format of "key=value". + * + * @param line full line of text to parse + * @param keyStartPos the starting position of the key in the given line */ private void parseKey(String line, int keyStartPos) { int endKeyPos = line.indexOf('=', keyStartPos); @@ -178,7 +234,8 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Parses the start of a key=value pair. + * Parses the start of a key=value pair. + * * @param line - full line of text to parse * @param valueStartPos - the starting position of the value in the given line */ @@ -188,20 +245,25 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Parses out a status code result. For consistency, stores the result as a CODE entry in - * key-value status map + * Parses out a status code result. */ private void parseStatusCode(String line) { - String value = line.substring(STATUS_PREFIX_CODE.length()).trim(); - mStatusValues.put(CODE_KEY, value); + String value = line.substring(Prefixes.STATUS_CODE.length()).trim(); + TestResult testInfo = getCurrentTestInfo(); + try { + testInfo.mCode = Integer.parseInt(value); + } + catch (NumberFormatException e) { + Log.e(LOG_TAG, "Expected integer status code, received: " + value); + } // this means we're done with current test result bundle - reportResult(mStatusValues); - mStatusValues.clear(); + reportResult(testInfo); + clearCurrentTestInfo(); } /** - * Returns true if test run canceled + * Returns true if test run canceled. * * @see IShellOutputReceiver#isCancelled() */ @@ -210,7 +272,7 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Requests cancellation of test result parsing + * Requests cancellation of test run. */ public void cancel() { mIsCancelled = true; @@ -219,82 +281,62 @@ public class InstrumentationResultParser extends MultiLineReceiver { /** * Reports a test result to the test run listener. Must be called when a individual test * result has been fully parsed. - * @param statusMap - key-value status pairs of test result + * + * @param statusMap key-value status pairs of test result */ - private void reportResult(Map statusMap) { - String className = statusMap.get(CLASS_KEY); - String testName = statusMap.get(TEST_KEY); - String statusCodeString = statusMap.get(CODE_KEY); - - if (className == null || testName == null || statusCodeString == null) { - Log.e(LOG_TAG, "invalid instrumentation status bundle " + statusMap.toString()); + private void reportResult(TestResult testInfo) { + if (!testInfo.isComplete()) { + Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString()); return; } - className = className.trim(); - testName = testName.trim(); + reportTestRunStarted(testInfo); + TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName); - reportTestStarted(statusMap); - - try { - int statusCode = Integer.parseInt(statusCodeString); - - switch (statusCode) { - case START_STATUS_CODE: - mTestListener.testStarted(className, testName); - break; - case FAILURE_STATUS_CODE: - mTestListener.testFailed(ITestRunListener.STATUS_FAILURE, className, testName, - getTrace(statusMap)); - mTestListener.testEnded(className, testName); - break; - case ERROR_STATUS_CODE: - mTestListener.testFailed(ITestRunListener.STATUS_ERROR, className, testName, - getTrace(statusMap)); - mTestListener.testEnded(className, testName); - break; - case OK_STATUS_CODE: - mTestListener.testEnded(className, testName); - break; - default: - Log.e(LOG_TAG, "Expected status code, received: " + statusCodeString); - mTestListener.testEnded(className, testName); - break; - } - } - catch (NumberFormatException e) { - Log.e(LOG_TAG, "Expected integer status code, received: " + statusCodeString); + switch (testInfo.mCode) { + case StatusCodes.START: + mTestListener.testStarted(testId); + break; + case StatusCodes.FAILURE: + mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.ERROR: + mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId, + getTrace(testInfo)); + mTestListener.testEnded(testId); + break; + case StatusCodes.OK: + mTestListener.testEnded(testId); + break; + default: + Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode); + mTestListener.testEnded(testId); + break; } + } /** * Reports the start of a test run, and the total test count, if it has not been previously - * reported - * @param statusMap - key-value status pairs + * reported. + * + * @param testInfo current test status values */ - private void reportTestStarted(Map statusMap) { + private void reportTestRunStarted(TestResult testInfo) { // if start test run not reported yet - if (!mTestStartReported) { - String numTestsString = statusMap.get(NUMTESTS_KEY); - if (numTestsString != null) { - try { - int numTests = Integer.parseInt(numTestsString); - mTestListener.testRunStarted(numTests); - mTestStartReported = true; - } - catch (NumberFormatException e) { - Log.e(LOG_TAG, "Unexpected numTests format " + numTestsString); - } - } + if (!mTestStartReported && testInfo.mNumTests != null) { + mTestListener.testRunStarted(testInfo.mNumTests); + mTestStartReported = true; } } /** - * Returns the stack trace of the current failed test, from the provided key-value status map + * Returns the stack trace of the current failed test, from the provided testInfo. */ - private String getTrace(Map statusMap) { - String stackTrace = statusMap.get(STACK_KEY); - if (stackTrace != null) { - return stackTrace; + private String getTrace(TestResult testInfo) { + if (testInfo.mStackTrace != null) { + return testInfo.mStackTrace; } else { Log.e(LOG_TAG, "Could not find stack trace for failed test "); @@ -303,7 +345,7 @@ public class InstrumentationResultParser extends MultiLineReceiver { } /** - * Parses out and store the elapsed time + * Parses out and store the elapsed time. */ private void parseTime(String line, int startPos) { String timeString = line.substring(startPos); diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java index 5de632e73..4edbbbbd3 100644 --- a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java @@ -23,7 +23,7 @@ import com.android.ddmlib.Log; import java.io.IOException; /** - * Runs a Android test command remotely and reports results + * Runs a Android test command remotely and reports results. */ public class RemoteAndroidTestRunner { @@ -43,11 +43,12 @@ public class RemoteAndroidTestRunner { "android.test.InstrumentationTestRunner"; /** - * Creates a remote android test runner. - * @param packageName - the Android application package that contains the tests to run - * @param runnerName - the instrumentation test runner to execute. If null, will use default + * Creates a remote Android test runner. + * + * @param packageName the Android application package that contains the tests to run + * @param runnerName the instrumentation test runner to execute. If null, will use default * runner - * @param remoteDevice - the Android device to execute tests on + * @param remoteDevice the Android device to execute tests on */ public RemoteAndroidTestRunner(String packageName, String runnerName, @@ -62,9 +63,10 @@ public class RemoteAndroidTestRunner { } /** - * Alternate constructor. Uses default instrumentation runner - * @param packageName - the Android application package that contains the tests to run - * @param remoteDevice - the Android device to execute tests on + * Alternate constructor. Uses default instrumentation runner. + * + * @param packageName the Android application package that contains the tests to run + * @param remoteDevice the Android device to execute tests on */ public RemoteAndroidTestRunner(String packageName, IDevice remoteDevice) { @@ -72,14 +74,14 @@ public class RemoteAndroidTestRunner { } /** - * Returns the application package name + * Returns the application package name. */ public String getPackageName() { return mPackageName; } /** - * Returns the runnerName + * Returns the runnerName. */ public String getRunnerName() { if (mRunnerName == null) { @@ -89,7 +91,7 @@ public class RemoteAndroidTestRunner { } /** - * Returns the complete instrumentation component path + * Returns the complete instrumentation component path. */ private String getRunnerPath() { return getPackageName() + RUNNER_SEPARATOR + getRunnerName(); @@ -97,8 +99,9 @@ public class RemoteAndroidTestRunner { /** * Sets to run only tests in this class - * Must be called before 'run' - * @param className - fully qualified class name (eg x.y.z) + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) */ public void setClassName(String className) { mClassArg = className; @@ -106,10 +109,12 @@ public class RemoteAndroidTestRunner { /** * Sets to run only tests in the provided classes - * Must be called before 'run' + * Must be called before 'run'. + *

* If providing more than one class, requires a InstrumentationTestRunner that supports - * the multiple class argument syntax - * @param classNames - array of fully qualified class name (eg x.y.z) + * the multiple class argument syntax. + * + * @param classNames array of fully qualified class names (eg x.y.z) */ public void setClassNames(String[] classNames) { StringBuilder classArgBuilder = new StringBuilder(); @@ -125,9 +130,10 @@ public class RemoteAndroidTestRunner { /** * Sets to run only specified test method - * Must be called before 'run' - * @param className - fully qualified class name (eg x.y.z) - * @param testName - method name + * Must be called before 'run'. + * + * @param className fully qualified class name (eg x.y.z) + * @param testName method name */ public void setMethodName(String className, String testName) { mClassArg = className + METHOD_SEPARATOR + testName; @@ -135,8 +141,9 @@ public class RemoteAndroidTestRunner { /** * Sets extra arguments to include in instrumentation command. - * Must be called before 'run' - * @param instrumentationArgs - must not be null + * Must be called before 'run'. + * + * @param instrumentationArgs must not be null */ public void setExtraArgs(String instrumentationArgs) { if (instrumentationArgs == null) { @@ -146,23 +153,23 @@ public class RemoteAndroidTestRunner { } /** - * Returns the extra instrumentation arguments + * Returns the extra instrumentation arguments. */ public String getExtraArgs() { return mExtraArgs; } /** - * Sets this test run to log only mode - skips test execution + * Sets this test run to log only mode - skips test execution. */ public void setLogOnly(boolean logOnly) { mLogOnlyMode = logOnly; } /** - * Execute this test run + * Execute this test run. * - * @param listener - listener to report results to + * @param listener listens for test results */ public void run(ITestRunListener listener) { final String runCaseCommandStr = "am instrument -w -r " @@ -179,7 +186,7 @@ public class RemoteAndroidTestRunner { } /** - * Requests cancellation of this test run + * Requests cancellation of this test run. */ public void cancel() { if (mParser != null) { @@ -188,7 +195,7 @@ public class RemoteAndroidTestRunner { } /** - * Returns the test class argument + * Returns the test class argument. */ private String getClassArg() { return mClassArg; @@ -196,7 +203,7 @@ public class RemoteAndroidTestRunner { /** * Returns the full instrumentation command which specifies the test classes to execute. - * Returns an empty string if no classes were specified + * Returns an empty string if no classes were specified. */ private String getClassCmd() { String classArg = getClassArg(); @@ -208,7 +215,7 @@ public class RemoteAndroidTestRunner { /** * Returns the full command to enable log only mode - if specified. Otherwise returns an - * empty string + * empty string. */ private String getLogCmd() { if (mLogOnlyMode) { diff --git a/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java new file mode 100644 index 000000000..4d3b1080b --- /dev/null +++ b/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 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.ddmlib.testrunner; + +/** + * Identifies a parsed instrumentation test + */ +public class TestIdentifier { + + private final String mClassName; + private final String mTestName; + + /** + * Creates a test identifier + * + * @param className fully qualified class name of the test. Cannot be null. + * @param testName name of the test. Cannot be null. + */ + public TestIdentifier(String className, String testName) { + if (className == null || testName == null) { + throw new IllegalArgumentException("className and testName must " + + "be non-null"); + } + mClassName = className; + mTestName = testName; + } + + /** + * Returns the fully qualified class name of the test + */ + public String getClassName() { + return mClassName; + } + + /** + * Returns the name of the test + */ + public String getTestName() { + return mTestName; + } + + /** + * Tests equality by comparing class and method name + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof TestIdentifier)) { + return false; + } + TestIdentifier otherTest = (TestIdentifier)other; + return getClassName().equals(otherTest.getClassName()) && + getTestName().equals(otherTest.getTestName()); + } + + /** + * Generates hashCode based on class and method name. + */ + @Override + public int hashCode() { + return getClassName().hashCode() * 31 + getTestName().hashCode(); + } +} diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java index 67f61987d..77d10c1d1 100644 --- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java +++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java @@ -20,7 +20,7 @@ import junit.framework.TestCase; /** - * Tests InstrumentationResultParser + * Tests InstrumentationResultParser. */ public class InstrumentationResultParserTest extends TestCase { @@ -51,7 +51,7 @@ public class InstrumentationResultParserTest extends TestCase { /** * Tests that the test run started and test start events is sent on first - * bundle received + * bundle received. */ public void testTestStarted() { StringBuilder output = buildCommonResult(); @@ -63,7 +63,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * Tests that a single successful test execution + * Tests that a single successful test execution. */ public void testTestSuccess() { StringBuilder output = buildCommonResult(); @@ -74,11 +74,11 @@ public class InstrumentationResultParserTest extends TestCase { injectTestString(output.toString()); assertCommonAttributes(); assertEquals(1, mTestResult.mNumTestsRun); - assertEquals(0, mTestResult.mTestStatus); + assertEquals(null, mTestResult.mTestStatus); } /** - * Test basic parsing of failed test case + * Test basic parsing of failed test case. */ public void testTestFailed() { StringBuilder output = buildCommonResult(); @@ -91,12 +91,12 @@ public class InstrumentationResultParserTest extends TestCase { assertCommonAttributes(); assertEquals(1, mTestResult.mNumTestsRun); - assertEquals(ITestRunListener.STATUS_FAILURE, mTestResult.mTestStatus); + assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus); assertEquals(STACK_TRACE, mTestResult.mTrace); } /** - * Test basic parsing and conversion of time from output + * Test basic parsing and conversion of time from output. */ public void testTimeParsing() { final String timeString = "Time: 4.9"; @@ -105,7 +105,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * builds a common test result using TEST_NAME and TEST_CLASS + * builds a common test result using TEST_NAME and TEST_CLASS. */ private StringBuilder buildCommonResult() { StringBuilder output = new StringBuilder(); @@ -118,7 +118,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * Adds common status results to the provided output + * Adds common status results to the provided output. */ private void addCommonStatus(StringBuilder output) { addStatusKey(output, "stream", "\r\n" + CLASS_NAME); @@ -130,7 +130,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * Adds a stack trace status bundle to output + * Adds a stack trace status bundle to output. */ private void addStackTrace(StringBuilder output) { addStatusKey(output, "stack", STACK_TRACE); @@ -138,7 +138,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * Helper method to add a status key-value bundle + * Helper method to add a status key-value bundle. */ private void addStatusKey(StringBuilder outputBuilder, String key, String value) { @@ -168,7 +168,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * inject a test string into the result parser + * inject a test string into the result parser. * * @param result */ @@ -185,7 +185,7 @@ public class InstrumentationResultParserTest extends TestCase { } /** - * A specialized test listener that stores a single test events + * A specialized test listener that stores a single test events. */ private class VerifyingTestResult implements ITestRunListener { @@ -194,29 +194,28 @@ public class InstrumentationResultParserTest extends TestCase { int mNumTestsRun; String mTestName; long mTestTime; - int mTestStatus; + TestFailure mTestStatus; String mTrace; boolean mStopped; VerifyingTestResult() { mNumTestsRun = 0; - mTestStatus = 0; + mTestStatus = null; mStopped = false; } - public void testEnded(String className, String testName) { + public void testEnded(TestIdentifier test) { mNumTestsRun++; - assertEquals("Unexpected class name", mSuiteName, className); - assertEquals("Unexpected test ended", mTestName, testName); + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); } - public void testFailed(int status, String className, String testName, - String trace) { + public void testFailed(TestFailure status, TestIdentifier test, String trace) { mTestStatus = status; mTrace = trace; - assertEquals("Unexpected class name", mSuiteName, className); - assertEquals("Unexpected test ended", mTestName, testName); + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); } public void testRunEnded(long elapsedTime) { @@ -233,9 +232,9 @@ public class InstrumentationResultParserTest extends TestCase { mStopped = true; } - public void testStarted(String className, String testName) { - mSuiteName = className; - mTestName = testName; + public void testStarted(TestIdentifier test) { + mSuiteName = test.getClassName(); + mTestName = test.getTestName(); } public void testRunFailed(String errorMessage) { diff --git a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java index 556fc9b8e..9acaaf954 100644 --- a/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java +++ b/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java @@ -31,16 +31,16 @@ import java.io.IOException; import java.util.Map; /** - * Test RemoteAndroidTestRunner. + * Tests RemoteAndroidTestRunner. */ public class RemoteAndroidTestRunnerTest extends TestCase { private RemoteAndroidTestRunner mRunner; private MockDevice mMockDevice; - + private static final String TEST_PACKAGE = "com.test"; private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner"; - + /** * @see junit.framework.TestCase#setUp() */ @@ -49,40 +49,40 @@ public class RemoteAndroidTestRunnerTest extends TestCase { mMockDevice = new MockDevice(); mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice); } - + /** - * Test the basic case building of the instrumentation runner command with no arguments + * Test the basic case building of the instrumentation runner command with no arguments. */ public void testRun() { mRunner.run(new EmptyListener()); - assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER), + assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); } /** - * Test the building of the instrumentation runner command with log set + * Test the building of the instrumentation runner command with log set. */ public void testRunWithLog() { mRunner.setLogOnly(true); mRunner.run(new EmptyListener()); - assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE, + assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); } /** - * Test the building of the instrumentation runner command with method set + * Test the building of the instrumentation runner command with method set. */ public void testRunWithMethod() { final String className = "FooTest"; final String testName = "fooTest"; mRunner.setMethodName(className, testName); mRunner.run(new EmptyListener()); - assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className, + assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className, testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); } - + /** - * Test the building of the instrumentation runner command with extra args set + * Test the building of the instrumentation runner command with extra args set. */ public void testRunWithExtraArgs() { final String extraArgs = "blah"; @@ -94,37 +94,37 @@ public class RemoteAndroidTestRunnerTest extends TestCase { /** - * Assert two strings are equal ignoring whitespace + * Assert two strings are equal ignoring whitespace. */ private void assertStringsEquals(String str1, String str2) { String strippedStr1 = str1.replaceAll(" ", ""); String strippedStr2 = str2.replaceAll(" ", ""); assertEquals(strippedStr1, strippedStr2); } - + /** - * A dummy device that does nothing except store the provided executed shell command for - * later retrieval + * A dummy device that does nothing except store the provided executed shell command for + * later retrieval. */ private static class MockDevice implements IDevice { private String mLastShellCommand; - + /** - * Stores the provided command for later retrieval from getLastShellCommand + * Stores the provided command for later retrieval from getLastShellCommand. */ public void executeShellCommand(String command, IShellOutputReceiver receiver) throws IOException { mLastShellCommand = command; } - + /** - * Get the last command provided to executeShellCommand + * Get the last command provided to executeShellCommand. */ public String getLastShellCommand() { return mLastShellCommand; } - + public boolean createForward(int localPort, int remotePort) { throw new UnsupportedOperationException(); } @@ -201,22 +201,26 @@ public class RemoteAndroidTestRunnerTest extends TestCase { throw new UnsupportedOperationException(); } - public String getVmName() { + public void runLogService(String logname, LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public String getAvdName() { return ""; } } - - /** An empty implementation of TestRunListener + + /** + * An empty implementation of ITestRunListener. */ private static class EmptyListener implements ITestRunListener { - public void testEnded(String className, String testName) { + public void testEnded(TestIdentifier test) { // ignore } - public void testFailed(int status, String className, String testName, - String trace) { + public void testFailed(TestFailure status, TestIdentifier test, String trace) { // ignore } @@ -236,9 +240,9 @@ public class RemoteAndroidTestRunnerTest extends TestCase { // ignore } - public void testStarted(String className, String testName) { + public void testStarted(TestIdentifier test) { // ignore } - + } } diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java index 1331a097c..81b757e75 100644 --- a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/DevicePanel.java @@ -200,22 +200,30 @@ public final class DevicePanel extends Panel implements IDebugBridgeChangeListen case DEVICE_COL_STATE: return getStateString(device); case DEVICE_COL_BUILD: { - String vmName = device.getVmName(); - String debuggable = device.getProperty(Device.PROP_DEBUGGABLE); String version = device.getProperty(Device.PROP_BUILD_VERSION); - if (device.isEmulator()) { - if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ - return String.format("%1$s [%2$s, debug]", vmName, //$NON-NLS-1$ - version); + if (version != null) { + String debuggable = device.getProperty(Device.PROP_DEBUGGABLE); + if (device.isEmulator()) { + String avdName = device.getAvdName(); + if (avdName == null) { + avdName = "?"; // the device is probably not online yet, so + // we don't know its AVD name just yet. + } + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s [%2$s, debug]", avdName, + version); + } else { + return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$ + } } else { - return String.format("%1$s [%2$s]", vmName, version); //$NON-NLS-1$ + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return String.format("%1$s, debug", version); + } else { + return String.format("%1$s", version); //$NON-NLS-1$ + } } } else { - if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ - return String.format("%1$s, debug", version); //$NON-NLS-1$ - } else { - return String.format("%1$s", version); //$NON-NLS-1$ - } + return "unknown"; } } } diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java index 149d689f0..46461bf32 100644 --- a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/NativeHeapPanel.java @@ -237,7 +237,7 @@ public final class NativeHeapPanel extends BaseHeapPanel { */ private HashMap mSourceCache = new HashMap(); - private int mTotalSize; + private long mTotalSize; private Button mSaveButton; private Button mSymbolsButton; diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 000000000..473387aa2 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayFilteredLog.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; + +import java.util.ArrayList; + +public class DisplayFilteredLog extends DisplayLog { + + public DisplayFilteredLog(String name) { + super(name); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_FILTERED_LOG; + } +} diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 000000000..0cffd7e07 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayGraph.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2008 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYAreaRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class DisplayGraph extends EventDisplay { + + public DisplayGraph(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + Collection datasets = mValueTypeDataSetMap.values(); + for (TimeSeriesCollection dataset : datasets) { + dataset.removeAllSeries(); + } + if (mOccurrenceDataSet != null) { + mOccurrenceDataSet.removeAllSeries(); + } + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + String title = getChartTitle(logParser); + return createCompositeChart(parent, logParser, title); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined + * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from + * the two lists. + *

This method is only called when at least one of the descriptor list is non empty. + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + private void updateChart(EventContainer event, EventLogParser logParser, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + Map tagMap = logParser.getTagMap(); + + Millisecond millisecondTime = null; + long msec = -1; + + // If the event container is a cpu container (tag == 2721), and there is no descriptor + // for the total CPU load, then we do accumulate all the values. + boolean accumulateValues = false; + double accumulatedValue = 0; + + if (event.mTag == 2721) { + accumulateValues = true; + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + accumulateValues &= (descriptor.valueIndex != 0); + } + } + + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mValueDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it + if (timeSeries == null) { + // get the series name + String seriesFullName = null; + String seriesLabel = getSeriesLabel(event, descriptor); + + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + seriesFullName = String.format("%1$s / %2$s", seriesLabel, + descriptor.valueName); + break; + case EVENT_CHECK_SAME_VALUE: + seriesFullName = String.format("%1$s", seriesLabel); + break; + default: + seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, + tagMap.get(descriptor.eventTag), + descriptor.valueName); + break; + } + + // get the data set for this ValueType + TimeSeriesCollection dataset = getValueDataset( + logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] + .getValueType(), + accumulateValues); + + // create the series + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); + } + + dataset.addSeries(timeSeries); + + // add it to the map. + map.put(event.pid, timeSeries); + } + + // update the timeSeries. + + // get the value from the event + double value = event.getValueAsDouble(descriptor.valueIndex); + + // accumulate the values if needed. + if (accumulateValues) { + accumulatedValue += value; + value = accumulatedValue; + } + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, value); + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mOcurrenceDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it. + if (timeSeries == null) { + String seriesLabel = getSeriesLabel(event, descriptor); + + String seriesFullName = String.format("[%1$s:%2$s]", + tagMap.get(descriptor.eventTag), seriesLabel); + + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge); + } + + getOccurrenceDataSet().addSeries(timeSeries); + + map.put(event.pid, timeSeries); + } + + // update the series + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + // go through all the series and remove old values. + if (msec != -1 && mMaximumChartItemAge != -1) { + Collection> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + } + } + + /** + * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. + * If the data set is not yet created, it is first allocated and set up into the + * {@link org.jfree.chart.JFreeChart} object. + * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. + * @param accumulateValues + */ + private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { + TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); + if (dataset == null) { + // create the data set and store it in the map + dataset = new TimeSeriesCollection(); + mValueTypeDataSetMap.put(type, dataset); + + // create the renderer and configure it depending on the ValueType + AbstractXYItemRenderer renderer; + if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { + renderer = new XYAreaRenderer(); + } else { + XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); + r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); + + renderer = r; + } + + // set both the dataset and the renderer in the plot object. + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, dataset); + xyPlot.setRenderer(mDataSetCount, renderer); + + // put a new axis label, and configure it. + NumberAxis axis = new NumberAxis(type.toString()); + + if (type == EventValueDescription.ValueType.PERCENT) { + // force percent range to be (0,100) fixed. + axis.setAutoRange(false); + axis.setRange(0., 100.); + } + + // for the index, we ignore the occurrence dataset + int count = mDataSetCount; + if (mOccurrenceDataSet != null) { + count--; + } + + xyPlot.setRangeAxis(count, axis); + if ((count % 2) == 0) { + xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); + } else { + xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); + } + + // now we link the dataset and the axis + xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); + + mDataSetCount++; + } + + return dataset; + } + + /** + * Return the series label for this event. This only contains the pid information. + * @param event the {@link EventContainer} + * @param descriptor the {@link OccurrenceDisplayDescriptor} + * @return the series label. + * @throws InvalidTypeException + */ + private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) + throws InvalidTypeException { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid == false) { + return event.getValueAsString(descriptor.seriesValueIndex); + } else { + return String.format("%1$s (%2$d)", + event.getValueAsString(descriptor.seriesValueIndex), event.pid); + } + } + + return Integer.toString(event.pid); + } + + /** + * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not + * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. + */ + private TimeSeriesCollection getOccurrenceDataSet() { + if (mOccurrenceDataSet == null) { + mOccurrenceDataSet = new TimeSeriesCollection(); + + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); + + OccurrenceRenderer renderer = new OccurrenceRenderer(); + renderer.setBaseShapesVisible(false); + xyPlot.setRenderer(mDataSetCount, renderer); + + mDataSetCount++; + } + + return mOccurrenceDataSet; + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_GRAPH; + } + + /** + * Sets the current {@link EventLogParser} object. + */ + @Override + protected void setNewLogParser(EventLogParser logParser) { + if (mChart != null) { + mChart.setTitle(getChartTitle(logParser)); + } + } + /** + * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. + * + * @param logParser the logParser. + * @return the chart title. + */ + private String getChartTitle(EventLogParser logParser) { + if (mValueDescriptors.size() > 0) { + String chartDesc = null; + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + if (logParser != null) { + chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); + } + break; + case EVENT_CHECK_SAME_VALUE: + if (logParser != null) { + chartDesc = String.format("%1$s / %2$s", + logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), + mValueDescriptors.get(0).valueName); + } + break; + } + + if (chartDesc != null) { + return String.format("%1$s - %2$s", mName, chartDesc); + } + } + + return mName; + } +} \ No newline at end of file diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 000000000..26296f31c --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2008 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +import com.android.ddmlib.log.InvalidTypeException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.TableHelper; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.ArrayList; +import java.util.Calendar; + +public class DisplayLog extends EventDisplay { + public DisplayLog(String name) { + super(name); + } + + private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ + private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ + private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ + private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLogTable.removeAll(); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + addToLog(event, logParser); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) { + return createLogUI(parent, listener); + } + + /** + * Adds an {@link EventContainer} to the log. + * + * @param event the event. + * @param logParser the log parser. + */ + private void addToLog(EventContainer event, EventLogParser logParser) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + // get the value description + EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); + if (valueDescription != null) { + for (int i = 0; i < valueDescription.length; i++) { + EventValueDescription description = valueDescription[i]; + try { + String value = event.getValueAsString(i); + + logValue(date, pidName, eventName, description.getName(), value, + description.getEventValueType(), description.getValueType()); + } catch (InvalidTypeException e) { + logValue(date, pidName, eventName, description.getName(), e.getMessage(), + description.getEventValueType(), description.getValueType()); + } + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + } + + /** + * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by + * the list of descriptors. If an event is configured to be displayed by value and occurrence, + * only the values are displayed (as they mark an event occurrence anyway). + *

This method is only called when at least one of the descriptor list is non empty. + * + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + protected void addToLog(EventContainer event, EventLogParser logParser, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + ScrollBar bar = mLogTable.getVerticalBar(); + boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); + + // get the date. + Calendar c = Calendar.getInstance(); + long msec = (long) event.sec * 1000L; + c.setTimeInMillis(msec); + + // convert the time into a string + String date = String.format("%1$tF %1$tT", c); + + String eventName = logParser.getTagMap().get(event.mTag); + String pidName = Integer.toString(event.pid); + + if (valueDescriptors.size() > 0) { + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + logDescriptor(event, descriptor, date, pidName, eventName, logParser); + } + } else { + // we display the event. Since the StringBuilder contains the header (date, event name, + // pid) at this point, there isn't anything else to display. + } + + // scroll if needed, by showing the last item + if (scroll) { + int itemCount = mLogTable.getItemCount(); + if (itemCount > 0) { + mLogTable.showItem(mLogTable.getItem(itemCount - 1)); + } + } + } + + + /** + * Logs a value in the ui. + * + * @param date + * @param pid + * @param event + * @param valueName + * @param value + * @param eventValueType + * @param valueType + */ + private void logValue(String date, String pid, String event, String valueName, + String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) { + + TableItem item = new TableItem(mLogTable, SWT.NONE); + item.setText(0, date); + item.setText(1, pid); + item.setText(2, event); + item.setText(3, valueName); + item.setText(4, value); + + String type; + if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) { + type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); + } else { + type = eventValueType.toString(); + } + + item.setText(5, type); + } + + /** + * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. + * + * @param event the EventContainer + * @param descriptor the ValueDisplayDescriptor defining which value to display. + * @param date the date of the event in a string. + * @param pidName + * @param eventName + * @param logParser + */ + private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, + String date, String pidName, String eventName, EventLogParser logParser) { + + String value; + try { + value = event.getValueAsString(descriptor.valueIndex); + } catch (InvalidTypeException e) { + value = e.getMessage(); + } + + EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); + + EventValueDescription valueDescription = values[descriptor.valueIndex]; + + logValue(date, pidName, eventName, descriptor.valueName, value, + valueDescription.getEventValueType(), valueDescription.getValueType()); + } + + /** + * Creates the UI for a log display. + * + * @param parent the parent {@link Composite} + * @param listener the {@link ILogColumnListener} to notify on column resize events. + * @return the top Composite of the UI. + */ + private Control createLogUI(Composite parent, final ILogColumnListener listener) { + Composite mainComp = new Composite(parent, SWT.NONE); + GridLayout gl; + mainComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mainComp.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mLogTable = null; + } + }); + + Label l = new Label(mainComp, SWT.CENTER); + l.setText(mName); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | + SWT.BORDER); + mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + TableColumn col = TableHelper.createTableColumn( + mLogTable, "Time", + SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(0, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "pid", + SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(1, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Event", + SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(2, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Name", + SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(3, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Value", + SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(4, (TableColumn) source); + } + } + }); + + col = TableHelper.createTableColumn( + mLogTable, "Type", + SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ + col.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Object source = e.getSource(); + if (source instanceof TableColumn) { + listener.columnResized(5, (TableColumn) source); + } + } + }); + + mLogTable.setHeaderVisible(true); + mLogTable.setLinesVisible(true); + + return mainComp; + } + + /** + * Resizes the index-th column of the log {@link Table} (if applicable). + *

+ * This does nothing if the Table object is null (because the display + * type does not use a column) or if the index-th column is in fact the originating + * column passed as argument. + * + * @param index the index of the column to resize + * @param sourceColumn the original column that was resize, and on which we need to sync the + * index-th column width. + */ + @Override + void resizeColumn(int index, TableColumn sourceColumn) { + if (mLogTable != null) { + TableColumn col = mLogTable.getColumn(index); + if (col != sourceColumn) { + col.setWidth(sourceColumn.getWidth()); + } + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_LOG_ALL; + } +} diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 000000000..82cc7a440 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2008 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.time.FixedMillisecond; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.util.ShapeUtilities; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class DisplaySync extends SyncCommon { + + // Information to graph for each authority + private TimePeriodValues mDatasetsSync[]; + private List mTooltipsSync[]; + private CustomXYToolTipGenerator mTooltipGenerators[]; + private TimeSeries mDatasetsSyncTickle[]; + + // Dataset of error events to graph + private TimeSeries mDatasetError; + + public DisplaySync(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Status"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + XYBarRenderer br = new XYBarRenderer(); + mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; + mTooltipsSync = new List[NUM_AUTHS]; + mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(0, br); + + XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); + ls.setBaseLinesVisible(false); + mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; + TimeSeriesCollection tsc = new TimeSeriesCollection(); + xyPlot.setDataset(1, tsc); + xyPlot.setRenderer(1, ls); + + mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); + xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); + XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); + errls.setBaseLinesVisible(false); + errls.setSeriesPaint(0, Color.RED); + xyPlot.setRenderer(2, errls); + + for (int i = 0; i < NUM_AUTHS; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + ls.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSync[i]); + mTooltipsSync[i] = new ArrayList(); + mTooltipGenerators[i] = new CustomXYToolTipGenerator(); + br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); + mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); + + mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", + FixedMillisecond.class); + tsc.addSeries(mDatasetsSyncTickle[i]); + ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_TICKLE) { + int auth = getAuth(event.getValueAsString(0)); + if (auth >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Generate the height for an event. + * Height is somewhat arbitrarily the count of "things" that happened + * during the sync. + * When network traffic measurements are available, code should be modified + * to use that instead. + * @param details The details string associated with the event + * @return The height in arbirary units (0-100) + */ + private int getHeightFromDetails(String details) { + if (details == null) { + return 1; // Arbitrary + } + int total = 0; + String parts[] = details.split("[a-zA-Z]"); + for (String part : parts) { + if ("".equals(part)) continue; + total += Integer.parseInt(part); + } + if (total == 0) { + total = 1; + } + return total; + } + + /** + * Generates the tooltips text for an event. + * This method decodes the cryptic details string. + * @param auth The authority associated with the event + * @param details The details string + * @param eventSource server, poll, etc. + * @return The text to display in the tooltips + */ + private String getTextFromDetails(int auth, String details, int eventSource) { + + StringBuffer sb = new StringBuffer(); + sb.append(AUTH_NAMES[auth]).append(": \n"); + + Scanner scanner = new Scanner(details); + Pattern charPat = Pattern.compile("[a-zA-Z]"); + Pattern numPat = Pattern.compile("[0-9]+"); + while (scanner.hasNext()) { + String key = scanner.findInLine(charPat); + int val = Integer.parseInt(scanner.findInLine(numPat)); + if (auth == GMAIL && "M".equals(key)) { + sb.append("messages from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "L".equals(key)) { + sb.append("labels from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "C".equals(key)) { + sb.append("check conversation requests from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "A".equals(key)) { + sb.append("attachments from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "U".equals(key)) { + sb.append("op updates from server: ").append(val).append("\n"); + } else if (auth == GMAIL && "u".equals(key)) { + sb.append("op updates to server: ").append(val).append("\n"); + } else if (auth == GMAIL && "S".equals(key)) { + sb.append("send/receive cycles: ").append(val).append("\n"); + } else if ("Q".equals(key)) { + sb.append("queries to server: ").append(val).append("\n"); + } else if ("E".equals(key)) { + sb.append("entries from server: ").append(val).append("\n"); + } else if ("u".equals(key)) { + sb.append("updates from client: ").append(val).append("\n"); + } else if ("i".equals(key)) { + sb.append("inserts from client: ").append(val).append("\n"); + } else if ("d".equals(key)) { + sb.append("deletes from client: ").append(val).append("\n"); + } else if ("f".equals(key)) { + sb.append("full sync requested\n"); + } else if ("r".equals(key)) { + sb.append("partial sync unavailable\n"); + } else if ("X".equals(key)) { + sb.append("hard error\n"); + } else if ("e".equals(key)) { + sb.append("number of parse exceptions: ").append(val).append("\n"); + } else if ("c".equals(key)) { + sb.append("number of conflicts: ").append(val).append("\n"); + } else if ("a".equals(key)) { + sb.append("number of auth exceptions: ").append(val).append("\n"); + } else if ("D".equals(key)) { + sb.append("too many deletions\n"); + } else if ("R".equals(key)) { + sb.append("too many retries: ").append(val).append("\n"); + } else if ("b".equals(key)) { + sb.append("database error\n"); + } else if ("x".equals(key)) { + sb.append("soft error\n"); + } else if ("l".equals(key)) { + sb.append("sync already in progress\n"); + } else if ("I".equals(key)) { + sb.append("io exception\n"); + } else if (auth == CONTACTS && "p".equals(key)) { + sb.append("photos uploaded from client: ").append(val).append("\n"); + } else if (auth == CONTACTS && "P".equals(key)) { + sb.append("photos downloaded from server: ").append(val).append("\n"); + } else if (auth == CALENDAR && "F".equals(key)) { + sb.append("server refresh\n"); + } else if (auth == CALENDAR && "s".equals(key)) { + sb.append("server diffs fetched\n"); + } else { + sb.append(key).append("=").append(val); + } + } + if (eventSource == 0) { + sb.append("(server)"); + } else if (eventSource == 1) { + sb.append("(local)"); + } else if (eventSource == 2) { + sb.append("(poll)"); + } else if (eventSource == 3) { + sb.append("(user)"); + } + return sb.toString(); + } + + + /** + * Callback to process a sync event. + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (!newEvent) { + // Details arrived for a previous sync event + // Remove event before reinserting. + int lastItem = mDatasetsSync[auth].getItemCount(); + mDatasetsSync[auth].delete(lastItem-1, lastItem-1); + mTooltipsSync[auth].remove(lastItem-1); + } + double height = getHeightFromDetails(details); + height = height / (stopTime - startTime + 1) * 10000; + if (height > 30) { + height = 30; + } + mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height); + mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource)); + mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC; + } +} \ No newline at end of file diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 000000000..36d90ce61 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.RegularTimePeriod; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class DisplaySyncHistogram extends SyncCommon { + + Map mTimePeriodMap[]; + + // Information to graph for each authority + private TimePeriodValues mDatasetsSyncHist[]; + + public DisplaySyncHistogram(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Histogram"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + + AbstractXYItemRenderer br = new XYBarRenderer(); + mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; + mTimePeriodMap = new HashMap[NUM_AUTHS + 1]; + + TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_AUTHS + 1; i++) { + br.setSeriesPaint(i, AUTH_COLORS[i]); + mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); + tpvc.addSeries(mDatasetsSyncHist[i]); + mTimePeriodMap[i] = new HashMap(); + + } + } + + /** + * Callback to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + auth = ERRORS; + } + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, delta); + } else { + // sync_details arrived for an event that has already been graphed. + if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { + // Item turns out to be in error, so transfer time from old auth to error. + double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour + addHistEvent(0, auth, -delta); + addHistEvent(0, ERRORS, delta); + } + } + } + + /** + * Helper to add an event to the data series. + * Also updates error series if appropriate (x or X in details). + * @param stopTime Time event ends + * @param auth Sync authority + * @param value Value to graph for event + */ + private void addHistEvent(long stopTime, int auth, double value) { + SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth); + + // Loop over all datasets to do the stacking. + for (int i = auth; i <= ERRORS; i++) { + addToPeriod(mDatasetsSyncHist, i, hour, value); + } + } + + private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, + double value) { + int index; + if (mTimePeriodMap[auth].containsKey(period)) { + index = mTimePeriodMap[auth].get(period); + double oldValue = tpv[auth].getValue(index).doubleValue(); + tpv[auth].update(index, oldValue + value); + } else { + index = tpv[auth].getItemCount(); + mTimePeriodMap[auth].put(period, index); + tpv[auth].add(period, value); + } + } + + /** + * Creates a multiple-hour time period for the histogram. + * @param time Time in milliseconds. + * @param numHoursWide: should divide into a day. + * @return SimpleTimePeriod covering the number of hours and containing time. + */ + private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { + Date date = new Date(time); + TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; + Calendar calendar = Calendar.getInstance(zone); + calendar.setTime(date); + long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + + calendar.get(Calendar.DAY_OF_YEAR) * 24; + int year = calendar.get(Calendar.YEAR); + hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; + calendar.clear(); + calendar.set(year, 0, 1, 0, 0); // Jan 1 + long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; + return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_HIST; + } +} diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 000000000..9ce704550 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,219 @@ +/* + * 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYBarRenderer; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.time.TimePeriodValues; +import org.jfree.data.time.TimePeriodValuesCollection; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +public class DisplaySyncPerf extends SyncCommon { + + CustomXYToolTipGenerator mTooltipGenerator; + List mTooltips[]; + + // The series number for each graphed item. + // sync authorities are 0-3 + private static final int DB_QUERY = 4; + private static final int DB_WRITE = 5; + private static final int HTTP_NETWORK = 6; + private static final int HTTP_PROCESSING = 7; + private static final int NUM_SERIES = (HTTP_PROCESSING + 1); + private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "DB Query", "DB Write", "HTTP Response", "HTTP Processing",}; + private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY}; + private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2}; + + // Values from data/etc/event-log-tags + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_HTTP_STATS = 52001; + // op types for EVENT_DB_OPERATION + final int EVENT_DB_QUERY = 0; + final int EVENT_DB_WRITE = 1; + + // Information to graph for each authority + private TimePeriodValues mDatasets[]; + + /** + * TimePeriodValuesCollection that supports Y intervals. This allows the + * creation of "floating" bars, rather than bars rooted to the axis. + */ + class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection { + /** default serial UID */ + private static final long serialVersionUID = 1L; + + private double yheight; + + /** + * Constructs a collection of bars with a fixed Y height. + * + * @param yheight The height of the bars. + */ + YIntervalTimePeriodValuesCollection(double yheight) { + this.yheight = yheight; + } + + /** + * Returns ending Y value that is a fixed amount greater than the starting value. + * + * @param series the series (zero-based index). + * @param item the item (zero-based index). + * @return The ending Y value for the specified series and item. + */ + @Override + public Number getEndY(int series, int item) { + return getY(series, item).doubleValue() + yheight; + } + } + + /** + * Constructs a graph of network and database stats. + * + * @param name The name of this graph in the graph list. + */ + public DisplaySyncPerf(String name) { + super(name); + } + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + Control composite = createCompositeChart(parent, logParser, "Sync Performance"); + resetUI(); + return composite; + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + super.resetUI(); + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.getRangeAxis().setVisible(false); + mTooltipGenerator = new CustomXYToolTipGenerator(); + mTooltips = new List[NUM_SERIES]; + + XYBarRenderer br = new XYBarRenderer(); + br.setUseYInterval(true); + mDatasets = new TimePeriodValues[NUM_SERIES]; + + TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1); + xyPlot.setDataset(tpvc); + xyPlot.setRenderer(br); + + for (int i = 0; i < NUM_SERIES; i++) { + br.setSeriesPaint(i, SERIES_COLORS[i]); + mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]); + tpvc.addSeries(mDatasets[i]); + mTooltips[i] = new ArrayList(); + mTooltipGenerator.addToolTipSeries(mTooltips[i]); + br.setSeriesToolTipGenerator(i, mTooltipGenerator); + } + } + + /** + * Updates the display with a new event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + super.newEvent(event, logParser); // Handle sync operation + try { + if (event.mTag == EVENT_DB_OPERATION) { + // 52000 db_operation (name|3),(op_type|1|5),(time|2|3) + String tip = event.getValueAsString(0); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + int opType = Integer.parseInt(event.getValueAsString(1)); + long duration = Long.parseLong(event.getValueAsString(2)); + + if (opType == EVENT_DB_QUERY) { + mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_QUERY]); + mTooltips[DB_QUERY].add(tip); + } else if (opType == EVENT_DB_WRITE) { + mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime), + SERIES_YCOORD[DB_WRITE]); + mTooltips[DB_WRITE].add(tip); + } + } else if (event.mTag == EVENT_HTTP_STATS) { + // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2) + String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) + + ", rx: " + event.getValueAsString(4); + long endTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + long netEndTime = endTime - Long.parseLong(event.getValueAsString(2)); + long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1)); + mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime), + SERIES_YCOORD[HTTP_NETWORK]); + mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime), + SERIES_YCOORD[HTTP_PROCESSING]); + mTooltips[HTTP_NETWORK].add(tip); + mTooltips[HTTP_PROCESSING].add(tip); + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback from super.newEvent to process a sync event. + * + * @param event The sync event + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource + */ + @Override + void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource) { + if (newEvent) { + mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_SYNC_PERF; + } +} diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java index e36192c08..2223a4d70 100644 --- a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplay.java @@ -21,86 +21,47 @@ import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventContainer.CompareMethod; import com.android.ddmlib.log.EventContainer.EventValueType; import com.android.ddmlib.log.EventLogParser; -import com.android.ddmlib.log.EventValueDescription; import com.android.ddmlib.log.EventValueDescription.ValueType; import com.android.ddmlib.log.InvalidTypeException; -import com.android.ddmuilib.DdmUiPreferences; -import com.android.ddmuilib.TableHelper; - -import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; -import org.eclipse.swt.events.ControlAdapter; -import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; -import org.eclipse.swt.widgets.TableItem; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.AxisLocation; -import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.event.ChartChangeEvent; import org.jfree.chart.event.ChartChangeEventType; import org.jfree.chart.event.ChartChangeListener; -import org.jfree.chart.labels.CustomXYToolTipGenerator; import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; -import org.jfree.chart.renderer.xy.XYAreaRenderer; -import org.jfree.chart.renderer.xy.XYBarRenderer; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.TextTitle; -import org.jfree.data.time.FixedMillisecond; import org.jfree.data.time.Millisecond; -import org.jfree.data.time.RegularTimePeriod; -import org.jfree.data.time.SimpleTimePeriod; -import org.jfree.data.time.TimePeriodValues; -import org.jfree.data.time.TimePeriodValuesCollection; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.experimental.chart.swt.ChartComposite; import org.jfree.experimental.swt.SWTUtils; -import org.jfree.util.ShapeUtilities; -import java.awt.Color; import java.security.InvalidParameterException; import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Scanner; import java.util.Set; -import java.util.TimeZone; import java.util.regex.Pattern; /** * Represents a custom display of one or more events. */ -final class EventDisplay { - +abstract class EventDisplay { + private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$ private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$ private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$ private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$ - - private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ - private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ - private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ - private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ - private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ - private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ private final static String FILTER_VALUE_NULL = ""; //$NON-NLS-1$ @@ -109,20 +70,76 @@ final class EventDisplay { public final static int DISPLAY_TYPE_GRAPH = 2; public final static int DISPLAY_TYPE_SYNC = 3; public final static int DISPLAY_TYPE_SYNC_HIST = 4; + public final static int DISPLAY_TYPE_SYNC_PERF = 5; private final static int EVENT_CHECK_FAILED = 0; - private final static int EVENT_CHECK_SAME_TAG = 1; - private final static int EVENT_CHECK_SAME_VALUE = 2; - + protected final static int EVENT_CHECK_SAME_TAG = 1; + protected final static int EVENT_CHECK_SAME_VALUE = 2; + + /** + * Creates the appropriate EventDisplay subclass. + * + * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc) + * @param name the name of the display + * @return the created object + */ + public static EventDisplay eventDisplayFactory(int type, String name) { + switch (type) { + case DISPLAY_TYPE_LOG_ALL: + return new DisplayLog(name); + case DISPLAY_TYPE_FILTERED_LOG: + return new DisplayFilteredLog(name); + case DISPLAY_TYPE_SYNC: + return new DisplaySync(name); + case DISPLAY_TYPE_SYNC_HIST: + return new DisplaySyncHistogram(name); + case DISPLAY_TYPE_GRAPH: + return new DisplayGraph(name); + case DISPLAY_TYPE_SYNC_PERF: + return new DisplaySyncPerf(name); + default: + throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$ + } + } + + /** + * Adds event to the display. + * @param event The event + * @param logParser The log parser. + */ + abstract void newEvent(EventContainer event, EventLogParser logParser); + + /** + * Resets the display. + */ + abstract void resetUI(); + + /** + * Gets display type + * + * @return display type as an integer + */ + abstract int getDisplayType(); + + /** + * Creates the UI for the event display. + * + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + abstract Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener); + interface ILogColumnListener { void columnResized(int index, TableColumn sourceColumn); } /** - * Describes an event to be displayed. + * Describes an event to be displayed. */ static class OccurrenceDisplayDescriptor { - + int eventTag = -1; int seriesValueIndex = -1; boolean includePid = false; @@ -158,17 +175,19 @@ final class EventDisplay { /** * Loads the descriptor parameter from a storage string. The storage string must have * been generated with {@link #getStorageString()}. + * * @param storageString the storage string */ final void loadFrom(String storageString) { String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR)); loadFrom(values, 0); } - + /** * Loads the parameters from an array of strings. + * * @param storageStrings the strings representing each parameter. - * @param index the starting index in the array of strings. + * @param index the starting index in the array of strings. * @return the new index in the array. */ protected int loadFrom(String[] storageStrings, int index) { @@ -221,7 +240,7 @@ final class EventDisplay { } /** - * Describes an event value to be displayed. + * Describes an event value to be displayed. */ static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor { String valueName; @@ -253,7 +272,7 @@ final class EventDisplay { void replaceWith(OccurrenceDisplayDescriptor descriptor) { super.replaceWith(descriptor); if (descriptor instanceof ValueDisplayDescriptor) { - ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)descriptor; + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor; valueName = valueDescriptor.valueName; valueIndex = valueDescriptor.valueIndex; } @@ -261,8 +280,9 @@ final class EventDisplay { /** * Loads the parameters from an array of strings. + * * @param storageStrings the strings representing each parameter. - * @param index the starting index in the array of strings. + * @param index the starting index in the array of strings. * @return the new index in the array. */ @Override @@ -294,100 +314,78 @@ final class EventDisplay { /* ================== * Event Display parameters. * ================== */ - private String mName; - - private int mDisplayType = DISPLAY_TYPE_GRAPH; + protected String mName; + private boolean mPidFiltering = false; private ArrayList mPidFilterList = null; - - private final ArrayList mValueDescriptors = - new ArrayList(); + + protected final ArrayList mValueDescriptors = + new ArrayList(); private final ArrayList mOccurrenceDescriptors = - new ArrayList(); + new ArrayList(); /* ================== * Event Display members for display purpose. * ================== */ // chart objects - /** This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) */ - private final HashMap> mValueDescriptorSeriesMap = - new HashMap>(); - /** This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) */ - private final HashMap> mOcurrenceDescriptorSeriesMap = - new HashMap>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap> mValueDescriptorSeriesMap = + new HashMap>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap> mOcurrenceDescriptorSeriesMap = + new HashMap>(); - /** This is a map of (ValueType, dataset) */ - private final HashMap mValueTypeDataSetMap = - new HashMap(); + /** + * This is a map of (ValueType, dataset) + */ + protected final HashMap mValueTypeDataSetMap = + new HashMap(); - private JFreeChart mChart; - private TimeSeriesCollection mOccurrenceDataSet; - private int mDataSetCount; + protected JFreeChart mChart; + protected TimeSeriesCollection mOccurrenceDataSet; + protected int mDataSetCount; private ChartComposite mChartComposite; - private long mMaximumChartItemAge = -1; - private long mHistWidth = 1; + protected long mMaximumChartItemAge = -1; + protected long mHistWidth = 1; // log objects. - private Table mLogTable; + protected Table mLogTable; /* ================== * Misc data. * ================== */ - private int mValueDescriptorCheck = EVENT_CHECK_FAILED; - - /** - * Loads a new {@link EventDisplay} from a storage string. The string must have been created - * with {@link #getStorageString()}. - * @param storageString the storage string - * @return a new {@link EventDisplay} or null if the load failed. - */ - static EventDisplay load(String storageString) { - EventDisplay ed = new EventDisplay(); - if (ed.loadFrom(storageString)) { - return ed; - } - - return null; - } + protected int mValueDescriptorCheck = EVENT_CHECK_FAILED; EventDisplay(String name) { mName = name; } - /** - * Builds an {@link EventDisplay}. - * @param name the name of the display - * @param displayType the display type: {@link #DISPLAY_TYPE_GRAPH} or - * {@value #DISPLAY_TYPE_FILTERED_LOG}. - * @param filterByPid the flag indicating whether to filter by pid. - */ - EventDisplay(String name, int displayType, boolean filterByPid) { - mName = name; - mDisplayType = displayType; - mPidFiltering = filterByPid; - } - - EventDisplay(EventDisplay from) { - mName = from.mName; - mDisplayType = from.mDisplayType; - mPidFiltering = from.mPidFiltering; - mMaximumChartItemAge = from.mMaximumChartItemAge; - mHistWidth = from.mHistWidth; + static EventDisplay clone(EventDisplay from) { + EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName()); + ed.mName = from.mName; + ed.mPidFiltering = from.mPidFiltering; + ed.mMaximumChartItemAge = from.mMaximumChartItemAge; + ed.mHistWidth = from.mHistWidth; if (from.mPidFilterList != null) { - mPidFilterList = new ArrayList(); - mPidFilterList.addAll(from.mPidFilterList); + ed.mPidFilterList = new ArrayList(); + ed.mPidFilterList.addAll(from.mPidFilterList); } for (ValueDisplayDescriptor desc : from.mValueDescriptors) { - mValueDescriptors.add(new ValueDisplayDescriptor(desc)); + ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc)); } - mValueDescriptorCheck = from.mValueDescriptorCheck; + ed.mValueDescriptorCheck = from.mValueDescriptorCheck; for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) { - mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); + ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); } + return ed; } /** @@ -398,7 +396,7 @@ final class EventDisplay { sb.append(mName); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); - sb.append(mDisplayType); + sb.append(getDisplayType()); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(Boolean.toString(mPidFiltering)); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); @@ -419,35 +417,27 @@ final class EventDisplay { void setName(String name) { mName = name; } - + String getName() { return mName; } - - void setDisplayType(int value) { - mDisplayType = value; - } - - int getDisplayType() { - return mDisplayType; - } - + void setPidFiltering(boolean filterByPid) { mPidFiltering = filterByPid; } - + boolean getPidFiltering() { return mPidFiltering; } - + void setPidFilterList(ArrayList pids) { if (mPidFiltering == false) { new InvalidParameterException(); } - + mPidFilterList = pids; } - + ArrayList getPidFilterList() { return mPidFilterList; } @@ -456,11 +446,11 @@ final class EventDisplay { if (mPidFiltering == false) { new InvalidParameterException(); } - + if (mPidFilterList == null) { mPidFilterList = new ArrayList(); } - + mPidFilterList.add(pid); } @@ -485,27 +475,29 @@ final class EventDisplay { Iterator getOccurrenceDescriptors() { return mOccurrenceDescriptors.iterator(); } - + /** - * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a + * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a * {@link ValueDisplayDescriptor}. + * * @param descriptor the descriptor to be added. */ void addDescriptor(OccurrenceDisplayDescriptor descriptor) { if (descriptor instanceof ValueDisplayDescriptor) { - mValueDescriptors.add((ValueDisplayDescriptor)descriptor); + mValueDescriptors.add((ValueDisplayDescriptor) descriptor); mValueDescriptorCheck = checkDescriptors(); } else { mOccurrenceDescriptors.add(descriptor); } } - + /** * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}). + * * @param descriptorClass the class of the descriptor to return. - * @param index the index of the descriptor to return. + * @param index the index of the descriptor to return. * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor} - * or null if descriptorClass is another class. + * or null if descriptorClass is another class. */ OccurrenceDisplayDescriptor getDescriptor( Class descriptorClass, int index) { @@ -515,14 +507,15 @@ final class EventDisplay { } else if (descriptorClass == ValueDisplayDescriptor.class) { return mValueDescriptors.get(index); } - + return null; } - + /** * Removes a descriptor based on its class and index. + * * @param descriptorClass the class of the descriptor. - * @param index the index of the descriptor to be removed. + * @param index the index of the descriptor to be removed. */ void removeDescriptor(Class descriptorClass, int index) { if (descriptorClass == OccurrenceDisplayDescriptor.class) { @@ -533,137 +526,107 @@ final class EventDisplay { } } - /** - * Creates the UI for the event display. - * @param parent the parent composite. - * @param logParser the current log parser. - * @return the created control (which may have children). - */ - Control createComposite(final Composite parent, EventLogParser logParser, - final ILogColumnListener listener) { - switch (mDisplayType) { - case DISPLAY_TYPE_LOG_ALL: - // intended fall-through - case DISPLAY_TYPE_FILTERED_LOG: - return createLogUI(parent, listener); - case DISPLAY_TYPE_GRAPH: - // intended fall-through - case DISPLAY_TYPE_SYNC: - // intended fall-through - case DISPLAY_TYPE_SYNC_HIST: - String title = getChartTitle(logParser); - mChart = ChartFactory.createTimeSeriesChart( - null, - null /* timeAxisLabel */, - null /* valueAxisLabel */, - null, /* dataset. set below */ - true /* legend */, - false /* tooltips */, - false /* urls */); - - // get the font to make a proper title. We need to convert the swt font, - // into an awt font. - Font f = parent.getFont(); - FontData[] fData = f.getFontData(); - - // event though on Mac OS there could be more than one fontData, we'll only use - // the first one. - FontData firstFontData = fData[0]; + Control createCompositeChart(final Composite parent, EventLogParser logParser, + String title) { + mChart = ChartFactory.createTimeSeriesChart( + null, + null /* timeAxisLabel */, + null /* valueAxisLabel */, + null, /* dataset. set below */ + true /* legend */, + false /* tooltips */, + false /* urls */); - java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), - firstFontData, true /* ensureSameSize */); + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = parent.getFont(); + FontData[] fData = f.getFontData(); - if (mDisplayType == DISPLAY_TYPE_SYNC) { - title = "Sync Status"; - } else if (mDisplayType == DISPLAY_TYPE_SYNC_HIST) { - title = "Sync Histogram"; - } + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; - mChart.setTitle(new TextTitle(title, awtFont)); - - final XYPlot xyPlot = mChart.getXYPlot(); - xyPlot.setRangeCrosshairVisible(true); - xyPlot.setRangeCrosshairLockedOnData(true); - xyPlot.setDomainCrosshairVisible(true); - xyPlot.setDomainCrosshairLockedOnData(true); + java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), + firstFontData, true /* ensureSameSize */); - mChart.addChangeListener(new ChartChangeListener() { - public void chartChanged(ChartChangeEvent event) { - ChartChangeEventType type = event.getType(); - if (type == ChartChangeEventType.GENERAL) { - // because the value we need (rangeCrosshair and domainCrosshair) are - // updated on the draw, but the notification happens before the draw, - // we process the click in a future runnable! - parent.getDisplay().asyncExec(new Runnable() { - public void run() { - processClick(xyPlot); - } - }); + + mChart.setTitle(new TextTitle(title, awtFont)); + + final XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setRangeCrosshairVisible(true); + xyPlot.setRangeCrosshairLockedOnData(true); + xyPlot.setDomainCrosshairVisible(true); + xyPlot.setDomainCrosshairLockedOnData(true); + + mChart.addChangeListener(new ChartChangeListener() { + public void chartChanged(ChartChangeEvent event) { + ChartChangeEventType type = event.getType(); + if (type == ChartChangeEventType.GENERAL) { + // because the value we need (rangeCrosshair and domainCrosshair) are + // updated on the draw, but the notification happens before the draw, + // we process the click in a future runnable! + parent.getDisplay().asyncExec(new Runnable() { + public void run() { + processClick(xyPlot); } - } - }); - - mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, - ChartComposite.DEFAULT_WIDTH, - ChartComposite.DEFAULT_HEIGHT, - ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, - ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, - 3000, // max draw width. We don't want it to zoom, so we put a big number - 3000, // max draw height. We don't want it to zoom, so we put a big number - true, // off-screen buffer - true, // properties - true, // save - true, // print - true, // zoom - true); // tooltips - - mChartComposite.addDisposeListener(new DisposeListener() { - public void widgetDisposed(DisposeEvent e) { - mValueTypeDataSetMap.clear(); - mDataSetCount = 0; - mOccurrenceDataSet = null; - mChart = null; - mChartComposite = null; - mValueDescriptorSeriesMap.clear(); - mOcurrenceDescriptorSeriesMap.clear(); - } - }); - - if (mDisplayType == DISPLAY_TYPE_SYNC) { - initSyncDisplay(); - } else if (mDisplayType == DISPLAY_TYPE_SYNC_HIST) { - initSyncHistDisplay(); + }); } + } + }); + + mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + true, // zoom + true); // tooltips + + mChartComposite.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mValueTypeDataSetMap.clear(); + mDataSetCount = 0; + mOccurrenceDataSet = null; + mChart = null; + mChartComposite = null; + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + }); + + return mChartComposite; - return mChartComposite; - default: - throw new InvalidParameterException("Unknown Display Type"); //$NON-NLS-1$ - } } private void processClick(XYPlot xyPlot) { double rangeValue = xyPlot.getRangeCrosshairValue(); - if (rangeValue != 0) { + if (rangeValue != 0) { double domainValue = xyPlot.getDomainCrosshairValue(); - - Millisecond msec = new Millisecond(new Date((long)domainValue)); - + + Millisecond msec = new Millisecond(new Date((long) domainValue)); + // look for values in the dataset that contains data at this TimePeriod Set descKeys = mValueDescriptorSeriesMap.keySet(); - + for (ValueDisplayDescriptor descKey : descKeys) { HashMap map = mValueDescriptorSeriesMap.get(descKey); - + Set pidKeys = map.keySet(); - + for (Integer pidKey : pidKeys) { TimeSeries series = map.get(pidKey); - + Number value = series.getValue(msec); if (value != null) { // found a match. lets check against the actual value. if (value.doubleValue() == rangeValue) { - + return; } } @@ -672,176 +635,29 @@ final class EventDisplay { } } - /** - * Creates the UI for a log display. - * @param parent the parent {@link Composite} - * @param listener the {@link ILogColumnListener} to notify on column resize events. - * @return the top Composite of the UI. - */ - private Control createLogUI(Composite parent, final ILogColumnListener listener) { - Composite mainComp = new Composite(parent, SWT.NONE); - GridLayout gl; - mainComp.setLayout(gl = new GridLayout(1, false)); - gl.marginHeight = gl.marginWidth = 0; - mainComp.addDisposeListener(new DisposeListener() { - public void widgetDisposed(DisposeEvent e) { - mLogTable = null; - } - }); - Label l = new Label(mainComp, SWT.CENTER); - l.setText(mName); - l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | - SWT.BORDER); - mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); - - IPreferenceStore store = DdmUiPreferences.getStore(); - - TableColumn col = TableHelper.createTableColumn( - mLogTable, "Time", - SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(0, (TableColumn)source); - } - } - }); - - col = TableHelper.createTableColumn( - mLogTable, "pid", - SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(1, (TableColumn)source); - } - } - }); - - col = TableHelper.createTableColumn( - mLogTable, "Event", - SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(2, (TableColumn)source); - } - } - }); - - col = TableHelper.createTableColumn( - mLogTable, "Name", - SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(3, (TableColumn)source); - } - } - }); - - col = TableHelper.createTableColumn( - mLogTable, "Value", - SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(4, (TableColumn)source); - } - } - }); - - col = TableHelper.createTableColumn( - mLogTable, "Type", - SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ - col.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Object source = e.getSource(); - if (source instanceof TableColumn) { - listener.columnResized(5, (TableColumn)source); - } - } - }); - - mLogTable.setHeaderVisible(true); - mLogTable.setLinesVisible(true); - - return mainComp; - } - /** * Resizes the index-th column of the log {@link Table} (if applicable). + * Subclasses can override if necessary. *

* This does nothing if the Table object is null (because the display * type does not use a column) or if the index-th column is in fact the originating * column passed as argument. - * @param index the index of the column to resize + * + * @param index the index of the column to resize * @param sourceColumn the original column that was resize, and on which we need to sync the - * index-th column width. + * index-th column width. */ void resizeColumn(int index, TableColumn sourceColumn) { - if (mLogTable != null) { - TableColumn col = mLogTable.getColumn(index); - if (col != sourceColumn) { - col.setWidth(sourceColumn.getWidth()); - } - } } - + /** * Sets the current {@link EventLogParser} object. + * Subclasses can override if necessary. */ - void setNewLogParser(EventLogParser logParser) { - if (mDisplayType == DISPLAY_TYPE_GRAPH) { - if (mChart != null) { - mChart.setTitle(getChartTitle(logParser)); - } - } + protected void setNewLogParser(EventLogParser logParser) { } - - void resetUI() { - switch (mDisplayType) { - case DISPLAY_TYPE_LOG_ALL: - // intended fall-through - case DISPLAY_TYPE_FILTERED_LOG: - mLogTable.removeAll(); - break; - case DISPLAY_TYPE_SYNC: - initSyncDisplay(); - break; - case DISPLAY_TYPE_SYNC_HIST: - initSyncHistDisplay(); - break; - case DISPLAY_TYPE_GRAPH: - Collection datasets = mValueTypeDataSetMap.values(); - for (TimeSeriesCollection dataset : datasets) { - dataset.removeAllSeries(); - } - if (mOccurrenceDataSet != null) { - mOccurrenceDataSet.removeAllSeries(); - } - mValueDescriptorSeriesMap.clear(); - mOcurrenceDescriptorSeriesMap.clear(); - break; - default: - throw new InvalidParameterException("Unknown Display Type"); //$NON-NLS-1$ - } - } - + /** * Prepares the {@link EventDisplay} for a multi event display. */ @@ -860,103 +676,71 @@ final class EventDisplay { } } - /** - * Processes a new event. This must be called from the ui thread. - * @param event the event to process. - */ - void newEvent(EventContainer event, EventLogParser logParser) { - ArrayList valueDescriptors = - new ArrayList(); - - ArrayList occurrenceDescriptors = - new ArrayList(); - - if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { - switch (mDisplayType) { - case DISPLAY_TYPE_LOG_ALL: - addToLog(event, logParser); - break; - case DISPLAY_TYPE_FILTERED_LOG: - addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); - break; - case DISPLAY_TYPE_SYNC: - updateSyncDisplay(event); - break; - case DISPLAY_TYPE_SYNC_HIST: - updateSyncHistDisplay(event); - break; - case DISPLAY_TYPE_GRAPH: - updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); - break; - default: - throw new InvalidParameterException("Unknown Display Type"); //$NON-NLS-1$ - } - } - } - /** * Returns the {@link Table} object used to display events, if any. + * * @return a Table object or null. */ Table getTable() { return mLogTable; } - /** Private constructor used for loading from storage */ - private EventDisplay() { - // nothing to be done here. - } - /** - * Loads the {@link EventDisplay} parameters from the storage string. - * @param storageString the string containing the parameters. + * Loads a new {@link EventDisplay} from a storage string. The string must have been created + * with {@link #getStorageString()}. + * + * @param storageString the storage string + * @return a new {@link EventDisplay} or null if the load failed. */ - private boolean loadFrom(String storageString) { + static EventDisplay load(String storageString) { if (storageString.length() > 0) { // the storage string is separated by ':' String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR)); - + try { int index = 0; - - mName = values[index++]; - mDisplayType = Integer.parseInt(values[index++]); - mPidFiltering = Boolean.parseBoolean(values[index++]); + + String name = values[index++]; + int displayType = Integer.parseInt(values[index++]); + boolean pidFiltering = Boolean.parseBoolean(values[index++]); + + EventDisplay ed = eventDisplayFactory(displayType, name); + ed.setPidFiltering(pidFiltering); // because empty sections are removed by String.split(), we have to check // the index for those. if (index < values.length) { - loadPidFilters(values[index++]); - } - - if (index < values.length) { - loadValueDescriptors(values[index++]); - } - - if (index < values.length) { - loadOccurrenceDescriptors(values[index++]); - } - - updateValueDescriptorCheck(); - - if (index < values.length) { - mMaximumChartItemAge = Long.parseLong(values[index++]); + ed.loadPidFilters(values[index++]); } if (index < values.length) { - mHistWidth = Long.parseLong(values[index++]); + ed.loadValueDescriptors(values[index++]); } - - return true; + + if (index < values.length) { + ed.loadOccurrenceDescriptors(values[index++]); + } + + ed.updateValueDescriptorCheck(); + + if (index < values.length) { + ed.mMaximumChartItemAge = Long.parseLong(values[index++]); + } + + if (index < values.length) { + ed.mHistWidth = Long.parseLong(values[index++]); + } + + return ed; } catch (RuntimeException re) { - // we'll return false below. + // we'll return null below. Log.e("ddms", re); } } - - return false; + + return null; } - + private String getPidStorageString() { if (mPidFilterList != null) { StringBuilder sb = new StringBuilder(); @@ -969,17 +753,17 @@ final class EventDisplay { } sb.append(i); } - + return sb.toString(); } return ""; //$NON-NLS-1$ } - + private void loadPidFilters(String storageString) { if (storageString.length() > 0) { String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR)); - + for (String value : values) { if (mPidFilterList == null) { mPidFilterList = new ArrayList(); @@ -988,12 +772,12 @@ final class EventDisplay { } } } - + private String getDescriptorStorageString( ArrayList descriptorList) { StringBuilder sb = new StringBuilder(); boolean first = true; - + for (OccurrenceDisplayDescriptor descriptor : descriptorList) { if (first == false) { sb.append(DESCRIPTOR_STORAGE_SEPARATOR); @@ -1002,7 +786,7 @@ final class EventDisplay { } sb.append(descriptor.getStorageString()); } - + return sb.toString(); } @@ -1012,21 +796,21 @@ final class EventDisplay { } String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); - + for (String value : values) { OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor(); desc.loadFrom(value); mOccurrenceDescriptors.add(desc); } } - + private void loadValueDescriptors(String storageString) { if (storageString.length() == 0) { return; } String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); - + for (String value : values) { ValueDisplayDescriptor desc = new ValueDisplayDescriptor(); desc.loadFrom(value); @@ -1034,293 +818,13 @@ final class EventDisplay { } } - /** - * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not - * yet created, it is first allocated and set up into the {@link JFreeChart} object. - */ - private TimeSeriesCollection getOccurrenceDataSet() { - if (mOccurrenceDataSet == null) { - mOccurrenceDataSet = new TimeSeriesCollection(); - - XYPlot xyPlot = mChart.getXYPlot(); - xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); - - OccurrenceRenderer renderer = new OccurrenceRenderer(); - renderer.setBaseShapesVisible(false); - xyPlot.setRenderer(mDataSetCount, renderer); - - mDataSetCount++; - } - - return mOccurrenceDataSet; - } - - /** - * Returns a {@link TimeSeriesCollection} for a specific {@link ValueType}. - * If the data set is not yet created, it is first allocated and set up into the - * {@link JFreeChart} object. - * @param type the {@link ValueType} of the data set. - * @param accumulateValues - */ - private TimeSeriesCollection getValueDataset(ValueType type, boolean accumulateValues) { - TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); - if (dataset == null) { - // create the data set and store it in the map - dataset = new TimeSeriesCollection(); - mValueTypeDataSetMap.put(type, dataset); - - // create the renderer and configure it depending on the ValueType - AbstractXYItemRenderer renderer; - if (type == ValueType.PERCENT && accumulateValues) { - renderer = new XYAreaRenderer(); - } else { - XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); - r.setBaseShapesVisible(type != ValueType.PERCENT); - - renderer = r; - } - - // set both the dataset and the renderer in the plot object. - XYPlot xyPlot = mChart.getXYPlot(); - xyPlot.setDataset(mDataSetCount, dataset); - xyPlot.setRenderer(mDataSetCount, renderer); - - // put a new axis label, and configure it. - NumberAxis axis = new NumberAxis(type.toString()); - - if (type == ValueType.PERCENT) { - // force percent range to be (0,100) fixed. - axis.setAutoRange(false); - axis.setRange(0., 100.); - } - - // for the index, we ignore the occurrence dataset - int count = mDataSetCount; - if (mOccurrenceDataSet != null) { - count--; - } - - xyPlot.setRangeAxis(count, axis); - if ((count % 2) == 0) { - xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); - } else { - xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); - } - - // now we link the dataset and the axis - xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); - - mDataSetCount++; - } - - return dataset; - } - - - /** - * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined - * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from - * the two lists. - *

This method is only called when at least one of the descriptor list is non empty. - * @param event - * @param logParser - * @param valueDescriptors - * @param occurrenceDescriptors - */ - private void updateChart(EventContainer event, EventLogParser logParser, - ArrayList valueDescriptors, - ArrayList occurrenceDescriptors) { - Map tagMap = logParser.getTagMap(); - - Millisecond millisecondTime = null; - long msec = -1; - - // If the event container is a cpu container (tag == 2721), and there is no descriptor - // for the total CPU load, then we do accumulate all the values. - boolean accumulateValues = false; - double accumulatedValue = 0; - - if (event.mTag == 2721) { - accumulateValues = true; - for (ValueDisplayDescriptor descriptor : valueDescriptors) { - accumulateValues &= (descriptor.valueIndex != 0); - } - } - - for (ValueDisplayDescriptor descriptor : valueDescriptors) { - try { - // get the hashmap for this descriptor - HashMap map = mValueDescriptorSeriesMap.get(descriptor); - - // if it's not there yet, we create it. - if (map == null) { - map = new HashMap(); - mValueDescriptorSeriesMap.put(descriptor, map); - } - - // get the TimeSeries for this pid - TimeSeries timeSeries = map.get(event.pid); - - // if it doesn't exist yet, we create it - if (timeSeries == null) { - // get the series name - String seriesFullName = null; - String seriesLabel = getSeriesLabel(event, descriptor); - - switch (mValueDescriptorCheck) { - case EVENT_CHECK_SAME_TAG: - seriesFullName = String.format("%1$s / %2$s", seriesLabel, - descriptor.valueName); - break; - case EVENT_CHECK_SAME_VALUE: - seriesFullName = String.format("%1$s", seriesLabel); - break; - default: - seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, - tagMap.get(descriptor.eventTag), - descriptor.valueName); - break; - } - - // get the data set for this ValueType - TimeSeriesCollection dataset = getValueDataset( - logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] - .getValueType(), - accumulateValues); - - // create the series - timeSeries = new TimeSeries(seriesFullName, Millisecond.class); - if (mMaximumChartItemAge != -1) { - timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); - } - - dataset.addSeries(timeSeries); - - // add it to the map. - map.put(event.pid, timeSeries); - } - - // update the timeSeries. - - // get the value from the event - double value = event.getValueAsDouble(descriptor.valueIndex); - - // accumulate the values if needed. - if (accumulateValues) { - accumulatedValue += value; - value = accumulatedValue; - } - - // get the time - if (millisecondTime == null) { - msec = (long)event.sec * 1000L + (event.nsec / 1000000L); - millisecondTime = new Millisecond(new Date(msec)); - } - - // add the value to the time series - timeSeries.addOrUpdate(millisecondTime, value); - } catch (InvalidTypeException e) { - // just ignore this descriptor if there's a type mismatch - } - } - - for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { - try { - // get the hashmap for this descriptor - HashMap map = mOcurrenceDescriptorSeriesMap.get(descriptor); - - // if it's not there yet, we create it. - if (map == null) { - map = new HashMap(); - mOcurrenceDescriptorSeriesMap.put(descriptor, map); - } - - // get the TimeSeries for this pid - TimeSeries timeSeries = map.get(event.pid); - - // if it doesn't exist yet, we create it. - if (timeSeries == null) { - String seriesLabel = getSeriesLabel(event, descriptor); - - String seriesFullName = String.format("[%1$s:%2$s]", - tagMap.get(descriptor.eventTag), seriesLabel); - - timeSeries = new TimeSeries(seriesFullName, Millisecond.class); - if (mMaximumChartItemAge != -1) { - timeSeries.setMaximumItemAge(mMaximumChartItemAge); - } - - getOccurrenceDataSet().addSeries(timeSeries); - - map.put(event.pid, timeSeries); - } - - // update the series - - // get the time - if (millisecondTime == null) { - msec = (long)event.sec * 1000L + (event.nsec / 1000000L); - millisecondTime = new Millisecond(new Date(msec)); - } - - // add the value to the time series - timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused - } catch (InvalidTypeException e) { - // just ignore this descriptor if there's a type mismatch - } - } - - // go through all the series and remove old values. - if (msec != -1 && mMaximumChartItemAge != -1) { - Collection> pidMapValues = - mValueDescriptorSeriesMap.values(); - - for (HashMap pidMapValue : pidMapValues) { - Collection seriesCollection = pidMapValue.values(); - - for (TimeSeries timeSeries : seriesCollection) { - timeSeries.removeAgedItems(msec, true); - } - } - - pidMapValues = mOcurrenceDescriptorSeriesMap.values(); - for (HashMap pidMapValue : pidMapValues) { - Collection seriesCollection = pidMapValue.values(); - - for (TimeSeries timeSeries : seriesCollection) { - timeSeries.removeAgedItems(msec, true); - } - } - } - } - - /** - * Return the series label for this event. This only contains the pid information. - * @param event the {@link EventContainer} - * @param descriptor the {@link OccurrenceDisplayDescriptor} - * @return the series label. - * @throws InvalidTypeException - */ - private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) - throws InvalidTypeException { - if (descriptor.seriesValueIndex != -1) { - if (descriptor.includePid == false) { - return event.getValueAsString(descriptor.seriesValueIndex); - } else { - return String.format("%1$s (%2$d)", - event.getValueAsString(descriptor.seriesValueIndex), event.pid); - } - } - - return Integer.toString(event.pid); - } - /** * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another * list if they are configured to display the {@link EventContainer} - * @param event the event container + * + * @param event the event container * @param fullList the list with all the descriptors. - * @param outList the list to fill. + * @param outList the list to fill. */ @SuppressWarnings("unchecked") private void getDescriptors(EventContainer event, @@ -1348,200 +852,21 @@ final class EventDisplay { } } } - - /** - * Adds an {@link EventContainer} to the log. - * @param event the event. - * @param logParser the log parser. - */ - private void addToLog(EventContainer event, EventLogParser logParser) { - ScrollBar bar = mLogTable.getVerticalBar(); - boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); - - // get the date. - Calendar c = Calendar.getInstance(); - long msec = (long)event.sec * 1000L; - c.setTimeInMillis(msec); - - // convert the time into a string - String date = String.format("%1$tF %1$tT", c); - - String eventName = logParser.getTagMap().get(event.mTag); - String pidName = Integer.toString(event.pid); - - // get the value description - EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); - if (valueDescription != null) { - for (int i = 0 ; i < valueDescription.length ; i++) { - EventValueDescription description = valueDescription[i]; - try { - String value = event.getValueAsString(i); - - logValue(date, pidName, eventName, description.getName(), value, - description.getEventValueType(), description.getValueType()); - } catch (InvalidTypeException e) { - logValue(date, pidName, eventName, description.getName(), e.getMessage(), - description.getEventValueType(), description.getValueType()); - } - } - - // scroll if needed, by showing the last item - if (scroll) { - int itemCount = mLogTable.getItemCount(); - if (itemCount > 0) { - mLogTable.showItem(mLogTable.getItem(itemCount-1)); - } - } - } - } /** - * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by - * the list of descriptors. If an event is configured to be displayed by value and occurrence, - * only the values are displayed (as they mark an event occurrence anyway). - *

This method is only called when at least one of the descriptor list is non empty. - * @param event - * @param logParser - * @param valueDescriptors - * @param occurrenceDescriptors - */ - private void addToLog(EventContainer event, EventLogParser logParser, - ArrayList valueDescriptors, - ArrayList occurrenceDescriptors) { - ScrollBar bar = mLogTable.getVerticalBar(); - boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); - - // get the date. - Calendar c = Calendar.getInstance(); - long msec = (long)event.sec * 1000L; - c.setTimeInMillis(msec); - - // convert the time into a string - String date = String.format("%1$tF %1$tT", c); - - String eventName = logParser.getTagMap().get(event.mTag); - String pidName = Integer.toString(event.pid); - - if (valueDescriptors.size() > 0) { - for (ValueDisplayDescriptor descriptor : valueDescriptors) { - logDescriptor(event, descriptor, date, pidName, eventName, logParser); - } - } else { - // we display the event. Since the StringBuilder contains the header (date, event name, - // pid) at this point, there isn't anything else to display. - } - - // scroll if needed, by showing the last item - if (scroll) { - int itemCount = mLogTable.getItemCount(); - if (itemCount > 0) { - mLogTable.showItem(mLogTable.getItem(itemCount-1)); - } - } - } - - /** - * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. - * @param event the EventContainer - * @param descriptor the ValueDisplayDescriptor defining which value to display. - * @param date the date of the event in a string. - * @param pidName - * @param eventName - * @param logParser - */ - private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, - String date, String pidName, String eventName, EventLogParser logParser) { - - String value; - try { - value = event.getValueAsString(descriptor.valueIndex); - } catch (InvalidTypeException e) { - value = e.getMessage(); - } - - EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); - - EventValueDescription valueDescription = values[descriptor.valueIndex]; - - logValue(date, pidName, eventName, descriptor.valueName, value, - valueDescription.getEventValueType(), valueDescription.getValueType()); - } - - /** - * Logs a value in the ui. - * @param date - * @param pid - * @param event - * @param valueName - * @param value - * @param eventValueType - * @param valueType - */ - private void logValue(String date, String pid, String event, String valueName, - String value, EventValueType eventValueType, ValueType valueType) { - - TableItem item = new TableItem(mLogTable, SWT.NONE); - item.setText(0, date); - item.setText(1, pid); - item.setText(2, event); - item.setText(3, valueName); - item.setText(4, value); - - String type; - if (valueType != ValueType.NOT_APPLICABLE) { - type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); - } else { - type = eventValueType.toString(); - } - - item.setText(5, type); - } - - /** - * Show the current value(s) of an {@link EventContainer}. The values to show are defined by - * the {@link ValueDisplayDescriptor}s and {@link OccurrenceDisplayDescriptor}s passed in the - * two lists. - * @param event - * @param logParser - * @param valueDescriptors - * @param occurrenceDescriptors - */ - private void showCurrent(EventContainer event, EventLogParser logParser, - ArrayList valueDescriptors, - ArrayList occurrenceDescriptors) { - // TODO Auto-generated method stub - } - - // Values from data/etc/event-log-tags - final int EVENT_SYNC = 2720; - final int EVENT_TICKLE = 2742; - final int EVENT_SYNC_DETAILS = 2743; - - /** - * Filters the {@link EventContainer}, and fills two list of {@link ValueDisplayDescriptor} - * and {@link OccurrenceDisplayDescriptor} configured to display the event. + * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor} + * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event. + * * @param event * @param valueDescriptors * @param occurrenceDescriptors * @return true if the event should be displayed. */ - private boolean filterEvent(EventContainer event, + + protected boolean filterEvent(EventContainer event, ArrayList valueDescriptors, ArrayList occurrenceDescriptors) { - if (mDisplayType == DISPLAY_TYPE_LOG_ALL) { - return true; - } - - if (mDisplayType == DISPLAY_TYPE_SYNC || mDisplayType == DISPLAY_TYPE_SYNC_HIST) { - if (event.mTag == EVENT_SYNC || event.mTag == EVENT_TICKLE || - event.mTag == EVENT_SYNC_DETAILS) { - return true; - } else { - return false; - } - } - // test the pid first (if needed) if (mPidFiltering && mPidFilterList != null) { boolean found = false; @@ -1551,7 +876,7 @@ final class EventDisplay { break; } } - + if (found == false) { return false; } @@ -1569,7 +894,8 @@ final class EventDisplay { * Checks all the {@link ValueDisplayDescriptor} for similarity. * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG. * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE - * @return + * + * @return flag as described above */ private int checkDescriptors() { if (mValueDescriptors.size() < 2) { @@ -1598,39 +924,8 @@ final class EventDisplay { if (index == -1) { return EVENT_CHECK_SAME_TAG; } - - return EVENT_CHECK_SAME_VALUE; - } - - /** - * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. - * @param logParser the logParser. - * @return the chart title. - */ - private String getChartTitle(EventLogParser logParser) { - if (mValueDescriptors.size() > 0) { - String chartDesc = null; - switch (mValueDescriptorCheck) { - case EVENT_CHECK_SAME_TAG: - if (logParser != null) { - chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); - } - break; - case EVENT_CHECK_SAME_VALUE: - if (logParser != null) { - chartDesc = String.format("%1$s / %2$s", - logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), - mValueDescriptors.get(0).valueName); - } - break; - } - - if (chartDesc != null) { - return String.format("%1$s - %2$s", mName, chartDesc); - } - } - return mName; + return EVENT_CHECK_SAME_VALUE; } /** @@ -1642,17 +937,19 @@ final class EventDisplay { /** * Sets the time limit on the charts. + * * @param timeLimit the time limit in seconds. */ void setChartTimeLimit(long timeLimit) { mMaximumChartItemAge = timeLimit; } - + long getChartTimeLimit() { return mMaximumChartItemAge; } /** + * m * Resets the histogram width */ void resetHistWidth() { @@ -1661,443 +958,14 @@ final class EventDisplay { /** * Sets the histogram width + * * @param histWidth the width in hours */ void setHistWidth(long histWidth) { mHistWidth = histWidth; } - + long getHistWidth() { return mHistWidth; } - - // Implementation of the Sync display - // TODO: DISPLAY_TYPE_LOG, DISPLAY_TYPE_GRAPH, and DISPLAY_TYPE_SYNC should be subclasses - // of EventDisplay.java - - private static final int CALENDAR = 0; - private static final int GMAIL = 1; - private static final int FEEDS = 2; - private static final int CONTACTS = 3; - private static final int ERRORS = 4; - private static final int NUM_AUTHS = (CONTACTS+1); - private static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", "Errors"}; - private static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, Color.ORANGE, Color.RED}; - - // Information to graph for each authority - private TimePeriodValues mDatasetsSync[]; - private List mTooltipsSync[]; - private CustomXYToolTipGenerator mTooltipGenerators[]; - private TimeSeries mDatasetsSyncTickle[]; - - // Dataset of error events to graph - private TimeSeries mDatasetError; - - /** - * Initialize the Plot and series data for the sync display. - */ - void initSyncDisplay() { - XYPlot xyPlot = mChart.getXYPlot(); - - XYBarRenderer br = new XYBarRenderer(); - mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; - mTooltipsSync = new List[NUM_AUTHS]; - mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; - mLastDetails = ""; - - TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); - xyPlot.setDataset(tpvc); - xyPlot.setRenderer(0, br); - - XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); - ls.setBaseLinesVisible(false); - mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; - TimeSeriesCollection tsc = new TimeSeriesCollection(); - xyPlot.setDataset(1, tsc); - xyPlot.setRenderer(1, ls); - - mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); - xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); - XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); - errls.setBaseLinesVisible(false); - errls.setSeriesPaint(0, Color.RED); - xyPlot.setRenderer(2, errls); - - for (int i = 0; i < NUM_AUTHS; i++) { - br.setSeriesPaint(i, AUTH_COLORS[i]); - ls.setSeriesPaint(i, AUTH_COLORS[i]); - mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); - tpvc.addSeries(mDatasetsSync[i]); - mTooltipsSync[i] = new ArrayList(); - mTooltipGenerators[i] = new CustomXYToolTipGenerator(); - br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); - mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); - - mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", FixedMillisecond.class); - tsc.addSeries(mDatasetsSyncTickle[i]); - ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); - } - } - - // State information while processing the event stream - private int mLastState; // 0 if event started, 1 if event stopped - private long mLastStartTime; // ms - private long mLastStopTime; //ms - private String mLastDetails; - private int mLastEvent; // server, poll, etc - - /** - * Updates the display with a new event. This is the main entry point for - * each event. This method has the logic to tie together the start event, - * stop event, and details event into one graph item. Note that the details - * can happen before or after the stop event. - * @param event The event - */ - private void updateSyncDisplay(EventContainer event) { - try { - if (event.mTag == EVENT_SYNC) { - int state = Integer.parseInt(event.getValueAsString(1)); - if (state == 0) { // start - mLastStartTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - mLastState = 0; - mLastEvent = Integer.parseInt(event.getValueAsString(2)); - mLastDetails = ""; - } else if (state == 1) { // stop - if (mLastState == 0) { - mLastStopTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - if (mLastStartTime == 0) { - // Log starts with a stop event - mLastStartTime = mLastStopTime; - } - addEvent(event); - mLastState = 1; - } - } - } else if (event.mTag == EVENT_TICKLE) { - int auth = getAuth(event.getValueAsString(0)); - if (auth >= 0) { - long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); - mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); - } - } else if (event.mTag == EVENT_SYNC_DETAILS) { - int auth = getAuth(event.getValueAsString(0)); - mLastDetails = event.getValueAsString(3); - if (mLastState != 0) { // Not inside event - long updateTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - if (updateTime - mLastStopTime <= 250) { - // Got details within 250ms after event, so delete and re-insert - // Details later than 250ms (arbitrary) are discarded as probably - // unrelated. - int lastItem = mDatasetsSync[auth].getItemCount(); - mDatasetsSync[auth].delete(lastItem-1, lastItem-1); - mTooltipsSync[auth].remove(lastItem-1); - addEvent(event); - } - } - } - } catch (InvalidTypeException e) { - } - } - - /** - * Convert authority name to auth number. - * @param authname "calendar", etc. - * @return number series number associated with the authority - */ - private int getAuth(String authname) throws InvalidTypeException { - if ("calendar".equals(authname) || "cl".equals(authname)) { - return CALENDAR; - } else if ("contacts".equals(authname) || "cp".equals(authname)) { - return CONTACTS; - } else if ("subscribedfeeds".equals(authname)) { - return FEEDS; - } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { - return GMAIL; - } else if ("gmail-live".equals(authname)) { - return GMAIL; - } else if ("unknown".equals(authname)) { - return -1; // Unknown tickles; discard - } else { - throw new InvalidTypeException("Unknown authname " + authname); - } - } - - /** - * Generate the height for an event. - * Height is somewhat arbitrarily the count of "things" that happened - * during the sync. - * When network traffic measurements are available, code should be modified - * to use that instead. - * @param details The details string associated with the event - * @return The height in arbirary units (0-100) - */ - private int getHeightFromDetails(String details) { - if (details == null) { - return 1; // Arbitrary - } - int total = 0; - String parts[] = details.split("[a-zA-Z]"); - for (String part : parts) { - if ("".equals(part)) continue; - total += Integer.parseInt(part); - } - if (total == 0) { - total = 1; - } - return total; - } - - /** - * Generates the tooltips text for an event. - * This method decodes the cryptic details string. - * @param auth The authority associated with the event - * @param details The details string - * @param eventSource server, poll, etc. - * @return The text to display in the tooltips - */ - private String getTextFromDetails(int auth, String details, int eventSource) { - - StringBuffer sb = new StringBuffer(); - sb.append(AUTH_NAMES[auth]).append(": \n"); - - Scanner scanner = new Scanner(details); - Pattern charPat = Pattern.compile("[a-zA-Z]"); - Pattern numPat = Pattern.compile("[0-9]+"); - while (scanner.hasNext()) { - String key = scanner.findInLine(charPat); - int val = Integer.parseInt(scanner.findInLine(numPat)); - if (auth == GMAIL && "M".equals(key)) { - sb.append("messages from server: ").append(val).append("\n"); - } else if (auth == GMAIL && "L".equals(key)) { - sb.append("labels from server: ").append(val).append("\n"); - } else if (auth == GMAIL && "C".equals(key)) { - sb.append("check conversation requests from server: ").append(val).append("\n"); - } else if (auth == GMAIL && "A".equals(key)) { - sb.append("attachments from server: ").append(val).append("\n"); - } else if (auth == GMAIL && "U".equals(key)) { - sb.append("op updates from server: ").append(val).append("\n"); - } else if (auth == GMAIL && "u".equals(key)) { - sb.append("op updates to server: ").append(val).append("\n"); - } else if (auth == GMAIL && "S".equals(key)) { - sb.append("send/receive cycles: ").append(val).append("\n"); - } else if ("Q".equals(key)) { - sb.append("queries to server: ").append(val).append("\n"); - } else if ("E".equals(key)) { - sb.append("entries from server: ").append(val).append("\n"); - } else if ("u".equals(key)) { - sb.append("updates from client: ").append(val).append("\n"); - } else if ("i".equals(key)) { - sb.append("inserts from client: ").append(val).append("\n"); - } else if ("d".equals(key)) { - sb.append("deletes from client: ").append(val).append("\n"); - } else if ("f".equals(key)) { - sb.append("full sync requested\n"); - } else if ("r".equals(key)) { - sb.append("partial sync unavailable\n"); - } else if ("X".equals(key)) { - sb.append("hard error\n"); - } else if ("e".equals(key)) { - sb.append("number of parse exceptions: ").append(val).append("\n"); - } else if ("c".equals(key)) { - sb.append("number of conflicts: ").append(val).append("\n"); - } else if ("a".equals(key)) { - sb.append("number of auth exceptions: ").append(val).append("\n"); - } else if ("D".equals(key)) { - sb.append("too many deletions\n"); - } else if ("R".equals(key)) { - sb.append("too many retries: ").append(val).append("\n"); - } else if ("b".equals(key)) { - sb.append("database error\n"); - } else if ("x".equals(key)) { - sb.append("soft error\n"); - } else if ("l".equals(key)) { - sb.append("sync already in progress\n"); - } else if ("I".equals(key)) { - sb.append("io exception\n"); - } else if (auth == CONTACTS && "p".equals(key)) { - sb.append("photos uploaded from client: ").append(val).append("\n"); - } else if (auth == CONTACTS && "P".equals(key)) { - sb.append("photos downloaded from server: ").append(val).append("\n"); - } else if (auth == CALENDAR && "F".equals(key)) { - sb.append("server refresh\n"); - } else if (auth == CALENDAR && "s".equals(key)) { - sb.append("server diffs fetched\n"); - } else { - sb.append(key).append("=").append(val); - } - } - if (eventSource == 0) { - sb.append("(server)"); - } else if (eventSource == 1) { - sb.append("(local)"); - } else if (eventSource == 2) { - sb.append("(poll)"); - } else if (eventSource == 3) { - sb.append("(user)"); - } - return sb.toString(); - } - - /** - * Helper to add an event to the data series. - * Also updates error series if appropriate (x or X in details). - * @param event The event - */ - private void addEvent(EventContainer event) { - try { - int auth = getAuth(event.getValueAsString(0)); - double height = getHeightFromDetails(mLastDetails); - height = height / (mLastStopTime - mLastStartTime + 1) * 10000; - if (height > 30) { - height = 30; - } - mDatasetsSync[auth].add(new SimpleTimePeriod(mLastStartTime, mLastStopTime), height); - mTooltipsSync[auth].add(getTextFromDetails(auth, mLastDetails, - mLastEvent)); - mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); - if (mLastDetails.indexOf('x') >= 0 || mLastDetails.indexOf('X') >= 0) { - long msec = (long)event.sec * 1000L + (event.nsec / 1000000L); - mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); - } - } catch (InvalidTypeException e) { - e.printStackTrace(); - } - } - - // Implementation of the Sync Histogram display - - // Information to graph for each authority - private TimePeriodValues mDatasetsSyncHist[]; - - /** - * Initialize the Plot and series data for the sync display. - */ - void initSyncHistDisplay() { - XYPlot xyPlot = mChart.getXYPlot(); - - AbstractXYItemRenderer br = new XYBarRenderer(); - mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; - mLastDetails = ""; - mTimePeriodMap = new HashMap[NUM_AUTHS + 1]; - - TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); - xyPlot.setDataset(tpvc); - xyPlot.setRenderer(br); - - for (int i = 0; i < NUM_AUTHS + 1; i++) { - br.setSeriesPaint(i, AUTH_COLORS[i]); - mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); - tpvc.addSeries(mDatasetsSyncHist[i]); - mTimePeriodMap[i] = new HashMap(); - - } - } - - /** - * Updates the display with a new event. This is the main entry point for - * each event. This method has the logic to tie together the start event, - * stop event, and details event into one graph item. Note that the details - * can happen before or after the stop event. - * @param event The event - */ - private void updateSyncHistDisplay(EventContainer event) { - try { - if (event.mTag == EVENT_SYNC) { - int state = Integer.parseInt(event.getValueAsString(1)); - if (state == 0) { // start - mLastStartTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - mLastState = 0; - mLastEvent = Integer.parseInt(event.getValueAsString(2)); - mLastDetails = ""; - } else if (state == 1) { // stop - if (mLastState == 0) { - mLastStopTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - if (mLastStartTime == 0) { - // Log starts with a stop event - mLastStartTime = mLastStopTime; - } - int auth = getAuth(event.getValueAsString(0)); - if (mLastDetails.indexOf('x') >= 0 || mLastDetails.indexOf('X') >= 0) { - auth = ERRORS; - } - double delta = (mLastStopTime - mLastStartTime) * 100. / 1000 / 3600; // Percent of hour - addHistEvent(event, auth, delta); - mLastState = 1; - } - } - } else if (event.mTag == EVENT_SYNC_DETAILS) { - int auth = getAuth(event.getValueAsString(0)); - mLastDetails = event.getValueAsString(3); - if (mLastState != 0) { // Not inside event - long updateTime = (long)event.sec * 1000L + (event.nsec / 1000000L); - if (updateTime - mLastStopTime <= 250) { - // Got details within 250ms after event, so delete and re-insert - // Details later than 250ms (arbitrary) are discarded as probably - // unrelated. - //int lastItem = mDatasetsSync[auth].getItemCount(); - //addHistEvent(event); - if (mLastDetails.indexOf('x') >= 0 || mLastDetails.indexOf('X') >= 0) { - // Item turns out to be in error, so transfer time from old auth to error. - - double delta = (mLastStopTime - mLastStartTime) * 100. / 1000 / 3600; // Percent of hour - addHistEvent(event, auth, -delta); - addHistEvent(event, ERRORS, delta); - } - } - } - } - } catch (InvalidTypeException e) { - } - } - - /** - * Helper to add an event to the data series. - * Also updates error series if appropriate (x or X in details). - * @param event The event - * @param auth - * @param value - */ - private void addHistEvent(EventContainer event, int auth, double value) { - SimpleTimePeriod hour = getTimePeriod(mLastStopTime, mHistWidth); - - // Loop over all datasets to do the stacking. - for (int i = auth; i <= ERRORS; i++) { - addToPeriod(mDatasetsSyncHist, i, hour, value); - } - } - - Map mTimePeriodMap[]; - - private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, double value) { - int index; - if (mTimePeriodMap[auth].containsKey(period)) { - index = mTimePeriodMap[auth].get(period); - double oldValue = tpv[auth].getValue(index).doubleValue(); - tpv[auth].update(index, oldValue + value); - } else { - index = tpv[auth].getItemCount(); - mTimePeriodMap[auth].put(period, index); - tpv[auth].add(period, value); - } - } - - /** - * Creates a multiple-hour time period for the histogram. - * @param time Time in milliseconds. - * @param numHoursWide: should divide into a day. - * @return SimpleTimePeriod covering the number of hours and containing time. - */ - private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { - Date date = new Date(time); - TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; - Calendar calendar = Calendar.getInstance(zone); - calendar.setTime(date); - long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + calendar.get(Calendar.DAY_OF_YEAR) * 24; - int year = calendar.get(Calendar.YEAR); - hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; - calendar.clear(); - calendar.set(year, 0, 1, 0, 0); // Jan 1 - long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; - return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); - } } diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java index 94f04d7b2..88c3cb299 100644 --- a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -23,7 +23,6 @@ import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.IImageLoader; import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; - import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; @@ -439,15 +438,19 @@ class EventDisplayOptions extends Dialog { mDisplayTypeCombo.add("Filtered Log"); mDisplayTypeCombo.add("Graph"); mDisplayTypeCombo.add("Sync"); - mDisplayTypeCombo.add("Sync histogram"); + mDisplayTypeCombo.add("Sync Histogram"); + mDisplayTypeCombo.add("Sync Performance"); mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { EventDisplay eventDisplay = getCurrentEventDisplay(); - if (eventDisplay != null) { + if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) { + /* Replace the EventDisplay object with a different subclass */ setModified(); - eventDisplay.setDisplayType(mDisplayTypeCombo.getSelectionIndex()); - fillUiWith(eventDisplay); + String name = eventDisplay.getName(); + EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name); + setCurrentEventDisplay(newEventDisplay); + fillUiWith(newEventDisplay); } } }); @@ -693,7 +696,7 @@ class EventDisplayOptions extends Dialog { private void duplicateEventDisplay(ArrayList displayList) { for (EventDisplay eventDisplay : displayList) { - mDisplayList.add(new EventDisplay(eventDisplay)); + mDisplayList.add(EventDisplay.clone(eventDisplay)); } } @@ -744,7 +747,7 @@ class EventDisplayOptions extends Dialog { String name = String.format("display %1$d", count + 1); - EventDisplay eventDisplay = new EventDisplay(name); + EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name); mDisplayList.add(eventDisplay); mEventDisplayList.add(name); @@ -779,6 +782,13 @@ class EventDisplayOptions extends Dialog { return null; } + + private void setCurrentEventDisplay(EventDisplay eventDisplay) { + int selection = mEventDisplayList.getSelectionIndex(); + if (selection != -1) { + mDisplayList.set(selection, eventDisplay); + } + } private void handleEventDisplaySelection() { EventDisplay eventDisplay = getCurrentEventDisplay(); diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java index 2ace78afe..a1303f675 100644 --- a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/EventLogImporter.java @@ -38,7 +38,7 @@ public class EventLogImporter { if (top == null) { throw new FileNotFoundException(); } - final String tagFile = top + "/data/etc/event-log-tags"; + final String tagFile = top + "/system/core/logcat/event-log-tags"; BufferedReader tagReader = new BufferedReader( new InputStreamReader(new FileInputStream(tagFile))); BufferedReader eventReader = new BufferedReader( diff --git a/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 000000000..108c097a4 --- /dev/null +++ b/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib/log/event/SyncCommon.java @@ -0,0 +1,158 @@ +/* + * 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.ddmuilib.log.event; + +import com.android.ddmlib.log.EventContainer; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.InvalidTypeException; + +import java.awt.Color; + +abstract public class SyncCommon extends EventDisplay { + + // State information while processing the event stream + private int mLastState; // 0 if event started, 1 if event stopped + private long mLastStartTime; // ms + private long mLastStopTime; //ms + private String mLastDetails; + private int mLastSyncSource; // poll, server, user, etc. + + // Some common variables for sync display. These define the sync backends + //and how they should be displayed. + protected static final int CALENDAR = 0; + protected static final int GMAIL = 1; + protected static final int FEEDS = 2; + protected static final int CONTACTS = 3; + protected static final int ERRORS = 4; + protected static final int NUM_AUTHS = (CONTACTS + 1); + protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", + "Errors"}; + protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, + Color.ORANGE, Color.RED}; + + // Values from data/etc/event-log-tags + final int EVENT_SYNC = 2720; + final int EVENT_TICKLE = 2742; + final int EVENT_SYNC_DETAILS = 2743; + + protected SyncCommon(String name) { + super(name); + } + + /** + * Resets the display. + */ + @Override + void resetUI() { + mLastStartTime = 0; + mLastStopTime = 0; + mLastState = -1; + mLastSyncSource = -1; + mLastDetails = ""; + } + + /** + * Updates the display with a new event. This is the main entry point for + * each event. This method has the logic to tie together the start event, + * stop event, and details event into one graph item. The combined sync event + * is handed to the subclass via processSycnEvent. Note that the details + * can happen before or after the stop event. + * + * @param event The event + * @param logParser The parser providing the event. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + try { + if (event.mTag == EVENT_SYNC) { + int state = Integer.parseInt(event.getValueAsString(1)); + if (state == 0) { // start + mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + mLastState = 0; + mLastSyncSource = Integer.parseInt(event.getValueAsString(2)); + mLastDetails = ""; + } else if (state == 1) { // stop + if (mLastState == 0) { + mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (mLastStartTime == 0) { + // Log starts with a stop event + mLastStartTime = mLastStopTime; + } + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + true, mLastSyncSource); + mLastState = 1; + } + } + } else if (event.mTag == EVENT_SYNC_DETAILS) { + mLastDetails = event.getValueAsString(3); + if (mLastState != 0) { // Not inside event + long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L); + if (updateTime - mLastStopTime <= 250) { + // Got details within 250ms after event, so delete and re-insert + // Details later than 250ms (arbitrary) are discarded as probably + // unrelated. + int auth = getAuth(event.getValueAsString(0)); + processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, + false, mLastSyncSource); + } + } + } + } catch (InvalidTypeException e) { + } + } + + /** + * Callback hook for subclass to process a sync event. newEvent has the logic + * to combine start and stop events and passes a processed event to the + * subclass. + * + * @param event The sync event + * @param auth The sync authority + * @param startTime Start time (ms) of events + * @param stopTime Stop time (ms) of events + * @param details Details associated with the event. + * @param newEvent True if this event is a new sync event. False if this event + * @param syncSource Poll, user, server, etc. + */ + abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, + String details, boolean newEvent, int syncSource); + + /** + * Converts authority name to auth number. + * + * @param authname "calendar", etc. + * @return number series number associated with the authority + */ + protected int getAuth(String authname) throws InvalidTypeException { + if ("calendar".equals(authname) || "cl".equals(authname)) { + return CALENDAR; + } else if ("contacts".equals(authname) || "cp".equals(authname)) { + return CONTACTS; + } else if ("subscribedfeeds".equals(authname)) { + return FEEDS; + } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { + return GMAIL; + } else if ("gmail-live".equals(authname)) { + return GMAIL; + } else if ("unknown".equals(authname)) { + return -1; // Unknown tickles; discard + } else { + throw new InvalidTypeException("Unknown authname " + authname); + } + } +} diff --git a/tools/draw9patch/etc/draw9patch.bat b/tools/draw9patch/etc/draw9patch.bat index 1d56d8595..e267b0665 100755 --- a/tools/draw9patch/etc/draw9patch.bat +++ b/tools/draw9patch/etc/draw9patch.bat @@ -20,9 +20,9 @@ rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 -rem Change current directory to where ddms is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=draw9patch.jar set frameworkdir= diff --git a/tools/draw9patch/src/com/android/draw9patch/Application.java b/tools/draw9patch/src/com/android/draw9patch/Application.java index c7c6aaf95..68c792ab7 100644 --- a/tools/draw9patch/src/com/android/draw9patch/Application.java +++ b/tools/draw9patch/src/com/android/draw9patch/Application.java @@ -40,11 +40,12 @@ public class Application { } } - public static void main(String... args) { + public static void main(final String... args) { initUserInterface(); SwingUtilities.invokeLater(new Runnable() { public void run() { - MainFrame frame = new MainFrame(); + String arg = args.length > 0 ? args[0] : null; + MainFrame frame = new MainFrame(arg); frame.setDefaultCloseOperation(MainFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); diff --git a/tools/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java b/tools/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java index 86c801f12..84b96a538 100644 --- a/tools/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java +++ b/tools/draw9patch/src/com/android/draw9patch/ui/ImageEditorPanel.java @@ -478,6 +478,14 @@ class ImageEditorPanel extends JPanel { start = rect.x; } } + } else { + int start = -1; + for (Rectangle rect : patches) { + if (rect.x > start) { + horizontalPatchesSum += rect.width; + start = rect.x; + } + } } verticalPatchesSum = 0; @@ -489,6 +497,14 @@ class ImageEditorPanel extends JPanel { start = rect.y; } } + } else { + int start = -1; + for (Rectangle rect : patches) { + if (rect.y > start) { + verticalPatchesSum += rect.height; + start = rect.y; + } + } } setSize(size); @@ -528,8 +544,7 @@ class ImageEditorPanel extends JPanel { x = 0; y = 0; - if (patches.size() == 0 || horizontalPatches.size() == 0 || - verticalPatches.size() == 0) { + if (patches.size() == 0) { g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null); g2.dispose(); return; @@ -651,6 +666,7 @@ class ImageEditorPanel extends JPanel { private int lastPositionX; private int lastPositionY; + private int currentButton; private boolean showCursor; private JLabel helpLabel; @@ -687,16 +703,20 @@ class ImageEditorPanel extends JPanel { addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent event) { - paint(event.getX(), event.getY(), event.isShiftDown() ? MouseEvent.BUTTON3 : - event.getButton()); + // Store the button here instead of retrieving it again in MouseDragged + // below, because on linux, calling MouseEvent.getButton() for the drag + // event returns 0, which appears to be technically correct (no button + // changed state). + currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton(); + paint(event.getX(), event.getY(), currentButton); } }); addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseDragged(MouseEvent event) { if (!checkLockedRegion(event.getX(), event.getY())) { - paint(event.getX(), event.getY(), event.isShiftDown() ? MouseEvent.BUTTON3 : - event.getButton()); + // use the stored button, see note above + paint(event.getX(), event.getY(), currentButton); } } @@ -1023,7 +1043,15 @@ class ImageEditorPanel extends JPanel { horizontalPatches = getRectangles(left.first, top.second); verticalPatches = getRectangles(left.second, top.first); } else { - horizontalPatches = verticalPatches = new ArrayList(0); + if (top.first.size() > 0) { + horizontalPatches = new ArrayList(0); + verticalPatches = getVerticalRectangles(top.first); + } else if (left.first.size() > 0) { + horizontalPatches = getHorizontalRectangles(left.first); + verticalPatches = new ArrayList(0); + } else { + horizontalPatches = verticalPatches = new ArrayList(0); + } } row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row); @@ -1036,6 +1064,28 @@ class ImageEditorPanel extends JPanel { verticalPadding = getPadding(left.first); } + private List getVerticalRectangles(List> topPairs) { + List rectangles = new ArrayList(); + for (Pair top : topPairs) { + int x = top.first; + int width = top.second - top.first; + + rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2)); + } + return rectangles; + } + + private List getHorizontalRectangles(List> leftPairs) { + List rectangles = new ArrayList(); + for (Pair left : leftPairs) { + int y = left.first; + int height = left.second - left.first; + + rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height)); + } + return rectangles; + } + private Pair getPadding(List> pairs) { if (pairs.size() == 0) { return new Pair(0, 0); @@ -1058,7 +1108,7 @@ class ImageEditorPanel extends JPanel { for (Pair left : leftPairs) { int y = left.first; int height = left.second - left.first; - for (Pair top: topPairs) { + for (Pair top : topPairs) { int x = top.first; int width = top.second - top.first; @@ -1103,6 +1153,7 @@ class ImageEditorPanel extends JPanel { startWithPatch[0] = true; fixed.clear(); } + return new Pair>>(fixed, patches); } diff --git a/tools/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java b/tools/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java index a62884fe8..f14cd7764 100644 --- a/tools/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java +++ b/tools/draw9patch/src/com/android/draw9patch/ui/ImageTransferHandler.java @@ -36,25 +36,48 @@ class ImageTransferHandler extends TransferHandler { @Override public boolean importData(JComponent component, Transferable transferable) { try { - Object data = transferable.getTransferData(DataFlavor.javaFileListFlavor); - //noinspection unchecked - final File file = ((List) data).get(0); - mainFrame.open(file).execute(); + for (DataFlavor flavor : transferable.getTransferDataFlavors()) { + if (flavor.isFlavorJavaFileListType()) { + Object data = transferable.getTransferData(DataFlavor.javaFileListFlavor); + //noinspection unchecked + final File file = ((List) data).get(0); + mainFrame.open(file).execute(); + return true; + } else if (flavor.isFlavorTextType()) { + if (flavor.getRepresentationClass() == String.class) { + String mime = flavor.getMimeType(); + DataFlavor flave = new DataFlavor(mime); + Object data = transferable.getTransferData(flave); + final String path = convertPath(data.toString()); + mainFrame.open(new File(path)).execute(); + return true; + } + } + } } catch (UnsupportedFlavorException e) { - return false; + // Ignore } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } - return true; + return false; + } + + private static String convertPath(String path) { + if (path.startsWith("file://")) path = path.substring("file://".length()); + if (path.indexOf('\n') != -1) path = path.substring(0, path.indexOf('\n')); + if (path.indexOf('\r') != -1) path = path.substring(0, path.indexOf('\r')); + return path; } @Override public boolean canImport(JComponent component, DataFlavor[] dataFlavors) { for (DataFlavor flavor : dataFlavors) { - if (flavor.isFlavorJavaFileListType()) { + if (flavor.isFlavorJavaFileListType() || flavor.isFlavorTextType()) { return true; } } diff --git a/tools/draw9patch/src/com/android/draw9patch/ui/MainFrame.java b/tools/draw9patch/src/com/android/draw9patch/ui/MainFrame.java index 9ffd93e6a..d5b640918 100644 --- a/tools/draw9patch/src/com/android/draw9patch/ui/MainFrame.java +++ b/tools/draw9patch/src/com/android/draw9patch/ui/MainFrame.java @@ -40,14 +40,24 @@ public class MainFrame extends JFrame { private JMenuItem saveMenuItem; private ImageEditorPanel imageEditor; - public MainFrame() throws HeadlessException { + public MainFrame(String path) throws HeadlessException { super("Draw 9-patch"); buildActions(); buildMenuBar(); buildContent(); - showOpenFilePanel(); + if (path == null) { + showOpenFilePanel(); + } else { + try { + File file = new File(path); + BufferedImage img = GraphicsUtilities.loadCompatibleImage(file.toURI().toURL()); + showImageEditor(img, file.getAbsolutePath()); + } catch (Exception ex) { + showOpenFilePanel(); + } + } // pack(); setSize(1024, 600); diff --git a/tools/eclipse/buildConfig/allElements.xml b/tools/eclipse/buildConfig/allElements.xml index 99ab3aad8..2c8229c28 100644 --- a/tools/eclipse/buildConfig/allElements.xml +++ b/tools/eclipse/buildConfig/allElements.xml @@ -14,6 +14,11 @@ + + + + + @@ -44,6 +49,10 @@ + + + + diff --git a/tools/eclipse/changes.txt b/tools/eclipse/changes.txt index 5cb82452a..8cd843e20 100644 --- a/tools/eclipse/changes.txt +++ b/tools/eclipse/changes.txt @@ -1,14 +1,19 @@ 0.9.0 (work in progress) -- Support for SDK with multiple versions of the Android platform and vendor supplied add-ons. +- Projects now store generated Java files (R.java/Manifest.java and output from aidl) in a 'gen' source folder. +- Support for the new Android SDK with support for multiple versions of the Android platform and for vendor supplied add-ons. + * New Project Wizard lets you choose which platform/add-on to target. + * Project properties (right click project in Package Explorer, then "Properties"), lets you edit project target. + * New Launch configuration option to choose debug deployment target. +- Ability to export multiple apk from one project, using resource filters. See the 'android' property for Android projects. 0.8.1: -- Alternate Layout wizard. In the layout editor, the "create" button is now enabled, and allows to easily create alternate versions. +- Alternate Layout wizard. In the layout editor, the "create" button is now enabled to easily create alternate versions of the current layout. - Fixed issue with custom themes/styles in the layout editor. -- Export Wizard: To export an application for release, sign with a non debug key. Accessible from the export menu, from the Android Tools contextual menu, or from the overview page of the manifest editor. +- Export Wizard: To export an application for release, and sign it with a non debug key. Accessible from the export menu, from the Android Tools contextual menu, or from the overview page of the manifest editor. - New XML File Wizard: To easily create new XML resources file in the /res directory. - New checks on launch when attempting to debug on a device. -- Basic support for drag'n'drop in Graphical layout editor. You can add new items by drag'n'drop from the palette. There's is no support for moving/resizing yet. +- Basic support for drag'n'drop in Graphical layout editor. You can add new items by drag'n'drop from the palette. There is no support for moving/resizing yet. - Undo/redo support in all XML form editors and Graphical layout editor. 0.8.0: diff --git a/tools/eclipse/features/com.android.ide.eclipse.adt/feature.xml b/tools/eclipse/features/com.android.ide.eclipse.adt/feature.xml index 676a89e49..97bc8b14a 100644 --- a/tools/eclipse/features/com.android.ide.eclipse.adt/feature.xml +++ b/tools/eclipse/features/com.android.ide.eclipse.adt/feature.xml @@ -7,15 +7,101 @@ plugin="com.android.ide.eclipse.adt"> - This feature provides support for Android Projects in Eclipse. + Android Developer Tools. Copyright (C) 2007 The Android Open Source Project - - License TBD. + + Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and + +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. @@ -23,6 +109,7 @@ + @@ -47,6 +134,7 @@ + - - diff --git a/tools/eclipse/features/com.android.ide.eclipse.ddms/.project b/tools/eclipse/features/com.android.ide.eclipse.ddms/.project new file mode 100644 index 000000000..f80ff609a --- /dev/null +++ b/tools/eclipse/features/com.android.ide.eclipse.ddms/.project @@ -0,0 +1,17 @@ + + + ddms-feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + + org.eclipse.pde.FeatureNature + + diff --git a/tools/eclipse/features/com.android.ide.eclipse.ddms/build.properties b/tools/eclipse/features/com.android.ide.eclipse.ddms/build.properties new file mode 100644 index 000000000..64f93a9f0 --- /dev/null +++ b/tools/eclipse/features/com.android.ide.eclipse.ddms/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/tools/eclipse/features/com.android.ide.eclipse.ddms/feature.xml b/tools/eclipse/features/com.android.ide.eclipse.ddms/feature.xml new file mode 100644 index 000000000..dfdf98569 --- /dev/null +++ b/tools/eclipse/features/com.android.ide.eclipse.ddms/feature.xml @@ -0,0 +1,237 @@ + + + + + Android Dalvik Debug Monitor Service + + + + Copyright (C) 2007 The Android Open Source Project + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + + + + + + + + + + + + + + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/tools/eclipse/plugins/com.android.ide.eclipse.adt/.classpath index c3c8c1004..a24fc8721 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/.classpath +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/.classpath @@ -10,7 +10,7 @@ - - + + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index a464d5c48..3750f6602 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -39,10 +39,12 @@ Require-Bundle: com.android.ide.eclipse.ddms, org.eclipse.wst.sse.core, org.eclipse.wst.sse.ui, org.eclipse.wst.xml.core, - org.eclipse.wst.xml.ui + org.eclipse.wst.xml.ui, + org.eclipse.jdt.junit Eclipse-LazyStart: true Export-Package: com.android.ide.eclipse.adt, com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.launch;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.project;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.project.internal;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.sdk;x-friends:="com.android.ide.eclipse.tests", diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png new file mode 100644 index 000000000..0f0e883f0 Binary files /dev/null and b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png differ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png new file mode 100644 index 000000000..8273185ed Binary files /dev/null and b/tools/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png differ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index ade464661..39e6dd584 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -17,6 +17,14 @@ + + + + + @@ -132,8 +140,8 @@ - + id="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard"> @@ -337,24 +339,24 @@ name="Debug Android Application" description="Debug Android Application" categoryId="org.eclipse.debug.ui.category.run" - id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.debug"> + id="com.android.ide.eclipse.adt.launch.LaunchShortcut.debug"> + id="com.android.ide.eclipse.adt.launch.LaunchShortcut.run"> @@ -464,4 +466,48 @@ + + + + + + + + + + + + + + + + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index 62bc7edf2..48a21d1d6 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -20,8 +20,7 @@ import com.android.ddmuilib.StackTracePanel; import com.android.ddmuilib.StackTracePanel.ISourceRevealer; import com.android.ddmuilib.console.DdmConsole; import com.android.ddmuilib.console.IDdmConsole; -import com.android.ide.eclipse.adt.build.DexWrapper; -import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController; +import com.android.ide.eclipse.adt.launch.AndroidLaunchController; import com.android.ide.eclipse.adt.preferences.BuildPreferencePage; import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.adt.project.export.ExportWizard; @@ -29,6 +28,7 @@ import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerIni import com.android.ide.eclipse.adt.sdk.AndroidTargetParser; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.EclipseUiHelper; import com.android.ide.eclipse.common.SdkStatsHelper; @@ -166,11 +166,16 @@ public class AdtPlugin extends AbstractUIPlugin { /** Load status of the SDK. Any access MUST be in a synchronized(mPostLoadProjects) block */ private LoadStatus mSdkIsLoaded = LoadStatus.LOADING; /** Project to update once the SDK is loaded. - * Any access MUST be in a synchronized(mPostLoadProjects) block */ - private final ArrayList mPostLoadProjects = new ArrayList(); + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList mPostLoadProjectsToResolve = + new ArrayList(); + /** Project to check validity of cache vs actual once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList mPostLoadProjectsToCheck = new ArrayList(); private ResourceMonitor mResourceMonitor; - private ArrayList mResourceRefreshListener = new ArrayList(); + private ArrayList mTargetChangeListeners = + new ArrayList(); /** * Custom PrintStream for Dx output. This class overrides the method @@ -306,12 +311,12 @@ public class AdtPlugin extends AbstractUIPlugin { if (checkSdkLocationAndId()) { // if sdk if valid, reparse it - // add the current Android project to the list of projects to be updated + // add all the opened Android projects to the list of projects to be updated // after the SDK is reloaded - synchronized (mPostLoadProjects) { + synchronized (getSdkLockObject()) { // get the project to refresh. IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(); - mPostLoadProjects.addAll(Arrays.asList(androidProjects)); + mPostLoadProjectsToResolve.addAll(Arrays.asList(androidProjects)); } // parse the SDK resources at the new location @@ -419,8 +424,6 @@ public class AdtPlugin extends AbstractUIPlugin { stopEditors(); - DexWrapper.unloadDex(); - mRed.dispose(); synchronized (AdtPlugin.class) { sPlugin = null; @@ -461,21 +464,11 @@ public class AdtPlugin extends AbstractUIPlugin { return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_ADB; } - /** Returns the aapt path relative to the sdk folder */ - public static String getOsRelativeAapt() { - return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_AAPT; - } - /** Returns the emulator path relative to the sdk folder */ public static String getOsRelativeEmulator() { return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_EMULATOR; } - /** Returns the aidl path relative to the sdk folder */ - public static String getOsRelativeAidl() { - return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_AIDL; - } - /** Returns the absolute adb path */ public static String getOsAbsoluteAdb() { return getOsSdkFolder() + getOsRelativeAdb(); @@ -487,21 +480,11 @@ public class AdtPlugin extends AbstractUIPlugin { AndroidConstants.FN_TRACEVIEW; } - /** Returns the absolute aapt path */ - public static String getOsAbsoluteAapt() { - return getOsSdkFolder() + getOsRelativeAapt(); - } - /** Returns the absolute emulator path */ public static String getOsAbsoluteEmulator() { return getOsSdkFolder() + getOsRelativeEmulator(); } - /** Returns the absolute aidl path */ - public static String getOsAbsoluteAidl() { - return getOsSdkFolder() + getOsRelativeAidl(); - } - /** * Returns a Url file path to the javaDoc folder. */ @@ -869,19 +852,43 @@ public class AdtPlugin extends AbstractUIPlugin { } /** - * Returns whether the Sdk has been loaded. If the SDK has not been loaded, the given - * project is added to a list of projects to recompile after the SDK is loaded. + * Returns whether the Sdk has been loaded. */ - public LoadStatus getSdkLoadStatus(IJavaProject project) { - synchronized (mPostLoadProjects) { - // only add the project to the list, if we are still loading. - if (mSdkIsLoaded == LoadStatus.LOADING && project != null) { - mPostLoadProjects.add(project); - } - + public final LoadStatus getSdkLoadStatus() { + synchronized (getSdkLockObject()) { return mSdkIsLoaded; } } + + /** + * Returns the lock object for SDK loading. If you wish to do things while the SDK is loading, + * you must synchronize on this object. + */ + public final Object getSdkLockObject() { + return mPostLoadProjectsToResolve; + } + + /** + * Sets the given {@link IJavaProject} to have its target resolved again once the SDK finishes + * to load. + */ + public final void setProjectToResolve(IJavaProject javaProject) { + synchronized (getSdkLockObject()) { + mPostLoadProjectsToResolve.add(javaProject); + } + } + + /** + * Sets the given {@link IJavaProject} to have its target checked for consistency + * once the SDK finishes to load. This is used if the target is resolved using cached + * information while the SDK is loading. + */ + public final void setProjectToCheck(IJavaProject javaProject) { + // only lock on + synchronized (getSdkLockObject()) { + mPostLoadProjectsToCheck.add(javaProject); + } + } /** * Checks the location of the SDK is valid and if it is, grab the SDK API version @@ -939,8 +946,6 @@ public class AdtPlugin extends AbstractUIPlugin { // check the path to various tools we use String[] filesToCheck = new String[] { osSdkLocation + getOsRelativeAdb(), - osSdkLocation + getOsRelativeAapt(), - osSdkLocation + getOsRelativeAidl(), osSdkLocation + getOsRelativeEmulator() }; for (String file : filesToCheck) { @@ -982,7 +987,7 @@ public class AdtPlugin extends AbstractUIPlugin { Constants.BUNDLE_VERSION); Version version = new Version(versionString); - SdkStatsHelper.pingUsageServer("editors", version); //$NON-NLS-1$ + SdkStatsHelper.pingUsageServer("adt", version); //$NON-NLS-1$ return Status.OK_STATUS; } catch (Throwable t) { @@ -1015,61 +1020,69 @@ public class AdtPlugin extends AbstractUIPlugin { progress.setTaskName(Messages.AdtPlugin_Parsing_Resources); - for (IAndroidTarget target : sdk.getTargets()) { - IStatus status = new AndroidTargetParser(target).run(progress); - if (status.getCode() != IStatus.OK) { - synchronized (mPostLoadProjects) { - mSdkIsLoaded = LoadStatus.FAILED; - mPostLoadProjects.clear(); + int n = sdk.getTargets().length; + if (n > 0) { + int w = 60 / n; + for (IAndroidTarget target : sdk.getTargets()) { + SubMonitor p2 = progress.newChild(w); + IStatus status = new AndroidTargetParser(target).run(p2); + if (status.getCode() != IStatus.OK) { + synchronized (getSdkLockObject()) { + mSdkIsLoaded = LoadStatus.FAILED; + mPostLoadProjectsToResolve.clear(); + } + return status; } - return status; } } - // FIXME: move this per platform, or somewhere else. - progress = SubMonitor.convert(monitor, - Messages.AdtPlugin_Parsing_Resources, 20); - DexWrapper.unloadDex(); - - IStatus res = DexWrapper.loadDex( - mOsSdkLocation + AndroidConstants.OS_SDK_LIBS_DX_JAR); - if (res != Status.OK_STATUS) { - synchronized (mPostLoadProjects) { - mSdkIsLoaded = LoadStatus.FAILED; - mPostLoadProjects.clear(); - } - return res; - } - - synchronized (mPostLoadProjects) { + synchronized (getSdkLockObject()) { mSdkIsLoaded = LoadStatus.LOADED; + progress.setTaskName("Check Projects"); + + // check the projects that need checking. + // The method modifies the list (it removes the project that + // do not need to be resolved again). + AndroidClasspathContainerInitializer.checkProjectsCache( + mPostLoadProjectsToCheck); + + mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck); + // update the project that needs recompiling. - if (mPostLoadProjects.size() > 0) { - IJavaProject[] array = mPostLoadProjects.toArray( - new IJavaProject[mPostLoadProjects.size()]); + if (mPostLoadProjectsToResolve.size() > 0) { + IJavaProject[] array = mPostLoadProjectsToResolve.toArray( + new IJavaProject[mPostLoadProjectsToResolve.size()]); AndroidClasspathContainerInitializer.updateProjects(array); - mPostLoadProjects.clear(); + mPostLoadProjectsToResolve.clear(); } + + progress.worked(10); } } // Notify resource changed listeners - progress.subTask("Refresh UI"); - progress.setWorkRemaining(mResourceRefreshListener.size()); + progress.setTaskName("Refresh UI"); + progress.setWorkRemaining(mTargetChangeListeners.size()); // Clone the list before iterating, to avoid Concurrent Modification // exceptions - List listeners = (List)mResourceRefreshListener.clone(); - for (Runnable listener : listeners) { - try { - AdtPlugin.getDisplay().syncExec(listener); - } catch (Exception e) { - AdtPlugin.log(e, "ResourceRefreshListener Failed"); //$NON-NLS-1$ - } finally { - progress.worked(1); + final List listeners = + (List)mTargetChangeListeners.clone(); + final SubMonitor progress2 = progress; + AdtPlugin.getDisplay().syncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onTargetsLoaded(); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } finally { + progress2.worked(1); + } + } } - } + }); } finally { if (monitor != null) { monitor.done(); @@ -1315,12 +1328,42 @@ public class AdtPlugin extends AbstractUIPlugin { }, IResourceDelta.ADDED | IResourceDelta.CHANGED); } - public void addResourceChangedListener(Runnable resourceRefreshListener) { - mResourceRefreshListener.add(resourceRefreshListener); + /** + * Adds a new {@link ITargetChangeListener} to be notified when a new SDK is loaded, or when + * a project has its target changed. + */ + public void addTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.add(listener); } - public void removeResourceChangedListener(Runnable resourceRefreshListener) { - mResourceRefreshListener.remove(resourceRefreshListener); + /** + * Removes an existing {@link ITargetChangeListener}. + * @see #addTargetListener(ITargetChangeListener) + */ + public void removeTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.remove(listener); + } + + /** + * Updates all the {@link ITargetChangeListener} that a target has changed for a given project. + *

Only editors related to that project should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListener(final IProject project) { + final List listeners = + (List)mTargetChangeListeners.clone(); + + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onProjectTargetChange(project); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); } public static synchronized OutputStream getErrorStream() { diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java index 6743246b5..f8a969e94 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java @@ -19,7 +19,7 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.BaseProjectHelper; @@ -67,6 +67,8 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; public class ApkBuilder extends BaseBuilder { @@ -181,7 +183,7 @@ public class ApkBuilder extends BaseBuilder { return mMakeFinalPackage; } } - + /** * {@link IZipEntryFilter} to filter out everything that is not a standard java resources. *

Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when @@ -201,6 +203,9 @@ public class ApkBuilder extends BaseBuilder { // get a project object IProject project = getProject(); + // Top level check to make sure the build can move forward. + abortOnBadSetup(project); + // get the list of referenced projects. IProject[] referencedProjects = ProjectHelper.getReferencedProjects(project); IJavaProject[] referencedJavaProjects = getJavaProjects(referencedProjects); @@ -215,6 +220,7 @@ public class ApkBuilder extends BaseBuilder { // First thing we do is go through the resource delta to not // lose it if we have to abort the build for any reason. + ApkDeltaVisitor dv = null; if (kind == FULL_BUILD) { AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, Messages.Start_Full_Apk_Build); @@ -233,22 +239,13 @@ public class ApkBuilder extends BaseBuilder { mConvertToDex = true; mBuildFinalPackage = true; } else { - ApkDeltaVisitor dv = new ApkDeltaVisitor(this, sourceList, outputFolder); + dv = new ApkDeltaVisitor(this, sourceList, outputFolder); delta.accept(dv); // save the state mPackageResources |= dv.getPackageResources(); mConvertToDex |= dv.getConvertToDex(); mBuildFinalPackage |= dv.getMakeFinalPackage(); - - if (dv.mXmlError) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Xml_Error); - - // if there was some XML errors, we just return w/o doing - // anything since we've put some markers in the files anyway - return referencedProjects; - } } // also go through the delta for all the referenced projects, until we are forced to @@ -258,78 +255,28 @@ public class ApkBuilder extends BaseBuilder { IJavaProject referencedJavaProject = referencedJavaProjects[i]; delta = getDelta(referencedJavaProject.getProject()); if (delta != null) { - ReferencedProjectDeltaVisitor dv = new ReferencedProjectDeltaVisitor( + ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor( referencedJavaProject); - delta.accept(dv); + delta.accept(refProjectDv); // save the state - mConvertToDex |= dv.needDexConvertion(); - mBuildFinalPackage |= dv.needMakeFinalPackage(); + mConvertToDex |= refProjectDv.needDexConvertion(); + mBuildFinalPackage |= refProjectDv.needMakeFinalPackage(); } } } - - // do some extra check, in case the output files are not present. This - // will force to recreate them. - IResource tmp = null; - - if (mPackageResources == false && outputFolder != null) { - tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_); - if (tmp == null || tmp.exists() == false) { - mPackageResources = true; - mBuildFinalPackage = true; - } - } - if (mConvertToDex == false && outputFolder != null) { - tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX); - if (tmp == null || tmp.exists() == false) { - mConvertToDex = true; - mBuildFinalPackage = true; - } - } - - // also check the final file! - String finalPackageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; - if (mBuildFinalPackage == false && outputFolder != null) { - tmp = outputFolder.findMember(finalPackageName); - if (tmp == null || (tmp instanceof IFile && - tmp.exists() == false)) { - String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName); - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); - mBuildFinalPackage = true; - } - } - + // store the build status in the persistent storage saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex); saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); - // At this point, we can abort the build if we have to, as we have computed - // our resource delta and stored the result. - - // check if we have finished loading the SDK. - if (AdtPlugin.getDefault().getSdkLoadStatus(javaProject) != LoadStatus.LOADED) { - // we exit silently - return referencedProjects; - } - - // Now check the compiler compliance level, not displaying the error - // message since this is not the first builder. - if (ProjectHelper.checkCompilerCompliance(getProject()) - != ProjectHelper.COMPILER_COMPLIANCE_OK) { - return referencedProjects; - } - - // now check if the project has problem marker already - if (ProjectHelper.hasError(project, true)) { - // we found a marker with error severity: we abort the build. - // Since this is going to happen every time we save a file while - // errors are remaining, we do not force the display of the console, which - // would, in most cases, show on top of the Problem view (which is more - // important in that case). + if (dv != null && dv.mXmlError) { AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Project_Has_Errors); + Messages.Xml_Error); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway return referencedProjects; } @@ -350,6 +297,82 @@ public class ApkBuilder extends BaseBuilder { return referencedProjects; } + // get the extra configs for the project. + // The map contains (name, filter) where 'name' is a name to be used in the apk filename, + // and filter is the resource filter to be used in the aapt -c parameters to restrict + // which resource configurations to package in the apk. + Map configs = Sdk.getCurrent().getProjectApkConfigs(project); + + // do some extra check, in case the output files are not present. This + // will force to recreate them. + IResource tmp = null; + + if (mPackageResources == false) { + // check the full resource package + tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_); + if (tmp == null || tmp.exists() == false) { + mPackageResources = true; + mBuildFinalPackage = true; + } else { + // if the full package is present, we check the filtered resource packages as well + if (configs != null) { + Set> entrySet = configs.entrySet(); + + for (Entry entry : entrySet) { + String filename = String.format(AndroidConstants.FN_RESOURCES_S_AP_, + entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, filename); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mPackageResources = true; + mBuildFinalPackage = true; + break; + } + } + } + } + } + + // check classes.dex is present. If not we force to recreate it. + if (mConvertToDex == false) { + tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX); + if (tmp == null || tmp.exists() == false) { + mConvertToDex = true; + mBuildFinalPackage = true; + } + } + + // also check the final file(s)! + String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/); + if (mBuildFinalPackage == false) { + tmp = outputFolder.findMember(finalPackageName); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mBuildFinalPackage = true; + } else if (configs != null) { + // if the full apk is present, we check the filtered apk as well + Set> entrySet = configs.entrySet(); + + for (Entry entry : entrySet) { + String filename = ProjectHelper.getApkFilename(project, entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, filename); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mBuildFinalPackage = true; + break; + } + } + } + } + // at this point we know if we need to recreate the temporary apk // or the dex file, but we don't know if we simply need to recreate them // because they are missing @@ -381,9 +404,23 @@ public class ApkBuilder extends BaseBuilder { // handle already present .apk, and if that one failed as well, the user will be // notified. finalPackage.delete(); + + if (configs != null) { + Set> entrySet = configs.entrySet(); + for (Entry entry : entrySet) { + String packageFilepath = osBinPath + File.separator + + ProjectHelper.getApkFilename(project, entry.getKey()); + + finalPackage = new File(packageFilepath); + finalPackage.delete(); + } + } // first we check if we need to package the resources. if (mPackageResources) { + // remove some aapt_package only markers. + removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE); + // need to figure out some path before we can execute aapt; // resource to the AndroidManifest.xml file @@ -424,13 +461,30 @@ public class ApkBuilder extends BaseBuilder { osAssetsPath = assetsFolder.getLocation().toOSString(); } + // build the default resource package if (executeAapt(project, osManifestPath, osResPath, osAssetsPath, osBinPath + File.separator + - AndroidConstants.FN_RESOURCES_AP_) == false) { + AndroidConstants.FN_RESOURCES_AP_, null /*configFilter*/) == false) { // aapt failed. Whatever files that needed to be marked // have already been marked. We just return. return referencedProjects; } + + // now do the same thing for all the configured resource packages. + if (configs != null) { + Set> entrySet = configs.entrySet(); + for (Entry entry : entrySet) { + String outPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + String outPath = String.format(outPathFormat, entry.getKey()); + if (executeAapt(project, osManifestPath, osResPath, + osAssetsPath, outPath, entry.getValue()) == false) { + // aapt failed. Whatever files that needed to be marked + // have already been marked. We just return. + return referencedProjects; + } + } + } // build has been done. reset the state of the builder mPackageResources = false; @@ -456,25 +510,49 @@ public class ApkBuilder extends BaseBuilder { } // now we need to make the final package from the intermediary apk - // and classes.dex + // and classes.dex. + // This is the default package with all the resources. + String classesDexPath = osBinPath + File.separator + AndroidConstants.FN_CLASSES_DEX; if (finalPackage(osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_, - osBinPath + File.separator + AndroidConstants.FN_CLASSES_DEX, - osFinalPackagePath, javaProject, referencedJavaProjects) == false) { + classesDexPath,osFinalPackagePath, javaProject, + referencedJavaProjects) == false) { return referencedProjects; - } else { - // get the resource to bin - outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); - - // build has been done. reset the state of the builder - mBuildFinalPackage = false; - - // and store it - saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); - - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), - "Build Success!"); } + + // now do the same thing for all the configured resource packages. + if (configs != null) { + String resPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + + Set> entrySet = configs.entrySet(); + for (Entry entry : entrySet) { + // make the filename for the resource package. + String resPath = String.format(resPathFormat, entry.getKey()); + + // make the filename for the apk to generate + String apkOsFilePath = osBinPath + File.separator + + ProjectHelper.getApkFilename(project, entry.getKey()); + if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject, + referencedJavaProjects) == false) { + return referencedProjects; + } + } + } + + // we are done. + + // get the resource to bin + outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); + + // build has been done. reset the state of the builder + mBuildFinalPackage = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + "Build Success!"); } return referencedProjects; } @@ -498,19 +576,27 @@ public class ApkBuilder extends BaseBuilder { * @param osResPath The path to the res folder * @param osAssetsPath The path to the assets folder. This can be null. * @param osOutFilePath The path to the temporary resource file to create. + * @param configFilter The configuration filter for the resources to include + * (used with -c option, for example "port,en,fr" to include portrait, English and French + * resources.) * @return true if success, false otherwise. */ private boolean executeAapt(IProject project, String osManifestPath, - String osResPath, String osAssetsPath, String osOutFilePath) { + String osResPath, String osAssetsPath, String osOutFilePath, String configFilter) { + IAndroidTarget target = Sdk.getCurrent().getTarget(project); // Create the command line. ArrayList commandArray = new ArrayList(); - commandArray.add(AdtPlugin.getOsAbsoluteAapt()); + commandArray.add(target.getPath(IAndroidTarget.AAPT)); commandArray.add("package"); //$NON-NLS-1$ commandArray.add("-f");//$NON-NLS-1$ if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { commandArray.add("-v"); //$NON-NLS-1$ } + if (configFilter != null) { + commandArray.add("-c"); //$NON-NLS-1$ + commandArray.add(configFilter); + } commandArray.add("-M"); //$NON-NLS-1$ commandArray.add(osManifestPath); commandArray.add("-S"); //$NON-NLS-1$ @@ -520,8 +606,7 @@ public class ApkBuilder extends BaseBuilder { commandArray.add(osAssetsPath); } commandArray.add("-I"); //$NON-NLS-1$ - commandArray.add( - Sdk.getCurrent().getTarget(project).getPath(IAndroidTarget.ANDROID_JAR)); + commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR)); commandArray.add("-F"); //$NON-NLS-1$ commandArray.add(osOutFilePath); @@ -599,14 +684,19 @@ public class ApkBuilder extends BaseBuilder { */ private boolean executeDx(IJavaProject javaProject, String osBinPath, String osOutFilePath, IJavaProject[] referencedJavaProjects) throws CoreException { + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + if (targetData == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + // get the dex wrapper - DexWrapper wrapper = DexWrapper.getWrapper(); + DexWrapper wrapper = targetData.getDexWrapper(); if (wrapper == null) { - if (DexWrapper.getStatus() == LoadStatus.FAILED) { - throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, - Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); - } + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); } // make sure dx use the proper output streams. @@ -876,9 +966,10 @@ public class ApkBuilder extends BaseBuilder { * @param javaProject the javaProject object. * @param referencedJavaProjects the java projects that this project references. * @throws IOException + * @throws CoreException */ private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject, - IJavaProject[] referencedJavaProjects) throws IOException { + IJavaProject[] referencedJavaProjects) throws IOException, CoreException { IWorkspace ws = ResourcesPlugin.getWorkspace(); IWorkspaceRoot wsRoot = ws.getRoot(); @@ -888,7 +979,9 @@ public class ApkBuilder extends BaseBuilder { writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list); for (IJavaProject referencedJavaProject : referencedJavaProjects) { - writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list); + if (referencedJavaProject.getProject().hasNature(AndroidConstants.NATURE)) { + writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list); + } } } @@ -977,7 +1070,9 @@ public class ApkBuilder extends BaseBuilder { } /** - * Returns the list of the output folders for the specified {@link IJavaProject} objects. + * Returns the list of the output folders for the specified {@link IJavaProject} objects, if + * they are Android projects. + * * @param referencedJavaProjects the java projects. * @return an array, always. Can be empty. * @throws CoreException @@ -989,19 +1084,21 @@ public class ApkBuilder extends BaseBuilder { IWorkspaceRoot wsRoot = ws.getRoot(); for (IJavaProject javaProject : referencedJavaProjects) { - // get the output folder - IPath path = null; - try { - path = javaProject.getOutputLocation(); - } catch (JavaModelException e) { - continue; - } - - IResource outputResource = wsRoot.findMember(path); - if (outputResource != null && outputResource.getType() == IResource.FOLDER) { - String outputOsPath = outputResource.getLocation().toOSString(); - - list.add(outputOsPath); + if (javaProject.getProject().hasNature(AndroidConstants.NATURE)) { + // get the output folder + IPath path = null; + try { + path = javaProject.getOutputLocation(); + } catch (JavaModelException e) { + continue; + } + + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + String outputOsPath = outputResource.getLocation().toOSString(); + + list.add(outputOsPath); + } } } @@ -1026,7 +1123,7 @@ public class ApkBuilder extends BaseBuilder { return list.toArray(new IJavaProject[list.size()]); } - + /** * Checks a {@link IFile} to make sure it should be packaged as standard resources. * @param file the IFile representing the file. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java index aec703d1a..5d6793a92 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java @@ -192,11 +192,16 @@ public class ApkDeltaVisitor extends BaseDeltaVisitor IPath parentPath = path.removeLastSegments(1); if (mOutputPath.equals(parentPath)) { String resourceName = resource.getName(); + // check if classes.dex was removed if (resourceName.equalsIgnoreCase(AndroidConstants.FN_CLASSES_DEX)) { mConvertToDex = true; mMakeFinalPackage = true; } else if (resourceName.equalsIgnoreCase( - AndroidConstants.FN_RESOURCES_AP_)) { + AndroidConstants.FN_RESOURCES_AP_) || + AndroidConstants.PATTERN_RESOURCES_S_AP_.matcher( + resourceName).matches()) { + // or if the default resources.ap_ or a configured version + // (resources-###.ap_) was removed. mPackageResources = true; mMakeFinalPackage = true; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java index 534c12343..c3d5ba671 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.XmlErrorHandler; @@ -26,6 +27,7 @@ import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -34,6 +36,10 @@ import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; @@ -144,6 +150,15 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { private final static Pattern sPattern8Line1 = Pattern.compile( "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ + /** + * 2 line aapt error
+ * "ERROR: Invalid configuration: foo"
+ * " ^^^"
+ * There's no need to parse the 2nd line. + */ + private final static Pattern sPattern9Line1 = Pattern.compile( + "^Invalid configuration: (.+)$"); //$NON-NLS-1$ + /** SAX Parser factory. */ private SAXParserFactory mParserFactory; @@ -435,8 +450,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String location = m.group(1); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project, IMarker.SEVERITY_ERROR) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } continue; @@ -460,7 +475,7 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { // display the error if (checkAndMark(location, null, msg, osRoot, project, - IMarker.SEVERITY_ERROR) == false) { + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } @@ -483,8 +498,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String lineStr = m.group(2); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project, IMarker.SEVERITY_ERROR) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } continue; @@ -497,8 +512,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String msg = m.group(3); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project, IMarker.SEVERITY_ERROR) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } @@ -521,8 +536,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String lineStr = m.group(2); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project, IMarker.SEVERITY_ERROR) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } @@ -537,8 +552,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String msg = m.group(3); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project,IMarker.SEVERITY_WARNING) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { return true; } @@ -553,8 +568,8 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { String msg = m.group(3); // check the values and attempt to mark the file. - if (checkAndMark(location, lineStr, msg, osRoot, - project, IMarker.SEVERITY_ERROR) == false) { + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } @@ -569,7 +584,25 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { // check the values and attempt to mark the file. if (checkAndMark(location, null, msg, osRoot, project, - IMarker.SEVERITY_ERROR) == false) { + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern9Line1.matcher(p); + if (m.matches()) { + String badConfig = m.group(1); + String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); + + // skip the next line + i++; + + // check the values and attempt to mark the file. + if (checkAndMark(null /*location*/, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { return true; } @@ -654,23 +687,25 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { /** * Check if the parameters gotten from the error output are valid, and mark * the file with an AAPT marker. - * @param location + * @param location the full OS path of the error file. If null, the project is marked * @param lineStr * @param message * @param root The root directory of the project, in OS specific format. * @param project + * @param markerId The marker id to put. * @param severity The severity of the marker to put (IMarker.SEVERITY_*) - * @return true if the parameters were valid and the file was marked - * sucessfully. + * @return true if the parameters were valid and the file was marked successfully. * * @see IMarker */ private final boolean checkAndMark(String location, String lineStr, - String message, String root, IProject project, int severity) { + String message, String root, IProject project, String markerId, int severity) { // check this is in fact a file - File f = new File(location); - if (f.exists() == false) { - return false; + if (location != null) { + File f = new File(location); + if (f.exists() == false) { + return false; + } } // get the line number @@ -687,16 +722,18 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { } // add the marker - IResource f2 = getResourceFromFullPath(location, root, project); - if (f2 == null) { - return false; + IResource f2 = project; + if (location != null) { + f2 = getResourceFromFullPath(location, root, project); + if (f2 == null) { + return false; + } } // check if there's a similar marker already, since aapt is launched twice boolean markerAlreadyExists = false; try { - IMarker[] markers = f2.findMarkers(AndroidConstants.MARKER_AAPT, true, - IResource.DEPTH_ZERO); + IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); for (IMarker marker : markers) { int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); @@ -727,10 +764,10 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { if (markerAlreadyExists == false) { if (line != -1) { - BaseProjectHelper.addMarker(f2, AndroidConstants.MARKER_AAPT, message, line, + BaseProjectHelper.addMarker(f2, markerId, message, line, severity); } else { - BaseProjectHelper.addMarker(f2, AndroidConstants.MARKER_AAPT, message, severity); + BaseProjectHelper.addMarker(f2, markerId, message, severity); } } @@ -841,4 +878,64 @@ abstract class BaseBuilder extends IncrementalProjectBuilder { return oslibraryList.toArray(new String[oslibraryList.size()]); } + + /** + * Aborts the build if the SDK/project setups are broken. This does not + * display any errors. + * + * @param project The {@link IJavaProject} being compiled. + * @throws CoreException + */ + protected final void abortOnBadSetup(IProject project) throws CoreException { + // check if we have finished loading the SDK. + if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) { + // we exit silently + stopBuild("SDK is not loaded yet"); + } + + // abort if there are TARGET or ADT type markers + IMarker[] markers = project.findMarkers(AdtConstants.MARKER_TARGET, + false /*includeSubtypes*/, IResource.DEPTH_ZERO); + + if (markers.length > 0) { + stopBuild(""); + } + + markers = project.findMarkers(AdtConstants.MARKER_ADT, false /*includeSubtypes*/, + IResource.DEPTH_ZERO); + + if (markers.length > 0) { + stopBuild(""); + } + } + + /** + * Throws an exception to cancel the build. + * + * @param error the error message + * @param args the printf-style arguments to the error message. + * @throws CoreException + */ + protected final void stopBuild(String error, Object... args) throws CoreException { + throw new CoreException(new Status(IStatus.CANCEL, AdtPlugin.PLUGIN_ID, + String.format(error, args))); + } + + /** + * Recursively delete all the derived resources. + */ + protected void removeDerivedResources(IResource resource, IProgressMonitor monitor) + throws CoreException { + if (resource.exists()) { + if (resource.isDerived()) { + resource.delete(true, new SubProgressMonitor(monitor, 10)); + } else if (resource.getType() == IResource.FOLDER) { + IFolder folder = (IFolder)resource; + IResource[] members = folder.members(); + for (IResource member : members) { + removeDerivedResources(member, monitor); + } + } + } + } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java index cba8ad76e..65ad4f5be 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java @@ -17,7 +17,6 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.sdk.LoadStatus; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; @@ -46,10 +45,6 @@ public final class DexWrapper { private final static String MAIN_RUN = "run"; //$NON-NLS-1$ - private static DexWrapper sWrapper; - - private static LoadStatus sLoadStatus = LoadStatus.LOADING; - private Method mRunMethod; private Constructor mArgConstructor; @@ -62,15 +57,16 @@ public final class DexWrapper { private Field mConsoleErr; /** - * Loads the dex library from a file path. The loaded library can be used with the - * {@link DexWrapper} object returned by {@link #getWrapper()} + * Loads the dex library from a file path. + * + * The loaded library can be used via + * {@link DexWrapper#run(String, String[], boolean, PrintStream, PrintStream)}. + * * @param osFilepath the location of the dex.jar file. * @return an IStatus indicating the result of the load. */ - public static synchronized IStatus loadDex(String osFilepath) { + public synchronized IStatus loadDex(String osFilepath) { try { - sWrapper = null; - File f = new File(osFilepath); if (f.isFile() == false) { return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( @@ -86,44 +82,38 @@ public final class DexWrapper { Class consoleClass = loader.loadClass(DEX_CONSOLE); Class argClass = loader.loadClass(DEX_ARGS); - sWrapper = new DexWrapper(mainClass, argClass, consoleClass); - + try { + // now get the fields/methods we need + mRunMethod = mainClass.getMethod(MAIN_RUN, argClass); + + mArgConstructor = argClass.getConstructor(); + mArgOutName = argClass.getField("outName"); //$NON-NLS-1$ + mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$ + mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$ + mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$ + + mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$ + mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$ + + } catch (SecurityException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e); + } catch (NoSuchMethodException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e); + } catch (NoSuchFieldException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e); + } + return Status.OK_STATUS; } catch (MalformedURLException e) { // really this should not happen. - return createErrorStatus(String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); } catch (ClassNotFoundException e) { - return createErrorStatus(String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); - } catch (CoreException e) { - return e.getStatus(); - } finally { - if (sWrapper == null) { - sLoadStatus = LoadStatus.FAILED; - } else { - sLoadStatus = LoadStatus.LOADED; - } + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); } } - /** - * Unloads the loaded dex wrapper. - */ - public static synchronized void unloadDex() { - sWrapper = null; - sLoadStatus = LoadStatus.LOADING; - } - - public static synchronized DexWrapper getWrapper() { - return sWrapper; - } - - /** - * Returns the {@link LoadStatus}. - */ - public static synchronized LoadStatus getStatus() { - return sLoadStatus; - } - /** * Runs the dex command. * @param osOutFilePath the OS path to the outputfile (classes.dex @@ -169,33 +159,6 @@ public final class DexWrapper { } } - private DexWrapper(Class mainClass, Class argClass, Class consoleClass) - throws CoreException { - try { - // now get the fields/methods we need - mRunMethod = mainClass.getMethod(MAIN_RUN, argClass); - - mArgConstructor = argClass.getConstructor(); - mArgOutName = argClass.getField("outName"); //$NON-NLS-1$ - mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$ - mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$ - mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$ - - mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$ - mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$ - - } catch (SecurityException e) { - throw new CoreException(createErrorStatus( - Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e)); - } catch (NoSuchMethodException e) { - throw new CoreException(createErrorStatus( - Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e)); - } catch (NoSuchFieldException e) { - throw new CoreException(createErrorStatus( - Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e)); - } - } - private static IStatus createErrorStatus(String message, Exception e) { AdtPlugin.log(e, message); AdtPlugin.printErrorToConsole(Messages.DexWrapper_Dex_Loader, message); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java index 9fc434880..c5082838a 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java @@ -19,17 +19,14 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.FixLaunchConfig; -import com.android.ide.eclipse.adt.project.ProjectHelper; -import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.XmlErrorHandler.BasicXmlErrorListener; import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; -import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; @@ -37,13 +34,13 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IWorkspaceRoot; -import org.eclipse.core.resources.ResourceAttributes; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; @@ -69,13 +66,8 @@ public class PreCompilerBuilder extends BaseBuilder { private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$ - private static final String PROPERTY_SOURCE_FOLDER = - "manifestPackageSourceFolder"; //$NON-NLS-1$ - private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$ - - static final String PROPERTY_ANDROID_GENERATED = "androidGenerated"; //$NON-NLS-1$ - static final String PROPERTY_ANDROID_CONFLICT = "androidConflict"; //$NON-NLS-1$ + private static final String PROPERTY_COMPILE_AIDL = "compileAidl"; //$NON-NLS-1$ /** * Single line aidl error
@@ -84,24 +76,58 @@ public class PreCompilerBuilder extends BaseBuilder { private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):\\s(.+)$"); //$NON-NLS-1$ /** - * Compile flag. This is set to true if one of the changed/added/removed - * file is a resource file. Upon visiting all the delta resources, if - * this flag is true, then we know we'll have to compile the resources - * into R.java + * Data to temporarly store aidl source file information */ - private boolean mCompileResources = false; + static class AidlData { + IFile aidlFile; + IFolder sourceFolder; + + AidlData(IFolder sourceFolder, IFile aidlFile) { + this.sourceFolder = sourceFolder; + this.aidlFile = aidlFile; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof AidlData) { + AidlData file = (AidlData)obj; + return aidlFile.equals(file.aidlFile) && sourceFolder.equals(file.sourceFolder); + } + + return false; + } + } + + /** + * Resource Compile flag. This flag is reset to false after each successful compilation, and + * stored in the project persistent properties. This allows the builder to remember its state + * when the project is closed/opened. + */ + private boolean mMustCompileResources = false; /** List of .aidl files found that are modified or new. */ - private final ArrayList mAidlToCompile = new ArrayList(); + private final ArrayList mAidlToCompile = new ArrayList(); /** List of .aidl files that have been removed. */ - private final ArrayList mAidlToRemove = new ArrayList(); + private final ArrayList mAidlToRemove = new ArrayList(); /** cache of the java package defined in the manifest */ private String mManifestPackage; + + /** Output folder for generated Java File. Created on the Builder init + * @see #startupOnInitialize() + */ + private IFolder mGenFolder; - /** Source folder containing the java package defined in the manifest. */ - private IFolder mManifestPackageSourceFolder; + /** + * Progress monitor used at the end of every build to refresh the content of the 'gen' folder + * and set the generated files as derived. + */ + private DerivedProgressMonitor mDerivedProgressMonitor; /** * Progress monitor waiting the end of the process to set a persistent value @@ -110,91 +136,36 @@ public class PreCompilerBuilder extends BaseBuilder { * to be known by eclipse, before we can call resource.setPersistentProperty on * a new file. */ - private static class RefreshProgressMonitor implements IProgressMonitor { - private boolean mCancelled = false; - private IFile mNewFile; - private IFile mSource; - private boolean mDoneExecuted = false; - public RefreshProgressMonitor(IFile newFile, IFile source) { - mNewFile = newFile; - mSource = source; - } - - public void beginTask(String name, int totalWork) { - } - - public void done() { - if (mDoneExecuted == false) { - mDoneExecuted = true; - if (mNewFile.exists()) { - ProjectHelper.saveResourceProperty(mNewFile, PROPERTY_ANDROID_GENERATED, - mSource); - try { - mNewFile.setDerived(true); - } catch (CoreException e) { - // This really shouldn't happen since we check that the resource exist. - // Worst case scenario, the resource isn't marked as derived. - } - } - } - } - - public void internalWorked(double work) { - } - - public boolean isCanceled() { - return mCancelled; - } - - public void setCanceled(boolean value) { - mCancelled = value; - } - - public void setTaskName(String name) { - } - - public void subTask(String name) { - } - - public void worked(int work) { - } - } - - /** - * Progress Monitor setting up to two files as derived once their parent is refreshed. - * This is used as ProgressMonitor to refresh the R.java/Manifest.java parent (to display - * the newly created files in the package explorer). - */ private static class DerivedProgressMonitor implements IProgressMonitor { private boolean mCancelled = false; - private IFile mFile1; - private IFile mFile2; - private boolean mDoneExecuted = false; - public DerivedProgressMonitor(IFile file1, IFile file2) { - mFile1 = file1; - mFile2 = file2; + private final ArrayList mFileList = new ArrayList(); + private boolean mDone = false; + public DerivedProgressMonitor() { + } + + void addFile(IFile file) { + mFileList.add(file); + } + + void reset() { + mFileList.clear(); + mDone = false; } public void beginTask(String name, int totalWork) { } public void done() { - if (mDoneExecuted == false) { - if (mFile1 != null && mFile1.exists()) { - mDoneExecuted = true; - try { - mFile1.setDerived(true); - } catch (CoreException e) { - // This really shouldn't happen since we check that the resource edit. - // Worst case scenario, the resource isn't marked as derived. - } - } - if (mFile2 != null && mFile2.exists()) { - try { - mFile2.setDerived(true); - } catch (CoreException e) { - // This really shouldn't happen since we check that the resource edit. - // Worst case scenario, the resource isn't marked as derived. + if (mDone == false) { + mDone = true; + for (IFile file : mFileList) { + if (file.exists()) { + try { + file.setDerived(true); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource exist. + // Worst case scenario, the resource isn't marked as derived. + } } } } @@ -224,344 +195,312 @@ public class PreCompilerBuilder extends BaseBuilder { public PreCompilerBuilder() { super(); } - + // build() returns a list of project from which this project depends for future compilation. @SuppressWarnings("unchecked") //$NON-NLS-1$ @Override protected IProject[] build(int kind, Map args, IProgressMonitor monitor) throws CoreException { - // First thing we do is go through the resource delta to not - // lose it if we have to abort the build for any reason. + try { + mDerivedProgressMonitor.reset(); - // get the project objects - IProject project = getProject(); - IJavaProject javaProject = JavaCore.create(project); - - // now we need to get the classpath list - ArrayList sourceList = BaseProjectHelper.getSourceClasspaths(javaProject); - - PreCompilerDeltaVisitor dv = null; - String javaPackage = null; - - if (kind == FULL_BUILD) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Start_Full_Pre_Compiler); - mCompileResources = true; - buildAidlCompilationList(project, sourceList); - } else { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Start_Inc_Pre_Compiler); - - // Go through the resources and see if something changed. - // Even if the mCompileResources flag is true from a previously aborted - // build, we need to go through the Resource delta to get a possible - // list of aidl files to compile/remove. - IResourceDelta delta = getDelta(project); - if (delta == null) { - mCompileResources = true; - buildAidlCompilationList(project, sourceList); + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + + // get the project objects + IProject project = getProject(); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(project); + + IJavaProject javaProject = JavaCore.create(project); + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + + // now we need to get the classpath list + ArrayList sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( + javaProject); + + PreCompilerDeltaVisitor dv = null; + String javaPackage = null; + + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Full_Pre_Compiler); + mMustCompileResources = true; + buildAidlCompilationList(project, sourceFolderPathList); } else { - dv = new PreCompilerDeltaVisitor(this, sourceList); - delta.accept(dv); - - // record the state - mCompileResources |= dv.getCompileResources(); - - // handle aidl modification - if (dv.getFullAidlRecompilation()) { - buildAidlCompilationList(project, sourceList); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Inc_Pre_Compiler); + + // Go through the resources and see if something changed. + // Even if the mCompileResources flag is true from a previously aborted + // build, we need to go through the Resource delta to get a possible + // list of aidl files to compile/remove. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mMustCompileResources = true; + buildAidlCompilationList(project, sourceFolderPathList); } else { + dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList); + delta.accept(dv); + + // record the state + mMustCompileResources |= dv.getCompileResources(); + + // handle aidl modification, and update mMustCompileAidl mergeAidlFileModifications(dv.getAidlToCompile(), dv.getAidlToRemove()); + + // get the java package from the visitor + javaPackage = dv.getManifestPackage(); } - - // if there was some XML errors, we just return w/o doing - // anything since we've put some markers in the files anyway. - if (dv.mXmlError) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Xml_Error); - - return null; - } - - // get the java package from the visitor - javaPackage = dv.getManifestPackage(); } - } - - // store the build status in the persistent storage - saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mCompileResources); - // TODO also needs to store the list of aidl to compile/remove - - // At this point we have stored what needs to be build, so we can - // do some high level test and abort if needed. - - // check if we have finished loading the SDK. - if (AdtPlugin.getDefault().getSdkLoadStatus(javaProject) != LoadStatus.LOADED) { - // we exit silently - return null; - } - - // check the compiler compliance level, not displaying the error message - // since this is not the first builder. - if (ProjectHelper.checkCompilerCompliance(getProject()) - != ProjectHelper.COMPILER_COMPLIANCE_OK) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Compiler_Compliance_Error); - return null; - } - - // Check that the SDK directory has been setup. - String osSdkFolder = AdtPlugin.getOsSdkFolder(); - - if (osSdkFolder == null || osSdkFolder.length() == 0) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.No_SDK_Setup_Error); - markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error, - IMarker.SEVERITY_ERROR); - return null; - } - - IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); - if (projectTarget == null) { - // no target. error has been output by the container initializer: exit silently. - return null; - } - - // get the manifest file - IFile manifest = AndroidManifestHelper.getManifest(project); - - if (manifest == null) { - String msg = String.format(Messages.s_File_Missing, - AndroidConstants.FN_ANDROID_MANIFEST); - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); - markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); - return null; - } - - // lets check the XML of the manifest first, if that hasn't been done by the - // resource delta visitor yet. - if (dv == null || dv.getCheckedManifestXml() == false) { - BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); - AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest, - errorListener); - - if (errorListener.mHasXmlError == true) { - // there was an error in the manifest, its file has been marked, - // by the XmlErrorHandler. - // We return; - String msg = String.format(Messages.s_Contains_Xml_Error, + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway. + if (dv != null && dv.mXmlError) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Xml_Error); + + // This interrupts the build. The next builders will not run. + stopBuild(Messages.Xml_Error); + } + + + // get the manifest file + IFile manifest = AndroidManifestParser.getManifest(project); + + if (manifest == null) { + String msg = String.format(Messages.s_File_Missing, AndroidConstants.FN_ANDROID_MANIFEST); AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); - return null; + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); } - - // get the java package from the parser - javaPackage = parser.getPackage(); - } - - if (javaPackage == null || javaPackage.length() == 0) { - // looks like the AndroidManifest file isn't valid. - String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, - AndroidConstants.FN_ANDROID_MANIFEST); - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - msg); - return null; - } - - // at this point we have the java package. We need to make sure it's not a different package - // than the previous one that were built. - if (javaPackage.equals(mManifestPackage) == false) { - // The manifest package has changed, the user may want to update - // the launch configuration - if (mManifestPackage != null) { + + // lets check the XML of the manifest first, if that hasn't been done by the + // resource delta visitor yet. + if (dv == null || dv.getCheckedManifestXml() == false) { + BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); + AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest, + errorListener); + + if (errorListener.mHasXmlError == true) { + // there was an error in the manifest, its file has been marked, + // by the XmlErrorHandler. + // We return; + String msg = String.format(Messages.s_Contains_Xml_Error, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // get the java package from the parser + javaPackage = parser.getPackage(); + } + + if (javaPackage == null || javaPackage.length() == 0) { + // looks like the AndroidManifest file isn't valid. + String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, + AndroidConstants.FN_ANDROID_MANIFEST); AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Checking_Package_Change); - - FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, javaPackage); - flc.start(); + msg); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); } - - // now we delete the generated classes from their previous location - deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, - mManifestPackageSourceFolder, mManifestPackage); - deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, - mManifestPackageSourceFolder, mManifestPackage); - - // record the new manifest package, and save it. - mManifestPackage = javaPackage; - saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); - } - - if (mCompileResources) { - // we need to figure out where to store the R class. - // get the parent folder for R.java and update mManifestPackageSourceFolder - IFolder packageFolder = getManifestPackageFolder(project, sourceList); - - // at this point, either we have found the package or not. - // if we haven't well it's time to tell the user and abort - if (mManifestPackageSourceFolder == null) { - // mark the manifest file - String message = String.format(Messages.Package_s_Doesnt_Exist_Error, + + // at this point we have the java package. We need to make sure it's not a different + // package than the previous one that were built. + if (javaPackage.equals(mManifestPackage) == false) { + // The manifest package has changed, the user may want to update + // the launch configuration + if (mManifestPackage != null) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Checking_Package_Change); + + FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, + javaPackage); + flc.start(); + } + + // now we delete the generated classes from their previous location + deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, mManifestPackage); - BaseProjectHelper.addMarker(manifest, AndroidConstants.MARKER_AAPT, message, - IMarker.SEVERITY_ERROR); - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, message); - - // abort - return null; + deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, + mManifestPackage); + + // record the new manifest package, and save it. + mManifestPackage = javaPackage; + saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); } - - - // found the folder in which to write the stuff - - // get the resource folder - IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES); - - // get the file system path - IPath outputLocation = mManifestPackageSourceFolder.getLocation(); - IPath resLocation = resFolder.getLocation(); - IPath manifestLocation = manifest.getLocation(); - - // those locations have to exist for us to do something! - if (outputLocation != null && resLocation != null - && manifestLocation != null) { - String osOutputPath = outputLocation.toOSString(); - String osResPath = resLocation.toOSString(); - String osManifestPath = manifestLocation.toOSString(); - - // remove the aapt markers - removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT); - removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT); - - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Preparing_Generated_Files); - - // since the R.java file may be already existing in read-only - // mode we need to make it readable so that aapt can overwrite - // it - IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS); - prepareFileForExternalModification(rJavaFile); - - // do the same for the Manifest.java class - IFile manifestJavaFile = packageFolder.getFile(AndroidConstants.FN_MANIFEST_CLASS); - prepareFileForExternalModification(manifestJavaFile); - - // we actually need to delete the manifest.java as it may become empty and in this - // case aapt doesn't generate an empty one, but instead doesn't touch it. - manifestJavaFile.delete(true, null); - - // launch aapt: create the command line - ArrayList array = new ArrayList(); - array.add(AdtPlugin.getOsAbsoluteAapt()); - array.add("package"); //$NON-NLS-1$ - array.add("-m"); //$NON-NLS-1$ - if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { - array.add("-v"); //$NON-NLS-1$ - } - array.add("-J"); //$NON-NLS-1$ - array.add(osOutputPath); - array.add("-M"); //$NON-NLS-1$ - array.add(osManifestPath); - array.add("-S"); //$NON-NLS-1$ - array.add(osResPath); - array.add("-I"); //$NON-NLS-1$ - array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); - - if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { - StringBuilder sb = new StringBuilder(); - for (String c : array) { - sb.append(c); - sb.append(' '); + + if (mMustCompileResources) { + // we need to figure out where to store the R class. + // get the parent folder for R.java and update mManifestPackageSourceFolder + IFolder packageFolder = getGenManifestPackageFolder(project); + + // get the resource folder + IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES); + + // get the file system path + IPath outputLocation = mGenFolder.getLocation(); + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifest.getLocation(); + + // those locations have to exist for us to do something! + if (outputLocation != null && resLocation != null + && manifestLocation != null) { + String osOutputPath = outputLocation.toOSString(); + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + // remove the aapt markers + removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT_COMPILE); + removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT_COMPILE); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Preparing_Generated_Files); + + // since the R.java file may be already existing in read-only + // mode we need to make it readable so that aapt can overwrite + // it + IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS); + + // do the same for the Manifest.java class + IFile manifestJavaFile = packageFolder.getFile( + AndroidConstants.FN_MANIFEST_CLASS); + + // we actually need to delete the manifest.java as it may become empty and + // in this case aapt doesn't generate an empty one, but instead doesn't + // touch it. + manifestJavaFile.delete(true, null); + + // launch aapt: create the command line + ArrayList array = new ArrayList(); + array.add(projectTarget.getPath(IAndroidTarget.AAPT)); + array.add("package"); //$NON-NLS-1$ + array.add("-m"); //$NON-NLS-1$ + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + array.add("-v"); //$NON-NLS-1$ } - String cmd_line = sb.toString(); - AdtPlugin.printToConsole(project, cmd_line); - } - - // launch - int execError = 1; - try { - // launch the command line process - Process process = Runtime.getRuntime().exec( - array.toArray(new String[array.size()])); - - // list to store each line of stderr - ArrayList results = new ArrayList(); - - // get the output and return code from the process - execError = grabProcessOutput(process, results); - - // attempt to parse the error output - boolean parsingError = parseAaptOutput(results, project); - - // if we couldn't parse the output we display it in the console. - if (parsingError) { - if (execError != 0) { - AdtPlugin.printErrorToConsole(project, results.toArray()); - } else { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_NORMAL, - project, results.toArray()); + array.add("-J"); //$NON-NLS-1$ + array.add(osOutputPath); + array.add("-M"); //$NON-NLS-1$ + array.add(osManifestPath); + array.add("-S"); //$NON-NLS-1$ + array.add(osResPath); + array.add("-I"); //$NON-NLS-1$ + array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : array) { + sb.append(c); + sb.append(' '); } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(project, cmd_line); } - - if (execError != 0) { - // if the exec failed, and we couldn't parse the error output (and therefore - // not all files that should have been marked, were marked), we put a - // generic marker on the project and abort. + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec( + array.toArray(new String[array.size()])); + + // list to store each line of stderr + ArrayList results = new ArrayList(); + + // get the output and return code from the process + execError = grabProcessOutput(process, results); + + // attempt to parse the error output + boolean parsingError = parseAaptOutput(results, project); + + // if we couldn't parse the output we display it in the console. if (parsingError) { - markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors, - IMarker.SEVERITY_ERROR); + if (execError != 0) { + AdtPlugin.printErrorToConsole(project, results.toArray()); + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_NORMAL, + project, results.toArray()); + } } - - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.AAPT_Error); - - // abort if exec failed. - return null; + + if (execError != 0) { + // if the exec failed, and we couldn't parse the error output + // (and therefore not all files that should have been marked, + // were marked), we put a generic marker on the project and abort. + if (parsingError) { + markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors, + IMarker.SEVERITY_ERROR); + } + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.AAPT_Error); + + // abort if exec failed. + // This interrupts the build. The next builders will not run. + stopBuild(Messages.AAPT_Error); + } + } catch (IOException e1) { + // something happen while executing the process, + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } catch (InterruptedException e) { + // we got interrupted waiting for the process to end... + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // if the return code was OK, we refresh the folder that + // contains R.java to force a java recompile. + if (execError == 0) { + // now add the R.java/Manifest.java to the list of file to be marked + // as derived. + mDerivedProgressMonitor.addFile(rJavaFile); + mDerivedProgressMonitor.addFile(manifestJavaFile); + + // build has been done. reset the state of the builder + mMustCompileResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, + mMustCompileResources); } - } catch (IOException e1) { - // something happen while executing the process, - // mark the project and exit - String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); - markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); - return null; - } catch (InterruptedException e) { - // we got interrupted waiting for the process to end... - // mark the project and exit - String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); - markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); - return null; - } - - // if the return code was OK, we refresh the folder that - // contains R.java to force a java recompile. - if (execError == 0) { - // now set the R.java/Manifest.java file as read only. - finishJavaFilesAfterExternalModification(rJavaFile, manifestJavaFile); - - // build has been done. reset the state of the builder - mCompileResources = false; - - // and store it - saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mCompileResources); } + } else { + // nothing to do } - } else { - // nothing to do - } - - // now handle the aidl stuff. - // look for a preprocessed aidl file - IResource projectAidl = project.findMember("project.aidl"); //$NON-NLS-1$ - String folderAidlPath = null; - if (projectAidl != null && projectAidl.exists()) { - folderAidlPath = projectAidl.getLocation().toOSString(); - } - boolean aidlStatus = handleAidl(sourceList, folderAidlPath, monitor); - - if (aidlStatus == false && mCompileResources == false) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, - Messages.Nothing_To_Compile); + + // now handle the aidl stuff. + boolean aidlStatus = handleAidl(projectTarget, sourceFolderPathList, monitor); + + if (aidlStatus == false && mMustCompileResources == false) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Nothing_To_Compile); + } + } finally { + // refresh the 'gen' source folder. Once this is done with the custom progress + // monitor to mark all new files as derived + mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); } return null; @@ -574,67 +513,57 @@ public class PreCompilerBuilder extends BaseBuilder { AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), Messages.Removing_Generated_Classes); - // check if we have the R.java info already. - if (mManifestPackageSourceFolder != null && mManifestPackage != null) { - deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, - mManifestPackageSourceFolder, mManifestPackage); - deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, - mManifestPackageSourceFolder, mManifestPackage); - } - - // FIXME: delete all java generated from aidl. + // remove all the derived resources from the 'gen' source folder. + removeDerivedResources(mGenFolder, monitor); } @Override protected void startupOnInitialize() { super.startupOnInitialize(); + + mDerivedProgressMonitor = new DerivedProgressMonitor(); + + IProject project = getProject(); // load the previous IFolder and java package. mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE); - IResource resource = loadProjectResourceProperty(PROPERTY_SOURCE_FOLDER); - if (resource instanceof IFolder) { - mManifestPackageSourceFolder = (IFolder)resource; - } + + // get the source folder in which all the Java files are created + mGenFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); - // Load the current compile flag. We ask for true if not found to force a + // Load the current compile flags. We ask for true if not found to force a // recompile. - mCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); + mMustCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); + boolean mustCompileAidl = loadProjectBooleanProperty(PROPERTY_COMPILE_AIDL, true); + + // if we stored that we have to compile some aidl, we build the list that will compile them + // all + if (mustCompileAidl) { + IJavaProject javaProject = JavaCore.create(project); + ArrayList sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( + javaProject); + + buildAidlCompilationList(project, sourceFolderPathList); + } } /** * Delete the a generated java class associated with the specified java package. * @param filename Name of the generated file to remove. - * @param sourceFolder The source Folder containing the old java package. * @param javaPackage the old java package */ - private void deleteObsoleteGeneratedClass(String filename, IFolder sourceFolder, - String javaPackage) { - if (sourceFolder == null || javaPackage == null) { + private void deleteObsoleteGeneratedClass(String filename, String javaPackage) { + if (javaPackage == null) { return; } - - // convert the java package into path - String[] segments = javaPackage.split(AndroidConstants.RE_DOT); - - StringBuilder path = new StringBuilder(); - for (String s : segments) { - path.append(AndroidConstants.WS_SEP_CHAR); - path.append(s); - } - - // appends the name of the generated file - path.append(AndroidConstants.WS_SEP_CHAR); - path.append(filename); - - Path iPath = new Path(path.toString()); + + IPath packagePath = getJavaPackagePath(javaPackage); + IPath iPath = packagePath.append(filename); // Find a matching resource object. - IResource javaFile = sourceFolder.findMember(iPath); + IResource javaFile = mGenFolder.findMember(iPath); if (javaFile != null && javaFile.exists() && javaFile.getType() == IResource.FILE) { try { - // remove the read-only tag - prepareFileForExternalModification((IFile)javaFile); - // delete javaFile.delete(true, null); @@ -643,7 +572,8 @@ public class PreCompilerBuilder extends BaseBuilder { } catch (CoreException e) { // failed to delete it, the user will have to delete it manually. - String message = String.format(Messages.Delete_Obsolete_Error, path); + String message = String.format(Messages.Delete_Obsolete_Error, + javaFile.getFullPath()); IProject project = getProject(); AdtPlugin.printErrorToConsole(project, message); AdtPlugin.printErrorToConsole(project, e.getMessage()); @@ -652,121 +582,61 @@ public class PreCompilerBuilder extends BaseBuilder { } /** - * Looks for the folder containing the package defined in the manifest. It looks in the - * list of source folders for the one containing folders matching the package defined in the - * manifest (from the field mManifestPackage). It returns the final folder, which - * will contain the R class, and update the field mManifestPackageSourceFolder - * to be the source folder containing the full package. + * Creates a relative {@link IPath} from a java package. + * @param javaPackageName the java package. + */ + private IPath getJavaPackagePath(String javaPackageName) { + // convert the java package into path + String[] segments = javaPackageName.split(AndroidConstants.RE_DOT); + + StringBuilder path = new StringBuilder(); + for (String s : segments) { + path.append(AndroidConstants.WS_SEP_CHAR); + path.append(s); + } + + return new Path(path.toString()); + } + + /** + * Returns an {@link IFolder} (located inside the 'gen' source folder), that matches the + * package defined in the manifest. This {@link IFolder} may not actually exist + * (aapt will create it anyway). * @param project The project. - * @param sourceList The list of source folders for the project. - * @return the package that will contain the R class or null if the folder was not found. + * @return the {@link IFolder} that will contain the R class or null if the folder was not found. * @throws CoreException */ - private IFolder getManifestPackageFolder(IProject project, ArrayList sourceList) + private IFolder getGenManifestPackageFolder(IProject project) throws CoreException { - // split the package in segments - String[] packageSegments = mManifestPackage.split(AndroidConstants.RE_DOT); - - // we look for 2 folders. - // 1. The source folder that contains the full java package. - // we will store the folder in the field mJavaSourceFolder, for reuse during - IFolder manifestPackageSourceFolder = null; - // subsequent builds. This is the folder we will give to aapt. - // 2. The folder actually containing the R.java files. We need this one to do a refresh - IFolder packageFolder = null; - - for (IPath iPath : sourceList) { - int packageSegmentIndex = 0; - - // the path is relative to the workspace. We ignore the first segment, - // when getting the resource from the IProject object. - IResource classpathEntry = project.getFolder(iPath.removeFirstSegments(1)); - - if (classpathEntry instanceof IFolder) { - IFolder classpathFolder = (IFolder)classpathEntry; - IFolder folder = classpathFolder; - - boolean failed = false; - while (failed == false - && packageSegmentIndex < packageSegments.length) { - - // loop on that folder content looking for folders - // that match the package - // defined in AndroidManifest.xml - - // get the folder content - IResource[] content = folder.members(); - - // this is the segment we look for - String segment = packageSegments[packageSegmentIndex]; - - // did we find it at this level - boolean found = false; - - for (IResource r : content) { - // look for the java package segment - if (r instanceof IFolder) { - if (r.getName().equals(segment)) { - // we need to skip to the next one - folder = (IFolder)r; - packageSegmentIndex++; - found = true; - break; - } - } - } - - // if we didn't find it at this level we just fail. - if (found == false) { - failed = true; - } - } - - // if we didn't fail then we found it. no point in - // looping through the rest - // or the classpathEntry - if (failed == false) { - // save the target folder reference - manifestPackageSourceFolder = classpathFolder; - packageFolder = folder; - break; - } - } - } - - // save the location of the folder into the persistent storage - if (manifestPackageSourceFolder != mManifestPackageSourceFolder) { - mManifestPackageSourceFolder = manifestPackageSourceFolder; - saveProjectResourceProperty(PROPERTY_SOURCE_FOLDER, mManifestPackageSourceFolder); - } - return packageFolder; + // get the path for the package + IPath packagePath = getJavaPackagePath(mManifestPackage); + + // get a folder for this path under the 'gen' source folder, and return it. + // This IFolder may not reference an actual existing folder. + return mGenFolder.getFolder(packagePath); } /** * Compiles aidl files into java. This will also removes old java files * created from aidl files that are now gone. + * @param projectTarget Target of the project * @param sourceFolders the list of source folders, relative to the workspace. - * @param folderAidlPath * @param monitor the projess monitor * @returns true if it did something * @throws CoreException */ - private boolean handleAidl(ArrayList sourceFolders, String folderAidlPath, + private boolean handleAidl(IAndroidTarget projectTarget, ArrayList sourceFolders, IProgressMonitor monitor) throws CoreException { if (mAidlToCompile.size() == 0 && mAidlToRemove.size() == 0) { return false; } - // create the command line - String[] command = new String[4 + sourceFolders.size() + (folderAidlPath != null ? 1 : 0)]; + String[] command = new String[4 + sourceFolders.size()]; int index = 0; - int aidlIndex; - command[index++] = AdtPlugin.getOsAbsoluteAidl(); - command[aidlIndex = index++] = "-p"; //$NON-NLS-1$ - if (folderAidlPath != null) { - command[index++] = "-p" + folderAidlPath; //$NON-NLS-1$ - } + command[index++] = projectTarget.getPath(IAndroidTarget.AIDL); + command[index++] = "-p" + Sdk.getCurrent().getTarget(getProject()).getPath( //$NON-NLS-1$ + IAndroidTarget.ANDROID_AIDL); // since the path are relative to the workspace and not the project itself, we need // the workspace root. @@ -777,81 +647,44 @@ public class PreCompilerBuilder extends BaseBuilder { } // list of files that have failed compilation. - ArrayList stillNeedCompilation = new ArrayList(); + ArrayList stillNeedCompilation = new ArrayList(); // if an aidl file is being removed before we managed to compile it, it'll be in // both list. We *need* to remove it from the compile list or it'll never go away. - for (IFile aidlFile : mAidlToRemove) { + for (AidlData aidlFile : mAidlToRemove) { int pos = mAidlToCompile.indexOf(aidlFile); if (pos != -1) { mAidlToCompile.remove(pos); } } - + // loop until we've compile them all - for (IFile aidlFile : mAidlToCompile) { + for (AidlData aidlData : mAidlToCompile) { // Remove the AIDL error markers from the aidl file - removeMarkersFromFile(aidlFile, AndroidConstants.MARKER_AIDL); + removeMarkersFromFile(aidlData.aidlFile, AndroidConstants.MARKER_AIDL); - // get the path - IPath iPath = aidlFile.getLocation(); - String osPath = iPath.toOSString(); - - // get the parent container - IContainer parentContainer = aidlFile.getParent(); - - // replace the extension in both the full path and the - // last segment - String osJavaPath = osPath.replaceAll(AndroidConstants.RE_AIDL_EXT, - AndroidConstants.DOT_JAVA); - String javaName = aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT, - AndroidConstants.DOT_JAVA); - - // check if we can compile it, or if there is a conflict with a java file - boolean conflict = ProjectHelper.loadBooleanProperty(aidlFile, - PROPERTY_ANDROID_CONFLICT, false); - if (conflict) { - String msg = String.format(Messages.AIDL_Java_Conflict, javaName, - aidlFile.getName()); - - // put a marker - BaseProjectHelper.addMarker(aidlFile, AndroidConstants.MARKER_AIDL, msg, - IMarker.SEVERITY_ERROR); - - // output an error - AdtPlugin.printErrorToConsole(getProject(), msg); - - stillNeedCompilation.add(aidlFile); - - // move on to next file - continue; - } - - // get the resource for the java file. - Path javaIPath = new Path(javaName); - IFile javaFile = parentContainer.getFile(javaIPath); - - // if the file was read-only, this will make it readable. - prepareFileForExternalModification(javaFile); + // get the path of the source file. + IPath sourcePath = aidlData.aidlFile.getLocation(); + String osSourcePath = sourcePath.toOSString(); + + IFile javaFile = getGenDestinationFile(aidlData, true /*createFolders*/, monitor); // finish to set the command line. - command[aidlIndex] = "-p" + Sdk.getCurrent().getTarget(aidlFile.getProject()).getPath( - IAndroidTarget.ANDROID_AIDL); //$NON-NLS-1$ - command[index] = osPath; - command[index + 1] = osJavaPath; + command[index] = osSourcePath; + command[index + 1] = javaFile.getLocation().toOSString(); // launch the process - if (execAidl(command, aidlFile) == false) { + if (execAidl(command, aidlData.aidlFile) == false) { // aidl failed. File should be marked. We add the file to the list // of file that will need compilation again. - stillNeedCompilation.add(aidlFile); + stillNeedCompilation.add(aidlData); // and we move on to the next one. continue; } else { - // since the exec worked, we refresh the parent, and set the - // file as read only. - finishFileAfterExternalModification(javaFile, aidlFile); + // make sure the file will be marked as derived once we refresh the 'gen' source + // folder. + mDerivedProgressMonitor.addFile(javaFile); } } @@ -860,43 +693,67 @@ public class PreCompilerBuilder extends BaseBuilder { mAidlToCompile.addAll(stillNeedCompilation); // Remove the java files created from aidl files that have been removed. - for (IFile aidlFile : mAidlToRemove) { - // make the java filename - String javaName = aidlFile.getName().replaceAll( - AndroidConstants.RE_AIDL_EXT, - AndroidConstants.DOT_JAVA); + for (AidlData aidlData : mAidlToRemove) { + IFile javaFile = getGenDestinationFile(aidlData, false /*createFolders*/, monitor); + if (javaFile.exists()) { + // This confirms the java file was generated by the builder, + // we can delete the aidlFile. + javaFile.delete(true, null); - // get the parent container - IContainer ic = aidlFile.getParent(); - - // and get the IFile corresponding to the java file. - IFile javaFile = ic.getFile(new Path(javaName)); - if (javaFile != null && javaFile.exists() ) { - // check if this java file has a persistent data marking it as generated by - // the builder. - // While we put the aidl path as a resource, internally it's all string anyway. - // We use loadStringProperty, because loadResourceProperty tries to match - // the string value (a path in this case) with an existing resource, but - // the aidl file was deleted, so it would return null, even though the property - // existed. - String aidlPath = ProjectHelper.loadStringProperty(javaFile, - PROPERTY_ANDROID_GENERATED); - - if (aidlPath != null) { - // This confirms the java file was generated by the builder, - // we can delete the aidlFile. - javaFile.delete(true, null); - - // Refresh parent. - ic.refreshLocal(IResource.DEPTH_ONE, monitor); - } + // Refresh parent. + javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, monitor); } } + mAidlToRemove.clear(); + // store the build state. If there are any files that failed to compile, we will + // force a full aidl compile on the next project open. (unless a full compilation succeed + // before the project is closed/re-opened.) + // TODO: Optimize by saving only the files that need compilation + saveProjectBooleanProperty(PROPERTY_COMPILE_AIDL , mAidlToCompile.size() > 0); + return true; } + /** + * Returns the {@link IFile} handle to the destination file for a given aild source file + * ({@link AidlData}). + * @param aidlData the data for the aidl source file. + * @param createFolders whether or not the parent folder of the destination should be created + * if it does not exist. + * @param monitor the progress monitor + * @return the handle to the destination file. + * @throws CoreException + */ + private IFile getGenDestinationFile(AidlData aidlData, boolean createFolders, + IProgressMonitor monitor) throws CoreException { + // build the destination folder path. + // Use the path of the source file, except for the path leading to its source folder, + // and for the last segment which is the filename. + int segmentToSourceFolderCount = aidlData.sourceFolder.getFullPath().segmentCount(); + IPath packagePath = aidlData.aidlFile.getFullPath().removeFirstSegments( + segmentToSourceFolderCount).removeLastSegments(1); + Path destinationPath = new Path(packagePath.toString()); + + // get an IFolder for this path. It's relative to the 'gen' folder already + IFolder destinationFolder = mGenFolder.getFolder(destinationPath); + + // create it if needed + if (destinationFolder.exists() == false && createFolders) { + destinationFolder.create(true /*force*/, true /*local*/, + new SubProgressMonitor(monitor, 10)); + } + + // Build the Java file name from the aidl name. + String javaName = aidlData.aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + + // get the resource for the java file. + IFile javaFile = destinationFolder.getFile(javaName); + return javaFile; + } + /** * Execute the aidl command line, parse the output, and mark the aidl file * with any reported errors. @@ -949,43 +806,28 @@ public class PreCompilerBuilder extends BaseBuilder { * Goes through the build paths and fills the list of aidl files to compile * ({@link #mAidlToCompile}). * @param project The project. - * @param buildPaths The list of build paths. + * @param sourceFolderPathList The list of source folder paths. */ private void buildAidlCompilationList(IProject project, - ArrayList buildPaths) { - for (IPath p : buildPaths) { - // Because the path contains the name of the project as well, we - // need to remove it, to access the final folder. - String[] segments = p.segments(); - IContainer folder = project; - for (int i = 1; i < segments.length; i++) { - IResource r = folder.findMember(segments[i]); - if (r != null && r.exists() && - r.getType() == IResource.FOLDER) { - folder = (IContainer)r; - } else { - // hmm looks like the build path is corrupted/wrong. - // reset and break - folder = project; - break; - } - } - - // did we ge a folder? - if (folder != project) { - // then we scan! - scanContainerForAidl(folder); + ArrayList sourceFolderPathList) { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath sourceFolderPath : sourceFolderPathList) { + IFolder sourceFolder = root.getFolder(sourceFolderPath); + // we don't look in the 'gen' source folder as there will be no source in there. + if (sourceFolder.exists() && sourceFolder.equals(mGenFolder) == false) { + scanFolderForAidl(sourceFolder, sourceFolder); } } } /** - * Scans a container and fills the list of aidl files to compile. - * @param container The container to scan. + * Scans a folder and fills the list of aidl files to compile. + * @param sourceFolder the root source folder. + * @param folder The folder to scan. */ - private void scanContainerForAidl(IContainer container) { + private void scanFolderForAidl(IFolder sourceFolder, IFolder folder) { try { - IResource[] members = container.members(); + IResource[] members = folder.members(); for (IResource r : members) { // get the type of the resource switch (r.getType()) { @@ -994,12 +836,12 @@ public class PreCompilerBuilder extends BaseBuilder { // and that it's an aidl file if (r.exists() && AndroidConstants.EXT_AIDL.equalsIgnoreCase(r.getFileExtension())) { - mAidlToCompile.add((IFile)r); + mAidlToCompile.add(new AidlData(sourceFolder, (IFile)r)); } break; case IResource.FOLDER: // recursively go through children - scanContainerForAidl((IFolder)r); + scanFolderForAidl(sourceFolder, (IFolder)r); break; default: // this would mean it's a project or the workspace root @@ -1067,13 +909,12 @@ public class PreCompilerBuilder extends BaseBuilder { * @param toCompile List of file to compile * @param toRemove List of file to remove */ - private void mergeAidlFileModifications(ArrayList toCompile, - ArrayList toRemove) { - + private void mergeAidlFileModifications(ArrayList toCompile, + ArrayList toRemove) { // loop through the new toRemove list, and add it to the old one, // plus remove any file that was still to compile and that are now // removed - for (IFile r : toRemove) { + for (AidlData r : toRemove) { if (mAidlToRemove.indexOf(r) == -1) { mAidlToRemove.add(r); } @@ -1088,7 +929,7 @@ public class PreCompilerBuilder extends BaseBuilder { // Also look for them in the remove list, this would mean that they // were removed, then added back, and we shouldn't remove them, just // recompile them. - for (IFile r : toCompile) { + for (AidlData r : toCompile) { if (mAidlToCompile.indexOf(r) == -1) { mAidlToCompile.add(r); } @@ -1099,68 +940,4 @@ public class PreCompilerBuilder extends BaseBuilder { } } } - - /** - * Prepare an already existing file for modification. File generated from - * command line processed are marked as read-only. This method prepares - * them (mark them as read-write) before the command line process is - * started. A check is made to be sure the file exists. - * @param file The IResource object for the file to prepare. - * @throws CoreException - */ - private void prepareFileForExternalModification(IFile file) - throws CoreException { - // file may not exist yet, so we check that. - if (file != null && file.exists()) { - // get the attributes. - ResourceAttributes ra = file.getResourceAttributes(); - if (ra != null) { - // change the attributes - ra.setReadOnly(false); - - // set the new attributes in the file. - file.setResourceAttributes(ra); - } - } - } - - /** - * Finish a file created/modified by an outside command line process. - * The file is marked as modified by Android, and the parent folder is refreshed, so that, - * in case the file didn't exist beforehand, the file appears in the package explorer. - * @param rFile The R file to "finish". - * @param manifestFile The manifest file to "finish". - * @throws CoreException - */ - private void finishJavaFilesAfterExternalModification(IFile rFile, IFile manifestFile) - throws CoreException { - IContainer parent = rFile.getParent(); - - IProgressMonitor monitor = new DerivedProgressMonitor(rFile, manifestFile); - - // refresh the parent node in the package explorer. Once this is done the custom progress - // monitor will mark them as derived. - parent.refreshLocal(IResource.DEPTH_ONE, monitor); - } - - /** - * Finish a file created/modified by an outside command line process. - * The file is marked as modified by Android, and the parent folder is refreshed, so that, - * in case the file didn't exist beforehand, the file appears in the package explorer. - * @param file The file to "finish". - * @param aidlFile The AIDL file to "finish". - * @throws CoreException - */ - private void finishFileAfterExternalModification(IFile file, IFile aidlFile) - throws CoreException { - IContainer parent = file.getParent(); - - // we need to add a link to the aidl file. - // We need to wait for the refresh of the parent to be done, so we'll do - // it in the monitor. This will also set the file as derived. - IProgressMonitor monitor = new RefreshProgressMonitor(file, aidlFile); - - // refresh the parent node in the package explorer. - parent.refreshLocal(IResource.DEPTH_ONE, monitor); - } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java index f4778d796..6841830e7 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java @@ -19,7 +19,7 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.build.BaseBuilder.BaseDeltaVisitor; -import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.build.PreCompilerBuilder.AidlData; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; @@ -31,14 +31,25 @@ import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.Path; import java.util.ArrayList; /** * Resource Delta visitor for the pre-compiler. + *

This delta visitor only cares about files that are the source or the result of actions of the + * {@link PreCompilerBuilder}: + *

  • R.java/Manifest.java generated by compiling the resources
  • + *
  • Any Java files generated by aidl
. + * + * Therefore it looks for the following: + *
  • Any modification in the resource folder
  • + *
  • Removed files from the source folder receiving generated Java files
  • + *
  • Any modification to aidl files.
  • + * */ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements IResourceDeltaVisitor { @@ -53,14 +64,11 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements private boolean mCompileResources = false; /** List of .aidl files found that are modified or new. */ - private final ArrayList mAidlToCompile = new ArrayList(); + private final ArrayList mAidlToCompile = new ArrayList(); /** List of .aidl files that have been removed. */ - private final ArrayList mAidlToRemove = new ArrayList(); + private final ArrayList mAidlToRemove = new ArrayList(); - /** Aidl forced recompilation flag. This is set to true if project.aidl is modified. */ - private boolean mFullAidlCompilation = false; - /** Manifest check/parsing flag. */ private boolean mCheckedManifestXml = false; @@ -75,36 +83,36 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements private boolean mInRes = false; /** - * In Source folder flag. This allows us to know if we're in a source - * folder. + * Current Source folder. This allows us to know if we're in a source + * folder, and which folder. */ - private boolean mInSrc = false; + private IFolder mSourceFolder = null; /** List of source folders. */ private ArrayList mSourceFolders; + private boolean mIsGenSourceFolder = false; + + private IWorkspaceRoot mRoot; public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList sourceFolders) { super(builder); mSourceFolders = sourceFolders; + mRoot = ResourcesPlugin.getWorkspace().getRoot(); } public boolean getCompileResources() { return mCompileResources; } - public ArrayList getAidlToCompile() { + public ArrayList getAidlToCompile() { return mAidlToCompile; } - public ArrayList getAidlToRemove() { + public ArrayList getAidlToRemove() { return mAidlToRemove; } - public boolean getFullAidlRecompilation() { - return mFullAidlCompilation; - } - /** * Returns whether the manifest file was parsed/checked for error during the resource delta * visiting. @@ -149,11 +157,13 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements // since the delta visitor also visits the root we return true if // segments.length = 1 if (segments.length == 1) { + // FIXME: check this is an Android project. return true; } else if (segments.length == 2) { // if we are at an item directly under the root directory, // then we are not yet in a source or resource folder - mInRes = mInSrc = false; + mInRes = false; + mSourceFolder = null; if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { // this is the resource folder that was modified. we want to @@ -162,7 +172,7 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements // since we're going to visit its children next, we set the // flag mInRes = true; - mInSrc = false; + mSourceFolder = null; return true; } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) { // any change in the manifest could trigger a new R.java @@ -183,9 +193,6 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements // we don't want to go to the children, not like they are // any for this resource anyway. return false; - } else if (AndroidConstants.FN_PROJECT_AIDL.equalsIgnoreCase(segments[1])) { - // need to force recompilation of all the aidl files - mFullAidlCompilation = true; } } @@ -198,7 +205,7 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements // so first we test if we already know we are in a source or // resource folder. - if (mInSrc) { + if (mSourceFolder != null) { // if we are in the res folder, we are looking for the following changes: // - added/removed/modified aidl files. // - missing R.java file @@ -216,127 +223,81 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements // get the modification kind int kind = delta.getKind(); - if (kind == IResourceDelta.ADDED) { - // we only care about added files (inside the source folders), if they - // are aidl files. + // we process normal source folder and the 'gen' source folder differently. + if (mIsGenSourceFolder) { + // this is the generated java file source folder. + // - if R.java/Manifest.java are removed/modified, we recompile the resources + // - if aidl files are removed/modified, we recompile them. - // get the extension of the resource - String ext = resource.getFileExtension(); + boolean outputWarning = false; - if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { - // look for an already existing matching java file - String javaName = resource.getName().replaceAll( - AndroidConstants.RE_AIDL_EXT, - AndroidConstants.DOT_JAVA); + String fileName = resource.getName(); - // get the parent container - IContainer ic = resource.getParent(); + // Special case of R.java/Manifest.java. + if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) || + AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) { + // if it was removed, there's a possibility that it was removed due to a + // package change, or an aidl that was removed, but the only thing + // that will happen is that we'll have an extra build. Not much of a problem. + mCompileResources = true; - IFile javaFile = ic.getFile(new Path(javaName)); - if (javaFile != null && javaFile.exists()) { - // check if that file was generated by the plugin. Normally those files are - // deleted automatically, but it's better to check. - String aidlPath = ProjectHelper.loadStringProperty(javaFile, - PreCompilerBuilder.PROPERTY_ANDROID_GENERATED); - if (aidlPath == null) { - // mark the aidl file that it cannot be compile just yet - ProjectHelper.saveBooleanProperty(file, - PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, true); - } - - // we add it anyway so that we can try to compile it at every compilation - // until the conflict is fixed. - mAidlToCompile.add(file); - - } else { - // the java file doesn't exist, we can safely add the file to the list - // of files to compile. - mAidlToCompile.add(file); - } - } - - return false; - } - - // get the filename - String fileName = segments[segments.length - 1]; - - boolean outputMessage = false; - - // Special case of R.java/Manifest.java. - // FIXME: This does not check the package. Any modification of R.java/Manifest.java in another project will trigger a new recompilation of the resources. - if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) || - AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) { - // if it was removed, there's a possibility that it was removed due to a - // package change, or an aidl that was removed, but the only thing - // that will happen is that we'll have an extra build. Not much of a problem. - mCompileResources = true; - - // we want a warning - outputMessage = true; - } else { - - // get the extension of the resource - String ext = resource.getFileExtension(); - - if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { - if (kind == IResourceDelta.REMOVED) { - mAidlToRemove.add(file); - } else { - mAidlToCompile.add(file); - } + // we want a warning + outputWarning = true; } else { - if (kind == IResourceDelta.REMOVED) { - // the file has been removed. we need to check it's a java file and that - // there's a matching aidl file. We can't check its persistent storage - // anymore. - if (AndroidConstants.EXT_JAVA.equalsIgnoreCase(ext)) { - String aidlFile = resource.getName().replaceAll( - AndroidConstants.RE_JAVA_EXT, - AndroidConstants.DOT_AIDL); - - // get the parent container - IContainer ic = resource.getParent(); - - IFile f = ic.getFile(new Path(aidlFile)); - if (f != null && f.exists() ) { - // make sure that the aidl file is not in conflict anymore, in - // case the java file was not generated by us. - if (ProjectHelper.loadBooleanProperty(f, - PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false)) { - ProjectHelper.saveBooleanProperty(f, - PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false); - } else { - outputMessage = true; - } - mAidlToCompile.add(f); + // this has to be a Java file created from an aidl file. + // Look for the source aidl file in all the source folders. + String aidlFileName = fileName.replaceAll(AndroidConstants.RE_JAVA_EXT, + AndroidConstants.DOT_AIDL); + + for (IPath sourceFolderPath : mSourceFolders) { + // do not search in the current source folder as it is the 'gen' folder. + if (sourceFolderPath.equals(mSourceFolder.getFullPath())) { + continue; + } + + IFolder sourceFolder = getFolder(sourceFolderPath); + if (sourceFolder != null) { + // go recursively, segment by segment. + // index starts at 2 (0 is project, 1 is 'gen' + IFile sourceFile = findFile(sourceFolder, segments, 2, aidlFileName); + + if (sourceFile != null) { + // found the source. add it to the list of files to compile + mAidlToCompile.add(new AidlData(sourceFolder, sourceFile)); + outputWarning = true; + break; } } - } else { - // check if it's an android generated java file. - IResource aidlSource = ProjectHelper.loadResourceProperty( - file, PreCompilerBuilder.PROPERTY_ANDROID_GENERATED); - - if (aidlSource != null && aidlSource.exists() && - aidlSource.getType() == IResource.FILE) { - // it looks like this was a java file created from an aidl file. - // we need to add the aidl file to the list of aidl file to compile - mAidlToCompile.add((IFile)aidlSource); - outputMessage = true; - } } } - } - if (outputMessage) { - if (kind == IResourceDelta.REMOVED) { - // We pring an error just so that it's red, but it's just a warning really. - String msg = String.format(Messages.s_Removed_Recreating_s, fileName); - AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); - } else if (kind == IResourceDelta.CHANGED) { - // the file was modified manually! we can't allow it. - String msg = String.format(Messages.s_Modified_Manually_Recreating_s, fileName); - AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + if (outputWarning) { + if (kind == IResourceDelta.REMOVED) { + // We pring an error just so that it's red, but it's just a warning really. + String msg = String.format(Messages.s_Removed_Recreating_s, fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } else if (kind == IResourceDelta.CHANGED) { + // the file was modified manually! we can't allow it. + String msg = String.format(Messages.s_Modified_Manually_Recreating_s, + fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } + } + + } else { + // this is another source folder. + // We only care about aidl files being added/modified/removed. + + // get the extension of the resource + String ext = resource.getFileExtension(); + if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { + if (kind == IResourceDelta.REMOVED) { + // we'll have to remove the generated file. + mAidlToRemove.add(new AidlData(mSourceFolder, file)); + } else { + // add the aidl file to the list of file to (re)compile + mAidlToCompile.add(new AidlData(mSourceFolder, file)); + } } } @@ -403,19 +364,25 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements } } else if (resource instanceof IFolder) { // in this case we may be inside a folder that contains a source - // folder. - String[] sourceFolderSegments = findMatchingSourceFolder(mSourceFolders, segments); - if (sourceFolderSegments != null) { - // we have a match! - mInRes = false; + // folder, go through the list of known source folders - // Check if the current folder is actually a source folder - if (sourceFolderSegments.length == segments.length) { - mInSrc = true; + for (IPath sourceFolderPath : mSourceFolders) { + // first check if they match exactly. + if (sourceFolderPath.equals(path)) { + // this is a source folder! + mInRes = false; + mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above + mIsGenSourceFolder = path.segmentCount() == 2 && + path.segment(1).equals(SdkConstants.FD_GEN_SOURCES); + return true; } - // and return true to visit the content, no matter what - return true; + // check if we are on the way to a source folder. + int count = sourceFolderPath.matchingFirstSegments(path); + if (count == path.segmentCount()) { + mInRes = false; + return true; + } } // if we're here, we are visiting another folder @@ -429,4 +396,46 @@ class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements return false; } + + /** + * Searches for and return a file in a folder. The file is defined by its segments, and a new + * name (replacing the last segment). + * @param folder the folder we are searching + * @param segments the segments of the file to search. + * @param index the index of the current segment we are looking for + * @param filename the new name to replace the last segment. + * @return the {@link IFile} representing the searched file, or null if not found + */ + private IFile findFile(IFolder folder, String[] segments, int index, String filename) { + boolean lastSegment = index == segments.length - 1; + IResource resource = folder.findMember(lastSegment ? filename : segments[index]); + if (resource != null && resource.exists()) { + if (lastSegment) { + if (resource.getType() == IResource.FILE) { + return (IFile)resource; + } + } else { + if (resource.getType() == IResource.FOLDER) { + return findFile((IFolder)resource, segments, index+1, filename); + } + } + } + return null; + } + + /** + * Returns a handle to the folder identified by the given path in this container. + *

    The different with {@link IContainer#getFolder(IPath)} is that this returns a non + * null object only if the resource actually exists and is a folder (and not a file) + * @param path the path of the folder to return. + * @return a handle to the folder if it exists, or null otherwise. + */ + private IFolder getFolder(IPath path) { + IResource resource = mRoot.findMember(path); + if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { + return (IFolder)resource; + } + + return null; + } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java index 1219aac1a..035aa5b73 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java @@ -19,24 +19,34 @@ package com.android.ide.eclipse.adt.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import java.util.ArrayList; import java.util.Map; /** * Resource manager builder whose only purpose is to refresh the resource folder * so that the other builder use an up to date version. */ -public class ResourceManagerBuilder extends IncrementalProjectBuilder { +public class ResourceManagerBuilder extends BaseBuilder { public static final String ID = "com.android.ide.eclipse.adt.ResourceManagerBuilder"; //$NON-NLS-1$ @@ -54,6 +64,10 @@ public class ResourceManagerBuilder extends IncrementalProjectBuilder { // Clear the project of the generic markers BaseBuilder.removeMarkersFromProject(project, AdtConstants.MARKER_ADT); + + // check for existing target marker, in which case we abort. + // (this means: no SDK, no target, or unresolvable target.) + abortOnBadSetup(project); // Check the compiler compliance level, displaying the error message // since this is the first builder. @@ -72,6 +86,109 @@ public class ResourceManagerBuilder extends IncrementalProjectBuilder { BaseProjectHelper.addMarker(project, AdtConstants.MARKER_ADT, errorMessage, IMarker.SEVERITY_ERROR); AdtPlugin.printErrorToConsole(project, errorMessage); + + // interrupt the build. The next builders will not run. + stopBuild(errorMessage); + } + + // Check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder == null || osSdkFolder.length() == 0) { + AdtPlugin.printErrorToConsole(project, Messages.No_SDK_Setup_Error); + markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error, + IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(Messages.No_SDK_Setup_Error); + } + + // check the project has a target + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + if (projectTarget == null) { + // no target. marker has been set by the container initializer: exit silently. + // This interrupts the build. The next builders will not run. + stopBuild("Project has no target"); + } + + // check the 'gen' source folder is present + boolean hasGenSrcFolder = false; // whether the project has a 'gen' source folder setup + IJavaProject javaProject = JavaCore.create(project); + + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + IPath path = e.getPath(); + if (path.segmentCount() == 2 && + path.segment(1).equals(SdkConstants.FD_GEN_SOURCES)) { + hasGenSrcFolder = true; + break; + } + } + } + } + + boolean genFolderPresent = false; // whether the gen folder actually exists + IResource resource = project.findMember(SdkConstants.FD_GEN_SOURCES); + genFolderPresent = resource != null && resource.exists(); + + if (hasGenSrcFolder == false && genFolderPresent) { + // No source folder setup for 'gen' in the project, but there's already a + // 'gen' resource (file or folder). + String message; + if (resource.getType() == IResource.FOLDER) { + // folder exists already! This is an error. If the folder had been created + // by the NewProjectWizard, it'd be a source folder. + message = String.format("%1$s already exists but is not a source folder. Convert to a source folder or rename it.", + resource.getFullPath().toString()); + } else { + // resource exists but is not a folder. + message = String.format( + "Resource %1$s is in the way. ADT needs a source folder called 'gen' to work. Rename or delete resource.", + resource.getFullPath().toString()); + } + + AdtPlugin.printErrorToConsole(project, message); + markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(message); + } else if (hasGenSrcFolder == false || genFolderPresent == false) { + // either there is no 'gen' source folder in the project (older SDK), + // or the folder does not exist (was deleted, or was a fresh svn checkout maybe.) + + // In case we are migrating from an older SDK, we go through the current source + // folders and delete the generated Java files. + ArrayList sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath path : sourceFolders) { + IResource member = root.findMember(path); + if (member != null) { + removeDerivedResources(member, monitor); + } + } + + // create the new source folder, if needed + IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); + if (genFolderPresent == false) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + "Creating 'gen' source folder for generated Java files"); + genFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + genFolder.setDerived(true); + } + + // add it to the source folder list, if needed only (or it will throw) + if (hasGenSrcFolder == false) { + IClasspathEntry[] entries = javaProject.getRawClasspath(); + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newSourceEntry(genFolder.getFullPath())); + javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); + } + + // refresh the whole project + project.refreshLocal(IResource.DEPTH_INFINITE, new SubProgressMonitor(monitor, 10)); } // Check the preference to be sure we are supposed to refresh diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AMReceiver.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AMReceiver.java new file mode 100644 index 000000000..8fdcdf9db --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AMReceiver.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ide.eclipse.adt.AdtPlugin; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Output receiver for am process (Activity Manager) + * + * Monitors adb output for am errors, and retries launch as appropriate. + */ +public class AMReceiver extends MultiLineReceiver { + + private static final int MAX_ATTEMPT_COUNT = 5; + private static final Pattern sAmErrorType = Pattern.compile("Error type (\\d+)"); //$NON-NLS-1$ + + private final DelayedLaunchInfo mLaunchInfo; + private final IDevice mDevice; + private final ILaunchController mLaunchController; + + /** + * Basic constructor. + * + * @param launchInfo the {@link DelayedLaunchInfo} associated with the am process. + * @param device the Android device on which the launch is done. + * @param launchController the {@link ILaunchController} that is managing the launch + */ + public AMReceiver(DelayedLaunchInfo launchInfo, IDevice device, + ILaunchController launchController) { + mLaunchInfo = launchInfo; + mDevice = device; + mLaunchController = launchController; + } + + /** + * Monitors the am process for error messages. If an error occurs, will reattempt launch up to + * MAX_ATTEMPT_COUNT times. + * + * @param lines a portion of the am output + * + * @see MultiLineReceiver#processNewLines(String[]) + */ + @Override + public void processNewLines(String[] lines) { + // first we check if one starts with error + ArrayList array = new ArrayList(); + boolean error = false; + boolean warning = false; + for (String s : lines) { + // ignore empty lines. + if (s.length() == 0) { + continue; + } + + // check for errors that output an error type, if the attempt count is still + // valid. If not the whole text will be output in the console + if (mLaunchInfo.getAttemptCount() < MAX_ATTEMPT_COUNT && + mLaunchInfo.isCancelled() == false) { + Matcher m = sAmErrorType.matcher(s); + if (m.matches()) { + // get the error type + int type = Integer.parseInt(m.group(1)); + + final int waitTime = 3; + String msg; + + switch (type) { + case 1: + /* Intended fall through */ + case 2: + msg = String.format( + "Device not ready. Waiting %1$d seconds before next attempt.", + waitTime); + break; + case 3: + msg = String.format( + "New package not yet registered with the system. Waiting %1$d seconds before next attempt.", + waitTime); + break; + default: + msg = String.format( + "Device not ready (%2$d). Waiting %1$d seconds before next attempt.", + waitTime, type); + break; + + } + + AdtPlugin.printToConsole(mLaunchInfo.getProject(), msg); + + // launch another thread, that waits a bit and attempts another launch + new Thread("Delayed Launch attempt") { + @Override + public void run() { + try { + sleep(waitTime * 1000); + } catch (InterruptedException e) { + // ignore + } + + mLaunchController.launchApp(mLaunchInfo, mDevice); + } + }.start(); + + // no need to parse the rest + return; + } + } + + // check for error if needed + if (error == false && s.startsWith("Error:")) { //$NON-NLS-1$ + error = true; + } + if (warning == false && s.startsWith("Warning:")) { //$NON-NLS-1$ + warning = true; + } + + // add the line to the list + array.add("ActivityManager: " + s); //$NON-NLS-1$ + } + + // then we display them in the console + if (warning || error) { + AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), array.toArray()); + } else { + AdtPlugin.printToConsole(mLaunchInfo.getProject(), array.toArray()); + } + + // if error then we cancel the launch, and remove the delayed info + if (error) { + mLaunchController.stopLaunch(mLaunchInfo); + } + } + + /** + * Returns true if launch has been cancelled + */ + public boolean isCancelled() { + return mLaunchInfo.isCancelled(); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ActivityLaunchAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ActivityLaunchAction.java new file mode 100644 index 000000000..1aa5a9ab8 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ActivityLaunchAction.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ddmlib.IDevice; +import com.android.ide.eclipse.adt.AdtPlugin; + +import java.io.IOException; + +/** + * Launches the given activity + */ +public class ActivityLaunchAction implements IAndroidLaunchAction { + + private final String mActivity; + private final ILaunchController mLaunchController; + + /** + * Creates a ActivityLaunchAction + * + * @param activity fully qualified activity name to launch + * @param controller the {@link ILaunchController} that performs launch + */ + public ActivityLaunchAction(String activity, ILaunchController controller) { + mActivity = activity; + mLaunchController = controller; + } + + /** + * Launches the activity on targeted device + * + * @param info the {@link DelayedLaunchInfo} that contains launch details + * @param device the Android device to perform action on + * + * @see IAndroidLaunchAction#doLaunchAction(DelayedLaunchInfo, IDevice) + */ + public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) { + try { + String msg = String.format("Starting activity %1$s on device ", mActivity, + device); + AdtPlugin.printToConsole(info.getProject(), msg); + + // In debug mode, we need to add the info to the list of application monitoring + // client changes. + // increment launch attempt count, to handle retries and timeouts + info.incrementAttemptCount(); + + // now we actually launch the app. + device.executeShellCommand("am start" //$NON-NLS-1$ + + (info.isDebugMode() ? " -D" //$NON-NLS-1$ + : "") //$NON-NLS-1$ + + " -n " //$NON-NLS-1$ + + info.getPackageName() + "/" //$NON-NLS-1$ + + mActivity.replaceAll("\\$", "\\\\\\$"), //$NON-NLS-1$ //$NON-NLS-2$ + new AMReceiver(info, device, mLaunchController)); + + // if the app is not a debug app, we need to do some clean up, as + // the process is done! + if (info.isDebugMode() == false) { + // stop the launch object, since there's no debug, and it can't + // provide any control over the app + return false; + } + } catch (IOException e) { + // something went wrong trying to launch the app. + // lets stop the Launch + AdtPlugin.printErrorToConsole(info.getProject(), + String.format("Launch error: %s", e.getMessage())); + return false; + } + return true; + } + + /** + * Returns a description of the activity being launched + * + * @see IAndroidLaunchAction#getLaunchDescription() + */ + public String getLaunchDescription() { + return String.format("%1$s activity launch", mActivity); + } + +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunch.java similarity index 95% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunch.java index 3d6040172..42927c2a8 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunch.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.launching; +package com.android.ide.eclipse.adt.launch; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.ILaunchConfiguration; @@ -26,7 +26,7 @@ import org.eclipse.debug.core.model.ISourceLocator; * Custom implementation of Launch to allow access to the LaunchManager * */ -class AndroidLaunch extends Launch { +public class AndroidLaunch extends Launch { /** * Basic constructor does nothing special diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java new file mode 100644 index 000000000..448cda6b5 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchConfiguration.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; + +/** + * Launch configuration data. This stores the result of querying the + * {@link ILaunchConfiguration} so that it's only done once. + */ +public class AndroidLaunchConfiguration { + + /** + * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT}, + * {@link LaunchConfigDelegate#ACTION_ACTIVITY}, + * {@link LaunchConfigDelegate#ACTION_DO_NOTHING} + */ + public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION; + + public static final boolean AUTO_TARGET_MODE = true; + + /** + * Target selection mode. + *

      + *
    • true: automatic mode, see {@link #AUTO_TARGET_MODE}
    • + *
    • false: manual mode
    • + *
    + */ + public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE; + + /** + * Indicates whether the emulator should be called with -wipe-data + */ + public boolean mWipeData = LaunchConfigDelegate.DEFAULT_WIPE_DATA; + + /** + * Indicates whether the emulator should be called with -no-boot-anim + */ + public boolean mNoBootAnim = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM; + + /** + * AVD Name. + */ + public String mAvdName = null; + + public String mNetworkSpeed = EmulatorConfigTab.getSpeed( + LaunchConfigDelegate.DEFAULT_SPEED); + public String mNetworkDelay = EmulatorConfigTab.getDelay( + LaunchConfigDelegate.DEFAULT_DELAY); + + /** + * Optional custom command line parameter to launch the emulator + */ + public String mEmulatorCommandLine; + + /** + * Initialized the structure from an ILaunchConfiguration object. + * @param config + */ + public void set(ILaunchConfiguration config) { + try { + mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, + mLaunchAction); + } catch (CoreException e1) { + // nothing to be done here, we'll use the default value + } + + try { + mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, + mTargetMode); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + + try { + mAvdName = config.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, mAvdName); + } catch (CoreException e) { + // ignore + } + + int index = LaunchConfigDelegate.DEFAULT_SPEED; + try { + index = config.getAttribute(LaunchConfigDelegate.ATTR_SPEED, index); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + mNetworkSpeed = EmulatorConfigTab.getSpeed(index); + + index = LaunchConfigDelegate.DEFAULT_DELAY; + try { + index = config.getAttribute(LaunchConfigDelegate.ATTR_DELAY, index); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + mNetworkDelay = EmulatorConfigTab.getDelay(index); + + try { + mEmulatorCommandLine = config.getAttribute( + LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$ + } catch (CoreException e) { + // lets not do anything here, we'll use the default value + } + + try { + mWipeData = config.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, mWipeData); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + + try { + mNoBootAnim = config.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, + mNoBootAnim); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + } +} + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java similarity index 66% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java index 2b7d01d81..88ee8b6ff 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/AndroidLaunchController.java @@ -14,29 +14,30 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.launching; +package com.android.ide.eclipse.adt.launch; import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.Client; -import com.android.ddmlib.ClientData; -import com.android.ddmlib.Device; -import com.android.ddmlib.Log; -import com.android.ddmlib.MultiLineReceiver; -import com.android.ddmlib.SyncService; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Device; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.SyncService; import com.android.ddmlib.SyncService.SyncResult; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.DeviceChooserDialog.DeviceChooserResponse; -import com.android.ide.eclipse.adt.debug.ui.EmulatorConfigTab; +import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo.InstallRetryMode; +import com.android.ide.eclipse.adt.launch.DeviceChooserDialog.DeviceChooserResponse; import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.Sdk; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkManager; -import com.android.sdklib.vm.VmManager; -import com.android.sdklib.vm.VmManager.VmInfo; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; @@ -54,6 +55,7 @@ import org.eclipse.debug.ui.DebugUITools; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.IVMConnector; import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; @@ -72,91 +74,23 @@ import java.util.regex.Pattern; * it. */ public final class AndroidLaunchController implements IDebugBridgeChangeListener, - IDeviceChangeListener, IClientChangeListener { + IDeviceChangeListener, IClientChangeListener, ILaunchController { - private static final String FLAG_VM = "-vm"; //$NON-NLS-1$ + private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$ private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$ private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$ private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$ private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$ - private static final int MAX_ATTEMPT_COUNT = 5; - - private final static Pattern sAmErrorType = Pattern.compile("Error type (\\d+)"); //$NON-NLS-1$ - - /** - * A delayed launch waiting for a device to be present or ready before the - * application is launched. - */ - static final class DelayedLaunchInfo { - /** The device on which to launch the app */ - Device mDevice = null; - - /** The eclipse project */ - IProject mProject; - - /** Package name */ - String mPackageName; - - /** fully qualified name of the activity */ - String mActivity; - - /** IFile to the package (.apk) file */ - IFile mPackageFile; - - /** Debuggable attribute of the manifest file. */ - Boolean mDebuggable = null; - - /** Required ApiVersionNumber by the app. 0 means no requirements */ - int mRequiredApiVersionNumber = 0; - - InstallRetryMode mRetryMode = InstallRetryMode.NEVER; - - /** - * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT}, - * {@link LaunchConfigDelegate#ACTION_ACTIVITY}, - * {@link LaunchConfigDelegate#ACTION_DO_NOTHING} - */ - int mLaunchAction; - - /** the launch object */ - AndroidLaunch mLaunch; - - /** the monitor object */ - IProgressMonitor mMonitor; - - /** debug mode flag */ - boolean mDebugMode; - - int mAttemptCount = 0; - - boolean mCancelled = false; - - /** Basic constructor with activity and package info. */ - public DelayedLaunchInfo(IProject project, String packageName, String activity, - IFile pack, Boolean debuggable, int requiredApiVersionNumber, int launchAction, - AndroidLaunch launch, IProgressMonitor monitor) { - mProject = project; - mPackageName = packageName; - mActivity = activity; - mPackageFile = pack; - mLaunchAction = launchAction; - mLaunch = launch; - mMonitor = monitor; - mDebuggable = debuggable; - mRequiredApiVersionNumber = requiredApiVersionNumber; - } - } - /** * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection * to running application. The integer is the port on which to connect. * ALL ACCESS MUST BE INSIDE A synchronized (sListLock) block! */ - private final static HashMap sRunningAppMap = + private static final HashMap sRunningAppMap = new HashMap(); - private final static Object sListLock = sRunningAppMap; + private static final Object sListLock = sRunningAppMap; /** * List of {@link DelayedLaunchInfo} waiting for an emulator to connect. @@ -190,242 +124,15 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener /** static instance for singleton */ private static AndroidLaunchController sThis = new AndroidLaunchController(); - enum InstallRetryMode { - NEVER, ALWAYS, PROMPT; - } - /** - * Represents a launch configuration. - */ - static final class AndroidLaunchConfiguration { - - /** - * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT}, - * {@link LaunchConfigDelegate#ACTION_ACTIVITY}, - * {@link LaunchConfigDelegate#ACTION_DO_NOTHING} - */ - public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION; - - public static final boolean AUTO_TARGET_MODE = true; - - /** - * Target selection mode. - *
      - *
    • true: automatic mode, see {@link #AUTO_TARGET_MODE}
    • - *
    • false: manual mode
    • - *
    - */ - public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE; - - /** - * Indicates whether the emulator should be called with -wipe-data - */ - public boolean mWipeData = LaunchConfigDelegate.DEFAULT_WIPE_DATA; - - /** - * Indicates whether the emulator should be called with -no-boot-anim - */ - public boolean mNoBootAnim = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM; - - /** - * Vm Name. - */ - public String mVmName = null; - - public String mNetworkSpeed = EmulatorConfigTab.getSpeed( - LaunchConfigDelegate.DEFAULT_SPEED); - public String mNetworkDelay = EmulatorConfigTab.getDelay( - LaunchConfigDelegate.DEFAULT_DELAY); - - /** - * Optional custom command line parameter to launch the emulator - */ - public String mEmulatorCommandLine; - - /** - * Initialized the structure from an ILaunchConfiguration object. - * @param config - */ - public void set(ILaunchConfiguration config) { - try { - mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, - mLaunchAction); - } catch (CoreException e1) { - // nothing to be done here, we'll use the default value - } - - try { - mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, - mTargetMode); - } catch (CoreException e) { - // nothing to be done here, we'll use the default value - } - - try { - mVmName = config.getAttribute(LaunchConfigDelegate.ATTR_VM_NAME, mVmName); - } catch (CoreException e) { - } - - int index = LaunchConfigDelegate.DEFAULT_SPEED; - try { - index = config.getAttribute(LaunchConfigDelegate.ATTR_SPEED, index); - } catch (CoreException e) { - // nothing to be done here, we'll use the default value - } - mNetworkSpeed = EmulatorConfigTab.getSpeed(index); - - index = LaunchConfigDelegate.DEFAULT_DELAY; - try { - index = config.getAttribute(LaunchConfigDelegate.ATTR_DELAY, index); - } catch (CoreException e) { - // nothing to be done here, we'll use the default value - } - mNetworkDelay = EmulatorConfigTab.getDelay(index); - - try { - mEmulatorCommandLine = config.getAttribute( - LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$ - } catch (CoreException e) { - // lets not do anything here, we'll use the default value - } - - try { - mWipeData = config.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, mWipeData); - } catch (CoreException e) { - // nothing to be done here, we'll use the default value - } - - try { - mNoBootAnim = config.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, - mNoBootAnim); - } catch (CoreException e) { - // nothing to be done here, we'll use the default value - } - } - } - - /** - * Output receiver for am process (activity Manager); - */ - private final class AMReceiver extends MultiLineReceiver { - private DelayedLaunchInfo mLaunchInfo; - private Device mDevice; - - /** - * Basic constructor. - * @param launchInfo The launch info associated with the am process. - * @param device The device on which the launch is done. - */ - public AMReceiver(DelayedLaunchInfo launchInfo, Device device) { - mLaunchInfo = launchInfo; - mDevice = device; - } - - @Override - public void processNewLines(String[] lines) { - // first we check if one starts with error - ArrayList array = new ArrayList(); - boolean error = false; - boolean warning = false; - for (String s : lines) { - // ignore empty lines. - if (s.length() == 0) { - continue; - } - - // check for errors that output an error type, if the attempt count is still - // valid. If not the whole text will be output in the console - if (mLaunchInfo.mAttemptCount < MAX_ATTEMPT_COUNT && - mLaunchInfo.mCancelled == false) { - Matcher m = sAmErrorType.matcher(s); - if (m.matches()) { - // get the error type - int type = Integer.parseInt(m.group(1)); - - final int waitTime = 3; - String msg; - - switch (type) { - case 1: - /* Intended fall through */ - case 2: - msg = String.format( - "Device not ready. Waiting %1$d seconds before next attempt.", - waitTime); - break; - case 3: - msg = String.format( - "New package not yet registered with the system. Waiting %1$d seconds before next attempt.", - waitTime); - break; - default: - msg = String.format( - "Device not ready (%2$d). Waiting %1$d seconds before next attempt.", - waitTime, type); - break; - - } - - AdtPlugin.printToConsole(mLaunchInfo.mProject, msg); - - // launch another thread, that waits a bit and attempts another launch - new Thread("Delayed Launch attempt") { - @Override - public void run() { - try { - sleep(waitTime * 1000); - } catch (InterruptedException e) { - } - - launchApp(mLaunchInfo, mDevice); - } - }.start(); - - // no need to parse the rest - return; - } - } - - // check for error if needed - if (error == false && s.startsWith("Error:")) { //$NON-NLS-1$ - error = true; - } - if (warning == false && s.startsWith("Warning:")) { //$NON-NLS-1$ - warning = true; - } - - // add the line to the list - array.add("ActivityManager: " + s); //$NON-NLS-1$ - } - - // then we display them in the console - if (warning || error) { - AdtPlugin.printErrorToConsole(mLaunchInfo.mProject, array.toArray()); - } else { - AdtPlugin.printToConsole(mLaunchInfo.mProject, array.toArray()); - } - - // if error then we cancel the launch, and remove the delayed info - if (error) { - mLaunchInfo.mLaunch.stopLaunch(); - synchronized (sListLock) { - mWaitingForReadyEmulatorList.remove(mLaunchInfo); - } - } - } - - public boolean isCancelled() { - return false; - } - } /** * Output receiver for "pm install package.apk" command line. */ - private final static class InstallReceiver extends MultiLineReceiver { + private static final class InstallReceiver extends MultiLineReceiver { - private final static String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ - private final static Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ + private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ + private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ private String mSuccess = null; @@ -531,8 +238,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, LaunchConfigDelegate.DEFAULT_TARGET_MODE); - // default VM: None - wc.setAttribute(LaunchConfigDelegate.ATTR_VM_NAME, (String)null); + // default AVD: None + wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null); // set the default network speed wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED, @@ -583,8 +290,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener ArrayList array = new ArrayList(2); array.add(project); - AndroidManifestHelper helper = new AndroidManifestHelper(project); - IFile manifest = helper.getManifestIFile(); + IFile manifest = AndroidManifestParser.getManifest(project); if (manifest != null) { array.add(manifest); } @@ -602,39 +308,33 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @param apk the resource to the apk to launch. * @param debuggable the debuggable value of the app, or null if not set. * @param requiredApiVersionNumber the api version required by the app, or -1 if none. - * @param activity the class to provide to am to launch + * @param launchAction the action to perform after app sync * @param config the launch configuration * @param launch the launch object */ public void launch(final IProject project, String mode, IFile apk, - String packageName, Boolean debuggable, int requiredApiVersionNumber, String activity, - final AndroidLaunchConfiguration config, final AndroidLaunch launch, - IProgressMonitor monitor) { + String packageName, Boolean debuggable, int requiredApiVersionNumber, + final IAndroidLaunchAction launchAction, final AndroidLaunchConfiguration config, + final AndroidLaunch launch, IProgressMonitor monitor) { - String message; - if (config.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { - message = String.format("Only Syncing Application Package"); - } else { - message = String.format("Launching: %1$s", activity); - } + String message = String.format("Performing %1$s", launchAction.getLaunchDescription()); AdtPlugin.printToConsole(project, message); // create the launch info final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName, - activity, apk, debuggable, requiredApiVersionNumber, config.mLaunchAction, - launch, monitor); + launchAction, apk, debuggable, requiredApiVersionNumber, launch, monitor); // set the debug mode - launchInfo.mDebugMode = mode.equals(ILaunchManager.DEBUG_MODE); + launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE)); // get the SDK Sdk currentSdk = Sdk.getCurrent(); - VmManager vmManager = currentSdk.getVmManager(); + AvdManager avdManager = currentSdk.getAvdManager(); // get the project target final IAndroidTarget projectTarget = currentSdk.getTarget(project); - // FIXME: check errors on missing sdk, vm manager, or project target. + // FIXME: check errors on missing sdk, AVD manager, or project target. // device chooser response object. final DeviceChooserResponse response = new DeviceChooserResponse(); @@ -644,81 +344,77 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * - Manually Mode * Always display a UI that lets a user see the current running emulators/devices. * The UI must show which devices are compatibles, and allow launching new emulators - * with compatible (and not yet running) VM. + * with compatible (and not yet running) AVD. * - Automatic Way - * * Preferred VM set. - * If Preferred VM is not running: launch it. - * Launch the application on the preferred VM. - * * No preferred VM. + * * Preferred AVD set. + * If Preferred AVD is not running: launch it. + * Launch the application on the preferred AVD. + * * No preferred AVD. * Count the number of compatible emulators/devices. * If != 1, display a UI similar to manual mode. - * If == 1, launch the application on this VM/device. + * If == 1, launch the application on this AVD/device. */ if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) { // if we are in automatic target mode, we need to find the current devices - Device[] devices = AndroidDebugBridge.getBridge().getDevices(); + IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); - // first check if we have a preferred VM name, and if it actually exists, and is valid + // first check if we have a preferred AVD name, and if it actually exists, and is valid // (ie able to run the project). - // We need to check this in case the VM was recreated with a different target that is + // We need to check this in case the AVD was recreated with a different target that is // not compatible. - VmInfo preferredVm = null; - if (config.mVmName != null) { - preferredVm = vmManager.getVm(config.mVmName); - if (projectTarget.isCompatibleBaseFor(preferredVm.getTarget()) == false) { - preferredVm = null; + AvdInfo preferredAvd = null; + if (config.mAvdName != null) { + preferredAvd = avdManager.getAvd(config.mAvdName); + if (projectTarget.isCompatibleBaseFor(preferredAvd.getTarget()) == false) { + preferredAvd = null; AdtPlugin.printErrorToConsole(project, String.format( - "Preferred VM '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible VM...", - config.mVmName, projectTarget.getName())); + "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...", + config.mAvdName, projectTarget.getName())); } } - if (preferredVm != null) { + if (preferredAvd != null) { // look for a matching device - for (Device d : devices) { - String deviceVm = d.getVmName(); - if (deviceVm != null && deviceVm.equals(config.mVmName)) { - response.mustContinue = true; - response.mustLaunchEmulator = false; - response.deviceToUse = d; + for (IDevice d : devices) { + String deviceAvd = d.getAvdName(); + if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) { + response.setDeviceToUse(d); AdtPlugin.printToConsole(project, String.format( - "Automatic Target Mode: Preferred VM '%1$s' is available on emulator '%2$s'", - config.mVmName, d)); + "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'", + config.mAvdName, d)); continueLaunch(response, project, launch, launchInfo, config); return; } } - // at this point we have a valid preferred VM that is not running. + // at this point we have a valid preferred AVD that is not running. // We need to start it. - response.mustContinue = true; - response.mustLaunchEmulator = true; - response.vmToLaunch = preferredVm; + response.setAvdToLaunch(preferredAvd); AdtPlugin.printToConsole(project, String.format( - "Automatic Target Mode: Preferred VM '%1$s' is not available. Launching new emulator.", - config.mVmName)); + "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.", + config.mAvdName)); continueLaunch(response, project, launch, launchInfo, config); return; } - // no (valid) preferred VM? look for one. - HashMap compatibleRunningVms = new HashMap(); + // no (valid) preferred AVD? look for one. + HashMap compatibleRunningAvds = new HashMap(); boolean hasDevice = false; // if there's 1+ device running, we may force manual mode, // as we cannot always detect proper compatibility with // devices. This is the case if the project target is not // a standard platform - for (Device d : devices) { - String deviceVm = d.getVmName(); - if (deviceVm != null) { // physical devices return null. - VmInfo info = vmManager.getVm(deviceVm); + for (IDevice d : devices) { + String deviceAvd = d.getAvdName(); + if (deviceAvd != null) { // physical devices return null. + AvdInfo info = avdManager.getAvd(deviceAvd); if (info != null && projectTarget.isCompatibleBaseFor(info.getTarget())) { - compatibleRunningVms.put(d, info); + compatibleRunningAvds.put(d, info); } } else { if (projectTarget.isPlatform()) { // means this can run on any device as long @@ -728,7 +424,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener int apiNumber = Integer.parseInt(apiString); if (apiNumber >= projectTarget.getApiVersionNumber()) { // device is compatible with project - compatibleRunningVms.put(d, null); + compatibleRunningAvds.put(d, null); continue; } } catch (NumberFormatException e) { @@ -741,58 +437,54 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // depending on the number of devices, we'll simulate an automatic choice // from the device chooser or simply show up the device chooser. - if (hasDevice == false && compatibleRunningVms.size() == 0) { + if (hasDevice == false && compatibleRunningAvds.size() == 0) { // if zero emulators/devices, we launch an emulator. - // We need to figure out which VM first. + // We need to figure out which AVD first. - // we are going to take the closest VM. ie a compatible VM that has the API level + // we are going to take the closest AVD. ie a compatible AVD that has the API level // closest to the project target. - VmInfo[] vms = vmManager.getVms(); - VmInfo defaultVm = null; - for (VmInfo vm : vms) { - if (projectTarget.isCompatibleBaseFor(vm.getTarget())) { - if (defaultVm == null || - vm.getTarget().getApiVersionNumber() < - defaultVm.getTarget().getApiVersionNumber()) { - defaultVm = vm; + AvdInfo[] avds = avdManager.getAvds(); + AvdInfo defaultAvd = null; + for (AvdInfo avd : avds) { + if (projectTarget.isCompatibleBaseFor(avd.getTarget())) { + if (defaultAvd == null || + avd.getTarget().getApiVersionNumber() < + defaultAvd.getTarget().getApiVersionNumber()) { + defaultAvd = avd; } } } - if (defaultVm != null) { - response.mustContinue = true; - response.mustLaunchEmulator = true; - response.vmToLaunch = defaultVm; + if (defaultAvd != null) { + response.setAvdToLaunch(defaultAvd); AdtPlugin.printToConsole(project, String.format( - "Automatic Target Mode: launching new emulator with compatible VM '%1$s'", - defaultVm.getName())); + "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'", + defaultAvd.getName())); continueLaunch(response, project, launch, launchInfo, config); return; } else { - // FIXME: ask the user if he wants to create a VM. - // we found no compatible VM. + // FIXME: ask the user if he wants to create a AVD. + // we found no compatible AVD. AdtPlugin.printErrorToConsole(project, String.format( - "Failed to find a VM compatible with target '%1$s'. Launch aborted.", + "Failed to find a AVD compatible with target '%1$s'. Launch aborted.", projectTarget.getName())); - launch.stopLaunch(); + stopLaunch(launchInfo); return; } - } else if (hasDevice == false && compatibleRunningVms.size() == 1) { - Entry e = compatibleRunningVms.entrySet().iterator().next(); - response.mustContinue = true; - response.mustLaunchEmulator = false; - response.deviceToUse = e.getKey(); + } else if (hasDevice == false && compatibleRunningAvds.size() == 1) { + Entry e = compatibleRunningAvds.entrySet().iterator().next(); + response.setDeviceToUse(e.getKey()); - // get the VmInfo, if null, the device is a physical device. - VmInfo vmInfo = e.getValue(); - if (vmInfo != null) { - message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible VM '%2$s'", - response.deviceToUse, e.getValue().getName()); + // get the AvdInfo, if null, the device is a physical device. + AvdInfo avdInfo = e.getValue(); + if (avdInfo != null) { + message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'", + response.getDeviceToUse(), e.getValue().getName()); } else { message = String.format("Automatic Target Mode: using device '%1$s'", - response.deviceToUse); + response.getDeviceToUse()); } AdtPlugin.printToConsole(project, message); @@ -801,7 +493,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } // if more than one device, we'll bring up the DeviceChooser dialog below. - if (compatibleRunningVms.size() >= 2) { + if (compatibleRunningAvds.size() >= 2) { message = "Automatic Target Mode: Several compatible targets. Please select a target device."; } else if (hasDevice) { message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device."; @@ -813,13 +505,35 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // bring up the device chooser. AdtPlugin.getDisplay().asyncExec(new Runnable() { public void run() { - DeviceChooserDialog dialog = new DeviceChooserDialog( - AdtPlugin.getDisplay().getActiveShell()); - dialog.open(response, project, projectTarget, launch, launchInfo, config); + try { + // open the chooser dialog. It'll fill 'response' with the device to use + // or the AVD to launch. + DeviceChooserDialog dialog = new DeviceChooserDialog( + AdtPlugin.getDisplay().getActiveShell(), + response, launchInfo.getPackageName(), projectTarget); + if (dialog.open() == Dialog.OK) { + AndroidLaunchController.this.continueLaunch(response, project, launch, + launchInfo, config); + } else { + AdtPlugin.printErrorToConsole(project, "Launch canceled!"); + stopLaunch(launchInfo); + return; + } + } catch (Exception e) { + // there seems to be some case where the shell will be null. (might be + // an OS X bug). Because of this the creation of the dialog will throw + // and IllegalArg exception interrupting the launch with no user feedback. + // So we trap all the exception and display something. + String msg = e.getMessage(); + if (msg == null) { + msg = e.getClass().getCanonicalName(); + } + AdtPlugin.printErrorToConsole(project, + String.format("Error during launch: %s", msg)); + stopLaunch(launchInfo); + } } }); - - return; } /** @@ -833,23 +547,21 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener void continueLaunch(final DeviceChooserResponse response, final IProject project, final AndroidLaunch launch, final DelayedLaunchInfo launchInfo, final AndroidLaunchConfiguration config) { - if (response.mustContinue == false) { - AdtPlugin.printErrorToConsole(project, "Launch canceled!"); - launch.stopLaunch(); - return; - } - // Since this is called from the DeviceChooserDialog open, we are in the UI - // thread. So we spawn a temporary new one to finish the launch. + // Since this is called from the UI thread we spawn a new thread + // to finish the launch. new Thread() { @Override public void run() { - if (response.mustLaunchEmulator) { + if (response.getAvdToLaunch() != null) { // there was no selected device, we start a new emulator. synchronized (sListLock) { + AvdInfo info = response.getAvdToLaunch(); mWaitingForEmulatorLaunches.add(launchInfo); - AdtPlugin.printToConsole(project, "Launching a new emulator."); - boolean status = launchEmulator(config, response.vmToLaunch); + AdtPlugin.printToConsole(project, String.format( + "Launching a new emulator with Virtual Device '%1$s'", + info.getName())); + boolean status = launchEmulator(config, info); if (status == false) { // launching the emulator failed! @@ -859,15 +571,15 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // stop the launch and return mWaitingForEmulatorLaunches.remove(launchInfo); AdtPlugin.printErrorToConsole(project, "Launch canceled!"); - launch.stopLaunch(); + stopLaunch(launchInfo); return; } return; } - } else if (response.deviceToUse != null) { - launchInfo.mDevice = response.deviceToUse; - simpleLaunch(launchInfo, response.deviceToUse); + } else if (response.getDeviceToUse() != null) { + launchInfo.setDevice(response.getDeviceToUse()); + simpleLaunch(launchInfo, launchInfo.getDevice()); } } }.start(); @@ -920,12 +632,12 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * "release" mode instead of "debug" *
      */ - private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, Device device) { + private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) { if (device != null) { // check the app required API level versus the target device API level - String deviceApiVersionName = device.getProperty(Device.PROP_BUILD_VERSION); - String value = device.getProperty(Device.PROP_BUILD_VERSION_NUMBER); + String deviceApiVersionName = device.getProperty(IDevice.PROP_BUILD_VERSION); + String value = device.getProperty(IDevice.PROP_BUILD_VERSION_NUMBER); int deviceApiVersionNumber = 0; try { deviceApiVersionNumber = Integer.parseInt(value); @@ -933,30 +645,30 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // pass, we'll keep the deviceVersionNumber value at 0. } - if (launchInfo.mRequiredApiVersionNumber == 0) { + if (launchInfo.getRequiredApiVersionNumber() == 0) { // warn the API level requirement is not set. - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "WARNING: Application does not specify an API level requirement!"); // and display the target device API level (if known) if (deviceApiVersionName == null || deviceApiVersionNumber == 0) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "WARNING: Unknown device API version!"); } else { - AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format( + AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format( "Device API version is %1$d (Android %2$s)", deviceApiVersionNumber, deviceApiVersionName)); } } else { // app requires a specific API level if (deviceApiVersionName == null || deviceApiVersionNumber == 0) { - AdtPlugin.printToConsole(launchInfo.mProject, + AdtPlugin.printToConsole(launchInfo.getProject(), "WARNING: Unknown device API version!"); - } else if (deviceApiVersionNumber < launchInfo.mRequiredApiVersionNumber) { + } else if (deviceApiVersionNumber < launchInfo.getRequiredApiVersionNumber()) { String msg = String.format( "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).", - launchInfo.mRequiredApiVersionNumber, deviceApiVersionNumber, + launchInfo.getRequiredApiVersionNumber(), deviceApiVersionNumber, deviceApiVersionName); - AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); // abort the launch return false; @@ -964,33 +676,33 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } // now checks that the device/app can be debugged (if needed) - if (device.isEmulator() == false && launchInfo.mDebugMode) { - String debuggableDevice = device.getProperty(Device.PROP_DEBUGGABLE); + if (device.isEmulator() == false && launchInfo.isDebugMode()) { + String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE); if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$ // the device is "secure" and requires apps to declare themselves as debuggable! - if (launchInfo.mDebuggable == null) { + if (launchInfo.getDebuggable() == null) { String message1 = String.format( "Device '%1$s' requires that applications explicitely declare themselves as debuggable in their manifest.", device.getSerialNumber()); String message2 = String.format("Application '%1$s' does not have the attribute 'debuggable' set to TRUE in its manifest and cannot be debugged.", - launchInfo.mPackageName); - AdtPlugin.printErrorToConsole(launchInfo.mProject, message1, message2); + launchInfo.getPackageName()); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), message1, message2); // because am -D does not check for ro.debuggable and the // 'debuggable' attribute, it is important we do not use the -D option // in this case or the app will wait for a debugger forever and never // really launch. - launchInfo.mDebugMode = false; - } else if (launchInfo.mDebuggable == Boolean.FALSE) { + launchInfo.setDebugMode(false); + } else if (launchInfo.getDebuggable() == Boolean.FALSE) { String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.", - launchInfo.mPackageName); - AdtPlugin.printErrorToConsole(launchInfo.mProject, message); + launchInfo.getPackageName()); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), message); // because am -D does not check for ro.debuggable and the // 'debuggable' attribute, it is important we do not use the -D option // in this case or the app will wait for a debugger forever and never // really launch. - launchInfo.mDebugMode = false; + launchInfo.setDebugMode(false); } } } @@ -1007,18 +719,18 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @param device * @return true if succeed */ - private boolean simpleLaunch(DelayedLaunchInfo launchInfo, Device device) { + private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) { // API level check if (checkBuildInfo(launchInfo, device) == false) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!"); - launchInfo.mLaunch.stopLaunch(); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); + stopLaunch(launchInfo); return false; } // sync the app if (syncApp(launchInfo, device) == false) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!"); - launchInfo.mLaunch.stopLaunch(); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); + stopLaunch(launchInfo); return false; } @@ -1036,16 +748,16 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @param device the device on which to sync the application * @return true if the install succeeded. */ - private boolean syncApp(DelayedLaunchInfo launchInfo, Device device) { + private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) { SyncService sync = device.getSyncService(); if (sync != null) { - IPath path = launchInfo.mPackageFile.getLocation(); + IPath path = launchInfo.getPackageFile().getLocation(); String message = String.format("Uploading %1$s onto device '%2$s'", path.lastSegment(), device.getSerialNumber()); - AdtPlugin.printToConsole(launchInfo.mProject, message); + AdtPlugin.printToConsole(launchInfo.getProject(), message); String osLocalPath = path.toOSString(); - String apkName = launchInfo.mPackageFile.getName(); + String apkName = launchInfo.getPackageFile().getName(); String remotePath = "/data/local/tmp/" + apkName; //$NON-NLS-1$ SyncResult result = sync.pushFile(osLocalPath, remotePath, @@ -1054,7 +766,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener if (result.getCode() != SyncService.RESULT_OK) { String msg = String.format("Failed to upload %1$s on '%2$s': %3$s", apkName, device.getSerialNumber(), result.getMessage()); - AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); return false; } @@ -1075,7 +787,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } }); } catch (IOException e) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format( + AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format( "Failed to delete temporary package: %1$s", e.getMessage())); return false; } @@ -1085,8 +797,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener String msg = String.format( "Failed to upload %1$s on device '%2$s': Unable to open sync connection!", - launchInfo.mPackageFile.getName(), device.getSerialNumber()); - AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + launchInfo.getPackageFile().getName(), device.getSerialNumber()); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); return false; } @@ -1098,10 +810,10 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @param device The device on which the launch is done. */ private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath, - final Device device) { + final IDevice device) { - String message = String.format("Installing %1$s...", launchInfo.mPackageFile.getName()); - AdtPlugin.printToConsole(launchInfo.mProject, message); + String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName()); + AdtPlugin.printToConsole(launchInfo.getProject(), message); try { String result = doInstall(launchInfo, remotePath, device, false /* reinstall */); @@ -1129,10 +841,10 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @return true if success, false otherwise. * @throws IOException */ - private boolean checkInstallResult(String result, Device device, DelayedLaunchInfo launchInfo, + private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo, String remotePath, InstallRetryMode retryMode) throws IOException { if (result == null) { - AdtPlugin.printToConsole(launchInfo.mProject, "Success!"); + AdtPlugin.printToConsole(launchInfo.getProject(), "Success!"); return true; } else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$ if (retryMode == InstallRetryMode.PROMPT) { @@ -1141,7 +853,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener if (prompt) { retryMode = InstallRetryMode.ALWAYS; } else { - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Installation error! The package already exists."); return false; } @@ -1166,35 +878,35 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } */ - AdtPlugin.printToConsole(launchInfo.mProject, + AdtPlugin.printToConsole(launchInfo.getProject(), "Application already exists. Attempting to re-install instead..."); String res = doInstall(launchInfo, remotePath, device, true /* reinstall */); return checkInstallResult(res, device, launchInfo, remotePath, InstallRetryMode.NEVER); } - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Installation error! The package already exists."); } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$ - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Installation failed due to invalid APK file!", "Please check logcat output for more details."); } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$ - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Installation failed due to invalid URI!", "Please check logcat output for more details."); } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$ - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format("Installation failed: Could not copy %1$s to its final location!", - launchInfo.mPackageFile.getName()), + launchInfo.getPackageFile().getName()), "Please check logcat output for more details."); } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Re-installation failed due to different application signatures.", "You must perform a full uninstall of the application. WARNING: This will remove the application data!", - String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.mPackageName)); + String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName())); } else { - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format("Installation error: %1$s", result), "Please check logcat output for more details."); } @@ -1210,15 +922,15 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @throws IOException */ @SuppressWarnings("unused") - private String doUninstall(Device device, DelayedLaunchInfo launchInfo) throws IOException { + private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo) throws IOException { InstallReceiver receiver = new InstallReceiver(); try { - device.executeShellCommand("pm uninstall " + launchInfo.mPackageName, //$NON-NLS-1$ + device.executeShellCommand("pm uninstall " + launchInfo.getPackageName(), //$NON-NLS-1$ receiver); } catch (IOException e) { String msg = String.format( - "Failed to uninstall %1$s: %2$s", launchInfo.mPackageName, e.getMessage()); - AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage()); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); throw e; } @@ -1236,9 +948,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @throws IOException */ private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath, - final Device device, boolean reinstall) throws IOException { + final IDevice device, boolean reinstall) throws IOException { // kill running application - Client application = device.getClient(launchInfo.mPackageName); + Client application = device.getClient(launchInfo.getPackageName()); if (application != null) { application.kill(); } @@ -1252,8 +964,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } catch (IOException e) { String msg = String.format( "Failed to install %1$s on device '%2$s': %3$s", - launchInfo.mPackageFile.getName(), device.getSerialNumber(), e.getMessage()); - AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + launchInfo.getPackageFile().getName(), device.getSerialNumber(), + e.getMessage()); + AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); throw e; } @@ -1263,71 +976,37 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener /** * launches an application on a device or emulator * - * @param info the {@link DelayedLaunchInfo} that indicates the activity to launch + * @param info the {@link DelayedLaunchInfo} that indicates the launch action * @param device the device or emulator to launch the application on */ - private void launchApp(final DelayedLaunchInfo info, Device device) { - // if we're not supposed to do anything, just stop the Launch item and return; - if (info.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { - String msg = String.format("%1$s installed on device", - info.mPackageFile.getFullPath().toOSString()); - AdtPlugin.printToConsole(info.mProject, msg, "Done!"); - info.mLaunch.stopLaunch(); - return; - } - try { - String msg = String.format("Starting activity %1$s on device ", info.mActivity, - info.mDevice); - AdtPlugin.printToConsole(info.mProject, msg); - - // In debug mode, we need to add the info to the list of application monitoring - // client changes. - if (info.mDebugMode) { - synchronized (sListLock) { - if (mWaitingForDebuggerApplications.contains(info) == false) { - mWaitingForDebuggerApplications.add(info); - } + public void launchApp(final DelayedLaunchInfo info, IDevice device) { + if (info.isDebugMode()) { + synchronized (sListLock) { + if (mWaitingForDebuggerApplications.contains(info) == false) { + mWaitingForDebuggerApplications.add(info); } } - - // increment launch attempt count, to handle retries and timeouts - info.mAttemptCount++; - - // now we actually launch the app. - device.executeShellCommand("am start" //$NON-NLS-1$ - + (info.mDebugMode ? " -D" //$NON-NLS-1$ - : "") //$NON-NLS-1$ - + " -n " //$NON-NLS-1$ - + info.mPackageName + "/" //$NON-NLS-1$ - + info.mActivity.replaceAll("\\$", "\\\\\\$"), //$NON-NLS-1$ //$NON-NLS-2$ - new AMReceiver(info, device)); - + } + if (info.getLaunchAction().doLaunchAction(info, device)) { // if the app is not a debug app, we need to do some clean up, as // the process is done! - if (info.mDebugMode == false) { + if (info.isDebugMode() == false) { // stop the launch object, since there's no debug, and it can't // provide any control over the app - info.mLaunch.stopLaunch(); + stopLaunch(info); } - } catch (IOException e) { - // something went wrong trying to launch the app. + } else { + // something went wrong or no further launch action needed // lets stop the Launch - AdtPlugin.printErrorToConsole(info.mProject, - String.format("Launch error: %s", e.getMessage())); - info.mLaunch.stopLaunch(); - - // and remove it from the list of app waiting for debuggers - synchronized (sListLock) { - mWaitingForDebuggerApplications.remove(info); - } + stopLaunch(info); } } - private boolean launchEmulator(AndroidLaunchConfiguration config, VmInfo vmToLaunch) { + private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) { // split the custom command line in segments ArrayList customArgs = new ArrayList(); - boolean has_wipe_data = false; + boolean hasWipeData = false; if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) { String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$ @@ -1335,17 +1014,17 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener for (String s : segments) { if (s.length() > 0) { customArgs.add(s); - if (!has_wipe_data && s.equals(FLAG_WIPE_DATA)) { - has_wipe_data = true; + if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) { + hasWipeData = true; } } } } - boolean needs_wipe_data = config.mWipeData && !has_wipe_data; - if (needs_wipe_data) { + boolean needsWipeData = config.mWipeData && !hasWipeData; + if (needsWipeData) { if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) { - needs_wipe_data = false; + needsWipeData = false; } } @@ -1353,8 +1032,8 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener ArrayList list = new ArrayList(); list.add(AdtPlugin.getOsAbsoluteEmulator()); - list.add(FLAG_VM); - list.add(vmToLaunch.getName()); + list.add(FLAG_AVD); + list.add(avdToLaunch.getName()); if (config.mNetworkSpeed != null) { list.add(FLAG_NETSPEED); @@ -1366,7 +1045,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener list.add(config.mNetworkDelay); } - if (needs_wipe_data) { + if (needsWipeData) { list.add(FLAG_WIPE_DATA); } @@ -1470,7 +1149,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * @param monitor A Progress monitor * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor) */ - public static void launchRemoteDebugger( final int debugPort, final AndroidLaunch androidLaunch, + public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch, final IProgressMonitor monitor) { new Thread("Debugger connection") { //$NON-NLS-1$ @Override @@ -1495,25 +1174,22 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener */ public void bridgeChanged(AndroidDebugBridge bridge) { // The adb server has changed. We cancel any pending launches. - String message1 = "adb server change: cancelling '%1$s' launch!"; - String message2 = "adb server change: cancelling sync!"; + String message = "adb server change: cancelling '%1$s'!"; synchronized (sListLock) { for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) { - if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, message2); - } else { - AdtPlugin.printErrorToConsole(launchInfo.mProject, - String.format(message1, launchInfo.mActivity)); - } + AdtPlugin.printErrorToConsole(launchInfo.getProject(), + String.format(message, launchInfo.getLaunchAction().getLaunchDescription())); + stopLaunch(launchInfo); } for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) { - if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, message2); - } else { - AdtPlugin.printErrorToConsole(launchInfo.mProject, - String.format(message1, launchInfo.mActivity)); - } + AdtPlugin.printErrorToConsole(launchInfo.getProject(), + String.format(message, + launchInfo.getLaunchAction().getLaunchDescription())); + stopLaunch(launchInfo); } + + mWaitingForReadyEmulatorList.clear(); + mWaitingForDebuggerApplications.clear(); } } @@ -1535,15 +1211,15 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener mWaitingForEmulatorLaunches.remove(0); // give the launch item its device for later use. - launchInfo.mDevice = device; + launchInfo.setDevice(device); // and move it to the other list mWaitingForReadyEmulatorList.add(launchInfo); // and tell the user about it - AdtPlugin.printToConsole(launchInfo.mProject, + AdtPlugin.printToConsole(launchInfo.getProject(), String.format("New emulator found: %1$s", device.getSerialNumber())); - AdtPlugin.printToConsole(launchInfo.mProject, + AdtPlugin.printToConsole(launchInfo.getProject(), String.format("Waiting for HOME ('%1$s') to be launched...", AdtPlugin.getDefault().getPreferenceStore().getString( AdtPlugin.PREFS_HOME_PACKAGE))); @@ -1559,24 +1235,31 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener * * @see IDeviceChangeListener#deviceDisconnected(Device) */ + @SuppressWarnings("unchecked") public void deviceDisconnected(Device device) { // any pending launch on this device must be canceled. - String message = "%1$s disconnected! Cancelling '%2$s' launch!"; + String message = "%1$s disconnected! Cancelling '%2$s'!"; synchronized (sListLock) { - for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) { - if (launchInfo.mDevice == device) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, - String.format(message, device.getSerialNumber(), launchInfo.mActivity)); + ArrayList copyList = + (ArrayList) mWaitingForReadyEmulatorList.clone(); + for (DelayedLaunchInfo launchInfo : copyList) { + if (launchInfo.getDevice() == device) { + AdtPlugin.printErrorToConsole(launchInfo.getProject(), + String.format(message, device.getSerialNumber(), + launchInfo.getLaunchAction().getLaunchDescription())); + stopLaunch(launchInfo); } } - for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) { - if (launchInfo.mDevice == device) { - AdtPlugin.printErrorToConsole(launchInfo.mProject, - String.format(message, device.getSerialNumber(), launchInfo.mActivity)); + copyList = (ArrayList) mWaitingForDebuggerApplications.clone(); + for (DelayedLaunchInfo launchInfo : copyList) { + if (launchInfo.getDevice() == device) { + AdtPlugin.printErrorToConsole(launchInfo.getProject(), + String.format(message, device.getSerialNumber(), + launchInfo.getLaunchAction().getLaunchDescription())); + stopLaunch(launchInfo); } } } - } /** @@ -1617,13 +1300,13 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener if (home.equals(applicationName)) { // looks like home is up, get its device - Device device = client.getDevice(); + IDevice device = client.getDevice(); // look for application waiting for home synchronized (sListLock) { - for (int i = 0 ; i < mWaitingForReadyEmulatorList.size() ;) { + for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) { DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i); - if (launchInfo.mDevice == device) { + if (launchInfo.getDevice() == device) { // it's match, remove from the list mWaitingForReadyEmulatorList.remove(i); @@ -1633,13 +1316,13 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // so we check now if (checkBuildInfo(launchInfo, device) == false) { // device is not the proper API! - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); - launchInfo.mLaunch.stopLaunch(); + stopLaunch(launchInfo); return; } - AdtPlugin.printToConsole(launchInfo.mProject, + AdtPlugin.printToConsole(launchInfo.getProject(), String.format("HOME is up on device '%1$s'", device.getSerialNumber())); @@ -1649,9 +1332,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener launchApp(launchInfo, device); } else { // failure! Cancel and return - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); - launchInfo.mLaunch.stopLaunch(); + stopLaunch(launchInfo); } break; @@ -1703,18 +1386,18 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener String applicationName = client.getClientData().getClientDescription(); Log.d("adt", "App Name: " + applicationName); synchronized (sListLock) { - for (int i = 0 ; i < mWaitingForDebuggerApplications.size() ;) { + for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) { final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i); - if (client.getDevice() == launchInfo.mDevice && - applicationName.equals(launchInfo.mPackageName)) { + if (client.getDevice() == launchInfo.getDevice() && + applicationName.equals(launchInfo.getPackageName())) { // this is a match. We remove the launch info from the list mWaitingForDebuggerApplications.remove(i); // and connect the debugger. String msg = String.format( "Attempting to connect debugger to '%1$s' on port %2$d", - launchInfo.mPackageName, client.getDebuggerListenPort()); - AdtPlugin.printToConsole(launchInfo.mProject, msg); + launchInfo.getPackageName(), client.getDebuggerListenPort()); + AdtPlugin.printToConsole(launchInfo.getProject(), msg); new Thread("Debugger Connection") { //$NON-NLS-1$ @Override @@ -1722,18 +1405,19 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener try { if (connectRemoteDebugger( client.getDebuggerListenPort(), - launchInfo.mLaunch, launchInfo.mMonitor) == false) { + launchInfo.getLaunch(), + launchInfo.getMonitor()) == false) { return; } } catch (CoreException e) { // well something went wrong. - AdtPlugin.printErrorToConsole(launchInfo.mProject, + AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format("Launch error: %s", e.getMessage())); // stop the launch - launchInfo.mLaunch.stopLaunch(); + stopLaunch(launchInfo); } - launchInfo.mMonitor.done(); + launchInfo.getMonitor().done(); } }.start(); @@ -1809,4 +1493,15 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener }.start(); } + /* (non-Javadoc) + * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo) + */ + public void stopLaunch(DelayedLaunchInfo launchInfo) { + launchInfo.getLaunch().stopLaunch(); + synchronized (sListLock) { + mWaitingForReadyEmulatorList.remove(launchInfo); + mWaitingForDebuggerApplications.remove(launchInfo); + } + } } + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java new file mode 100644 index 000000000..a59518cd7 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DelayedLaunchInfo.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; + +import com.android.ddmlib.IDevice; + +/** + * A delayed launch waiting for a device to be present or ready before the + * application is launched. + */ +public final class DelayedLaunchInfo { + + /** + * Used to indicate behavior when Android app already exists + */ + enum InstallRetryMode { + NEVER, ALWAYS, PROMPT; + } + + /** The device on which to launch the app */ + private IDevice mDevice = null; + + /** The eclipse project */ + private final IProject mProject; + + /** Package name */ + private final String mPackageName; + + /** IFile to the package (.apk) file */ + private final IFile mPackageFile; + + /** debuggable attribute of the manifest file. */ + private final Boolean mDebuggable; + + /** Required ApiVersionNumber by the app. 0 means no requirements */ + private final int mRequiredApiVersionNumber; + + private InstallRetryMode mRetryMode = InstallRetryMode.NEVER; + + /** Launch action. */ + private final IAndroidLaunchAction mLaunchAction; + + /** the launch object */ + private final AndroidLaunch mLaunch; + + /** the monitor object */ + private final IProgressMonitor mMonitor; + + /** debug mode flag */ + private boolean mDebugMode; + + /** current number of launch attempts */ + private int mAttemptCount = 0; + + /** cancellation state of launch */ + private boolean mCancelled = false; + + /** + * Basic constructor with activity and package info. + * + * @param project the eclipse project that corresponds to Android app + * @param packageName package name of Android app + * @param launchAction action to perform after app install + * @param pack IFile to the package (.apk) file + * @param debuggable debuggable attribute of the app's manifest file. + * @param requiredApiVersionNumber required SDK version by the app. 0 means no requirements. + * @param launch the launch object + * @param monitor progress monitor for launch + */ + public DelayedLaunchInfo(IProject project, String packageName, + IAndroidLaunchAction launchAction, IFile pack, Boolean debuggable, + int requiredApiVersionNumber, AndroidLaunch launch, IProgressMonitor monitor) { + mProject = project; + mPackageName = packageName; + mPackageFile = pack; + mLaunchAction = launchAction; + mLaunch = launch; + mMonitor = monitor; + mDebuggable = debuggable; + mRequiredApiVersionNumber = requiredApiVersionNumber; + } + + /** + * @return the device on which to launch the app + */ + public IDevice getDevice() { + return mDevice; + } + + /** + * Set the device on which to launch the app + */ + public void setDevice(IDevice device) { + mDevice = device; + } + + /** + * @return the eclipse project that corresponds to Android app + */ + public IProject getProject() { + return mProject; + } + + /** + * @return the package name of the Android app + */ + public String getPackageName() { + return mPackageName; + } + + /** + * @return the application package file + */ + public IFile getPackageFile() { + return mPackageFile; + } + + /** + * @return true if Android app is marked as debuggable in its manifest + */ + public Boolean getDebuggable() { + return mDebuggable; + } + + /** + * @return the required api version number for the Android app + */ + public int getRequiredApiVersionNumber() { + return mRequiredApiVersionNumber; + } + + /** + * @param retryMode the install retry mode to set + */ + public void setRetryMode(InstallRetryMode retryMode) { + this.mRetryMode = retryMode; + } + + /** + * @return the installation retry mode + */ + public InstallRetryMode getRetryMode() { + return mRetryMode; + } + + /** + * @return the launch action + */ + public IAndroidLaunchAction getLaunchAction() { + return mLaunchAction; + } + + /** + * @return the launch + */ + public AndroidLaunch getLaunch() { + return mLaunch; + } + + /** + * @return the launch progress monitor + */ + public IProgressMonitor getMonitor() { + return mMonitor; + } + + /** + * @param debugMode the debug mode to set + */ + public void setDebugMode(boolean debugMode) { + this.mDebugMode = debugMode; + } + + /** + * @return true if this is a debug launch + */ + public boolean isDebugMode() { + return mDebugMode; + } + + /** + * Increases the number of launch attempts + */ + public void incrementAttemptCount() { + mAttemptCount++; + } + + /** + * @return the number of launch attempts made + */ + public int getAttemptCount() { + return mAttemptCount; + } + + /** + * Set if launch has been cancelled + */ + public void setCancelled(boolean cancelled) { + this.mCancelled = cancelled; + } + + /** + * @return true if launch has been cancelled + */ + public boolean isCancelled() { + return mCancelled; + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DeviceChooserDialog.java similarity index 56% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DeviceChooserDialog.java index 19ec9a7c3..13bb83acb 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/DeviceChooserDialog.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.launching; +package com.android.ide.eclipse.adt.launch; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.Client; @@ -26,21 +26,18 @@ import com.android.ddmuilib.IImageLoader; import com.android.ddmuilib.ImageHelper; import com.android.ddmuilib.TableHelper; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController.AndroidLaunchConfiguration; -import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController.DelayedLaunchInfo; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.ddms.DdmsPlugin; import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.vm.VmManager.VmInfo; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdkuilib.AvdSelector; -import org.eclipse.core.resources.IProject; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.viewers.DoubleClickEvent; -import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ILabelProviderListener; -import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredContentProvider; -import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; @@ -50,33 +47,36 @@ import org.eclipse.swt.SWTException; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Event; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; +import java.util.ArrayList; + +/** + * A dialog that lets the user choose a device to deploy an application. + * The user can either choose an exiting running device (including running emulators) + * or start a new emulator using an Android Virtual Device configuration that matches + * the current project. + */ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { - private final static int DLG_WIDTH = 500; - private final static int DLG_HEIGHT = 300; private final static int ICON_WIDTH = 16; private final static String PREFS_COL_SERIAL = "deviceChooser.serial"; //$NON-NLS-1$ - private final static String PREFS_COL_STATE = "deviceChooser.state"; //$NON-NLS-1$ - private final static String PREFS_COL_VM = "deviceChooser.vm"; //$NON-NLS-1$ + private final static String PREFS_COL_STATE = "deviceChooser.state"; //$NON-NLS-1$ + private final static String PREFS_COL_AVD = "deviceChooser.avd"; //$NON-NLS-1$ private final static String PREFS_COL_TARGET = "deviceChooser.target"; //$NON-NLS-1$ - private final static String PREFS_COL_DEBUG = "deviceChooser.debug"; //$NON-NLS-1$ + private final static String PREFS_COL_DEBUG = "deviceChooser.debug"; //$NON-NLS-1$ private Table mDeviceTable; private TableViewer mViewer; + private AvdSelector mPreferredAvdSelector; private Image mDeviceImage; private Image mEmulatorImage; @@ -84,13 +84,16 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener private Image mNoMatchImage; private Image mWarningImage; - private Button mOkButton; - private Button mCreateButton; - - private DeviceChooserResponse mResponse; - private DelayedLaunchInfo mLaunchInfo; - private IAndroidTarget mProjectTarget; - private Sdk mSdk; + private final DeviceChooserResponse mResponse; + private final String mPackageName; + private final IAndroidTarget mProjectTarget; + private final Sdk mSdk; + + private final AvdInfo[] mFullAvdList; + + private Button mDeviceRadioButton; + + private boolean mDisableAvdSelectionChange = false; /** * Basic Content Provider for a table full of {@link Device} objects. The input is @@ -135,22 +138,26 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener try { String apiValue = device.getProperty( IDevice.PROP_BUILD_VERSION_NUMBER); - int api = Integer.parseInt(apiValue); - if (api >= mProjectTarget.getApiVersionNumber()) { - // if the project is compiling against an add-on, the optional - // API may be missing from the device. - return mProjectTarget.isPlatform() ? - mMatchImage : mWarningImage; + if (apiValue != null) { + int api = Integer.parseInt(apiValue); + if (api >= mProjectTarget.getApiVersionNumber()) { + // if the project is compiling against an add-on, the optional + // API may be missing from the device. + return mProjectTarget.isPlatform() ? + mMatchImage : mWarningImage; + } else { + return mNoMatchImage; + } } else { - return mNoMatchImage; + return mWarningImage; } } catch (NumberFormatException e) { // lets consider the device non compatible return mNoMatchImage; } } else { - // get the VmInfo - VmInfo info = mSdk.getVmManager().getVm(device.getVmName()); + // get the AvdInfo + AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName()); if (info == null) { return mWarningImage; } @@ -171,19 +178,23 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener return device.getSerialNumber(); case 1: if (device.isEmulator()) { - return device.getVmName(); + return device.getAvdName(); } else { - return "N/A"; // devices don't have VM names. + return "N/A"; // devices don't have AVD names. } case 2: if (device.isEmulator()) { - VmInfo info = mSdk.getVmManager().getVm(device.getVmName()); + AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName()); if (info == null) { return "?"; } return info.getTarget().getFullName(); } else { - return device.getProperty(IDevice.PROP_BUILD_VERSION); + String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); + if (deviceBuild == null) { + return "unknown"; + } + return deviceBuild; } case 3: String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); @@ -219,62 +230,48 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener } public static class DeviceChooserResponse { - public boolean mustContinue; - public boolean mustLaunchEmulator; - public VmInfo vmToLaunch; - public Device deviceToUse; - } - - public DeviceChooserDialog(Shell parent) { - super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); - } - - /** - * Prepare and display the dialog. - * @param response - * @param project - * @param projectTarget - * @param launch - * @param launchInfo - * @param config - */ - public void open(DeviceChooserResponse response, IProject project, - IAndroidTarget projectTarget, AndroidLaunch launch, DelayedLaunchInfo launchInfo, - AndroidLaunchConfiguration config) { - mResponse = response; - mProjectTarget = projectTarget; - mLaunchInfo = launchInfo; - mSdk = Sdk.getCurrent(); - - Shell parent = getParent(); - Shell shell = new Shell(parent, getStyle()); - shell.setText("Device Chooser"); - - loadImages(); - createContents(shell); + private AvdInfo mAvdToLaunch; + private IDevice mDeviceToUse; - // Set the dialog size. - shell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); - Rectangle r = parent.getBounds(); - // get the center new top left. - int cx = r.x + r.width/2; - int x = cx - DLG_WIDTH / 2; - int cy = r.y + r.height/2; - int y = cy - DLG_HEIGHT / 2; - shell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); - - shell.pack(); - shell.open(); - - // start the listening. - AndroidDebugBridge.addDeviceChangeListener(this); - - Display display = parent.getDisplay(); - while (!shell.isDisposed()) { - if (!display.readAndDispatch()) - display.sleep(); + public void setDeviceToUse(IDevice d) { + mDeviceToUse = d; + mAvdToLaunch = null; } + public void setAvdToLaunch(AvdInfo avd) { + mAvdToLaunch = avd; + mDeviceToUse = null; + } + + public IDevice getDeviceToUse() { + return mDeviceToUse; + } + + public AvdInfo getAvdToLaunch() { + return mAvdToLaunch; + } + } + + public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName, + IAndroidTarget projectTarget) { + super(parent); + mResponse = response; + mPackageName = packageName; + mProjectTarget = projectTarget; + mSdk = Sdk.getCurrent(); + + // get the full list of Android Virtual Devices + AvdManager avdManager = mSdk.getAvdManager(); + if (avdManager != null) { + mFullAvdList = avdManager.getAvds(); + } else { + mFullAvdList = null; + } + + loadImages(); + } + + private void cleanup() { // done listening. AndroidDebugBridge.removeDeviceChangeListener(this); @@ -283,30 +280,73 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener mMatchImage.dispose(); mNoMatchImage.dispose(); mWarningImage.dispose(); - - AndroidLaunchController.getInstance().continueLaunch(response, project, launch, - launchInfo, config); } - /** - * Create the device chooser dialog contents. - * @param shell the parent shell. - */ - private void createContents(final Shell shell) { - shell.setLayout(new GridLayout(1, true)); + @Override + protected void okPressed() { + cleanup(); + super.okPressed(); + } + + @Override + protected void cancelPressed() { + cleanup(); + super.cancelPressed(); + } + + @Override + protected Control createContents(Composite parent) { + Control content = super.createContents(parent); + + // this must be called after createContents() has happened so that the + // ok button has been created (it's created after the call to createDialogArea) + updateDefaultSelection(); - shell.addListener(SWT.Close, new Listener() { - public void handleEvent(Event event) { - event.doit = true; + return content; + } + + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, true)); + + mDeviceRadioButton = new Button(top, SWT.RADIO); + mDeviceRadioButton.setText("Choose a running Android device"); + mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean deviceMode = mDeviceRadioButton.getSelection(); + + mDeviceTable.setEnabled(deviceMode); + mPreferredAvdSelector.setEnabled(!deviceMode); + + if (deviceMode) { + handleDeviceSelection(); + } else { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getFirstSelected()); + } + + enableOkButton(); } }); + mDeviceRadioButton.setSelection(true); - Label l = new Label(shell, SWT.NONE); - l.setText("Select the target device."); + + // offset the selector from the radio button + Composite offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); - mDeviceTable = new Table(shell, SWT.SINGLE | SWT.FULL_SELECTION); - mDeviceTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION); + GridData gd; + mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.heightHint = 100; + mDeviceTable.setHeaderVisible(true); mDeviceTable.setLinesVisible(true); @@ -314,9 +354,9 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ PREFS_COL_SERIAL, store); - TableHelper.createTableColumn(mDeviceTable, "VM Name", + TableHelper.createTableColumn(mDeviceTable, "AVD Name", SWT.LEFT, "engineering", //$NON-NLS-1$ - PREFS_COL_VM, store); + PREFS_COL_AVD, store); TableHelper.createTableColumn(mDeviceTable, "Target", SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ @@ -335,98 +375,78 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener mViewer.setContentProvider(new ContentProvider()); mViewer.setLabelProvider(new LabelProvider()); mViewer.setInput(AndroidDebugBridge.getBridge()); - mViewer.addDoubleClickListener(new IDoubleClickListener() { - public void doubleClick(DoubleClickEvent event) { - ISelection selection = event.getSelection(); - if (selection instanceof IStructuredSelection) { - IStructuredSelection structuredSelection = (IStructuredSelection)selection; - Object object = structuredSelection.getFirstElement(); - if (object instanceof Device) { - Device selectedDevice = (Device)object; - - mResponse.deviceToUse = selectedDevice; - mResponse.mustContinue = true; - shell.close(); - } - } - } - }); - - // bottom part with the ok/cancel - Composite bottomComp = new Composite(shell, SWT.NONE); - bottomComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - // 3 items in the layout: createButton, spacer, composite with ok/cancel - // (to force same width). - bottomComp.setLayout(new GridLayout(3 /* numColums */, false /* makeColumnsEqualWidth */)); - - mCreateButton = new Button(bottomComp, SWT.NONE); - mCreateButton.setText("Launch Emulator"); - mCreateButton.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - mResponse.mustContinue = true; - mResponse.mustLaunchEmulator = true; - shell.close(); - } - }); - - // the spacer - Composite spacer = new Composite(bottomComp, SWT.NONE); - GridData gd; - spacer.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); - gd.heightHint = 0; - - // the composite to contain ok/cancel - Composite buttonContainer = new Composite(bottomComp, SWT.NONE); - GridLayout gl = new GridLayout(2 /* numColums */, true /* makeColumnsEqualWidth */); - gl.marginHeight = gl.marginWidth = 0; - buttonContainer.setLayout(gl); - - mOkButton = new Button(buttonContainer, SWT.NONE); - mOkButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mOkButton.setEnabled(false); - mOkButton.setText("OK"); - mOkButton.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - mResponse.mustContinue = true; - shell.close(); - } - }); - - Button cancelButton = new Button(buttonContainer, SWT.NONE); - cancelButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - cancelButton.setText("Cancel"); - cancelButton.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - mResponse.mustContinue = false; - shell.close(); - } - }); mDeviceTable.addSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the device selector. + * {@inheritDoc} + */ @Override public void widgetSelected(SelectionEvent e) { - int count = mDeviceTable.getSelectionCount(); - if (count != 1) { - handleSelection(null); - } else { - int index = mDeviceTable.getSelectionIndex(); - Object data = mViewer.getElementAt(index); - if (data instanceof Device) { - handleSelection((Device)data); - } else { - handleSelection(null); - } + handleDeviceSelection(); + } + + /** + * Handles double-click selection on the device selector. + * Note that the single-click handler will probably already have been called. + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handleDeviceSelection(); + if (isOkButtonEnabled()) { + okPressed(); } } }); - mDeviceTable.setFocus(); - shell.setDefaultButton(mOkButton); + Button radio2 = new Button(top, SWT.RADIO); + radio2.setText("Launch a new Android Virtual Device"); + + // offset the selector from the radio button + offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); - updateDefaultSelection(); + mPreferredAvdSelector = new AvdSelector(offsetComp, getNonRunningAvds(), mProjectTarget); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setEnabled(false); + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the AVD selector. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (mDisableAvdSelectionChange == false) { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getFirstSelected()); + enableOkButton(); + } + } + + /** + * Handles double-click selection on the AVD selector. + * + * Note that the single-click handler will probably already have been called + * but the selected item can have changed in between. + * + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + widgetSelected(e); + if (isOkButtonEnabled()) { + okPressed(); + } + } + }); + + AndroidDebugBridge.addDeviceChangeListener(this); + + return top; } private void loadImages() { @@ -504,6 +524,10 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener // update the selection updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD.) + refillAvdList(); } else { // table is disposed, we need to do something. // lets remove ourselves from the listener. @@ -546,23 +570,57 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener // update the defaultSelection. updateDefaultSelection(); - + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD). This is done on deviceChanged because the avd name + // of a (emulator) device may be updated as the emulator boots. + refillAvdList(); + // if the changed device is the current selection, // we update the OK button based on its state. - if (device == mResponse.deviceToUse) { - mOkButton.setEnabled(mResponse.deviceToUse.isOnline()); + if (device == mResponse.getDeviceToUse()) { + enableOkButton(); } + } else { // table is disposed, we need to do something. // lets remove ourselves from the listener. AndroidDebugBridge.removeDeviceChangeListener(dialog); } - } }); } } + /** + * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). + */ + private boolean isDeviceMode() { + return mDeviceRadioButton.getSelection(); + } + + /** + * Enables or disables the OK button of the dialog based on various selections in the dialog. + */ + private void enableOkButton() { + Button okButton = getButton(IDialogConstants.OK_ID); + + if (isDeviceMode()) { + okButton.setEnabled(mResponse.getDeviceToUse() != null && + mResponse.getDeviceToUse().isOnline()); + } else { + okButton.setEnabled(mResponse.getAvdToLaunch() != null); + } + } + + /** + * Returns true if the ok button is enabled. + */ + private boolean isOkButtonEnabled() { + Button okButton = getButton(IDialogConstants.OK_ID); + return okButton.isEnabled(); + } + /** * Executes the {@link Runnable} in the UI thread. * @param runnable the runnable to execute. @@ -577,16 +635,31 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener } } + private void handleDeviceSelection() { + int count = mDeviceTable.getSelectionCount(); + if (count != 1) { + handleSelection(null); + } else { + int index = mDeviceTable.getSelectionIndex(); + Object data = mViewer.getElementAt(index); + if (data instanceof Device) { + handleSelection((Device)data); + } else { + handleSelection(null); + } + } + } + private void handleSelection(Device device) { - mResponse.deviceToUse = device; - mOkButton.setEnabled(device != null && mResponse.deviceToUse.isOnline()); + mResponse.setDeviceToUse(device); + enableOkButton(); } /** * Look for a default device to select. This is done by looking for the running * clients on each device and finding one similar to the one being launched. *

      - * This is done every time the device list changed, until there is a selection.. + * This is done every time the device list changed unless there is a already selection. */ private void updateDefaultSelection() { if (mDeviceTable.getSelectionCount() == 0) { @@ -599,8 +672,7 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener for (Client client : clients) { - if (mLaunchInfo.mPackageName.equals( - client.getClientData().getClientDescription())) { + if (mPackageName.equals(client.getClientData().getClientDescription())) { // found a match! Select it. mViewer.setSelection(new StructuredSelection(device)); handleSelection(device); @@ -611,6 +683,58 @@ public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener } } } - } + handleDeviceSelection(); + } + + /** + * Returns the list of {@link AvdInfo} that are not already running in an emulator. + */ + private AvdInfo[] getNonRunningAvds() { + ArrayList list = new ArrayList(); + + Device[] devices = AndroidDebugBridge.getBridge().getDevices(); + + // loop through all the Avd and put the one that are not running in the list. + avdLoop: for (AvdInfo info : mFullAvdList) { + for (Device d : devices) { + if (info.getName().equals(d.getAvdName())) { + continue avdLoop; + } + } + list.add(info); + } + + return list.toArray(new AvdInfo[list.size()]); + } + + /** + * Refills the AVD list keeping the current selection. + */ + private void refillAvdList() { + AvdInfo[] array = getNonRunningAvds(); + + // save the current selection + AvdInfo selected = mPreferredAvdSelector.getFirstSelected(); + + // disable selection change. + mDisableAvdSelectionChange = true; + + // set the new list in the selector + mPreferredAvdSelector.setAvds(array, mProjectTarget); + + // attempt to reselect the proper avd if needed + if (selected != null) { + if (mPreferredAvdSelector.setSelection(selected) == false) { + // looks like the selection is lost. this can happen if an emulator + // running the AVD that was selected was launched from outside of Eclipse). + mResponse.setAvdToLaunch(null); + enableOkButton(); + } + } + + // enable the selection change + mDisableAvdSelectionChange = false; + } } + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmptyLaunchAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmptyLaunchAction.java new file mode 100644 index 000000000..02ae6757c --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmptyLaunchAction.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ddmlib.IDevice; +import com.android.ide.eclipse.adt.AdtPlugin; + +/** + * A launch action that does nothing after the application has been installed + */ +public class EmptyLaunchAction implements IAndroidLaunchAction { + + public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) { + // we're not supposed to do anything, just return; + String msg = String.format("%1$s installed on device", + info.getPackageFile().getFullPath().toOSString()); + AdtPlugin.printToConsole(info.getProject(), msg, "Done!"); + // return false so launch controller will not wait for debugger to attach + return false; + } + + public String getLaunchDescription() { + return "sync"; + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java similarity index 89% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java index f4f5281c7..b898f63c5 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/EmulatorConfigTab.java @@ -14,17 +14,16 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.ui; +package com.android.ide.eclipse.adt.launch; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.ddms.DdmsPlugin; import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.vm.VmManager; -import com.android.sdklib.vm.VmManager.VmInfo; -import com.android.sdkuilib.VmSelector; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdkuilib.AvdSelector; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; @@ -75,7 +74,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { private Button mAutoTargetButton; private Button mManualTargetButton; - private VmSelector mPreferredVmSelector; + private AvdSelector mPreferredAvdSelector; private Combo mSpeedCombo; @@ -89,6 +88,8 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { private Button mNoBootAnimButton; + private Label mPreferredAvdLabel; + /** * Returns the emulator ready speed option value. * @param value The index of the combo selection. @@ -160,14 +161,26 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { @Override public void widgetSelected(SelectionEvent e) { updateLaunchConfigurationDialog(); + + boolean auto = mAutoTargetButton.getSelection(); + mPreferredAvdSelector.setEnabled(auto); + mPreferredAvdLabel.setEnabled(auto); } }); - new Label(targetModeGroup, SWT.NONE).setText("Preferred VM"); - VmInfo[] vms = new VmInfo[0]; - mPreferredVmSelector = new VmSelector(targetModeGroup, vms, - false /*allowMultipleSelection*/); - mPreferredVmSelector.setSelectionListener(new SelectionAdapter() { + Composite offsetComp = new Composite(targetModeGroup, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mPreferredAvdLabel = new Label(offsetComp, SWT.NONE); + mPreferredAvdLabel.setText("Select a preferred Android Virtual Device:"); + AvdInfo[] avds = new AvdInfo[0]; + mPreferredAvdSelector = new AvdSelector(offsetComp, avds); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { updateLaunchConfigurationDialog(); @@ -277,7 +290,7 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration) */ public void initializeFrom(ILaunchConfiguration configuration) { - VmManager vmManager = Sdk.getCurrent().getVmManager(); + AvdManager avdManager = Sdk.getCurrent().getAvdManager(); boolean value = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic try { @@ -311,34 +324,34 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { } } - // update the VM list - VmInfo[] vms = null; - if (vmManager != null) { - vms = vmManager.getVms(); + // update the AVD list + AvdInfo[] avds = null; + if (avdManager != null) { + avds = avdManager.getAvds(); } IAndroidTarget projectTarget = null; if (project != null) { projectTarget = Sdk.getCurrent().getTarget(project); } else { - vms = null; // no project? we don't want to display any "compatible" VMs. + avds = null; // no project? we don't want to display any "compatible" AVDs. } - mPreferredVmSelector.setVms(vms, projectTarget); + mPreferredAvdSelector.setAvds(avds, projectTarget); stringValue = ""; try { - stringValue = configuration.getAttribute(LaunchConfigDelegate.ATTR_VM_NAME, + stringValue = configuration.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, stringValue); } catch (CoreException e) { // let's not do anything here, we'll use the default value } - if (stringValue != null && stringValue.length() > 0 && vmManager != null) { - VmInfo targetVm = vmManager.getVm(stringValue); - mPreferredVmSelector.setSelection(targetVm); + if (stringValue != null && stringValue.length() > 0 && avdManager != null) { + AvdInfo targetAvd = avdManager.getAvd(stringValue); + mPreferredAvdSelector.setSelection(targetAvd); } else { - mPreferredVmSelector.setSelection(null); + mPreferredAvdSelector.setSelection(null); } value = LaunchConfigDelegate.DEFAULT_WIPE_DATA; @@ -404,11 +417,11 @@ public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { public void performApply(ILaunchConfigurationWorkingCopy configuration) { configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, mAutoTargetButton.getSelection()); - VmInfo vm = mPreferredVmSelector.getFirstSelected(); - if (vm != null) { - configuration.setAttribute(LaunchConfigDelegate.ATTR_VM_NAME, vm.getName()); + AvdInfo avd = mPreferredAvdSelector.getFirstSelected(); + if (avd != null) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, avd.getName()); } else { - configuration.setAttribute(LaunchConfigDelegate.ATTR_VM_NAME, (String)null); + configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null); } configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED, mSpeedCombo.getSelectionIndex()); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/IAndroidLaunchAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/IAndroidLaunchAction.java new file mode 100644 index 000000000..2f3cb8934 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/IAndroidLaunchAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ddmlib.IDevice; +import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo; + +/** + * An action to perform after performing a launch of an Android application + */ +public interface IAndroidLaunchAction { + + /** + * Do the launch + * + * @param info the {@link DelayedLaunchInfo} that contains launch details + * @param device the Android device to perform action on + * @returns true if launch was successfully, and controller should wait for debugger to attach + * (if applicable) + */ + boolean doLaunchAction(DelayedLaunchInfo info, IDevice device); + + /** + * Return a description of launch, to be used for logging and error messages + */ + String getLaunchDescription(); + +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ILaunchController.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ILaunchController.java new file mode 100644 index 000000000..2372c2d27 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/ILaunchController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ddmlib.IDevice; + +/** + * Interface for managing Android launches + */ +public interface ILaunchController { + + /** + * Launches an application on a device or emulator + * + * @param launchInfo the {@link DelayedLaunchInfo} that indicates the launch action + * @param device the device or emulator to launch the application on + */ + public void launchApp(DelayedLaunchInfo launchInfo, IDevice device); + + /** + * Cancels a launch + * + * @param launchInfo the {@link DelayedLaunchInfo} to cancel + */ + void stopLaunch(DelayedLaunchInfo launchInfo); +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegate.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegate.java new file mode 100644 index 000000000..7a743098a --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegate.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Platform; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate; +import org.osgi.framework.Bundle; +import java.io.IOException; +import java.net.URL; + +/** + *

      + * For Android projects, android.jar gets added to the launch configuration of + * JUnit tests as a bootstrap entry. This breaks JUnit tests as android.jar + * contains a skeleton version of JUnit classes and the JVM will stop with an error similar + * to:

      Error occurred during initialization of VM + * java/lang/NoClassDefFoundError: java/lang/ref/FinalReference
      + *

      + * At compile time, Eclipse does not know that there is no valid junit.jar in + * the classpath since it can find a correct reference to all the necessary + * org.junit.* classes in the android.jar so it does not prompt the user to add + * the JUnit3 or JUnit4 jar. + *

      + * This delegates removes the android.jar from the bootstrap path and if + * necessary also puts back the junit.jar in the user classpath. + *

      + * This delegate will be present for both Java and Android projects (delegates + * setting instead of only the current project) but the behavior for Java + * projects should be neutral since: + *

        + *
      1. Java tests can only compile (and then run) when a valid junit.jar is + * present + *
      2. There is no android.jar in Java projects + *
      + */ +public class JUnitLaunchConfigDelegate extends JUnitLaunchConfigurationDelegate { + + private static final String JUNIT_JAR = "junit.jar"; //$NON-NLS-1$ + + @Override + public String[][] getBootpathExt(ILaunchConfiguration configuration) throws CoreException { + String[][] bootpath = super.getBootpathExt(configuration); + return fixBootpathExt(bootpath); + } + + @Override + public String[] getClasspath(ILaunchConfiguration configuration) throws CoreException { + String[] classpath = super.getClasspath(configuration); + return fixClasspath(classpath, getJavaProjectName(configuration)); + } + + /** + * Removes the android.jar from the bootstrap path if present. + * + * @param bootpath Array of Arrays of bootstrap class paths + * @return a new modified (if applicable) bootpath + */ + public static String[][] fixBootpathExt(String[][] bootpath) { + for (int i = 0; i < bootpath.length; i++) { + if (bootpath[i] != null) { + // we assume that the android.jar can only be present in the + // bootstrap path of android tests + if (bootpath[i][0].endsWith(SdkConstants.FN_FRAMEWORK_LIBRARY)) { + bootpath[i] = null; + } + } + } + return bootpath; + } + + /** + * Add the junit.jar to the user classpath; since Eclipse was relying on + * android.jar to provide the appropriate org.junit classes, it does not + * know it actually needs the junit.jar. + * + * @param classpath Array containing classpath + * @param projectName The name of the project (for logging purposes) + * + * @return a new modified (if applicable) classpath + */ + public static String[] fixClasspath(String[] classpath, String projectName) { + // search for junit.jar; if any are found return immediately + for (int i = 0; i < classpath.length; i++) { + if (classpath[i].endsWith(JUNIT_JAR)) { + return classpath; + } + } + + // This delegate being called without a junit.jar present is only + // possible for Android projects. In a non-Android project, the test + // would not compile and would be unable to run. + try { + // junit4 is backward compatible with junit3 and they uses the + // same junit.jar from bundle org.junit: + // When a project has mixed JUnit3 and JUnit4 tests, if JUnit3 jar + // is added first it is then replaced by the JUnit4 jar when user is + // prompted to fix the JUnit4 test failure + String jarLocation = getJunitJarLocation(); + // we extend the classpath by one element and append junit.jar + String[] newClasspath = new String[classpath.length + 1]; + System.arraycopy(classpath, 0, newClasspath, 0, classpath.length); + newClasspath[newClasspath.length - 1] = jarLocation; + classpath = newClasspath; + } catch (IOException e) { + // This should not happen as we depend on the org.junit + // plugin explicitly; the error is logged here so that the user can + // trace back the cause when the test fails to run + AdtPlugin.log(e, "Could not find a valid junit.jar"); + AdtPlugin.printErrorToConsole(projectName, + "Could not find a valid junit.jar"); + // Return the classpath as-is (with no junit.jar) anyway because we + // will let the actual launch config fails. + } + + return classpath; + } + + /** + * Returns the path of the junit jar in the highest version bundle. + * + * (This is public only so that the test can call it) + * + * @return the path as a string + * @throws IOException + */ + public static String getJunitJarLocation() throws IOException { + Bundle bundle = Platform.getBundle("org.junit"); //$NON-NLS-1$ + if (bundle == null) { + throw new IOException("Cannot find org.junit bundle"); + } + URL jarUrl = bundle.getEntry(AndroidConstants.WS_SEP + JUNIT_JAR); + return FileLocator.resolve(jarUrl).getFile(); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java similarity index 95% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java index 68deec378..80f62eaa8 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigDelegate.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.launching; +package com.android.ide.eclipse.adt.launch; import com.android.ddmlib.AndroidDebugBridge; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController.AndroidLaunchConfiguration; import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.AndroidManifestParser; @@ -80,7 +79,7 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate { */ public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$ - public static final String ATTR_VM_NAME = AdtPlugin.PLUGIN_ID + ".vm"; //$NON-NLS-1$ + public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$ public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$ @@ -233,7 +232,16 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate { return; } - String activityName = null; + doLaunch(configuration, mode, monitor, project, androidLaunch, config, controller, + applicationPackage, manifestParser); + } + + protected void doLaunch(ILaunchConfiguration configuration, String mode, + IProgressMonitor monitor, IProject project, AndroidLaunch androidLaunch, + AndroidLaunchConfiguration config, AndroidLaunchController controller, + IFile applicationPackage, AndroidManifestParser manifestParser) { + + String activityName = null; if (config.mLaunchAction == ACTION_ACTIVITY) { // Get the activity name defined in the config @@ -292,11 +300,16 @@ public class LaunchConfigDelegate extends LaunchConfigurationDelegate { } } + IAndroidLaunchAction launchAction = new EmptyLaunchAction(); + if (activityName != null) { + launchAction = new ActivityLaunchAction(activityName, controller); + } + // everything seems fine, we ask the launch controller to handle // the rest controller.launch(project, mode, applicationPackage, manifestParser.getPackage(), manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(), - activityName, config, androidLaunch, monitor); + launchAction, config, androidLaunch, monitor); } @Override diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigTabGroup.java similarity index 96% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigTabGroup.java index c0dbd54f7..f1dbd265a 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchConfigTabGroup.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.ui; +package com.android.ide.eclipse.adt.launch; import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; import org.eclipse.debug.ui.CommonTab; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchShortcut.java similarity index 98% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchShortcut.java index 92677f1d9..6b2744cbd 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/LaunchShortcut.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.launching; +package com.android.ide.eclipse.adt.launch; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java similarity index 98% rename from tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java rename to tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java index 6a40ed0f6..599da5f51 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/launch/MainLaunchConfigTab.java @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.debug.ui; +package com.android.ide.eclipse.adt.launch; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController; -import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.ProjectChooserHelper; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java index c27c106d7..458f78e49 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java @@ -17,11 +17,19 @@ package com.android.ide.eclipse.adt.preferences; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.sdklib.IAndroidTarget; +import com.android.sdkuilib.SdkTargetSelector; +import org.eclipse.core.resources.IProject; import org.eclipse.jface.preference.DirectoryFieldEditor; import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; @@ -80,6 +88,9 @@ public class AndroidPreferencePage extends FieldEditorPreferencePage implements */ private static class SdkDirectoryFieldEditor extends DirectoryFieldEditor { + private SdkTargetSelector mTargetSelector; + private TargetChangedListener mTargetChangeListener; + public SdkDirectoryFieldEditor(String name, String labelText, Composite parent) { super(name, labelText, parent); setEmptyStringAllowed(false); @@ -131,5 +142,68 @@ public class AndroidPreferencePage extends FieldEditorPreferencePage implements setValidateStrategy(VALIDATE_ON_KEY_STROKE); return super.getTextControl(parent); } + + /* (non-Javadoc) + * Method declared on StringFieldEditor (and FieldEditor). + */ + @Override + protected void doFillIntoGrid(Composite parent, int numColumns) { + super.doFillIntoGrid(parent, numColumns); + + GridData gd; + Label l = new Label(parent, SWT.NONE); + l.setText("Note: The list of SDK Targets below is only reloaded once you hit 'Apply' or 'OK'."); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = numColumns; + l.setLayoutData(gd); + + try { + // We may not have an sdk if the sdk path pref is empty or not valid. + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null; + + mTargetSelector = new SdkTargetSelector(parent, + targets, + false, /*allowSelection*/ + false /*multipleSelection*/); + gd = (GridData) mTargetSelector.getLayoutData(); + gd.horizontalSpan = numColumns; + + if (mTargetChangeListener == null) { + mTargetChangeListener = new TargetChangedListener(); + AdtPlugin.getDefault().addTargetListener(mTargetChangeListener); + } + } catch (Exception e) { + // We need to catch *any* exception that arises here, otherwise it disables + // the whole pref panel. We can live without the Sdk target selector but + // not being able to actually set an sdk path. + AdtPlugin.log(e, "SdkTargetSelector failed"); + } + } + + @Override + public void dispose() { + super.dispose(); + if (mTargetChangeListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetChangeListener); + mTargetChangeListener = null; + } + } + + private class TargetChangedListener implements ITargetChangeListener { + public void onProjectTargetChange(IProject changedProject) { + // do nothing. + } + + public void onTargetsLoaded() { + if (mTargetSelector != null) { + // We may not have an sdk if the sdk path pref is empty or not valid. + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null; + + mTargetSelector.setTargets(targets); + } + } + } } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java deleted file mode 100644 index a1b3c38f5..000000000 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php - * - * 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.ide.eclipse.adt.project; - -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.common.AndroidConstants; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jdt.core.ICompilationUnit; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IPackageFragment; -import org.eclipse.jdt.core.IPackageFragmentRoot; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.ITypeHierarchy; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.ui.IObjectActionDelegate; -import org.eclipse.ui.IWorkbenchPart; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * Action going through all the source of a project and creating a pre-processed aidl file - * with all the custom parcelable classes. - */ -public class CreateAidlImportAction implements IObjectActionDelegate { - - private ISelection mSelection; - - public CreateAidlImportAction() { - // pass - } - - public void setActivePart(IAction action, IWorkbenchPart targetPart) { - // pass - } - - /* - * (non-Javadoc) - * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction) - */ - public void run(IAction action) { - if (mSelection instanceof IStructuredSelection) { - for (Iterator it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { - Object element = it.next(); - IProject project = null; - if (element instanceof IProject) { - project = (IProject)element; - } else if (element instanceof IAdaptable) { - project = (IProject)((IAdaptable)element).getAdapter(IProject.class); - } - if (project != null) { - final IProject fproject = project; - new Job("Aidl preprocess") { - @Override - protected IStatus run(IProgressMonitor monitor) { - return createImportFile(fproject, monitor); - } - }.schedule(); - } - } - } - } - - public void selectionChanged(IAction action, ISelection selection) { - mSelection = selection; - } - - private IStatus createImportFile(IProject project, IProgressMonitor monitor) { - try { - if (monitor != null) { - monitor.beginTask(String.format( - "Creating aid preprocess file for %1$s", project.getName()), 1); - } - - ArrayList parcelables = new ArrayList(); - - IJavaProject javaProject = JavaCore.create(project); - - IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); - - for (IPackageFragmentRoot root : roots) { - if (root.isArchive() == false && root.isExternal() == false) { - parsePackageFragmentRoot(root, parcelables, monitor); - } - } - - // create the file with the parcelables - if (parcelables.size() > 0) { - IPath path = project.getLocation(); - path = path.append(AndroidConstants.FN_PROJECT_AIDL); - - File f = new File(path.toOSString()); - if (f.exists() == false) { - if (f.createNewFile() == false) { - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, - "Failed to create /project.aidl"); - } - } - - FileWriter fw = new FileWriter(f); - - fw.write("// This file is auto-generated by the\n"); - fw.write("// 'Create Aidl preprocess file for Parcelable classes'\n"); - fw.write("// action. Do not modify!\n\n"); - - for (String parcelable : parcelables) { - fw.write("parcelable "); //$NON-NLS-1$ - fw.write(parcelable); - fw.append(";\n"); //$NON-NLS-1$ - } - - fw.close(); - - // need to refresh the level just below the project to make sure it's being picked - // up by eclipse. - project.refreshLocal(IResource.DEPTH_ONE, monitor); - } - - if (monitor != null) { - monitor.worked(1); - monitor.done(); - } - - return Status.OK_STATUS; - } catch (JavaModelException e) { - return e.getJavaModelStatus(); - } catch (IOException e) { - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, - "Failed to create /project.aidl", e); - } catch (CoreException e) { - return e.getStatus(); - } finally { - if (monitor != null) { - monitor.done(); - } - } - } - - private void parsePackageFragmentRoot(IPackageFragmentRoot root, - ArrayList parcelables, IProgressMonitor monitor) throws JavaModelException { - - IJavaElement[] elements = root.getChildren(); - - for (IJavaElement element : elements) { - if (element instanceof IPackageFragment) { - ICompilationUnit[] compilationUnits = - ((IPackageFragment)element).getCompilationUnits(); - - for (ICompilationUnit unit : compilationUnits) { - IType[] types = unit.getTypes(); - - for (IType type : types) { - parseType(type, parcelables, monitor); - } - } - } - } - } - - private void parseType(IType type, ArrayList parcelables, IProgressMonitor monitor) - throws JavaModelException { - // first look in this type if it somehow extends parcelable. - ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(monitor); - - IType[] superInterfaces = typeHierarchy.getAllSuperInterfaces(type); - for (IType superInterface : superInterfaces) { - if (AndroidConstants.CLASS_PARCELABLE.equals(superInterface.getFullyQualifiedName())) { - parcelables.add(type.getFullyQualifiedName()); - } - } - - // then look in inner types. - IType[] innerTypes = type.getTypes(); - - for (IType innerType : innerTypes) { - parseType(innerType, parcelables, monitor); - } - } -} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java index b8a0b0c8d..49bbd8105 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java @@ -17,7 +17,7 @@ package com.android.ide.eclipse.adt.project; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; +import com.android.ide.eclipse.adt.launch.LaunchConfigDelegate; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java index 7fc3318f2..59b2b06c2 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java @@ -39,7 +39,7 @@ public class FolderDecorator implements ILightweightLabelDecorator { private ImageDescriptor mDescriptor; public FolderDecorator() { - mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); + mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); //$NON-NLS-1$ } public void decorate(Object element, IDecoration decoration) { @@ -55,13 +55,13 @@ public class FolderDecorator implements ILightweightLabelDecorator { if (folder.getParent().getType() == IResource.PROJECT) { String name = folder.getName(); if (name.equals(SdkConstants.FD_ASSETS)) { - decorate(decoration, " [Android assets]"); - decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); + doDecoration(decoration, null); } else if (name.equals(SdkConstants.FD_RESOURCES)) { - decorate(decoration, " [Android resources]"); - decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); - } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { - decorate(decoration, " [Native Libraries]"); + doDecoration(decoration, null); + } else if (name.equals(SdkConstants.FD_GEN_SOURCES)) { + doDecoration(decoration, " [Generated Java Files]"); + } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { + doDecoration(decoration, null); } } } @@ -72,20 +72,24 @@ public class FolderDecorator implements ILightweightLabelDecorator { } } - public void decorate(IDecoration decoration, String suffix) { - decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); + public void doDecoration(IDecoration decoration, String suffix) { + decoration.addOverlay(mDescriptor, IDecoration.TOP_LEFT); - // this is broken as it changes the color of the whole text, not only of the decoration. - // TODO: figure out how to change the color of the decoration only. -// decoration.addSuffix(suffix); -// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); -// ColorRegistry registry = theme.getColorRegistry(); -// decoration.setForegroundColor(registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations")); + if (suffix != null) { + decoration.addSuffix(suffix); + + // this is broken as it changes the color of the whole text, not only of the decoration. + // TODO: figure out how to change the color of the decoration only. +// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); +// ColorRegistry registry = theme.getColorRegistry(); +// decoration.setForegroundColor( +// registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations")); //$NON-NLS-1$ + } } public boolean isLabelProperty(Object element, String property) { - // at this time return false. + // Property change do not affect the label return false; } @@ -93,13 +97,11 @@ public class FolderDecorator implements ILightweightLabelDecorator { // No state change will affect the rendering. } - - public void removeListener(ILabelProviderListener listener) { // No state change will affect the rendering. } public void dispose() { - // nothind to dispose + // nothing to dispose } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java index 867892389..c650b9846 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java @@ -19,9 +19,9 @@ package com.android.ide.eclipse.adt.project; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; import com.android.ide.eclipse.common.project.AndroidManifestParser; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; @@ -361,7 +361,7 @@ public final class ProjectHelper { } /** - * Returns a {@link IProject} by its running application name, as it returned by the VM. + * Returns a {@link IProject} by its running application name, as it returned by the AVD. *

      * applicationName will in most case be the package declared in the manifest, but * can, in some cases, be a custom process name declared in the manifest, in the @@ -389,19 +389,20 @@ public final class ProjectHelper { continue; } - AndroidManifestHelper androidManifest = new AndroidManifestHelper(p); - // check that there is indeed a manifest file. - if (androidManifest.getManifestIFile() == null) { + IFile manifestFile = AndroidManifestParser.getManifest(p); + if (manifestFile == null) { // no file? skip this project. continue; } AndroidManifestParser parser = null; try { - parser = AndroidManifestParser.parseForData( - androidManifest.getManifestIFile()); + parser = AndroidManifestParser.parseForData(manifestFile); } catch (CoreException e) { + // ignore, handled below. + } + if (parser == null) { // skip this project. continue; } @@ -665,4 +666,17 @@ public final class ProjectHelper { return false; } + + /** + * Returns the apk filename for the given project + * @param project The project. + * @param config An optional config name. Can be null. + */ + public static String getApkFilename(IProject project, String config) { + if (config != null) { + return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$ + } + + return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java index 399eac958..6ede10dba 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java @@ -18,14 +18,20 @@ package com.android.ide.eclipse.adt.project.export; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.jarutils.KeystoreHelper; import com.android.jarutils.SignedJarBuilder; import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; import com.android.jarutils.DebugKeyProvider.KeytoolException; +import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.wizard.Wizard; @@ -35,12 +41,14 @@ import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IExportWizard; import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; @@ -49,6 +57,9 @@ import java.security.KeyStore.PrivateKeyEntry; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; /** * Export wizard to export an apk signed with a release key/certificate. @@ -66,7 +77,12 @@ public final class ExportWizard extends Wizard implements IExportWizard { static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$ static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$ static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$ + static final String PROPERTY_FILENAME = "baseFilename"; //$NON-NLS-1$ + static final int APK_FILE_SOURCE = 0; + static final int APK_FILE_DEST = 1; + static final int APK_COUNT = 2; + /** * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback. */ @@ -131,7 +147,7 @@ public final class ExportWizard extends Wizard implements IExportWizard { * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a * {@link Throwable} object. */ - protected final void onException(Throwable t) { + protected void onException(Throwable t) { String message = getExceptionMessage(t); setErrorMessage(message); @@ -155,9 +171,7 @@ public final class ExportWizard extends Wizard implements IExportWizard { private PrivateKey mPrivateKey; private X509Certificate mCertificate; - private String mDestinationPath; - private String mApkFilePath; - private String mApkFileName; + private File mDestinationParentFolder; private ExportWizardPage mKeystoreSelectionPage; private ExportWizardPage mKeyCreationPage; @@ -168,6 +182,8 @@ public final class ExportWizard extends Wizard implements IExportWizard { private List mExistingAliases; + private Map mApkMap; + public ExportWizard() { setHelpAvailable(false); // TODO have help setWindowTitle("Export Android Application"); @@ -186,24 +202,51 @@ public final class ExportWizard extends Wizard implements IExportWizard { @Override public boolean performFinish() { - // first we make sure export is fine if the destination file already exists - File f = new File(mDestinationPath); - if (f.isFile()) { - if (AdtPlugin.displayPrompt("Export Wizard", - "File already exists. Do you want to overwrite it?") == false) { - return false; - } - } - // save the properties ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore); ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias); - ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, mDestinationPath); + ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, + mDestinationParentFolder.getAbsolutePath()); + ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME, + mApkMap.get(null)[APK_FILE_DEST]); + // run the export in an UI runnable. + IWorkbench workbench = PlatformUI.getWorkbench(); + final boolean[] result = new boolean[1]; try { + workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() { + /** + * Run the export. + * @throws InvocationTargetException + * @throws InterruptedException + */ + public void run(IProgressMonitor monitor) throws InvocationTargetException, + InterruptedException { + try { + result[0] = doExport(monitor); + } finally { + monitor.done(); + } + } + }); + } catch (InvocationTargetException e) { + return false; + } catch (InterruptedException e) { + return false; + } + + return result[0]; + } + + private boolean doExport(IProgressMonitor monitor) { + try { + // first we make sure the project is built + mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor); + + // if needed, create the keystore and/or key. if (mKeystoreCreationMode || mKeyCreationMode) { final ArrayList output = new ArrayList(); - if (KeystoreHelper.createNewStore( + boolean createdStore = KeystoreHelper.createNewStore( mKeystore, null /*storeType*/, mKeystorePassword, @@ -218,7 +261,9 @@ public final class ExportWizard extends Wizard implements IExportWizard { public void out(String message) { output.add(message); } - }) == false) { + }); + + if (createdStore == false) { // keystore creation error! displayError(output.toArray(new String[output.size()])); return false; @@ -245,20 +290,42 @@ public final class ExportWizard extends Wizard implements IExportWizard { // check the private key/certificate again since it may have been created just above. if (mPrivateKey != null && mCertificate != null) { - FileOutputStream fos = new FileOutputStream(mDestinationPath); - SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); - - // get the input file. - FileInputStream fis = new FileInputStream(mApkFilePath); - try { - builder.writeZip(fis, null /* filter */); - } finally { - fis.close(); + // get the output folder of the project to export. + // this is where we'll find the built apks to resign and export. + IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject); + if (outputIFolder == null) { + return false; } - - builder.close(); - fos.close(); + String outputOsPath = outputIFolder.getLocation().toOSString(); + // now generate the packages. + Set> set = mApkMap.entrySet(); + for (Entry entry : set) { + String[] defaultApk = entry.getValue(); + String srcFilename = defaultApk[APK_FILE_SOURCE]; + String destFilename = defaultApk[APK_FILE_DEST]; + + FileOutputStream fos = new FileOutputStream( + new File(mDestinationParentFolder, destFilename)); + SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); + + // get the input file. + FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename)); + + // add the content of the source file to the output file, and sign it at + // the same time. + try { + builder.writeZip(fis, null /* filter */); + // close the builder: write the final signature files, and close the archive. + builder.close(); + } finally { + try { + fis.close(); + } finally { + fos.close(); + } + } + } return true; } } catch (FileNotFoundException e) { @@ -271,6 +338,8 @@ public final class ExportWizard extends Wizard implements IExportWizard { displayError(e); } catch (KeytoolException e) { displayError(e); + } catch (CoreException e) { + displayError(e); } return false; @@ -282,10 +351,10 @@ public final class ExportWizard extends Wizard implements IExportWizard { // a private key/certificate or the creation mode. In creation mode, unless // all the key/keystore info is valid, the user cannot reach the last page, so there's // no need to check them again here. - return mApkFilePath != null && + return mApkMap != null && mApkMap.size() > 0 && ((mPrivateKey != null && mCertificate != null) || mKeystoreCreationMode || mKeyCreationMode) && - mDestinationPath != null; + mDestinationParentFolder != null; } /* @@ -334,18 +403,12 @@ public final class ExportWizard extends Wizard implements IExportWizard { return mProject; } - void setProject(IProject project, String apkFilePath, String filename) { + void setProject(IProject project) { mProject = project; - mApkFilePath = apkFilePath; - mApkFileName = filename; updatePageOnChange(ExportWizardPage.DATA_PROJECT); } - String getApkFilename() { - return mApkFileName; - } - void setKeystore(String path) { mKeystore = path; mPrivateKey = null; @@ -444,10 +507,16 @@ public final class ExportWizard extends Wizard implements IExportWizard { mCertificate = certificate; } - void setDestination(String path) { - mDestinationPath = path; + void setDestination(File parentFolder, Map apkMap) { + mDestinationParentFolder = parentFolder; + mApkMap = apkMap; } - + + void resetDestination() { + mDestinationParentFolder = null; + mApkMap = null; + } + void updatePageOnChange(int changeMask) { for (ExportWizardPage page : mPages) { page.projectDataChanged(changeMask); @@ -484,7 +553,7 @@ public final class ExportWizard extends Wizard implements IExportWizard { *

      If no Throwable in the chain has a valid message, the canonical name of the first * exception is returned. */ - private static String getExceptionMessage(Throwable t) { + static String getExceptionMessage(Throwable t) { String message = t.getMessage(); if (message == null) { Throwable cause = t.getCause(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java index c64bf1059..7fd76e9e8 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java @@ -18,13 +18,18 @@ package com.android.ide.eclipse.adt.project.export; import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; +import com.android.ide.eclipse.adt.sdk.Sdk; import org.eclipse.core.resources.IProject; import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; @@ -47,6 +52,10 @@ import java.security.KeyStore.PrivateKeyEntry; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; /** * Final page of the wizard that checks the key and ask for the ouput location. @@ -59,6 +68,12 @@ final class KeyCheckPage extends ExportWizardPage { private Text mDestination; private boolean mFatalSigningError; private FormText mDetailText; + /** The Apk Config map for the current project */ + private Map mApkConfig; + private ScrolledComposite mScrolledComposite; + + private String mKeyDetails; + private String mDestinationDetails; protected KeyCheckPage(ExportWizard wizard, String pageName) { super(pageName); @@ -86,7 +101,7 @@ final class KeyCheckPage extends ExportWizardPage { mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); mDestination.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { - onDestinationChange(); + onDestinationChange(false /*forceDetailUpdate*/); } }); final Button browseButton = new Button(composite, SWT.PUSH); @@ -97,7 +112,10 @@ final class KeyCheckPage extends ExportWizardPage { FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE); fileDialog.setText("Destination file name"); - fileDialog.setFileName(mWizard.getApkFilename()); + // get a default apk name based on the project + String filename = ProjectHelper.getApkFilename(mWizard.getProject(), + null /*config*/); + fileDialog.setFileName(filename); String saveLocation = fileDialog.open(); if (saveLocation != null) { @@ -106,9 +124,21 @@ final class KeyCheckPage extends ExportWizardPage { } }); - mDetailText = new FormText(composite, SWT.NONE); - mDetailText.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL); + mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); gd.horizontalSpan = 3; + mScrolledComposite.setExpandHorizontal(true); + mScrolledComposite.setExpandVertical(true); + + mDetailText = new FormText(mScrolledComposite, SWT.NONE); + mScrolledComposite.setContent(mDetailText); + + mScrolledComposite.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + updateScrolling(); + } + }); setControl(composite); } @@ -119,11 +149,14 @@ final class KeyCheckPage extends ExportWizardPage { if ((mProjectDataChanged & DATA_PROJECT) != 0) { // reset the destination from the content of the project IProject project = mWizard.getProject(); + mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project); String destination = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_DESTINATION); - if (destination != null) { - mDestination.setText(destination); + String filename = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_FILENAME); + if (destination != null && filename != null) { + mDestination.setText(destination + File.separator + filename); } } @@ -134,11 +167,14 @@ final class KeyCheckPage extends ExportWizardPage { // reset the wizard with no key/cert to make it not finishable, unless a valid // key/cert is found. mWizard.setSigningInfo(null, null); + mPrivateKey = null; + mCertificate = null; + mKeyDetails = null; if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) { int validity = mWizard.getValidity(); StringBuilder sb = new StringBuilder( - String.format("

      Certificate expires in %d years.

      ", + String.format("

      Certificate expires in %d years.

      ", validity)); if (validity < 25) { @@ -149,8 +185,7 @@ final class KeyCheckPage extends ExportWizardPage { sb.append("

      Android Market currently requires certificates to be valid until 2033.

      "); } - sb.append("
      "); - mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */); + mKeyDetails = sb.toString(); } else { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -192,10 +227,9 @@ final class KeyCheckPage extends ExportWizardPage { Calendar today = Calendar.getInstance(); if (expirationCalendar.before(today)) { - mDetailText.setText(String.format( - "

      Certificate expired on %s

      ", - mCertificate.getNotAfter().toString()), - true /* parseTags */, true /* expandURLs */); + mKeyDetails = String.format( + "

      Certificate expired on %s

      ", + mCertificate.getNotAfter().toString()); // fatal error = nothing can make the page complete. mFatalSigningError = true; @@ -207,7 +241,7 @@ final class KeyCheckPage extends ExportWizardPage { mWizard.setSigningInfo(mPrivateKey, mCertificate); StringBuilder sb = new StringBuilder(String.format( - "

      Certificate expires on %s.

      ", + "

      Certificate expires on %s.

      ", mCertificate.getNotAfter().toString())); int expirationYear = expirationCalendar.get(Calendar.YEAR); @@ -232,11 +266,8 @@ final class KeyCheckPage extends ExportWizardPage { sb.append("

      Android Market currently requires certificates to be valid until 2033.

      "); } - sb.append("
      "); - - mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */); + mKeyDetails = sb.toString(); } - mDetailText.getParent().layout(); } else { // fatal error = nothing can make the page complete. mFatalSigningError = true; @@ -244,10 +275,15 @@ final class KeyCheckPage extends ExportWizardPage { } } - onDestinationChange(); + onDestinationChange(true /*forceDetailUpdate*/); } - private void onDestinationChange() { + /** + * Callback for destination field edition + * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal + * error has happened in the signing. + */ + private void onDestinationChange(boolean forceDetailUpdate) { if (mFatalSigningError == false) { // reset messages for now. setErrorMessage(null); @@ -257,7 +293,8 @@ final class KeyCheckPage extends ExportWizardPage { if (path.length() == 0) { setErrorMessage("Enter destination for the APK file."); - mWizard.setDestination(null); // this is to reset canFinish in the wizard + // reset canFinish in the wizard. + mWizard.resetDestination(); setPageComplete(false); return; } @@ -265,27 +302,142 @@ final class KeyCheckPage extends ExportWizardPage { File file = new File(path); if (file.isDirectory()) { setErrorMessage("Destination is a directory."); - mWizard.setDestination(null); // this is to reset canFinish in the wizard + // reset canFinish in the wizard. + mWizard.resetDestination(); setPageComplete(false); return; } - File parentFile = file.getParentFile(); - if (parentFile == null || parentFile.isDirectory() == false) { + File parentFolder = file.getParentFile(); + if (parentFolder == null || parentFolder.isDirectory() == false) { setErrorMessage("Not a valid directory."); - mWizard.setDestination(null); // this is to reset canFinish in the wizard + // reset canFinish in the wizard. + mWizard.resetDestination(); setPageComplete(false); return; } + // display the list of files that will actually be created + Map apkFileMap = getApkFileMap(file); + + // display them + boolean fileExists = false; + StringBuilder sb = new StringBuilder(String.format( + "

      This will create the following files:

      ")); + + Set> set = apkFileMap.entrySet(); + for (Entry entry : set) { + String[] apkArray = entry.getValue(); + String filename = apkArray[ExportWizard.APK_FILE_DEST]; + File f = new File(parentFolder, filename); + if (f.isFile()) { + fileExists = true; + sb.append(String.format("
    • %1$s (WARNING: already exists)
    • ", filename)); + } else if (f.isDirectory()) { + setErrorMessage(String.format("%1$s is a directory.", filename)); + // reset canFinish in the wizard. + mWizard.resetDestination(); + setPageComplete(false); + return; + } else { + sb.append(String.format("
    • %1$s
    • ", filename)); + } + } + + mDestinationDetails = sb.toString(); + // no error, set the destination in the wizard. - mWizard.setDestination(path); + mWizard.setDestination(parentFolder, apkFileMap); setPageComplete(true); - + // However, we should also test if the file already exists. - if (file.isFile()) { - setMessage("Destination file already exists.", WARNING); + if (fileExists) { + setMessage("A destination file already exists.", WARNING); } + + updateDetailText(); + } else if (forceDetailUpdate) { + updateDetailText(); } } + + /** + * Updates the scrollbar to match the content of the {@link FormText} or the new size + * of the {@link ScrolledComposite}. + */ + private void updateScrolling() { + if (mDetailText != null) { + Rectangle r = mScrolledComposite.getClientArea(); + mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT)); + mScrolledComposite.layout(); + } + } + + private void updateDetailText() { + StringBuilder sb = new StringBuilder("
      "); + if (mKeyDetails != null) { + sb.append(mKeyDetails); + } + + if (mDestinationDetails != null && mFatalSigningError == false) { + sb.append(mDestinationDetails); + } + + sb.append("
      "); + + mDetailText.setText(sb.toString(), true /* parseTags */, + true /* expandURLs */); + + mDetailText.getParent().layout(); + + updateScrolling(); + + } + + /** + * Creates the list of destination filenames based on the content of the destination field + * and the list of APK configurations for the project. + * + * @param file File name from the destination field + * @return A list of destination filenames based file and the list of APK + * configurations for the project. + */ + private Map getApkFileMap(File file) { + String filename = file.getName(); + + HashMap map = new HashMap(); + + // add the default APK filename + String[] apkArray = new String[ExportWizard.APK_COUNT]; + apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( + mWizard.getProject(), null /*config*/); + apkArray[ExportWizard.APK_FILE_DEST] = filename; + map.put(null, apkArray); + + // add the APKs for each APK configuration. + if (mApkConfig != null && mApkConfig.size() > 0) { + // remove the extension. + int index = filename.lastIndexOf('.'); + String base = filename.substring(0, index); + String extension = filename.substring(index); + + Set> set = mApkConfig.entrySet(); + for (Entry entry : set) { + apkArray = new String[ExportWizard.APK_COUNT]; + apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename( + mWizard.getProject(), entry.getKey()); + apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension; + map.put(entry.getKey(), apkArray); + } + } + + return map; + } + + @Override + protected void onException(Throwable t) { + super.onException(t); + + mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t)); + } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java index e161e1887..054a07211 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java @@ -266,7 +266,7 @@ final class ProjectCheckPage extends ExportWizardPage { } // update the wizard with the new project - mWizard.setProject(null, null, null); + mWizard.setProject(null); //test the project name first! String text = mProjectText.getText().trim(); @@ -289,7 +289,7 @@ final class ProjectCheckPage extends ExportWizardPage { setErrorMessage(null); // update the wizard with the new project - setApkFilePathInWizard(found); + mWizard.setProject(found); // now rebuild the error ui. buildErrorUi(found); @@ -299,24 +299,4 @@ final class ProjectCheckPage extends ExportWizardPage { } } } - - private void setApkFilePathInWizard(IProject project) { - if (project != null) { - IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project); - if (outputIFolder != null) { - String outputOsPath = outputIFolder.getLocation().toOSString(); - String apkFilePath = outputOsPath + File.separator + project.getName() + - AndroidConstants.DOT_ANDROID_PACKAGE; - - File f = new File(apkFilePath); - if (f.isFile()) { - mWizard.setProject(project, apkFilePath, f.getName()); - return; - } - } - } - - mWizard.setProject(null, null, null); - } - } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java index afa1fb54d..5aeb3358c 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.project.internal; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.project.BaseProjectHelper; @@ -44,8 +45,12 @@ import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; +import java.util.regex.Pattern; /** * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to @@ -56,6 +61,21 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit private final static String CONTAINER_ID = "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ + /** path separator to store multiple paths in a single property. This is guaranteed to not + * be in a path. + */ + private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ + + private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ + private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ + private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ + private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; + + private final static int PATH_ANDROID_JAR = 0; + private final static int PATH_ANDROID_SRC = 1; + private final static int PATH_ANDROID_DOCS = 2; + private final static int PATH_ANDROID_OPT_DOCS = 3; + public AndroidClasspathContainerInitializer() { // pass } @@ -71,7 +91,7 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit if (CONTAINER_ID.equals(containerPath.toString())) { JavaCore.setClasspathContainer(new Path(CONTAINER_ID), new IJavaProject[] { project }, - new IClasspathContainer[] { allocateAndroidContainer(CONTAINER_ID, project) }, + new IClasspathContainer[] { allocateAndroidContainer(project) }, new NullProgressMonitor()); } } @@ -111,7 +131,7 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit IClasspathContainer[] containers = new IClasspathContainer[projectCount]; for (int i = 0 ; i < projectCount; i++) { - containers[i] = allocateAndroidContainer(CONTAINER_ID, androidProjects[i]); + containers[i] = allocateAndroidContainer(androidProjects[i]); } // give each project their new container in one call. @@ -128,133 +148,171 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit /** * Allocates and returns an {@link AndroidClasspathContainer} object with the proper * path to the framework jar file. - * @param containerId the container id to be used. * @param javaProject The java project that will receive the container. */ - private static IClasspathContainer allocateAndroidContainer(String containerId, - IJavaProject javaProject) { + private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { final IProject iProject = javaProject.getProject(); - // remove potential MARKER_TARGETs. - try { - if (iProject.exists()) { - iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, - IResource.DEPTH_INFINITE); - } - } catch (CoreException ce) { - // just log the error - AdtPlugin.log(ce, "Error removing target marker."); - } - - // First we check if the SDK has been loaded. - // By passing the javaProject to getSdkLoadStatus(), we ensure that, should the SDK - // not be loaded yet, the classpath container will be resolved again once the SDK is loaded. - boolean sdkIsLoaded = AdtPlugin.getDefault().getSdkLoadStatus(javaProject) == - LoadStatus.LOADED; - - // then we check if the project has a valid target. - IAndroidTarget target = null; - if (sdkIsLoaded) { - target = Sdk.getCurrent().getTarget(iProject); - } - - // if we are loaded and the target is non null, we create a valid ClassPathContainer - if (sdkIsLoaded && target != null) { - String targetName = null; - if (target.isPlatform()) { - targetName = target.getName(); - } else { - targetName = String.format("%1$s (%2$s)", target.getName(), - target.getApiVersionName()); - } - - return new AndroidClasspathContainer(createFrameworkClasspath(target), - new Path(containerId), targetName); - } - - // else we put a marker on the project, and return a dummy container (to replace the - // previous one if there was one.) - - // Get the project's target's hash string (if it exists) - String hashString = Sdk.getProjectTargetHashString(iProject); - - String message = null; + String markerMessage = null; boolean outputToConsole = true; - if (hashString == null || hashString.length() == 0) { - // if there is no hash string we only show this if the SDK is loaded. - // For a project opened at start-up with no target, this would be displayed twice, - // once when the project is opened, and once after the SDK has finished loading. - // By testing the sdk is loaded, we only show this once in the console. - if (sdkIsLoaded) { - message = String.format( - "Project has no target set. Edit the project properties to set one."); - } - } else if (sdkIsLoaded) { - message = String.format( - "Unable to resolve target '%s'", hashString); - } else { - // this is the case where there is a hashString but the SDK is not yet - // loaded and therefore we can't get the target yet. - message = String.format( - "Unable to resolve target '%s' until the SDK is loaded.", hashString); - - // let's not log this one to the console as it will happen at every boot, - // and it's expected. (we do keep the error marker though). - outputToConsole = false; - } - if (message != null) { - // log the error and put the marker on the project if we can. - if (outputToConsole) { - AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, iProject, message); - } + try { + AdtPlugin plugin = AdtPlugin.getDefault(); - try { - BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, message, -1, - IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); - } catch (CoreException e) { - // In some cases, the workspace may be locked for modification when we pass here. - // We schedule a new job to put the marker after. - final String fmessage = message; - Job markerJob = new Job("Android SDK: Resolving error markers") { - @SuppressWarnings("unchecked") - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, - fmessage, -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); - } catch (CoreException e2) { - return e2.getStatus(); - } + // get the lock object for project manipulation during SDK load. + Object lock = plugin.getSdkLockObject(); + synchronized (lock) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + IAndroidTarget target = null; + if (sdkIsLoaded) { + target = Sdk.getCurrent().getTarget(iProject); + } - return Status.OK_STATUS; + // if we are loaded and the target is non null, we create a valid ClassPathContainer + if (sdkIsLoaded && target != null) { + String targetName = target.getFullName(); + + return new AndroidClasspathContainer( + createClasspathEntries(iProject, target, targetName), + new Path(CONTAINER_ID), targetName); + } + + // In case of error, we'll try different thing to provide the best error message + // possible. + // Get the project's target's hash string (if it exists) + String hashString = Sdk.getProjectTargetHashString(iProject); + + if (hashString == null || hashString.length() == 0) { + // if there is no hash string we only show this if the SDK is loaded. + // For a project opened at start-up with no target, this would be displayed + // twice, once when the project is opened, and once after the SDK has + // finished loading. + // By testing the sdk is loaded, we only show this once in the console. + if (sdkIsLoaded) { + markerMessage = String.format( + "Project has no target set. Edit the project properties to set one."); + } + } else if (sdkIsLoaded) { + markerMessage = String.format( + "Unable to resolve target '%s'", hashString); + } else { + // this is the case where there is a hashString but the SDK is not yet + // loaded and therefore we can't get the target yet. + // We check if there is a cache of the needed information. + AndroidClasspathContainer container = getContainerFromCache(iProject); + + if (container == null) { + // either the cache was wrong (ie folder does not exists anymore), or + // there was no cache. In this case we need to make sure the project + // is resolved again after the SDK is loaded. + plugin.setProjectToResolve(javaProject); + + markerMessage = String.format( + "Unable to resolve target '%s' until the SDK is loaded.", + hashString); + + // let's not log this one to the console as it will happen at every boot, + // and it's expected. (we do keep the error marker though). + outputToConsole = false; + + } else { + // we created a container from the cache, so we register the project + // to be checked for cache validity once the SDK is loaded + plugin.setProjectToCheck(javaProject); + + // and return the container + return container; + } + + } + + // return a dummy container to replace the one we may have had before. + // It'll be replaced by the real when if/when the target is resolved if/when the + // SDK finishes loading. + return new IClasspathContainer() { + public IClasspathEntry[] getClasspathEntries() { + return new IClasspathEntry[0]; + } + + public String getDescription() { + return "Unable to get system library for the project"; + } + + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + public IPath getPath() { + return null; } }; + } + } finally { + if (markerMessage != null) { + // log the error and put the marker on the project if we can. + if (outputToConsole) { + AdtPlugin.printErrorToConsole(iProject, markerMessage); + } + + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage, + -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); + } catch (CoreException e) { + // In some cases, the workspace may be locked for modification when we + // pass here. + // We schedule a new job to put the marker after. + final String fmessage = markerMessage; + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, + fmessage, -1, IMarker.SEVERITY_ERROR, + IMarker.PRIORITY_HIGH); + } catch (CoreException e2) { + return e2.getStatus(); + } - // build jobs are run after other interactive jobs - markerJob.setPriority(Job.BUILD); - markerJob.schedule(); + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + } else { + // no error, remove potential MARKER_TARGETs. + try { + if (iProject.exists()) { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + // In some cases, the workspace may be locked for modification when we pass + // here, so we schedule a new job to put the marker after. + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } catch (CoreException e2) { + return e2.getStatus(); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } } } - - // return a dummy container to replace the one we may have had before. - return new IClasspathContainer() { - public IClasspathEntry[] getClasspathEntries() { - return new IClasspathEntry[0]; - } - - public String getDescription() { - return "Unable to get system library for the project"; - } - - public int getKind() { - return IClasspathContainer.K_DEFAULT_SYSTEM; - } - - public IPath getPath() { - return null; - } - }; } /** @@ -264,21 +322,114 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit * java doc directory. This is dynamically created when a project is opened, * and never saved in the project itself, so there's no risk of storing an * obsolete path. - * + * The method also stores the paths used to create the entries in the project persistent + * properties. A new {@link AndroidClasspathContainer} can be created from the stored path + * using the {@link #getContainerFromCache(IProject)} method. + * @param project * @param target The target that contains the libraries. + * @param targetName */ - private static IClasspathEntry[] createFrameworkClasspath(IAndroidTarget target) { + private static IClasspathEntry[] createClasspathEntries(IProject project, + IAndroidTarget target, String targetName) { + + // get the path from the target + String[] paths = getTargetPaths(target); + + // create the classpath entry from the paths + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + // paths now contains all the path required to recreate the IClasspathEntry with no + // target info. We encode them in a single string, with each path separated by + // OS path separator. + StringBuilder sb = new StringBuilder(CACHE_VERSION); + for (String p : paths) { + sb.append(PATH_SEPARATOR); + sb.append(p); + } + + // store this in a project persistent property + ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); + ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); + + return entries; + } + + /** + * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. + */ + private static AndroidClasspathContainer getContainerFromCache(IProject project) { + // get the cached info from the project persistent properties. + String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); + String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); + if (cache == null || targetNameCache == null) { + return null; + } + + // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. + if (cache.startsWith(CACHE_VERSION_SEP) == false) { + return null; + } + + cache = cache.substring(CACHE_VERSION_SEP.length()); + + // the cache contains multiple paths, separated by a character guaranteed to not be in + // the path (\u001C). + // The first 3 are for android.jar (jar, source, doc), the rest are for the optional + // libraries and should contain at least one doc and a jar (if there are any libraries). + // Therefore, the path count should be 3 or 5+ + String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (paths.length < 3 || paths.length == 4) { + return null; + } + + // now we check the paths actually exist. + // There's an exception: If the source folder for android.jar does not exist, this is + // not a problem, so we skip it. + // Also paths[PATH_ANDROID_DOCS] is a URI to the javadoc, so we test it a bit differently. + try { + if (new File(paths[PATH_ANDROID_JAR]).exists() == false || + new File(new URI(paths[PATH_ANDROID_DOCS])).exists() == false) { + return null; + } + } catch (URISyntaxException e) { + return null; + } finally { + + } + + for (int i = 3 ; i < paths.length; i++) { + String path = paths[i]; + if (path.length() > 0) { + File f = new File(path); + if (f.exists() == false) { + return null; + } + } + } + + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + return new AndroidClasspathContainer(entries, + new Path(CONTAINER_ID), targetNameCache); + } + + /** + * Generates an array of {@link IClasspathEntry} from a set of paths. + * @see #getTargetPaths(IAndroidTarget) + */ + private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { ArrayList list = new ArrayList(); // First, we create the IClasspathEntry for the framework. // now add the android framework to the class path. // create the path object. - IPath android_lib = new Path(target.getPath(IAndroidTarget.ANDROID_JAR)); - IPath android_src = new Path(target.getPath(IAndroidTarget.SOURCES)); - + IPath android_lib = new Path(paths[PATH_ANDROID_JAR]); + IPath android_src = new Path(paths[PATH_ANDROID_SRC]); + // create the java doc link. IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( - IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, AdtPlugin.getUrlDoc()); + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + paths[PATH_ANDROID_DOCS]); // create the access rule to restrict access to classes in com.android.internal IAccessRule accessRule = JavaCore.newAccessRule( @@ -292,42 +443,184 @@ public class AndroidClasspathContainerInitializer extends ClasspathContainerInit new IClasspathAttribute[] { cpAttribute }, false // not exported. ); - + list.add(frameworkClasspathEntry); // now deal with optional libraries + if (paths.length >= 5) { + String docPath = paths[PATH_ANDROID_OPT_DOCS]; + int i = 4; + while (i < paths.length) { + Path jarPath = new Path(paths[i++]); + + IClasspathAttribute[] attributes = null; + if (docPath.length() > 0) { + attributes = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + docPath) + }; + } + + IClasspathEntry entry = JavaCore.newLibraryEntry( + jarPath, + null, // source attachment path + null, // default source attachment root path. + null, + attributes, + false // not exported. + ); + list.add(entry); + } + } + + return list.toArray(new IClasspathEntry[list.size()]); + } + + /** + * Checks the projects' caches. If the cache was valid, the project is removed from the list. + * @param projects the list of projects to check. + */ + public static void checkProjectsCache(ArrayList projects) { + int i = 0; + projectLoop: while (i < projects.size()) { + IJavaProject javaProject = projects.get(i); + IProject iProject = javaProject.getProject(); + + // get the target from the project and its paths + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + if (target == null) { + // this is really not supposed to happen. This would mean there are cached paths, + // but default.properties was deleted. Keep the project in the list to force + // a resolve which will display the error. + i++; + continue; + } + + String[] targetPaths = getTargetPaths(target); + + // now get the cached paths + String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); + if (cache == null) { + // this should not happen. We'll force resolve again anyway. + i++; + continue; + } + + String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (cachedPaths.length < 3 || cachedPaths.length == 4) { + // paths length is wrong. simply resolve the project again + i++; + continue; + } + + // Now we compare the paths. The first 4 can be compared directly. + // because of case sensitiveness we need to use File objects + + if (targetPaths.length != cachedPaths.length) { + // different paths, force resolve again. + i++; + continue; + } + + // compare the main paths (android.jar, main sources, main javadoc) + if (new File(targetPaths[PATH_ANDROID_JAR]).equals( + new File(cachedPaths[PATH_ANDROID_JAR])) == false || + new File(targetPaths[PATH_ANDROID_SRC]).equals( + new File(cachedPaths[PATH_ANDROID_SRC])) == false || + new File(targetPaths[PATH_ANDROID_DOCS]).equals( + new File(cachedPaths[PATH_ANDROID_DOCS])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + if (cachedPaths.length > PATH_ANDROID_OPT_DOCS) { + // compare optional libraries javadoc + if (new File(targetPaths[PATH_ANDROID_OPT_DOCS]).equals( + new File(cachedPaths[PATH_ANDROID_OPT_DOCS])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + // testing the optional jar files is a little bit trickier. + // The order is not guaranteed to be identical. + // From a previous test, we do know however that there is the same number. + // The number of libraries should be low enough that we can simply go through the + // lists manually. + targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { + String targetPath = targetPaths[tpi]; + + // look for a match in the other array + for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { + if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { + // found a match. Try the next targetPath + continue targetLoop; + } + } + + // if we stop here, we haven't found a match, which means there's a + // discrepancy in the libraries. We force a resolve. + i++; + continue projectLoop; + } + } + + // at the point the check passes, and we can remove the project from the list. + // we do not increment i in this case. + projects.remove(i); + } + } + + /** + * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. + *

      The paths are always in the same order. + *

        + *
      • Path to android.jar
      • + *
      • Path to the source code for android.jar
      • + *
      • Path to the javadoc for the android platform
      • + *
      + * Additionally, if there are optional libraries, the array will contain: + *
        + *
      • Path to the librairies javadoc
      • + *
      • Path to the first .jar file
      • + *
      • (more .jar as needed)
      • + *
      + */ + private static String[] getTargetPaths(IAndroidTarget target) { + ArrayList paths = new ArrayList(); + + // first, we get the path for android.jar + // The order is: android.jar, source folder, docs folder + paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + paths.add(target.getPath(IAndroidTarget.SOURCES)); + paths.add(AdtPlugin.getUrlDoc()); + + // now deal with optional libraries. IOptionalLibrary[] libraries = target.getOptionalLibraries(); if (libraries != null) { + // all the optional libraries use the same javadoc, so we start with this + String targetDocPath = target.getPath(IAndroidTarget.DOCS); + if (targetDocPath != null) { + paths.add(targetDocPath); + } else { + // we add an empty string, to always have the same count. + paths.add(""); + } + + // because different libraries could use the same jar file, we make sure we add + // each jar file only once. HashSet visitedJars = new HashSet(); for (IOptionalLibrary library : libraries) { String jarPath = library.getJarPath(); if (visitedJars.contains(jarPath) == false) { visitedJars.add(jarPath); - - // create the java doc link, if needed - String targetDocPath = target.getPath(IAndroidTarget.DOCS); - IClasspathAttribute[] attributes = null; - if (targetDocPath != null) { - attributes = new IClasspathAttribute[] { - JavaCore.newClasspathAttribute( - IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, - targetDocPath) - }; - } - - IClasspathEntry entry = JavaCore.newLibraryEntry( - new Path(library.getJarPath()), - null, // source attachment path - null, // default source attachment root path. - null, - attributes, - false // not exported. - ); - list.add(entry); + paths.add(jarPath); } } } - return list.toArray(new IClasspathEntry[list.size()]); + return paths.toArray(new String[paths.size()]); } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java index 584dd0d4f..a4c019f55 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.project.properties; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.sdklib.IAndroidTarget; +import com.android.sdkuilib.ApkConfigWidget; import com.android.sdkuilib.SdkTargetSelector; import org.eclipse.core.resources.IProject; @@ -32,6 +33,8 @@ import org.eclipse.swt.widgets.Label; import org.eclipse.ui.IWorkbenchPropertyPage; import org.eclipse.ui.dialogs.PropertyPage; +import java.util.Map; + /** * Property page for "Android" project. * This is accessible from the Package Explorer when right clicking a project and choosing @@ -42,6 +45,7 @@ public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPrope private IProject mProject; private SdkTargetSelector mSelector; + private ApkConfigWidget mApkConfigWidget; public AndroidPropertyPage() { // pass @@ -51,7 +55,14 @@ public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPrope protected Control createContents(Composite parent) { // get the element (this is not yet valid in the constructor). mProject = (IProject)getElement(); - + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + + // build the UI. Composite top = new Composite(parent, SWT.NONE); top.setLayoutData(new GridData(GridData.FILL_BOTH)); top.setLayout(new GridLayout(1, false)); @@ -59,20 +70,28 @@ public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPrope Label l = new Label(top, SWT.NONE); l.setText("Project Target"); - // get the targets from the sdk - IAndroidTarget[] targets = null; - if (Sdk.getCurrent() != null) { - targets = Sdk.getCurrent().getTargets(); - } - - // build the UI. mSelector = new SdkTargetSelector(top, targets, false /*allowMultipleSelection*/); - if (Sdk.getCurrent() != null) { - IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + l = new Label(top, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + l = new Label(top, SWT.NONE); + l.setText("Project APK Configurations"); + + mApkConfigWidget = new ApkConfigWidget(top); + + // fill the ui + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null && mProject.isOpen()) { + // get the target + IAndroidTarget target = currentSdk.getTarget(mProject); if (target != null) { mSelector.setSelection(target); } + + // get the apk configurations + Map configs = currentSdk.getProjectApkConfigs(mProject); + mApkConfigWidget.fillTable(configs); } mSelector.setSelectionListener(new SelectionAdapter() { @@ -83,14 +102,20 @@ public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPrope setValid(target != null); } }); + + if (mProject.isOpen() == false) { + // disable the ui. + } return top; } @Override public boolean performOk() { - if (Sdk.getCurrent() != null) { - Sdk.getCurrent().setProject(mProject, mSelector.getFirstSelected()); + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + currentSdk.setProject(mProject, mSelector.getFirstSelected(), + mApkConfigWidget.getApkConfigs()); } return true; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java index 60561abd1..34391c236 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.sdk; +import com.android.ide.eclipse.adt.build.DexWrapper; import com.android.ide.eclipse.common.resources.IResourceRepository; import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors; @@ -26,6 +27,7 @@ import com.android.ide.eclipse.editors.resources.manager.ProjectResources; import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors; import com.android.layoutlib.api.ILayoutBridge; import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; import java.util.Hashtable; import java.util.Map; @@ -42,6 +44,7 @@ public class AndroidTargetData { public final static int DESCRIPTOR_RESOURCES = 5; public final static int DESCRIPTOR_SEARCHABLE = 6; public final static int DESCRIPTOR_PREFERENCES = 7; + public final static int DESCRIPTOR_APPWIDGET_PROVIDER = 8; public final static class LayoutBridge { /** Link to the layout bridge */ @@ -50,10 +53,14 @@ public class AndroidTargetData { public LoadStatus status = LoadStatus.LOADING; public ClassLoader classLoader; + + public int apiLevel; } private final IAndroidTarget mTarget; + private DexWrapper mDexWrapper; + /** * mAttributeValues is a map { key => list [ values ] }. * The key for the map is "(element-xml-name,attribute-namespace:attribute-xml-local-name)". @@ -64,27 +71,35 @@ public class AndroidTargetData { * This is used for attributes that do not have a unique name, but still need to be populated * with values in the UI. Uniquely named attributes have their values in {@link #mEnumValueMap}. */ - private final Hashtable mAttributeValues = new Hashtable(); + private Hashtable mAttributeValues = new Hashtable(); private IResourceRepository mSystemResourceRepository; - private final AndroidManifestDescriptors mManifestDescriptors; - private final LayoutDescriptors mLayoutDescriptors; - private final MenuDescriptors mMenuDescriptors; - private final XmlDescriptors mXmlDescriptors; + private AndroidManifestDescriptors mManifestDescriptors; + private LayoutDescriptors mLayoutDescriptors; + private MenuDescriptors mMenuDescriptors; + private XmlDescriptors mXmlDescriptors; - private final Map> mEnumValueMap; + private Map> mEnumValueMap; - private final ProjectResources mFrameworkResources; - private final LayoutBridge mLayoutBridge; + private ProjectResources mFrameworkResources; + private LayoutBridge mLayoutBridge; private boolean mLayoutBridgeInit = false; + AndroidTargetData(IAndroidTarget androidTarget) { + mTarget = androidTarget; + } + + void setDexWrapper(DexWrapper wrapper) { + mDexWrapper = wrapper; + } + /** * Creates an AndroidTargetData object. + * @param optionalLibraries */ - AndroidTargetData(IAndroidTarget androidTarget, - IResourceRepository systemResourceRepository, + void setExtraData(IResourceRepository systemResourceRepository, AndroidManifestDescriptors manifestDescriptors, LayoutDescriptors layoutDescriptors, MenuDescriptors menuDescriptors, @@ -95,10 +110,10 @@ public class AndroidTargetData { String[] broadcastIntentActionValues, String[] serviceIntentActionValues, String[] intentCategoryValues, + IOptionalLibrary[] optionalLibraries, ProjectResources resources, LayoutBridge layoutBridge) { - mTarget = androidTarget; mSystemResourceRepository = systemResourceRepository; mManifestDescriptors = manifestDescriptors; mLayoutDescriptors = layoutDescriptors; @@ -111,6 +126,11 @@ public class AndroidTargetData { setPermissions(permissionValues); setIntentFilterActionsAndCategories(activityIntentActionValues, broadcastIntentActionValues, serviceIntentActionValues, intentCategoryValues); + setOptionalLibraries(optionalLibraries); + } + + public DexWrapper getDexWrapper() { + return mDexWrapper; } public IResourceRepository getSystemResources() { @@ -138,6 +158,8 @@ public class AndroidTargetData { return ResourcesDescriptors.getInstance(); case DESCRIPTOR_PREFERENCES: return mXmlDescriptors.getPreferencesProvider(); + case DESCRIPTOR_APPWIDGET_PROVIDER: + return mXmlDescriptors.getAppWidgetProvider(); case DESCRIPTOR_SEARCHABLE: return mXmlDescriptors.getSearchableProvider(); default : @@ -270,6 +292,20 @@ public class AndroidTargetData { setValues("(service,action,android:name)", serviceIntentActions); //$NON-NLS-1$ setValues("(category,android:name)", intentCategoryValues); //$NON-NLS-1$ } + + private void setOptionalLibraries(IOptionalLibrary[] optionalLibraries) { + String[] values; + + if (optionalLibraries == null) { + values = new String[0]; + } else { + values = new String[optionalLibraries.length]; + for (int i = 0; i < optionalLibraries.length; i++) { + values[i] = optionalLibraries[i].getName(); + } + } + setValues("(uses-library,android:name)", values); + } /** * Sets a (name, values) pair in the hash map. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java index dfe876f7f..67eec78b9 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.sdk; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.build.DexWrapper; import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.resources.AttrsXmlParser; @@ -91,13 +92,29 @@ public final class AndroidTargetParser { try { SubMonitor progress = SubMonitor.convert(monitor, String.format("Parsing SDK %1$s", mAndroidTarget.getName()), - 120); + 14); + AndroidTargetData targetData = new AndroidTargetData(mAndroidTarget); + + // load DX. + DexWrapper dexWrapper = new DexWrapper(); + IStatus res = dexWrapper.loadDex(mAndroidTarget.getPath(IAndroidTarget.DX_JAR)); + if (res != Status.OK_STATUS) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("dx.jar loading failed for target '%1$s'", + mAndroidTarget.getFullName())); + } + + // we have loaded dx. + targetData.setDexWrapper(dexWrapper); + progress.worked(1); + + // parse the rest of the data. + AndroidJarLoader classLoader = new AndroidJarLoader(mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); preload(classLoader, progress.newChild(40, SubMonitor.SUPPRESS_NONE)); - progress.setWorkRemaining(80); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -106,7 +123,7 @@ public final class AndroidTargetParser { // get the resource Ids. progress.subTask("Resource IDs"); IResourceRepository frameworkRepository = collectResourceIds(classLoader); - progress.worked(5); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -115,7 +132,7 @@ public final class AndroidTargetParser { // get the permissions progress.subTask("Permissions"); String[] permissionValues = collectPermissions(classLoader); - progress.worked(5); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -129,7 +146,7 @@ public final class AndroidTargetParser { ArrayList categories = new ArrayList(); collectIntentFilterActionsAndCategories(activity_actions, broadcast_actions, service_actions, categories); - progress.worked(5); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -140,12 +157,14 @@ public final class AndroidTargetParser { AttrsXmlParser attrsXmlParser = new AttrsXmlParser( mAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)); attrsXmlParser.preload(); + progress.worked(1); progress.subTask("Manifest definitions"); AttrsXmlParser attrsManifestXmlParser = new AttrsXmlParser( mAndroidTarget.getPath(IAndroidTarget.MANIFEST_ATTRIBUTES), attrsXmlParser); attrsManifestXmlParser.preload(); + progress.worked(1); Collection mainList = new ArrayList(); Collection groupList = new ArrayList(); @@ -153,7 +172,7 @@ public final class AndroidTargetParser { // collect the layout/widgets classes progress.subTask("Widgets and layouts"); collectLayoutClasses(classLoader, attrsXmlParser, mainList, groupList, - progress.newChild(40)); + progress.newChild(1)); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -167,7 +186,7 @@ public final class AndroidTargetParser { mainList.clear(); groupList.clear(); collectPreferenceClasses(classLoader, attrsXmlParser, mainList, groupList, - progress.newChild(5)); + progress.newChild(1)); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -184,6 +203,11 @@ public final class AndroidTargetParser { attrsManifestXmlParser); Map> enumValueMap = attrsXmlParser.getEnumFlagValues(); + Map xmlAppWidgetMap = null; + if (mAndroidTarget.getApiVersionNumber() >= 3) { + xmlAppWidgetMap = collectAppWidgetDefinitions(attrsXmlParser); + } + if (progress.isCanceled()) { return Status.CANCEL_STATUS; } @@ -192,7 +216,7 @@ public final class AndroidTargetParser { // the PlatformData object. AndroidManifestDescriptors manifestDescriptors = new AndroidManifestDescriptors(); manifestDescriptors.updateDescriptors(manifestMap); - progress.worked(10); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -200,7 +224,7 @@ public final class AndroidTargetParser { LayoutDescriptors layoutDescriptors = new LayoutDescriptors(); layoutDescriptors.updateDescriptors(layoutViewsInfo, layoutGroupsInfo); - progress.worked(10); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; @@ -208,29 +232,31 @@ public final class AndroidTargetParser { MenuDescriptors menuDescriptors = new MenuDescriptors(); menuDescriptors.updateDescriptors(xmlMenuMap); - progress.worked(10); + progress.worked(1); if (progress.isCanceled()) { return Status.CANCEL_STATUS; } XmlDescriptors xmlDescriptors = new XmlDescriptors(); - xmlDescriptors.updateDescriptors(xmlSearchableMap, preferencesInfo, + xmlDescriptors.updateDescriptors( + xmlSearchableMap, + xmlAppWidgetMap, + preferencesInfo, preferenceGroupsInfo); - progress.worked(10); + progress.worked(1); // load the framework resources. ProjectResources resources = ResourceManager.getInstance().loadFrameworkResources( mAndroidTarget); - progress.worked(10); + progress.worked(1); // now load the layout lib bridge LayoutBridge layoutBridge = loadLayoutBridge(); - progress.worked(10); + progress.worked(1); // and finally create the PlatformData with all that we loaded. - AndroidTargetData targetData = new AndroidTargetData(mAndroidTarget, - frameworkRepository, + targetData.setExtraData(frameworkRepository, manifestDescriptors, layoutDescriptors, menuDescriptors, @@ -241,6 +267,7 @@ public final class AndroidTargetParser { broadcast_actions.toArray(new String[broadcast_actions.size()]), service_actions.toArray(new String[service_actions.size()]), categories.toArray(new String[categories.size()]), + mAndroidTarget.getOptionalLibraries(), resources, layoutBridge); @@ -251,10 +278,6 @@ public final class AndroidTargetParser { AdtPlugin.logAndPrintError(e, TAG, "SDK parser failed"); //$NON-NLS-1$ AdtPlugin.printToConsole("SDK parser failed", e.getMessage()); return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "SDK parser failed", e); - } finally { - if (monitor != null) { - monitor.done(); - } } } @@ -587,6 +610,31 @@ public final class AndroidTargetParser { return Collections.unmodifiableMap(map2); } + /** + * Collects all appWidgetProviderInfo definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map collectAppWidgetDefinitions( + AttrsXmlParser attrsXmlParser) { + Map map = attrsXmlParser.getDeclareStyleableList(); + Map map2 = new HashMap(); + for (String key : new String[] { "AppWidgetProviderInfo" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "AppWidget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("AppWidget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + /** * Collects all manifest definition information from the attrs_manifest.xml and returns it. */ @@ -633,6 +681,15 @@ public final class AndroidTargetParser { layoutBridge.status = LoadStatus.FAILED; AdtPlugin.log(IStatus.ERROR, "Failed to load " + AndroidConstants.CLASS_BRIDGE); //$NON-NLS-1$ } else { + // get the api level + try { + layoutBridge.apiLevel = layoutBridge.bridge.getApiLevel(); + } catch (AbstractMethodError e) { + // the first version of the api did not have this method + layoutBridge.apiLevel = 1; + } + + // and mark the lib as loaded. layoutBridge.status = LoadStatus.LOADED; } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java index 172b4ae34..ba0b56803 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java @@ -18,16 +18,23 @@ package com.android.ide.eclipse.adt.sdk; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.ISdkLog; import com.android.sdklib.SdkConstants; import com.android.sdklib.SdkManager; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.project.ApkConfigurationHelper; import com.android.sdklib.project.ProjectProperties; import com.android.sdklib.project.ProjectProperties.PropertyType; -import com.android.sdklib.vm.VmManager; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; @@ -38,6 +45,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; /** * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used @@ -48,18 +56,35 @@ import java.util.HashMap; * * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. */ -public class Sdk { +public class Sdk implements IProjectListener { private static Sdk sCurrentSdk = null; private final SdkManager mManager; - private final VmManager mVmManager; + private final AvdManager mAvdManager; - private final HashMap mProjectMap = + private final HashMap mProjectTargetMap = new HashMap(); - private final HashMap mTargetMap = + private final HashMap mTargetDataMap = new HashMap(); + private final HashMap> mProjectApkConfigMap = + new HashMap>(); private final String mDocBaseUrl; - + + /** + * Classes implementing this interface will receive notification when targets are changed. + */ + public interface ITargetChangeListener { + /** + * Sent when project has its target changed. + */ + void onProjectTargetChange(IProject changedProject); + + /** + * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, + * or the SDK is changed). + */ + void onTargetsLoaded(); + } /** * Loads an SDK and returns an {@link Sdk} object if success. @@ -67,7 +92,7 @@ public class Sdk { */ public static Sdk loadSdk(String sdkLocation) { if (sCurrentSdk != null) { - // manual unload? + sCurrentSdk.dispose(); sCurrentSdk = null; } @@ -95,13 +120,13 @@ public class Sdk { // get an SdkManager object for the location SdkManager manager = SdkManager.createManager(sdkLocation, log); if (manager != null) { - VmManager vmManager = null; + AvdManager avdManager = null; try { - vmManager = new VmManager(manager, log); + avdManager = new AvdManager(manager, log); } catch (AndroidLocationException e) { - log.error(e, "Error parsing the VMs"); + log.error(e, "Error parsing the AVDs"); } - sCurrentSdk = new Sdk(manager, vmManager); + sCurrentSdk = new Sdk(manager, avdManager); return sCurrentSdk; } else { StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); @@ -156,24 +181,87 @@ public class Sdk { } /** - * Associates an {@link IProject} and an {@link IAndroidTarget}. + * Sets a new target and a new list of Apk configuration for a given project. + * + * @param project the project to receive the new apk configurations + * @param target The new target to set, or null to not change the current target. + * @param apkConfigMap a map of apk configurations. The map contains (name, filter) where name + * is the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c). Can be null if the + * apk configurations should not be updated. */ - public void setProject(IProject project, IAndroidTarget target) { - synchronized (mProjectMap) { - // look for the current target of the project - IAndroidTarget previousTarget = mProjectMap.get(project); - - if (target != previousTarget) { - // save the target hash string in the project persistent property - setProjectTargetHashString(project, target.hashString()); - - // put it in a local map for easy access. - mProjectMap.put(project, target); + public void setProject(IProject project, IAndroidTarget target, + Map apkConfigMap) { + synchronized (mProjectTargetMap) { + boolean resolveProject = false; + boolean compileProject = false; + boolean cleanProject = false; - // recompile the project if needed. + ProjectProperties properties = ProjectProperties.load( + project.getLocation().toOSString(), PropertyType.DEFAULT); + if (properties == null) { + // doesn't exist yet? we create it. + properties = ProjectProperties.create(project.getLocation().toOSString(), + PropertyType.DEFAULT); + } + + if (target != null) { + // look for the current target of the project + IAndroidTarget previousTarget = mProjectTargetMap.get(project); + + if (target != previousTarget) { + // save the target hash string in the project persistent property + properties.setAndroidTarget(target); + + // put it in a local map for easy access. + mProjectTargetMap.put(project, target); + + resolveProject = true; + } + } + + if (apkConfigMap != null) { + // save the apk configs in the project persistent property + cleanProject = ApkConfigurationHelper.setConfigs(properties, apkConfigMap); + + // put it in a local map for easy access. + mProjectApkConfigMap.put(project, apkConfigMap); + + compileProject = true; + } + + // we are done with the modification. Save the property file. + try { + properties.save(); + } catch (IOException e) { + AdtPlugin.log(e, "Failed to save default.properties for project '%s'", + project.getName()); + } + + if (resolveProject) { + // force a resolve of the project by updating the classpath container. IJavaProject javaProject = JavaCore.create(project); AndroidClasspathContainerInitializer.updateProjects( new IJavaProject[] { javaProject }); + } else if (compileProject) { + // If there was removed configs, we clean instead of build + // (to remove the obsolete ap_ and apk file from removed configs). + try { + project.build(cleanProject ? + IncrementalProjectBuilder.CLEAN_BUILD : + IncrementalProjectBuilder.FULL_BUILD, + null); + } catch (CoreException e) { + // failed to build? force resolve instead. + IJavaProject javaProject = JavaCore.create(project); + AndroidClasspathContainerInitializer.updateProjects( + new IJavaProject[] { javaProject }); + } + } + + // finally, update the opened editors. + if (resolveProject) { + AdtPlugin.getDefault().updateTargetListener(project); } } } @@ -182,11 +270,11 @@ public class Sdk { * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. */ public IAndroidTarget getTarget(IProject project) { - synchronized (mProjectMap) { - IAndroidTarget target = mProjectMap.get(project); + synchronized (mProjectTargetMap) { + IAndroidTarget target = mProjectTargetMap.get(project); if (target == null) { // get the value from the project persistent property. - String targetHashString = getProjectTargetHashString(project); + String targetHashString = loadProjectProperties(project, this); if (targetHashString != null) { target = mManager.getTargetFromHashString(targetHashString); @@ -197,16 +285,26 @@ public class Sdk { } } + /** - * Returns the hash string uniquely identifying the target of a project. This methods reads - * the string from the project persistent preferences/properties. - *

      The string is equivalent to the return of {@link IAndroidTarget#hashString()}. + * Parses the project properties and returns the hash string uniquely identifying the + * target of the given project. + *

      + * This methods reads the content of the default.properties file present in + * the root folder of the project. + *

      The returned string is equivalent to the return of {@link IAndroidTarget#hashString()}. * @param project The project for which to return the target hash string. + * @param sdkStorage The sdk in which to store the Apk Configs. Can be null. * @return the hash string or null if the project does not have a target set. */ - public static String getProjectTargetHashString(IProject project) { + private static String loadProjectProperties(IProject project, Sdk sdkStorage) { // load the default.properties from the project folder. - ProjectProperties properties = ProjectProperties.load(project.getLocation().toOSString(), + IPath location = project.getLocation(); + if (location == null) { // can return null when the project is being deleted. + // do nothing and return null; + return null; + } + ProjectProperties properties = ProjectProperties.load(location.toOSString(), PropertyType.DEFAULT); if (properties == null) { AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'", @@ -214,11 +312,32 @@ public class Sdk { return null; } + if (sdkStorage != null) { + Map configMap = ApkConfigurationHelper.getConfigs(properties); + + if (configMap != null) { + sdkStorage.mProjectApkConfigMap.put(project, configMap); + } + } + return properties.getProperty(ProjectProperties.PROPERTY_TARGET); } + + /** + * Returns the hash string uniquely identifying the target of a project. + *

      + * This methods reads the content of the default.properties file present in + * the root folder of the project. + *

      The string is equivalent to the return of {@link IAndroidTarget#hashString()}. + * @param project The project for which to return the target hash string. + * @return the hash string or null if the project does not have a target set. + */ + public static String getProjectTargetHashString(IProject project) { + return loadProjectProperties(project, null /*storeConfigs*/); + } /** - * Sets a target hash string in a project's persistent preferences/property storage. + * Sets a target hash string in given project's default.properties file. * @param project The project in which to save the hash string. * @param targetHashString The target hash string to save. This must be the result from * {@link IAndroidTarget#hashString()}. @@ -249,31 +368,51 @@ public class Sdk { * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. */ public AndroidTargetData getTargetData(IAndroidTarget target) { - synchronized (mTargetMap) { - return mTargetMap.get(target); + synchronized (mTargetDataMap) { + return mTargetDataMap.get(target); } } /** - * Returns the {@link VmManager}. If the VmManager failed to parse the VM folder, this could - * be null. + * Returns the configuration map for a given project. + *

      The Map key are name to be used in the apk filename, while the values are comma separated + * config values. The config value can be passed directly to aapt through the -c option. */ - public VmManager getVmManager() { - return mVmManager; + public Map getProjectApkConfigs(IProject project) { + return mProjectApkConfigMap.get(project); } - private Sdk(SdkManager manager, VmManager vmManager) { + /** + * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could + * be null. + */ + public AvdManager getAvdManager() { + return mAvdManager; + } + + private Sdk(SdkManager manager, AvdManager avdManager) { mManager = manager; - mVmManager = vmManager; + mAvdManager = avdManager; + + // listen to projects closing + ResourceMonitor monitor = ResourceMonitor.getMonitor(); + monitor.addProjectListener(this); // pre-compute some paths mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + SdkConstants.OS_SDK_DOCS_FOLDER); } + + /** + * Cleans and unloads the SDK. + */ + private void dispose() { + ResourceMonitor.getMonitor().removeProjectListener(this); + } void setTargetData(IAndroidTarget target, AndroidTargetData data) { - synchronized (mTargetMap) { - mTargetMap.put(target, data); + synchronized (mTargetDataMap) { + mTargetDataMap.put(target, data); } } @@ -314,6 +453,40 @@ public class Sdk { return null; } + public void projectClosed(IProject project) { + // get the target project + synchronized (mProjectTargetMap) { + IAndroidTarget target = mProjectTargetMap.get(project); + if (target != null) { + // get the bridge for the target, and clear the cache for this project. + AndroidTargetData data = mTargetDataMap.get(target); + if (data != null) { + LayoutBridge bridge = data.getLayoutBridge(); + if (bridge != null && bridge.status == LoadStatus.LOADED) { + bridge.bridge.clearCaches(project); + } + } + } + + // now remove the project for the maps. + mProjectTargetMap.remove(project); + mProjectApkConfigMap.remove(project); + } + } + + public void projectDeleted(IProject project) { + projectClosed(project); + } + + public void projectOpened(IProject project) { + // ignore this. The project will be added to the map the first time the target needs + // to be resolved. + } + + public void projectOpenedWithWorkspace(IProject project) { + // ignore this. The project will be added to the map the first time the target needs + // to be resolved. + } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java new file mode 100644 index 000000000..e0d0d5e3a --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.wizards.actions; + +import com.android.ide.eclipse.adt.wizards.newproject.NewProjectWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project". + * It displays the Android New Project wizard. + */ +public class NewProjectAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewProjectWizard(); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java new file mode 100644 index 000000000..8c4a115b4 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.wizards.actions; + +import com.android.ide.eclipse.editors.wizards.NewXmlFileWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project". + * It displays the Android New XML file wizard. + */ +public class NewXmlFileAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewXmlFileWizard(); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java new file mode 100644 index 000000000..4fc9deef1 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.wizards.actions; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.IWorkbenchWizard; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.IWorkbenchHelpContextIds; +import org.eclipse.ui.internal.LegacyResourceSupport; +import org.eclipse.ui.internal.actions.NewWizardShortcutAction; +import org.eclipse.ui.internal.util.Util; + +/** + * An abstract action that displays one of our wizards. + * Derived classes must provide the actual wizard to display. + */ +/*package*/ abstract class OpenWizardAction implements IWorkbenchWindowActionDelegate { + + /** + * The wizard dialog width, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_WIDTH = 500; + + /** + * The wizard dialog height, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_HEIGHT = 500; + + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose() + */ + public void dispose() { + // pass + } + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow) + */ + public void init(IWorkbenchWindow window) { + // pass + } + + /** + * Opens and display the Android New Project Wizard. + *

      + * Most of this implementation is extracted from {@link NewWizardShortcutAction#run()}. + * + * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction) + */ + public void run(IAction action) { + + // get the workbench and the current window + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + + // This code from NewWizardShortcutAction#run() gets the current window selection + // and converts it to a workbench structured selection for the wizard, if possible. + ISelection selection = window.getSelectionService().getSelection(); + IStructuredSelection selectionToPass = StructuredSelection.EMPTY; + if (selection instanceof IStructuredSelection) { + selectionToPass = (IStructuredSelection) selection; + } else { + // Build the selection from the IFile of the editor + IWorkbenchPart part = window.getPartService().getActivePart(); + if (part instanceof IEditorPart) { + IEditorInput input = ((IEditorPart) part).getEditorInput(); + Class fileClass = LegacyResourceSupport.getFileClass(); + if (input != null && fileClass != null) { + Object file = Util.getAdapter(input, fileClass); + if (file != null) { + selectionToPass = new StructuredSelection(file); + } + } + } + } + + // Create the wizard and initialize it with the selection + IWorkbenchWizard wizard = instanciateWizard(action); + wizard.init(workbench, selectionToPass); + + // It's not visible yet until a dialog is created and opened + Shell parent = window.getShell(); + WizardDialog dialog = new WizardDialog(parent, wizard); + dialog.create(); + + // This code comes straight from NewWizardShortcutAction#run() + Point defaultSize = dialog.getShell().getSize(); + dialog.getShell().setSize( + Math.max(SIZING_WIZARD_WIDTH, defaultSize.x), + Math.max(SIZING_WIZARD_HEIGHT, defaultSize.y)); + window.getWorkbench().getHelpSystem().setHelp(dialog.getShell(), + IWorkbenchHelpContextIds.NEW_WIZARD_SHORTCUT); + + dialog.open(); + } + + /** + * Called by {@link #run(IAction)} to instantiate the actual wizard. + * + * @param action The action parameter from {@link #run(IAction)}. + * @return A new wizard instance. Must not be null. + */ + protected abstract IWorkbenchWizard instanciateWizard(IAction action); + + /* (non-Javadoc) + * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection) + */ + public void selectionChanged(IAction action, ISelection selection) { + // pass + } + +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java index 33ec2bcff..0dd88c077 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java @@ -24,7 +24,7 @@ package com.android.ide.eclipse.adt.wizards.newproject; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; import com.android.sdklib.project.ProjectProperties; @@ -821,26 +821,36 @@ public class NewProjectCreationPage extends WizardPage { Path path = new Path(f.getPath()); String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString(); - AndroidManifestHelper manifest = new AndroidManifestHelper(osPath); - if (!manifest.exists()) { + + AndroidManifestParser manifestData = AndroidManifestParser.parseForData(osPath); + if (manifestData == null) { return; } String packageName = null; String activityName = null; - String minSdkVersion = null; + int minSdkVersion = 0; // 0 means no minSdkVersion provided in the manifest try { - packageName = manifest.getPackageName(); - activityName = manifest.getActivityName(1); - minSdkVersion = manifest.getMinSdkVersion(); + packageName = manifestData.getPackage(); + minSdkVersion = manifestData.getApiLevelRequirement(); + + // try to get the first launcher activity. If none, just take the first activity. + activityName = manifestData.getLauncherActivity(); + if (activityName == null) { + String[] activities = manifestData.getActivities(); + if (activities != null && activities.length > 0) { + activityName = activities[0]; + } + } } catch (Exception e) { // ignore exceptions } - if (packageName != null && packageName.length() > 0) { mPackageNameField.setText(packageName); } + + activityName = AndroidManifestParser.extractActivityName(activityName, packageName); if (activityName != null && activityName.length() > 0) { mInternalActivityNameUpdate = true; @@ -917,12 +927,10 @@ public class NewProjectCreationPage extends WizardPage { } } - if (!foundTarget && minSdkVersion != null) { + if (!foundTarget && minSdkVersion > 0) { try { - int sdkVersion = Integer.parseInt(minSdkVersion); - for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { - if (target.getApiVersionNumber() == sdkVersion) { + if (target.getApiVersionNumber() == minSdkVersion) { mSdkTargetSelector.setSelection(target); foundTarget = true; break; @@ -945,7 +953,8 @@ public class NewProjectCreationPage extends WizardPage { if (!foundTarget) { mInternalMinSdkVersionUpdate = true; - mMinSdkVersionField.setText(minSdkVersion == null ? "" : minSdkVersion); //$NON-NLS-1$ + mMinSdkVersionField.setText( + minSdkVersion <= 0 ? "" : Integer.toString(minSdkVersion)); //$NON-NLS-1$ mInternalMinSdkVersionUpdate = false; } } @@ -1072,8 +1081,8 @@ public class NewProjectCreationPage extends WizardPage { // Check there's an android manifest in the directory String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString(); - AndroidManifestHelper manifest = new AndroidManifestHelper(osPath); - if (!manifest.exists()) { + File manifestFile = new File(osPath); + if (!manifestFile.isFile()) { return setStatus( String.format("File %1$s not found in %2$s.", AndroidConstants.FN_ANDROID_MANIFEST, f.getName()), @@ -1081,15 +1090,22 @@ public class NewProjectCreationPage extends WizardPage { } // Parse it and check the important fields. - String packageName = manifest.getPackageName(); + AndroidManifestParser manifestData = AndroidManifestParser.parseForData(osPath); + if (manifestData == null) { + return setStatus( + String.format("File %1$s could not be parsed.", osPath), + MSG_ERROR); + } + + String packageName = manifestData.getPackage(); if (packageName == null || packageName.length() == 0) { return setStatus( String.format("No package name defined in %1$s.", osPath), MSG_ERROR); } - String activityName = manifest.getActivityName(1); - if (activityName == null || activityName.length() == 0) { + String[] activities = manifestData.getActivities(); + if (activities == null || activities.length == 0) { // This is acceptable now as long as no activity needs to be created if (isCreateActivity()) { return setStatus( @@ -1097,7 +1113,7 @@ public class NewProjectCreationPage extends WizardPage { MSG_ERROR); } } - + // If there's already a .project, tell the user to use import instead. if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ return setStatus("An Eclipse project already exists in this directory. Consider using File > Import > Existing Project instead.", diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java index 607159aed..af45fa9e6 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java @@ -105,6 +105,8 @@ public class NewProjectWizard extends Wizard implements INewWizard { SdkConstants.FD_LAYOUT + AndroidConstants.WS_SEP; private static final String VALUES_DIRECTORY = SdkConstants.FD_VALUES + AndroidConstants.WS_SEP; + private static final String GEN_SRC_DIRECTORY = + SdkConstants.FD_GEN_SOURCES + AndroidConstants.WS_SEP; private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$ private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY @@ -114,7 +116,7 @@ public class NewProjectWizard extends Wizard implements INewWizard { private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY + "uses-sdk.template"; //$NON-NLS-1$ private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY - + "launcher_intent_filter.template"; //$NON-NLS-1$ + + "launcher_intent_filter.template"; //$NON-NLS-1$ private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY + "strings.template"; //$NON-NLS-1$ @@ -341,15 +343,26 @@ public class NewProjectWizard extends Wizard implements INewWizard { // Create folders in the project if they don't already exist addDefaultDirectories(project, AndroidConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); - String[] sourceFolder = new String[] { (String) parameters.get(PARAM_SRC_FOLDER) }; - addDefaultDirectories(project, AndroidConstants.WS_ROOT, sourceFolder, monitor); + String[] sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER), + GEN_SRC_DIRECTORY + }; + addDefaultDirectories(project, AndroidConstants.WS_ROOT, sourceFolders, monitor); // Create the resource folders in the project if they don't already exist. addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor); - // Setup class path + // Setup class path: mark folders as source folders IJavaProject javaProject = JavaCore.create(project); - setupSourceFolder(javaProject, sourceFolder[0], monitor); + for (String sourceFolder : sourceFolders) { + setupSourceFolder(javaProject, sourceFolder, monitor); + } + + // Mark the gen source folder as derived + IFolder genSrcFolder = project.getFolder(AndroidConstants.WS_ROOT + GEN_SRC_DIRECTORY); + if (genSrcFolder.exists()) { + genSrcFolder.setDerived(true); + } if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) { // Create files in the project if they don't already exist @@ -359,7 +372,7 @@ public class NewProjectWizard extends Wizard implements INewWizard { addIcon(project, monitor); // Create the default package components - addSampleCode(project, sourceFolder[0], parameters, stringDictionary, monitor); + addSampleCode(project, sourceFolders[0], parameters, stringDictionary, monitor); // add the string definition file if needed if (stringDictionary.size() > 0) { @@ -371,7 +384,8 @@ public class NewProjectWizard extends Wizard implements INewWizard { monitor); } - Sdk.getCurrent().setProject(project, (IAndroidTarget) parameters.get(PARAM_SDK_TARGET)); + Sdk.getCurrent().setProject(project, (IAndroidTarget) parameters.get(PARAM_SDK_TARGET), + null /* apkConfigMap*/); // Fix the project to make sure all properties are as expected. // Necessary for existing projects and good for new ones to. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java index 65817c306..5abfd811d 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.common; import com.android.sdklib.SdkConstants; import java.io.File; +import java.util.regex.Pattern; /** * Constant definition class.
      @@ -49,17 +50,6 @@ public class AndroidConstants { /** Nature of android projects */ public final static String NATURE = "com.android.ide.eclipse.adt.AndroidNature"; //$NON-NLS-1$ - public final static int PLATFORM_UNKNOWN = 0; - public final static int PLATFORM_LINUX = 1; - public final static int PLATFORM_WINDOWS = 2; - public final static int PLATFORM_DARWIN = 3; - - /** - * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, - * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. - */ - public final static int CURRENT_PLATFORM = currentPlatform(); - /** Separator for workspace path, i.e. "/". */ public final static String WS_SEP = "/"; //$NON-NLS-1$ /** Separator character for workspace path, i.e. '/'. */ @@ -97,10 +87,7 @@ public class AndroidConstants { /** Name of the manifest file, i.e. "AndroidManifest.xml". */ public static final String FN_ANDROID_MANIFEST = "AndroidManifest.xml"; //$NON-NLS-1$ - public static final String FN_PROJECT_AIDL = "project.aidl"; //$NON-NLS-1$ - /** dex.jar file */ - public static final String FN_DX_JAR = "dx.jar"; //$NON-NLS-1$ /** Name of the android sources directory */ public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$ @@ -114,20 +101,21 @@ public class AndroidConstants { public final static String FN_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ /** Temporary packaged resources file name, i.e. "resources.ap_" */ public final static String FN_RESOURCES_AP_ = "resources.ap_"; //$NON-NLS-1$ + /** Temporary packaged resources file name for a specific set of configuration */ + public final static String FN_RESOURCES_S_AP_ = "resources-%s.ap_"; //$NON-NLS-1$ + public final static Pattern PATTERN_RESOURCES_S_AP_ = + Pattern.compile("resources-.*\\.ap_", Pattern.CASE_INSENSITIVE); - public final static String FN_ADB = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + public final static String FN_ADB = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? "adb.exe" : "adb"; //$NON-NLS-1$ //$NON-NLS-2$ - public final static String FN_AAPT = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? - "aapt.exe" : "aapt"; //$NON-NLS-1$ //$NON-NLS-2$ - - public final static String FN_AIDL = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? - "aidl.exe" : "aidl"; //$NON-NLS-1$ //$NON-NLS-2$ - - public final static String FN_EMULATOR = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + public final static String FN_EMULATOR = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? "emulator.exe" : "emulator"; //$NON-NLS-1$ //$NON-NLS-2$ - public final static String FN_TRACEVIEW = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + public final static String FN_TRACEVIEW = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? "traceview.exe" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$ /** Absolute path of the workspace root, i.e. "/" */ @@ -147,11 +135,6 @@ public class AndroidConstants { * FIXME: remove once the NPW is fixed. */ public final static String OS_SDK_SAMPLES_FOLDER = SdkConstants.FD_SAMPLES + File.separator; - /** Path of the dx.jar file relative to the sdk folder. */ - public final static String OS_SDK_LIBS_DX_JAR = - SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + FN_DX_JAR; - - /** Regexp for single dot */ public final static String RE_DOT = "\\."; //$NON-NLS-1$ /** Regexp for java extension, i.e. "\.java$" */ public final static String RE_JAVA_EXT = "\\.java$"; //$NON-NLS-1$ @@ -164,8 +147,11 @@ public class AndroidConstants { /** The old common plug-in ID. Please do not use for new features. */ public static final String COMMON_PLUGIN_ID = "com.android.ide.eclipse.common"; //$NON-NLS-1$ - /** aapt marker error. */ - public final static String MARKER_AAPT = COMMON_PLUGIN_ID + ".aaptProblem"; //$NON-NLS-1$ + /** aapt marker error when running the compile command */ + public final static String MARKER_AAPT_COMPILE = COMMON_PLUGIN_ID + ".aaptProblem"; //$NON-NLS-1$ + + /** aapt marker error when running the package command */ + public final static String MARKER_AAPT_PACKAGE = COMMON_PLUGIN_ID + ".aapt2Problem"; //$NON-NLS-1$ /** XML marker error. */ public final static String MARKER_XML = COMMON_PLUGIN_ID + ".xmlProblem"; //$NON-NLS-1$ @@ -229,22 +215,4 @@ public class AndroidConstants { /** The base URL where to find the Android class & manifest documentation */ public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$ - /** - * Returns current platform - * - * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, - * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. - */ - private static int currentPlatform() { - String os = System.getProperty("os.name"); //$NON-NLS-1$ - if (os.startsWith("Mac OS")) { //$NON-NLS-1$ - return PLATFORM_DARWIN; - } else if (os.startsWith("Windows")) { //$NON-NLS-1$ - return PLATFORM_WINDOWS; - } else if (os.startsWith("Linux")) { //$NON-NLS-1$ - return PLATFORM_LINUX; - } - - return PLATFORM_UNKNOWN; - } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java deleted file mode 100644 index cd238d2bf..000000000 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php - * - * 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.ide.eclipse.common.project; - -import com.android.ide.eclipse.common.AndroidConstants; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.xml.sax.InputSource; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; - -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpressionException; - -/** - * Utility class that manages the AndroidManifest.xml file. - *

      - * All the get method work by XPath. Repeated calls to those may warrant using - * {@link AndroidManifestParser} instead. - */ -public class AndroidManifestHelper { - private IFile mManifestIFile; - private File mManifestFile; - private XPath mXPath; - - /** - * Creates an AndroidManifest based on an existing Eclipse {@link IProject} object. - *

      - * Use {@link #exists()} to check if the manifest file really exists in the project. - * - * @param project The project to search for the manifest. - */ - public AndroidManifestHelper(IProject project) { - mXPath = AndroidXPathFactory.newXPath(); - mManifestIFile = getManifest(project); - } - - /** - * Creates an AndroidManifest based on a file path. - *

      - * Use {@link #exists()} to check if the manifest file really exists. - * - * @param osManifestFilePath the os path to the AndroidManifest.xml file. - */ - public AndroidManifestHelper(String osManifestFilePath) { - mXPath = AndroidXPathFactory.newXPath(); - mManifestFile = new File(osManifestFilePath); - } - - - /** - * Returns the underlying {@link IFile} for the android manifest XML file, if found in the - * given Eclipse project. - * - * Always return null if the constructor that takes an {@link IProject} was NOT called. - * - * @return The IFile for the androidManifest.xml or null if no such file could be found. - */ - public IFile getManifestIFile() { - return mManifestIFile; - } - - /** - * Returns the underlying {@link File} for the android manifest XML file. - */ - public File getManifestFile() { - if (mManifestIFile != null) { - return mManifestIFile.getLocation().toFile(); - } - - return mManifestFile; - } - - /** - * Returns the package name defined in the manifest file. - * - * @return A String object with the package or null if any error happened. - */ - public String getPackageName() { - try { - return mXPath.evaluate("/manifest/@package", getSource()); //$NON-NLS-1$ - } catch (XPathExpressionException e1) { - // If the XPath failed to evaluate, we'll return null. - } catch (Exception e) { - // if this happens this is due to the resource being out of sync. - // so we must refresh it and do it again - - // for any other kind of exception we must return null as well; - } - - return null; - } - - /** - * Returns the minSdkVersion defined in the manifest file. - * - * @return A String object with the package or null if any error happened. - */ - public String getMinSdkVersion() { - try { - return mXPath.evaluate("/manifest/uses-sdk/@" //$NON-NLS-1$ - + AndroidXPathFactory.DEFAULT_NS_PREFIX - + ":minSdkVersion", getSource()); //$NON-NLS-1$ - } catch (XPathExpressionException e1) { - // If the XPath failed to evaluate, we'll return null. - } catch (Exception e) { - // if this happens this is due to the resource being out of sync. - // so we must refresh it and do it again - - // for any other kind of exception we must return null as well; - } - - return null; - } - /** - * Returns the i-th activity defined in the manifest file. - * - * @param index The 1-based index of the activity to return. - * @return A String object with the activity or null if any error happened. - */ - public String getActivityName(int index) { - try { - return mXPath.evaluate("/manifest/application/activity[" //$NON-NLS-1$ - + index - + "]/@" //$NON-NLS-1$ - + AndroidXPathFactory.DEFAULT_NS_PREFIX +":name", //$NON-NLS-1$ - getSource()); - } catch (XPathExpressionException e1) { - // If the XPath failed to evaluate, we'll return null. - } catch (Exception e) { - // if this happens this is due to the resource being out of sync. - // so we must refresh it and do it again - - // for any other kind of exception we must return null as well; - } - return null; - } - - /** - * Returns an IFile object representing the manifest for the specified - * project. - * - * @param project The project containing the manifest file. - * @return An IFile object pointing to the manifest or null if the manifest - * is missing. - */ - public static IFile getManifest(IProject project) { - IResource r = project.findMember(AndroidConstants.WS_SEP - + AndroidConstants.FN_ANDROID_MANIFEST); - - if (r == null || r.exists() == false || (r instanceof IFile) == false) { - return null; - } - return (IFile) r; - } - - /** - * Combines a java package, with a class value from the manifest to make a fully qualified - * class name - * @param javaPackage the java package from the manifest. - * @param className the class name from the manifest. - * @return the fully qualified class name. - */ - public static String combinePackageAndClassName(String javaPackage, String className) { - if (className == null || className.length() == 0) { - return javaPackage; - } - if (javaPackage == null || javaPackage.length() == 0) { - return className; - } - - // the class name can be a subpackage (starts with a '.' - // char), a simple class name (no dot), or a full java package - boolean startWithDot = (className.charAt(0) == '.'); - boolean hasDot = (className.indexOf('.') != -1); - if (startWithDot || hasDot == false) { - - // add the concatenation of the package and class name - if (startWithDot) { - return javaPackage + className; - } else { - return javaPackage + '.' + className; - } - } else { - // just add the class as it should be a fully qualified java name. - return className; - } - } - - - - /** - * Returns true either if an androidManifest.xml file was found in the project - * or if the given file path exists. - */ - public boolean exists() { - if (mManifestIFile != null) { - return mManifestIFile.exists(); - } else if (mManifestFile != null) { - return mManifestFile.exists(); - } - - return false; - } - - /** - * Returns an InputSource for XPath. - * - * @throws FileNotFoundException if file does not exist. - * @throws CoreException if the {@link IFile} does not exist. - */ - private InputSource getSource() throws FileNotFoundException, CoreException { - if (mManifestIFile != null) { - return new InputSource(mManifestIFile.getContents()); - } else if (mManifestFile != null) { - return new InputSource(new FileReader(mManifestFile)); - } - - return null; - } - -} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java index 850c59d71..0a45196a4 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java @@ -22,6 +22,8 @@ import com.android.sdklib.SdkConstants; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.core.IJavaProject; import org.xml.sax.Attributes; @@ -30,6 +32,8 @@ import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; +import java.io.File; +import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Set; @@ -56,6 +60,8 @@ public class AndroidManifestParser { private final static String NODE_ACTION = "action"; //$NON-NLS-1$ private final static String NODE_CATEGORY = "category"; //$NON-NLS-1$ private final static String NODE_USES_SDK = "uses-sdk"; //$NON-NLS-1$ + private final static String NODE_INSTRUMENTATION = "instrumentation"; //$NON-NLS-1$ + private final static String NODE_USES_LIBRARY = "uses-library"; //$NON-NLS-1$ private final static int LEVEL_MANIFEST = 0; private final static int LEVEL_APPLICATION = 1; @@ -66,6 +72,12 @@ public class AndroidManifestParser { private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$ private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$ + /** + * XML error & data handler used when parsing the AndroidManifest.xml file. + *

      + * This serves both as an {@link XmlErrorHandler} to report errors and as a data repository + * to collect data from the manifest. + */ private static class ManifestHandler extends XmlErrorHandler { //--- data read from the parsing @@ -82,6 +94,10 @@ public class AndroidManifestParser { private Boolean mDebuggable = null; /** API level requirement. if 0 the attribute was not present. */ private int mApiLevelRequirement = 0; + /** List of all instrumentations declared by the manifest */ + private final ArrayList mInstrumentations = new ArrayList(); + /** List of all libraries in use declared by the manifest */ + private final ArrayList mLibraries = new ArrayList(); //--- temporary data/flags used during parsing private IJavaProject mJavaProject; @@ -95,12 +111,13 @@ public class AndroidManifestParser { private Locator mLocator; /** - * - * @param manifestFile - * @param errorListener - * @param gatherData - * @param javaProject - * @param markErrors + * Creates a new {@link ManifestHandler}, which is also an {@link XmlErrorHandler}. + * + * @param manifestFile The manifest file being parsed. Can be null. + * @param errorListener An optional error listener. + * @param gatherData True if data should be gathered. + * @param javaProject The java project holding the manifest file. Can be null. + * @param markErrors True if errors should be marked as Eclipse Markers on the resource. */ ManifestHandler(IFile manifestFile, XmlErrorListener errorListener, boolean gatherData, IJavaProject javaProject, boolean markErrors) { @@ -160,6 +177,23 @@ public class AndroidManifestParser { return mApiLevelRequirement; } + /** + * Returns the list of instrumentations found in the manifest. + * @return An array of instrumentation names, or empty if no instrumentations were + * found. + */ + String[] getInstrumentations() { + return mInstrumentations.toArray(new String[mInstrumentations.size()]); + } + + /** + * Returns the list of libraries in use found in the manifest. + * @return An array of library names, or empty if no libraries were found. + */ + String[] getUsesLibraries() { + return mLibraries.toArray(new String[mLibraries.size()]); + } + /* (non-Javadoc) * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) */ @@ -217,7 +251,13 @@ public class AndroidManifestParser { } catch (NumberFormatException e) { handleError(e, -1 /* lineNumber */); } - } + } else if (NODE_INSTRUMENTATION.equals(localName)) { + value = getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */); + if (value != null) { + mInstrumentations.add(value); + } + } break; case LEVEL_ACTIVITY: if (NODE_ACTIVITY.equals(localName)) { @@ -232,7 +272,13 @@ public class AndroidManifestParser { } else if (NODE_PROVIDER.equals(localName)) { processNode(attributes, AndroidConstants.CLASS_CONTENTPROVIDER); mValidLevel++; - } + } else if (NODE_USES_LIBRARY.equals(localName)) { + value = getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */); + if (value != null) { + mLibraries.add(value); + } + } break; case LEVEL_INTENT_FILTER: // only process this level if we are in an activity @@ -355,8 +401,7 @@ public class AndroidManifestParser { String activityName = getAttributeValue(attributes, ATTRIBUTE_NAME, true /* hasNamespace */); if (activityName != null) { - mCurrentActivity = AndroidManifestHelper.combinePackageAndClassName(mPackage, - activityName); + mCurrentActivity = combinePackageAndClassName(mPackage, activityName); mActivities.add(mCurrentActivity); if (mMarkErrors) { @@ -387,8 +432,7 @@ public class AndroidManifestParser { String serviceName = getAttributeValue(attributes, ATTRIBUTE_NAME, true /* hasNamespace */); if (serviceName != null) { - serviceName = AndroidManifestHelper.combinePackageAndClassName(mPackage, - serviceName); + serviceName = combinePackageAndClassName(mPackage, serviceName); if (mMarkErrors) { checkClass(serviceName, superClassName, false /* testVisibility */); @@ -412,6 +456,9 @@ public class AndroidManifestParser { * the class or of its constructors. */ private void checkClass(String className, String superClassName, boolean testVisibility) { + if (mJavaProject == null) { + return; + } // we need to check the validity of the activity. String result = BaseProjectHelper.testClassForManifest(mJavaProject, className, superClassName, testVisibility); @@ -477,6 +524,8 @@ public class AndroidManifestParser { private final String[] mProcesses; private final Boolean mDebuggable; private final int mApiLevelRequirement; + private final String[] mInstrumentations; + private final String[] mLibraries; static { sParserFactory = SAXParserFactory.newInstance(); @@ -484,8 +533,12 @@ public class AndroidManifestParser { } /** - * Parses the Android Manifest, and returns an object containing - * the result of the parsing. + * Parses the Android Manifest, and returns an object containing the result of the parsing. + *

      + * This method is useful to parse a specific {@link IFile} in a Java project. + *

      + * If you only want to gather data, consider {@link #parseForData(IFile)} instead. + * * @param javaProject The java project. * @param manifestFile the {@link IFile} representing the manifest file. * @param errorListener @@ -496,8 +549,12 @@ public class AndroidManifestParser { * @return an {@link AndroidManifestParser} or null if the parsing failed. * @throws CoreException */ - public static AndroidManifestParser parse(IJavaProject javaProject, IFile manifestFile, - XmlErrorListener errorListener, boolean gatherData, boolean markErrors) + public static AndroidManifestParser parse( + IJavaProject javaProject, + IFile manifestFile, + XmlErrorListener errorListener, + boolean gatherData, + boolean markErrors) throws CoreException { try { SAXParser parser = sParserFactory.newSAXParser(); @@ -512,7 +569,51 @@ public class AndroidManifestParser { return new AndroidManifestParser(manifestHandler.getPackage(), manifestHandler.getActivities(), manifestHandler.getLauncherActivity(), manifestHandler.getProcesses(), manifestHandler.getDebuggable(), - manifestHandler.getApiLevelRequirement()); + manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(), + manifestHandler.getUsesLibraries()); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return null; + } + + /** + * Parses the Android Manifest, and returns an object containing the result of the parsing. + *

      + * This version parses a real {@link File} file given by an actual path, which is useful for + * parsing a file that is not part of an Eclipse Java project. + *

      + * It assumes errors cannot be marked on the file and that data gathering is enabled. + * + * @param manifestFile the manifest file to parse. + * @return an {@link AndroidManifestParser} or null if the parsing failed. + * @throws CoreException + */ + private static AndroidManifestParser parse(File manifestFile) + throws CoreException { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + ManifestHandler manifestHandler = new ManifestHandler( + null, //manifestFile + null, //errorListener + true, //gatherData + null, //javaProject + false //markErrors + ); + + parser.parse(new InputSource(new FileReader(manifestFile)), manifestHandler); + + // get the result from the handler + + return new AndroidManifestParser(manifestHandler.getPackage(), + manifestHandler.getActivities(), manifestHandler.getLauncherActivity(), + manifestHandler.getProcesses(), manifestHandler.getDebuggable(), + manifestHandler.getApiLevelRequirement(), manifestHandler.getInstrumentations(), + manifestHandler.getUsesLibraries()); } catch (ParserConfigurationException e) { } catch (SAXException e) { } catch (IOException e) { @@ -535,13 +636,16 @@ public class AndroidManifestParser { * @return an {@link AndroidManifestParser} or null if the parsing failed. * @throws CoreException */ - public static AndroidManifestParser parse(IJavaProject javaProject, - XmlErrorListener errorListener, boolean gatherData, boolean markErrors) + public static AndroidManifestParser parse( + IJavaProject javaProject, + XmlErrorListener errorListener, + boolean gatherData, + boolean markErrors) throws CoreException { try { SAXParser parser = sParserFactory.newSAXParser(); - IFile manifestFile = AndroidManifestHelper.getManifest(javaProject.getProject()); + IFile manifestFile = getManifest(javaProject.getProject()); if (manifestFile != null) { ManifestHandler manifestHandler = new ManifestHandler(manifestFile, errorListener, gatherData, javaProject, markErrors); @@ -552,7 +656,8 @@ public class AndroidManifestParser { return new AndroidManifestParser(manifestHandler.getPackage(), manifestHandler.getActivities(), manifestHandler.getLauncherActivity(), manifestHandler.getProcesses(), manifestHandler.getDebuggable(), - manifestHandler.getApiLevelRequirement()); + manifestHandler.getApiLevelRequirement(), + manifestHandler.getInstrumentations(), manifestHandler.getUsesLibraries()); } } catch (ParserConfigurationException e) { } catch (SAXException e) { @@ -581,13 +686,30 @@ public class AndroidManifestParser { * Parses the manifest file, and collects data. * @param manifestFile The manifest file to parse. * @return an {@link AndroidManifestParser} or null if the parsing failed. - * @throws CoreException + * @throws CoreException for example the file does not exist in the workspace or + * the workspace needs to be refreshed. */ public static AndroidManifestParser parseForData(IFile manifestFile) throws CoreException { return parse(null /* javaProject */, manifestFile, null /* errorListener */, true /* gatherData */, false /* markErrors */); } + /** + * Parses the manifest file, and collects data. + * + * @param osManifestFilePath The OS path of the manifest file to parse. + * @return an {@link AndroidManifestParser} or null if the parsing failed. + */ + public static AndroidManifestParser parseForData(String osManifestFilePath) { + try { + return parse(new File(osManifestFilePath)); + } catch (CoreException e) { + // Ignore workspace errors (unlikely to happen since this parses an actual file, + // not a workspace resource). + return null; + } + } + /** * Returns the package defined in the manifest, if found. * @return The package name or null if not found. @@ -633,6 +755,22 @@ public class AndroidManifestParser { public int getApiLevelRequirement() { return mApiLevelRequirement; } + + /** + * Returns the list of instrumentations found in the manifest. + * @return An array of fully qualified class names, or empty if no instrumentations were found. + */ + public String[] getInstrumentations() { + return mInstrumentations; + } + + /** + * Returns the list of libraries in use found in the manifest. + * @return An array of library names, or empty if no uses-library declarations were found. + */ + public String[] getUsesLibraries() { + return mLibraries; + } /** @@ -647,15 +785,92 @@ public class AndroidManifestParser { * @param processes the list of custom processes declared in the manifest. * @param debuggable the debuggable attribute, or null if not set. * @param apiLevelRequirement the minSdkVersion attribute value or 0 if not set. + * @param instrumentations the list of instrumentations parsed from the manifest. + * @param libraries the list of libraries in use parsed from the manifest. */ private AndroidManifestParser(String javaPackage, String[] activities, String launcherActivity, String[] processes, Boolean debuggable, - int apiLevelRequirement) { + int apiLevelRequirement, String[] instrumentations, String[] libraries) { mJavaPackage = javaPackage; mActivities = activities; mLauncherActivity = launcherActivity; mProcesses = processes; mDebuggable = debuggable; mApiLevelRequirement = apiLevelRequirement; + mInstrumentations = instrumentations; + mLibraries = libraries; + } + + /** + * Returns an IFile object representing the manifest for the specified + * project. + * + * @param project The project containing the manifest file. + * @return An IFile object pointing to the manifest or null if the manifest + * is missing. + */ + public static IFile getManifest(IProject project) { + IResource r = project.findMember(AndroidConstants.WS_SEP + + AndroidConstants.FN_ANDROID_MANIFEST); + + if (r == null || r.exists() == false || (r instanceof IFile) == false) { + return null; + } + return (IFile) r; + } + + /** + * Combines a java package, with a class value from the manifest to make a fully qualified + * class name + * @param javaPackage the java package from the manifest. + * @param className the class name from the manifest. + * @return the fully qualified class name. + */ + public static String combinePackageAndClassName(String javaPackage, String className) { + if (className == null || className.length() == 0) { + return javaPackage; + } + if (javaPackage == null || javaPackage.length() == 0) { + return className; + } + + // the class name can be a subpackage (starts with a '.' + // char), a simple class name (no dot), or a full java package + boolean startWithDot = (className.charAt(0) == '.'); + boolean hasDot = (className.indexOf('.') != -1); + if (startWithDot || hasDot == false) { + + // add the concatenation of the package and class name + if (startWithDot) { + return javaPackage + className; + } else { + return javaPackage + '.' + className; + } + } else { + // just add the class as it should be a fully qualified java name. + return className; + } + } + + /** + * Given a fully qualified activity name (e.g. com.foo.test.MyClass) and given a project + * package base name (e.g. com.foo), returns the relative activity name that would be used + * the "name" attribute of an "activity" element. + * + * @param fullActivityName a fully qualified activity class name, e.g. "com.foo.test.MyClass" + * @param packageName The project base package name, e.g. "com.foo" + * @return The relative activity name if it can be computed or the original fullActivityName. + */ + public static String extractActivityName(String fullActivityName, String packageName) { + if (packageName != null && fullActivityName != null) { + if (packageName.length() > 0 && fullActivityName.startsWith(packageName)) { + String name = fullActivityName.substring(packageName.length()); + if (name.length() > 0 && name.charAt(0) == '.') { + return name; + } + } + } + + return fullActivityName; } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java index fda55c450..1810ad295 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java @@ -86,8 +86,13 @@ public class XmlErrorHandler extends DefaultHandler { */ @Override public void warning(SAXParseException exception) throws SAXException { - BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), - exception.getLineNumber(), IMarker.SEVERITY_WARNING); + if (mFile != null) { + BaseProjectHelper.addMarker(mFile, + AndroidConstants.MARKER_XML, + exception.getMessage(), + exception.getLineNumber(), + IMarker.SEVERITY_WARNING); + } } protected final IFile getFile() { @@ -104,12 +109,19 @@ public class XmlErrorHandler extends DefaultHandler { mErrorListener.errorFound(); } - if (lineNumber != -1) { - BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), - lineNumber, IMarker.SEVERITY_ERROR); - } else { - BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), - IMarker.SEVERITY_ERROR); + if (mFile != null) { + if (lineNumber != -1) { + BaseProjectHelper.addMarker(mFile, + AndroidConstants.MARKER_XML, + exception.getMessage(), + lineNumber, + IMarker.SEVERITY_ERROR); + } else { + BaseProjectHelper.addMarker(mFile, + AndroidConstants.MARKER_XML, + exception.getMessage(), + IMarker.SEVERITY_ERROR); + } } } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java index a325ccbf6..0d0883ecb 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java @@ -313,6 +313,11 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { if (currentUiNode != null) { // look for an UI attribute matching the current attribute name String attrName = attrInfo.name; + // remove any namespace prefix from the attribute name + int pos = attrName.indexOf(':'); + if (pos >= 0) { + attrName = attrName.substring(pos + 1); + } UiAttributeNode currAttrNode = null; for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { @@ -323,13 +328,13 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { } if (currAttrNode != null) { - choices = currAttrNode.getPossibleValues(); + choices = currAttrNode.getPossibleValues(value); if (currAttrNode instanceof UiFlagAttributeNode) { // A "flag" can consist of several values separated by "or" (|). // If the correct prefix contains such a pipe character, we change // it so that only the currently edited value is completed. - int pos = value.indexOf('|'); + pos = value.indexOf('|'); if (pos >= 0) { attrInfo.correctedPrefix = value = value.substring(pos + 1); attrInfo.needTag = 0; @@ -437,11 +442,15 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { tooltip = ((TextAttributeDescriptor) choice).getTooltip(); } + // Get the namespace URI for the attribute. Note that some attributes + // do not have a namespace and thus return null here. String nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); - nsPrefix = nsUriMap.get(nsUri); - if (nsPrefix == null) { - nsPrefix = lookupNamespacePrefix(currentNode, nsUri); - nsUriMap.put(nsUri, nsPrefix); + if (nsUri != null) { + nsPrefix = nsUriMap.get(nsUri); + if (nsPrefix == null) { + nsPrefix = lookupNamespacePrefix(currentNode, nsUri); + nsUriMap.put(nsUri, nsPrefix); + } } if (nsPrefix != null) { nsPrefix += ":"; //$NON-NLS-1$ @@ -452,9 +461,12 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { } else { continue; // discard unknown choice } + + String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword); if (keyword.startsWith(wordPrefix) || - (nsPrefix != null && keyword.startsWith(nsPrefix))) { + (nsPrefix != null && keyword.startsWith(nsPrefix)) || + (nsPrefix != null && nsKeyword.startsWith(wordPrefix))) { if (nsPrefix != null) { keyword = nsPrefix + keyword; } @@ -561,10 +573,11 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { /** * Heuristically extracts the prefix used for determining template relevance * from the viewer's document. The default implementation returns the String from - * offset backwards that forms a potential XML element name. + * offset backwards that forms a potential XML element name, attribute name or + * attribute value. * - * Code extracted from org.eclipse.jface.text.templatesTemplateCompletionProcessor - * and adapted to our needs. + * The part were we access the docment was extracted from + * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs. * * @param viewer the viewer * @param offset offset into document @@ -578,8 +591,19 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { try { for (; i > 0; --i) { char ch = document.getChar(i - 1); - // accepted characters are a-z and : (for attributes' namespace) - if (ch != ':' && (ch < 'a' || ch > 'z')) break; + + // We want all characters that can form a valid: + // - element name, e.g. anything that is a valid Java class/variable literal. + // - attribute name, including : for the namespace + // - attribute value. + // Before we were inclusive and that made the code fragile. So now we're + // going to be exclusive: take everything till we get one of: + // - any form of whitespace + // - any xml separator, e.g. < > ' " and = + if (Character.isWhitespace(ch) || + ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') { + break; + } } return document.get(i, offset - i); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java index dca7db09e..c7541e96b 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.editors; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.editors.uimodel.UiElementNode; import com.android.sdklib.IAndroidTarget; @@ -97,8 +98,9 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang private StructuredTextEditor mTextEditor; /** Listener for the XML model from the StructuredEditor */ private XmlModelStateListener mXmlModelStateListener; - /** Listener to update the root node if the resource framework changes */ - private Runnable mResourceRefreshListener; + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + private ITargetChangeListener mTargetListener; /** * Creates a form editor. @@ -107,15 +109,21 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang super(); ResourcesPlugin.getWorkspace().addResourceChangeListener(this); - mResourceRefreshListener = new Runnable() { - public void run() { - commitPages(false /* onSave */); + mTargetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == getProject()) { + onTargetsLoaded(); + } + } + public void onTargetsLoaded() { + commitPages(false /* onSave */); + // recreate the ui root node always initUiRootNode(true /*force*/); } }; - AdtPlugin.getDefault().addResourceChangedListener(mResourceRefreshListener); + AdtPlugin.getDefault().addTargetListener(mTargetListener); } // ---- Abstract Methods ---- @@ -340,9 +348,9 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang } ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); - if (mResourceRefreshListener != null) { - AdtPlugin.getDefault().removeResourceChangedListener(mResourceRefreshListener); - mResourceRefreshListener = null; + if (mTargetListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetListener); + mTargetListener = null; } super.dispose(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java index e3de3af2d..2c24772ae 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java @@ -18,7 +18,7 @@ package com.android.ide.eclipse.editors; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.common.AndroidConstants; +import com.android.sdklib.SdkConstants; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; @@ -231,7 +231,7 @@ public class IconFactory { // Text measurement varies so slightly depending on the platform int ofx = 0; int ofy = 0; - if (AndroidConstants.CURRENT_PLATFORM == AndroidConstants.PLATFORM_WINDOWS) { + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { ofx = +1; ofy = -1; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java index cc923bf54..f1d62a1c4 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java @@ -313,7 +313,7 @@ public final class DescriptorsUtils { * * @param attributes The list of {@link AttributeDescriptor} to compare to. * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. - * See {@link AndroidConstants#NS_RESOURCES} for a common value. + * See {@link SdkConstants#NS_RESOURCES} for a common value. * @param info The {@link AttributeInfo} to know whether it is included in the above list. * @return True if this {@link AttributeInfo} is already present in * the {@link AttributeDescriptor} list. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/AbstractGraphicalLayoutEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/AbstractGraphicalLayoutEditor.java new file mode 100644 index 000000000..049986745 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/AbstractGraphicalLayoutEditor.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; +import com.android.ide.eclipse.editors.layout.parts.ElementCreateCommand; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.gef.DefaultEditDomain; +import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette; +import org.eclipse.gef.ui.parts.SelectionSynchronizer; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.ui.IWorkbenchPart; + +/** + * Abstract GraphicalLayoutEditor. + */ +/*package*/ abstract class AbstractGraphicalLayoutEditor extends GraphicalEditorWithPalette + implements IWorkbenchPart, ILayoutReloadListener { + + /** + * Sets the UI for the edition of a new file. + * @param configuration the configuration of the new file. + */ + abstract void editNewFile(FolderConfiguration configuration); + + /** + * Reloads this editor, by getting the new model from the {@link LayoutEditor}. + */ + abstract void reloadEditor(); + + /** + * Callback for XML model changed. Only update/recompute the layout if the editor is visible + */ + abstract void onXmlModelChanged(); + + /** + * Responds to a page change that made the Graphical editor page the activated page. + */ + abstract void activated(); + + /** + * Responds to a page change that made the Graphical editor page the deactivated page + */ + abstract void deactivated(); + + /** + * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node + * created by {@link ElementCreateCommand#execute()}. + * + * @param uiNodeModel The {@link UiElementNode} to select. + */ + abstract void selectModel(UiElementNode uiNodeModel); + + /** + * Returns the selection synchronizer object. + * The synchronizer can be used to sync the selection of 2 or more EditPartViewers. + *

      + * This is changed from protected to public so that the outline can use it. + * + * @return the synchronizer + */ + @Override + public SelectionSynchronizer getSelectionSynchronizer() { + return super.getSelectionSynchronizer(); + } + + /** + * Returns the edit domain. + *

      + * This is changed from protected to public so that the outline can use it. + * + * @return the edit domain + */ + @Override + public DefaultEditDomain getEditDomain() { + return super.getEditDomain(); + } + + abstract void reloadPalette(); + + abstract void recomputeLayout(); + + abstract UiDocumentNode getModel(); + + abstract LayoutEditor getLayoutEditor(); + + abstract Clipboard getClipboard(); + +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java index 77467cd3c..9c529e5da 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java @@ -21,6 +21,7 @@ import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.adt.sdk.Sdk; import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.common.resources.ResourceType; import com.android.ide.eclipse.editors.IconFactory; import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions; @@ -85,8 +86,6 @@ import org.eclipse.gef.dnd.TemplateTransferDropTargetListener; import org.eclipse.gef.editparts.ScalableFreeformRootEditPart; import org.eclipse.gef.palette.PaletteRoot; import org.eclipse.gef.requests.CreationFactory; -import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette; -import org.eclipse.gef.ui.parts.SelectionSynchronizer; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; @@ -140,7 +139,7 @@ import java.util.Set; *

      * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html */ -public class GraphicalLayoutEditor extends GraphicalEditorWithPalette +public class GraphicalLayoutEditor extends AbstractGraphicalLayoutEditor implements ILayoutReloadListener { private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$ @@ -198,13 +197,21 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette private ProjectCallback mProjectCallback; private ILayoutLog mLogger; + private boolean mNeedsXmlReload = false; private boolean mNeedsRecompute = false; private int mPlatformThemeCount = 0; private boolean mDisableUpdates = false; - private boolean mActive = false; - private Runnable mFrameworkResourceChangeListener = new Runnable() { - public void run() { + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + private ITargetChangeListener mTargetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == getLayoutEditor().getProject()) { + onTargetsLoaded(); + } + } + + public void onTargetsLoaded() { // because the SDK changed we must reset the configured framework resource. mConfiguredFrameworkRes = null; @@ -228,7 +235,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette private final Runnable mConditionalRecomputeRunnable = new Runnable() { public void run() { - if (mActive) { + if (mLayoutEditor.isGraphicalEditorActive()) { recomputeLayout(); } else { mNeedsRecompute = true; @@ -253,7 +260,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette mMatchImage = factory.getIcon("match"); //$NON-NLS-1$ mErrorImage = factory.getIcon("error"); //$NON-NLS-1$ - AdtPlugin.getDefault().addResourceChangedListener(mFrameworkResourceChangeListener); + AdtPlugin.getDefault().addTargetListener(mTargetListener); } // ------------------------------------ @@ -561,10 +568,9 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette @Override public void dispose() { - if (mFrameworkResourceChangeListener != null) { - AdtPlugin.getDefault().removeResourceChangedListener( - mFrameworkResourceChangeListener); - mFrameworkResourceChangeListener = null; + if (mTargetListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetListener); + mTargetListener = null; } LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this); @@ -587,6 +593,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette return mPaletteRoot; } + @Override public Clipboard getClipboard() { return mClipboard; } @@ -708,7 +715,8 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette * * @param uiNodeModel The {@link UiElementNode} to select. */ - public void selectModel(UiElementNode uiNodeModel) { + @Override + void selectModel(UiElementNode uiNodeModel) { GraphicalViewer viewer = getGraphicalViewer(); // Give focus to the graphical viewer (in case the outline has it) @@ -726,6 +734,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette // Local methods //-------------- + @Override public LayoutEditor getLayoutEditor() { return mLayoutEditor; } @@ -855,7 +864,8 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette * Sets the UI for the edition of a new file. * @param configuration the configuration of the new file. */ - public void editNewFile(FolderConfiguration configuration) { + @Override + void editNewFile(FolderConfiguration configuration) { // update the configuration UI setConfiguration(configuration); @@ -1007,6 +1017,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette /** * Reloads this editor, by getting the new model from the {@link LayoutEditor}. */ + @Override void reloadEditor() { GraphicalViewer viewer = getGraphicalViewer(); viewer.setContents(getModel()); @@ -1026,25 +1037,37 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette } /** - * Update the layout editor when the Xml model is changed. + * Callback for XML model changed. Only update/recompute the layout if the editor is visible */ + @Override void onXmlModelChanged() { - GraphicalViewer viewer = getGraphicalViewer(); - - // try to preserve the selection before changing the content - SelectionManager selMan = viewer.getSelectionManager(); - ISelection selection = selMan.getSelection(); - - try { - viewer.setContents(getModel()); - } finally { - selMan.setSelection(selection); - } - if (mLayoutEditor.isGraphicalEditorActive()) { + doXmlReload(true /* force */); recomputeLayout(); } else { - mNeedsRecompute = true; + mNeedsXmlReload = true; + } + } + + /** + * Actually performs the XML reload + * @see #onXmlModelChanged() + */ + private void doXmlReload(boolean force) { + if (force || mNeedsXmlReload) { + GraphicalViewer viewer = getGraphicalViewer(); + + // try to preserve the selection before changing the content + SelectionManager selMan = viewer.getSelectionManager(); + ISelection selection = selMan.getSelection(); + + try { + viewer.setContents(getModel()); + } finally { + selMan.setSelection(selection); + } + + mNeedsXmlReload = false; } } @@ -1246,10 +1269,12 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette mCurrentLayoutLabel.setText(current != null ? current : "(Default)"); } + @Override UiDocumentNode getModel() { return mLayoutEditor.getUiRootNode(); } + @Override void reloadPalette() { PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData()); } @@ -1648,7 +1673,10 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette /** * Recomputes the layout with the help of layoutlib. */ + @Override + @SuppressWarnings("deprecation") void recomputeLayout() { + doXmlReload(false /* force */); try { // check that the resource exists. If the file is opened but the project is closed // or deleted for some reason (changed from outside of eclipse), then this will @@ -1691,7 +1719,7 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette // In this case data could be null, but this is not an error. // We can just silently return, as all the opened editors are automatically // refreshed once the SDK finishes loading. - if (AdtPlugin.getDefault().getSdkLoadStatus(null) != LoadStatus.LOADING) { + if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) { showErrorInEditor(String.format( "The project target (%s) was not properly loaded.", target.getName())); @@ -1763,20 +1791,47 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette if (themeIndex != -1) { String theme = mThemeCombo.getItem(themeIndex); - // change the string if it's a custom theme to make sure we can - // differentiate them - if (themeIndex >= mPlatformThemeCount) { - theme = "*" + theme; //$NON-NLS-1$ - } - // Compute the layout UiElementPullParser parser = new UiElementPullParser(getModel()); Rectangle rect = getBounds(); - ILayoutResult result = bridge.bridge.computeLayout(parser, - iProject /* projectKey */, - rect.width, rect.height, theme, - mConfiguredProjectRes, frameworkResources, mProjectCallback, - mLogger); + ILayoutResult result = null; + if (bridge.apiLevel >= 3) { + // call the new api with proper theme differentiator and + // density/dpi support. + boolean isProjectTheme = themeIndex >= mPlatformThemeCount; + + // FIXME pass the density/dpi from somewhere (resource config or skin). + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, 160, 160.f, 160.f, + theme, isProjectTheme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } else if (bridge.apiLevel == 2) { + // api with boolean for separation of project/framework theme + boolean isProjectTheme = themeIndex >= mPlatformThemeCount; + + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, theme, isProjectTheme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } else { + // oldest api with no density/dpi, and project theme boolean mixed + // into the theme name. + + // change the string if it's a custom theme to make sure we can + // differentiate them + if (themeIndex >= mPlatformThemeCount) { + theme = "*" + theme; //$NON-NLS-1$ + } + + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, theme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } // update the UiElementNode with the layout info. if (result.getSuccess() == ILayoutResult.SUCCESS) { @@ -1920,9 +1975,9 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette /** * Responds to a page change that made the Graphical editor page the activated page. */ + @Override void activated() { - mActive = true; - if (mNeedsRecompute) { + if (mNeedsRecompute || mNeedsXmlReload) { recomputeLayout(); } } @@ -1930,8 +1985,9 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette /** * Responds to a page change that made the Graphical editor page the deactivated page */ + @Override void deactivated() { - mActive = false; + // nothing to be done here for now. } /** @@ -2186,31 +2242,6 @@ public class GraphicalLayoutEditor extends GraphicalEditorWithPalette return mConfiguredFrameworkRes; } - /** - * Returns the selection synchronizer object. - * The synchronizer can be used to sync the selection of 2 or more EditPartViewers. - *

      - * This is changed from protected to public so that the outline can use it. - * - * @return the synchronizer - */ - @Override - public SelectionSynchronizer getSelectionSynchronizer() { - return super.getSelectionSynchronizer(); - } - - /** - * Returns the edit domain. - *

      - * This is changed from protected to public so that the outline can use it. - * - * @return the edit domain - */ - @Override - public DefaultEditDomain getEditDomain() { - return super.getEditDomain(); - } - /** * Creates a new layout file from the specificed {@link FolderConfiguration}. */ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java index 880ee2b91..f3a5113a9 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java @@ -55,7 +55,7 @@ public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPa /** Root node of the UI element hierarchy */ private UiDocumentNode mUiRootNode; - private GraphicalLayoutEditor mGraphicalEditor; + private AbstractGraphicalLayoutEditor mGraphicalEditor; private int mGraphicalEditorIndex; /** Implementation of the {@link IContentOutlinePage} for this editor */ private UiContentOutlinePage mOutline; @@ -269,8 +269,12 @@ public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPa protected void pageChange(int newPageIndex) { super.pageChange(newPageIndex); - if (mGraphicalEditor != null && newPageIndex == mGraphicalEditorIndex) { - mGraphicalEditor.activated(); + if (mGraphicalEditor != null) { + if (newPageIndex == mGraphicalEditorIndex) { + mGraphicalEditor.activated(); + } else { + mGraphicalEditor.deactivated(); + } } } @@ -278,8 +282,12 @@ public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPa public void partActivated(IWorkbenchPart part) { if (part == this) { - if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) { - mGraphicalEditor.activated(); + if (mGraphicalEditor != null) { + if (getActivePage() == mGraphicalEditorIndex) { + mGraphicalEditor.activated(); + } else { + mGraphicalEditor.deactivated(); + } } } } @@ -334,23 +342,23 @@ public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPa // ---- Local Methods ---- /** - * Returns true if the Graphics editor page is visible. - * This must be called from the UI thread. + * Returns true if the Graphics editor page is visible. This must be + * called from the UI thread. */ boolean isGraphicalEditorActive() { IWorkbenchPartSite workbenchSite = getSite(); IWorkbenchPage workbenchPage = workbenchSite.getPage(); - + // check if the editor is visible in the workbench page - if (workbenchPage.isPartVisible(this)) { + if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) { // and then if the page of the editor is visible (not to be confused with // the workbench page) return mGraphicalEditorIndex == getActivePage(); } - - return false; - } + return false; + } + @Override protected void initUiRootNode(boolean force) { // The root UI node is always created, even if there's no corresponding XML node. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java index 81fd2ed0c..94ad87a11 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java @@ -18,12 +18,14 @@ package com.android.ide.eclipse.editors.layout; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.editors.resources.manager.ProjectClassLoader; import com.android.ide.eclipse.editors.resources.manager.ProjectResources; import com.android.layoutlib.api.IProjectCallback; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; import java.lang.reflect.Constructor; import java.util.HashMap; @@ -83,17 +85,20 @@ public final class ProjectCallback implements IProjectCallback { } /** - * {@inheritDoc} - * * Returns the namespace for the project. The namespace contains a standard part + the * application package. + * + * @return The package namespace of the project or null in case of error. */ public String getNamespace() { if (mNamespace == null) { - AndroidManifestHelper manifest = new AndroidManifestHelper(mProject); - String javaPackage = manifest.getPackageName(); - - mNamespace = String.format(AndroidConstants.NS_CUSTOM_RESOURCES, javaPackage); + IFile manifestFile = AndroidManifestParser.getManifest(mProject); + try { + AndroidManifestParser data = AndroidManifestParser.parseForData(manifestFile); + String javaPackage = data.getPackage(); + mNamespace = String.format(AndroidConstants.NS_CUSTOM_RESOURCES, javaPackage); + } catch (CoreException e) { + } } return mNamespace; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java index 3e0f5d85c..536e9020b 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java @@ -70,7 +70,7 @@ import java.util.List; */ class UiContentOutlinePage extends ContentOutlinePage { - private GraphicalLayoutEditor mEditor; + private AbstractGraphicalLayoutEditor mEditor; private Action mAddAction; private Action mDeleteAction; @@ -79,7 +79,7 @@ class UiContentOutlinePage extends ContentOutlinePage { private UiOutlineActions mUiActions = new UiOutlineActions(); - public UiContentOutlinePage(GraphicalLayoutEditor editor, final EditPartViewer viewer) { + public UiContentOutlinePage(AbstractGraphicalLayoutEditor editor, final EditPartViewer viewer) { super(viewer); mEditor = editor; IconFactory factory = IconFactory.getInstance(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java index 0f388f417..d5ee2caa2 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java @@ -187,10 +187,10 @@ public final class CustomViewDescriptorService { /** * Computes (if needed) and returns the {@link ElementDescriptor} for the specified type. * - * @param type + * @param type * @param project * @param typeHierarchy - * @return A ViewElementDescriptor + * @return A ViewElementDescriptor or null if type or typeHierarchy is null. */ private ViewElementDescriptor getDescriptor(IType type, IProject project, ITypeHierarchy typeHierarchy) { @@ -198,12 +198,17 @@ public final class CustomViewDescriptorService { List builtInList = null; Sdk currentSdk = Sdk.getCurrent(); - IAndroidTarget target = currentSdk.getTarget(project); + IAndroidTarget target = currentSdk == null ? null : currentSdk.getTarget(project); if (target != null) { AndroidTargetData data = currentSdk.getTargetData(target); builtInList = data.getLayoutDescriptors().getViewDescriptors(); } + // give up if there's no type + if (type == null) { + return null; + } + String canonicalName = type.getFullyQualifiedName(); if (builtInList != null) { @@ -218,6 +223,11 @@ public final class CustomViewDescriptorService { } // it's not a built-in class? Lets look if the superclass is built-in + // give up if there's no type + if (typeHierarchy == null) { + return null; + } + IType parentType = typeHierarchy.getSuperclass(type); if (parentType != null) { ViewElementDescriptor parentDescriptor = getDescriptor(parentType, project, diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java index 7caa50f12..5726d7899 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.editors.layout.descriptors; import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo; import com.android.ide.eclipse.common.resources.ViewClassInfo; import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo; @@ -131,8 +132,23 @@ public final class LayoutDescriptors implements IDescriptorProvider { String xml_name = info.getShortClassName(); String tooltip = info.getJavaDoc(); - // Process all View attributes ArrayList attributes = new ArrayList(); + + // All views and groups have an implicit "style" attribute which is a reference. + AttributeInfo styleInfo = new DeclareStyleableInfo.AttributeInfo( + "style", //$NON-NLS-1$ xmlLocalName + new DeclareStyleableInfo.AttributeInfo.Format[] { + DeclareStyleableInfo.AttributeInfo.Format.REFERENCE + }); + styleInfo.setJavaDoc("A reference to a custom style"); //tooltip + DescriptorsUtils.appendAttribute(attributes, + "style", //$NON-NLS-1$ + null, //nsUri + styleInfo, + false, //required + null); // overrides + + // Process all View attributes DescriptorsUtils.appendAttributes(attributes, null, // elementName SdkConstants.NS_RESOURCES, @@ -155,7 +171,7 @@ public final class LayoutDescriptors implements IDescriptorProvider { null /* overrides */); } } - + // Process all LayoutParams attributes ArrayList layoutAttributes = new ArrayList(); LayoutParamsInfo layoutParams = info.getLayoutData(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java index 61b73a279..77c08b526 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java @@ -195,6 +195,8 @@ public final class AndroidManifestDescriptors implements IDescriptorProvider { overrides.put("*/permission", ListAttributeDescriptor.class); //$NON-NLS-1$ overrides.put("*/targetPackage", PackageAttributeDescriptor.class); //$NON-NLS-1$ + overrides.put("uses-library/name", ListAttributeDescriptor.class); //$NON-NLS-1$ + overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$ ListAttributeDescriptor.class); overrides.put("application/" + ANDROID_NAME_ATTR, ApplicationAttributeDescriptor.class); //$NON-NLS-1$ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java index abaf438d6..629b37c76 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java @@ -16,12 +16,12 @@ package com.android.ide.eclipse.editors.manifest.descriptors; -import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode; import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; /** * Describes an XML attribute representing a class name. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java index f8aac1de1..e32be86b0 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java @@ -17,7 +17,7 @@ package com.android.ide.eclipse.editors.manifest.model; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.editors.AndroidEditor; import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; @@ -251,8 +251,8 @@ public class UiClassAttributeNode extends UiTextAttributeNode { String javaPackage = getManifestPackage(); // build the fully qualified name of the class - String className = AndroidManifestHelper.combinePackageAndClassName(javaPackage, - textValue); + String className = AndroidManifestParser.combinePackageAndClassName( + javaPackage, textValue); // only test the vilibility for activities. boolean testVisibility = AndroidConstants.CLASS_ACTIVITY.equals( @@ -616,7 +616,7 @@ public class UiClassAttributeNode extends UiTextAttributeNode { } @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { // TODO: compute a list of existing classes for content assist completion return null; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java index 02fb44fb9..1fe9b75c2 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java @@ -311,7 +311,7 @@ public class UiPackageAttributeNode extends UiTextAttributeNode { } @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { // TODO: compute a list of existing packages for content assist completion return null; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java index 845db32dc..d1d88917f 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java @@ -160,6 +160,13 @@ public class ResourceExplorerView extends ViewPart implements ISelectionListener // set up the resource manager to send us resource change notification AdtPlugin.getDefault().getResourceMonitor().addResourceEventListener(this); } + + @Override + public void dispose() { + AdtPlugin.getDefault().getResourceMonitor().removeResourceEventListener(this); + + super.dispose(); + } @Override public void setFocus() { diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java index 455c825af..2d14c06a9 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java @@ -18,7 +18,7 @@ package com.android.ide.eclipse.editors.resources.manager; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.common.AndroidConstants; -import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; import com.android.ide.eclipse.common.resources.ResourceType; import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; @@ -28,6 +28,7 @@ import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -120,7 +121,14 @@ public final class CompiledResourcesMonitor implements IFileListener, IProjectLi if (projectResources != null) { // create the classname String className = getRClassName(project); - + if (className == null) { + // We need to abort. + AdtPlugin.log(IStatus.ERROR, + "loadAndParseRClass: failed to find manifest package for project %1$s", //$NON-NLS-1$ + project.getName()); + return; + } + // create a temporary class loader to load it. ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */, project); @@ -199,13 +207,30 @@ public final class CompiledResourcesMonitor implements IFileListener, IProjectLi } return false; } - + + /** + * Returns the class name of the R class, based on the project's manifest's package. + * + * @return A class name (e.g. "my.app.R") or null if there's no valid package in the manifest. + */ private String getRClassName(IProject project) { - // create the classname - AndroidManifestHelper manifest = new AndroidManifestHelper(project); - String javaPackage = manifest.getPackageName(); - - return javaPackage + ".R"; //$NON-NLS-1$ + try { + IFile manifestFile = AndroidManifestParser.getManifest(project); + AndroidManifestParser data = AndroidManifestParser.parseForData(manifestFile); + if (data != null) { + String javaPackage = data.getPackage(); + return javaPackage + ".R"; //$NON-NLS-1$ + } + } catch (CoreException e) { + // This will typically happen either because the manifest file is not present + // and/or the workspace needs to be refreshed. + AdtPlugin.logAndPrintError(e, + "Android Resources", + "Failed to find the package of the AndroidManifest of project %1$s. Reason: %2$s", + project.getName(), + e.getMessage()); + } + return null; } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java index dc0f50591..59a72fbdc 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java @@ -189,20 +189,15 @@ public class ResourceMonitor implements IResourceChangeListener { // the project is opening or closing. IProject project = (IProject)r; - // the OPEN flag represent a toggle in the open/close state of the - // project, but this is sent before the project actually toggles - // its state. - // This means that if the project is closing, isOpen() will return true. - boolean isClosing = project.isOpen(); - if (isClosing) { + if (project.isOpen()) { // notify the listeners. for (IProjectListener pl : mProjectListeners) { - pl.projectClosed(project); + pl.projectOpened(project); } } else { // notify the listeners. for (IProjectListener pl : mProjectListeners) { - pl.projectOpened(project); + pl.projectClosed(project); } } } @@ -286,6 +281,20 @@ public class ResourceMonitor implements IResourceChangeListener { mFolderListeners.add(bundle); } + /** + * Removes an existing folder listener. + * @param listener the listener to remove. + */ + public synchronized void removeFolderListener(IFolderListener listener) { + for (int i = 0 ; i < mFolderListeners.size() ; i++) { + FolderListenerBundle bundle = mFolderListeners.get(i); + if (bundle.listener == listener) { + mFolderListeners.remove(i); + return; + } + } + } + /** * Adds a project listener. * @param listener The listener to receive the events. @@ -305,10 +314,30 @@ public class ResourceMonitor implements IResourceChangeListener { } } + /** + * Removes an existing project listener. + * @param listener the listener to remove. + */ + public synchronized void removeProjectListener(IProjectListener listener) { + mProjectListeners.remove(listener); + } + + /** + * Adds a resource event listener. + * @param listener The listener to receive the events. + */ public synchronized void addResourceEventListener(IResourceEventListener listener) { mEventListeners.add(listener); } + /** + * Removes an existing Resource Event listener. + * @param listener the listener to remove. + */ + public synchronized void removeResourceEventListener(IResourceEventListener listener) { + mEventListeners.remove(listener); + } + /** * Processes the workspace resource change events. */ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java index 304dd14c4..dc4836db7 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java @@ -48,7 +48,7 @@ public class ListValueCellEditor extends ComboBoxCellEditor { UiListAttributeNode uiListAttribute = (UiListAttributeNode)value; // set the possible values in the combo - String[] items = uiListAttribute.getPossibleValues(); + String[] items = uiListAttribute.getPossibleValues(null); mItems = new String[items.length]; System.arraycopy(items, 0, mItems, 0, items.length); setItems(mItems); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java index e3255d9c7..fc384e8c0 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.editors.ui.tree; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; import com.android.ide.eclipse.editors.AndroidEditor; import com.android.ide.eclipse.editors.IconFactory; import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; @@ -26,6 +27,7 @@ import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import org.eclipse.core.resources.IProject; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; @@ -285,13 +287,21 @@ public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml } }; - final Runnable resourceRefreshListener = new Runnable() { - public void run() { + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + final ITargetChangeListener targetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == mEditor.getProject()) { + onTargetsLoaded(); + } + } + + public void onTargetsLoaded() { // If a details part has been created, we need to "refresh" it too. if (mDetailsPart != null) { // The details part does not directly expose access to its internal // page book. Instead it is possible to resize the page book to 0 and then - // back to its original value, which as the side effect of removing all + // back to its original value, which has the side effect of removing all // existing cached pages. int limit = mDetailsPart.getPageLimit(); mDetailsPart.setPageLimit(0); @@ -306,7 +316,7 @@ public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */); // Listen on resource framework changes to refresh the tree - AdtPlugin.getDefault().addResourceChangedListener(resourceRefreshListener); + AdtPlugin.getDefault().addTargetListener(targetListener); // Remove listeners when the tree widget gets disposed. tree.addDisposeListener(new DisposeListener() { @@ -318,7 +328,7 @@ public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml node.removeUpdateListener(mUiRefreshListener); mUiRootNode.removeUpdateListener(mUiEnableListener); - AdtPlugin.getDefault().removeResourceChangedListener(resourceRefreshListener); + AdtPlugin.getDefault().removeTargetListener(targetListener); if (mClipboard != null) { mClipboard.dispose(); mClipboard = null; @@ -580,7 +590,11 @@ public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml ui_node = ui_node.getUiParent()) { segments.add(0, ui_node); } - mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); + if (segments.size() > 0) { + mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); + } else { + mTreeViewer.setSelection(null); + } } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java index 590857403..4a9fbb123 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java @@ -79,7 +79,7 @@ public abstract class UiAbstractTextAttributeNode extends UiAttributeNode public void updateValue(Node xml_attribute_node) { mCurrentValue = DEFAULT_VALUE; if (xml_attribute_node != null) { - mCurrentValue = xml_attribute_node.getNodeValue().trim(); + mCurrentValue = xml_attribute_node.getNodeValue(); } if (isValid() && !getTextWidgetValue().equals(mCurrentValue)) { @@ -101,7 +101,7 @@ public abstract class UiAbstractTextAttributeNode extends UiAttributeNode public void commit() { UiElementNode parent = getUiParent(); if (parent != null && isValid() && isDirty()) { - String value = getTextWidgetValue().trim(); + String value = getTextWidgetValue(); if (parent.commitAttributeToXml(this, value)) { mCurrentValue = value; setDirty(false); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java index 5972f22b4..cada844b8 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java @@ -117,9 +117,13 @@ public abstract class UiAttributeNode { *

      * Implementations that do not have any known values should return null. * - * @return A list of possible completion values or null. + * @param prefix An optional prefix string, which is whatever the user has already started + * typing. Can be null or an empty string. The implementation can use this to filter choices + * and only return strings that match this prefix. A lazy or default implementation can + * simply ignore this and return everything. + * @return A list of possible completion values, and empty array or null. */ - public abstract String[] getPossibleValues(); + public abstract String[] getPossibleValues(String prefix); /** * Called when the XML is being loaded or has changed to diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java index ddcf0a0d4..c7995182d 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java @@ -124,9 +124,11 @@ public class UiFlagAttributeNode extends UiTextAttributeNode { /** * Get the flag names, either from the initial names set in the attribute * or by querying the framework resource parser. + * + * {@inheritDoc} */ @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { String attr_name = getDescriptor().getXmlLocalName(); String element_name = getUiParent().getDescriptor().getXmlName(); @@ -242,7 +244,7 @@ public class UiFlagAttributeNode extends UiTextAttributeNode { final TableColumn column = new TableColumn(mTable, SWT.NONE); // List all the expected flag names and check those which are currently used - String[] names = getPossibleValues(); + String[] names = getPossibleValues(null); if (names != null) { for (String name : names) { TableItem item = new TableItem(mTable, SWT.NONE); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java index c5c10aa11..faac01345 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java @@ -108,7 +108,7 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode { } protected void fillCombo() { - String[] values = getPossibleValues(); + String[] values = getPossibleValues(null); if (values == null) { AdtPlugin.log(IStatus.ERROR, @@ -124,9 +124,11 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode { /** * Get the list values, either from the initial values set in the attribute * or by querying the framework resource parser. + * + * {@inheritDoc} */ @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { AttributeDescriptor descriptor = getDescriptor(); UiElementNode uiParent = getUiParent(); @@ -134,13 +136,13 @@ public class UiListAttributeNode extends UiAbstractTextAttributeNode { String element_name = uiParent.getDescriptor().getXmlName(); // FrameworkResourceManager expects a specific prefix for the attribute. - String prefix = ""; + String nsPrefix = ""; if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) { - prefix = "android:"; //$NON-NLS-1$ + nsPrefix = "android:"; //$NON-NLS-1$ } else if (XmlnsAttributeDescriptor.XMLNS_URI.equals(descriptor.getNamespaceUri())) { - prefix = "xmlns:"; //$NON-NLS-1$ + nsPrefix = "xmlns:"; //$NON-NLS-1$ } - attr_name = prefix + attr_name; + attr_name = nsPrefix + attr_name; String[] values = null; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java index 1c1e1bde7..32cac9f2b 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.editors.uimodel; import com.android.ide.eclipse.adt.sdk.AndroidTargetData; import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; import com.android.ide.eclipse.common.resources.ResourceType; import com.android.ide.eclipse.editors.AndroidEditor; import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; @@ -44,6 +45,10 @@ import org.eclipse.ui.forms.IManagedForm; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.TableWrapData; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Represents an XML attribute for a resource that can be modified using a simple text field or * a dialog to choose an existing resource. @@ -153,9 +158,113 @@ public class UiResourceAttributeNode extends UiTextAttributeNode { return null; } + /** + * Gets all the values one could use to auto-complete a "resource" value in an XML + * content assist. + *

      + * Typically the user is editing the value of an attribute in a resource XML, e.g. + *

       "<Button android:test="@string/my_[caret]_string..." 
      + *

      + * + * "prefix" is the value that the user has typed so far (or more exactly whatever is on the + * left side of the insertion point). In the example above it would be "@style/my_". + *

      + * + * To avoid a huge long list of values, the completion works on two levels: + *

        + *
      • If a resource type as been typed so far (e.g. "@style/"), then limit the values to + * the possible completions that match this type. + *
      • If no resource type as been typed so far, then return the various types that could be + * completed. So if the project has only strings and layouts resources, for example, + * the returned list will only include "@string/" and "@layout/". + *
      + * + * Finally if anywhere in the string we find the special token "android:", we use the + * current framework system resources rather than the project resources. + * This works for both "@android:style/foo" and "@style/android:foo" conventions even though + * the reconstructed name will always be of the former form. + * + * Note that "android:" here is a keyword specific to Android resources and should not be + * mixed with an XML namespace for an XML attribute name. + */ @Override - public String[] getPossibleValues() { - // TODO: compute a list of existing resources for content assist completion - return null; + public String[] getPossibleValues(String prefix) { + IResourceRepository repository = null; + boolean isSystem = false; + + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + + if (prefix == null || prefix.indexOf("android:") < 0) { + IProject project = editor.getProject(); + if (project != null) { + // get the resource repository for this project and the system resources. + repository = ResourceManager.getInstance().getProjectResources(project); + } + } else { + // If there's a prefix with "android:" in it, use the system resources + // + // TODO find a way to only list *public* framework resources here. + AndroidTargetData data = editor.getTargetData(); + repository = data.getSystemResources(); + isSystem = true; + } + + // Get list of potential resource types, either specific to this project + // or the generic list. + ResourceType[] resTypes = (repository != null) ? + repository.getAvailableResourceTypes() : + ResourceType.values(); + + // Get the type name from the prefix, if any. It's any word before the / if there's one + String typeName = null; + if (prefix != null) { + Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix); + if (m.matches()) { + typeName = m.group(1); + } + } + + // Now collect results + ArrayList results = new ArrayList(); + + if (typeName == null) { + // This prefix does not have a / in it, so the resource string is either empty + // or does not have the resource type in it. Simply offer the list of potential + // resource types. + + for (ResourceType resType : resTypes) { + results.add("@" + resType.getName() + "/"); + if (resType == ResourceType.ID) { + // Also offer the + version to create an id from scratch + results.add("@+" + resType.getName() + "/"); + } + } + } else if (repository != null) { + // We have a style name and a repository. Find all resources that match this + // type and recreate suggestions out of them. + + ResourceType resType = ResourceType.getEnum(typeName); + if (resType != null) { + StringBuilder sb = new StringBuilder(); + sb.append('@'); + if (prefix.indexOf('+') >= 0) { + sb.append('+'); + } + + if (isSystem) { + sb.append("android:"); + } + + sb.append(typeName).append('/'); + String base = sb.toString(); + + for (ResourceItem item : repository.getResources(resType)) { + results.add(base + item.getName()); + } + } + } + + return results.toArray(new String[results.size()]); } } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java index 192f752c7..a6111d4e7 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java @@ -96,9 +96,13 @@ public class UiSeparatorAttributeNode extends UiAttributeNode { sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); } - /** No completion values for this UI attribute. */ + /** + * No completion values for this UI attribute. + * + * {@inheritDoc} + */ @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { return null; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java index 4c53f4c11..652debeb9 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java @@ -70,9 +70,13 @@ public class UiTextAttributeNode extends UiAbstractTextAttributeNode { setTextWidget(text); } - /** No completion values for this UI attribute. */ + /** + * No completion values for this UI attribute. + * + * {@inheritDoc} + */ @Override - public String[] getPossibleValues() { + public String[] getPossibleValues(String prefix) { return null; } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java index 4d171762c..e84c05123 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java @@ -23,6 +23,7 @@ import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.ProjectChooserHelper; import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors; import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; @@ -81,6 +82,7 @@ class NewXmlFileCreationPage extends WizardPage { private final String mXmlns; private final String mDefaultAttrs; private final String mDefaultRoot; + private final int mTargetApiLevel; public TypeInfo(String uiName, String tooltip, @@ -88,7 +90,8 @@ class NewXmlFileCreationPage extends WizardPage { Object rootSeed, String defaultRoot, String xmlns, - String defaultAttrs) { + String defaultAttrs, + int targetApiLevel) { mUiName = uiName; mResFolderType = resFolderType; mTooltip = tooltip; @@ -96,6 +99,7 @@ class NewXmlFileCreationPage extends WizardPage { mDefaultRoot = defaultRoot; mXmlns = xmlns; mDefaultAttrs = defaultAttrs; + mTargetApiLevel = targetApiLevel; } /** Returns the UI name for the resource type. Unique. Never null. */ @@ -176,6 +180,13 @@ class NewXmlFileCreationPage extends WizardPage { String getDefaultAttrs() { return mDefaultAttrs; } + + /** + * The minimum API level required by the current SDK target to support this feature. + */ + public int getTargetApiLevel() { + return mTargetApiLevel; + } } /** @@ -190,7 +201,8 @@ class NewXmlFileCreationPage extends WizardPage { "LinearLayout", // default root SdkConstants.NS_RESOURCES, // xmlns "android:layout_width=\"wrap_content\"\n" + // default attributes - "android:layout_height=\"wrap_content\"" + "android:layout_height=\"wrap_content\"", + 1 // target API level ), new TypeInfo("Values", // UI name "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip @@ -198,7 +210,8 @@ class NewXmlFileCreationPage extends WizardPage { ResourcesDescriptors.ROOT_ELEMENT, // root seed null, // default root null, // xmlns - null // default attributes + null, // default attributes + 1 // target API level ), new TypeInfo("Menu", // UI name "An XML file that describes an menu.", // tooltip @@ -206,7 +219,17 @@ class NewXmlFileCreationPage extends WizardPage { MenuDescriptors.MENU_ROOT_ELEMENT, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns - null // default attributes + null, // default attributes + 1 // target API level + ), + new TypeInfo("AppWidget Provider", // UI name + "An XML file that describes a widget provider.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 3 // target API level ), new TypeInfo("Preference", // UI name "An XML file that describes preferences.", // tooltip @@ -214,15 +237,17 @@ class NewXmlFileCreationPage extends WizardPage { AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed AndroidConstants.CLASS_PREFERENCE_SCREEN, // default root SdkConstants.NS_RESOURCES, // xmlns - null // default attributes + null, // default attributes + 1 // target API level ), new TypeInfo("Searchable", // UI name - "An XML file that describes a searchable [TODO].", // tooltip + "An XML file that describes a searchable.", // tooltip ResourceFolderType.XML, // folder type AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns - null // default attributes + null, // default attributes + 1 // target API level ), new TypeInfo("Animation", // UI name "An XML file that describes an animation.", // tooltip @@ -237,10 +262,14 @@ class NewXmlFileCreationPage extends WizardPage { }, "set", //$NON-NLS-1$ // default root null, // xmlns - null // default attributes + null, // default attributes + 1 // target API level ), }; + /** Number of columns in the grid layout */ + final static int NUM_COL = 4; + /** Absolute destination folder root, e.g. "/res/" */ private static String sResFolderAbs = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP; /** Relative destination folder root, e.g. "res/" */ @@ -290,7 +319,7 @@ class NewXmlFileCreationPage extends WizardPage { initializeDialogUnits(parent); - composite.setLayout(new GridLayout(3, false /*makeColumnsEqualWidth*/)); + composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/)); composite.setLayoutData(new GridData(GridData.FILL_BOTH)); createProjectGroup(composite); @@ -303,8 +332,9 @@ class NewXmlFileCreationPage extends WizardPage { setControl(composite); // Update state the first time - initializeRootValues(); initializeFromSelection(mInitialSelection); + initializeRootValues(); + enableTypesBasedOnApi(); validatePage(); } @@ -418,17 +448,35 @@ class NewXmlFileCreationPage extends WizardPage { new Label(parent, SWT.NONE); } + /** + * Pads the parent with empty cells to match the number of columns of the parent grid. + * + * @param parent A grid layout with NUM_COL columns + * @param col The current number of columns used. + * @return 0, the new number of columns used, for convenience. + */ + private int padWithEmptyCells(Composite parent, int col) { + for (; col < NUM_COL; ++col) { + emptyCell(parent); + } + col = 0; + return col; + } + /** * Creates the project & filename fields. *

      - * The parent must be a GridLayout with 3 colums. + * The parent must be a GridLayout with NUM_COL colums. */ private void createProjectGroup(Composite parent) { + int col = 0; + // project name String tooltip = "The Android Project where the new resource file will be created."; Label label = new Label(parent, SWT.NONE); label.setText("Project"); label.setToolTipText(tooltip); + ++col; mProjectTextField = new Text(parent, SWT.BORDER); mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); @@ -438,6 +486,7 @@ class NewXmlFileCreationPage extends WizardPage { onProjectFieldUpdated(); } }); + ++col; mProjectBrowseButton = new Button(parent, SWT.NONE); mProjectBrowseButton.setText("Browse..."); @@ -449,12 +498,16 @@ class NewXmlFileCreationPage extends WizardPage { } }); mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); + ++col; + col = padWithEmptyCells(parent, col); + // file name tooltip = "The name of the resource file to create."; label = new Label(parent, SWT.NONE); label.setText("File"); label.setToolTipText(tooltip); + ++col; mFileNameTextField = new Text(parent, SWT.BORDER); mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); @@ -464,31 +517,32 @@ class NewXmlFileCreationPage extends WizardPage { validatePage(); } }); + ++col; - emptyCell(parent); + padWithEmptyCells(parent, col); } /** * Creates the type field, {@link ConfigurationSelector} and the folder field. *

      - * The parent must be a GridLayout with 3 colums. + * The parent must be a GridLayout with NUM_COL colums. */ private void createTypeGroup(Composite parent) { // separator Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); - label.setLayoutData(newGridData(3, GridData.GRAB_HORIZONTAL)); + label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); // label before type radios label = new Label(parent, SWT.NONE); label.setText("What type of resource would you like to create?"); - label.setLayoutData(newGridData(3)); + label.setLayoutData(newGridData(NUM_COL)); // display the types on three columns of radio buttons. emptyCell(parent); Composite grid = new Composite(parent, SWT.NONE); - emptyCell(parent); + padWithEmptyCells(parent, 2); - grid.setLayout(new GridLayout(3, true /*makeColumnsEqualWidth*/)); + grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/)); SelectionListener radioListener = new SelectionAdapter() { @Override @@ -501,23 +555,27 @@ class NewXmlFileCreationPage extends WizardPage { }; int n = sTypes.length; - int num_lines = n/3; - for (int line = 0; line < num_lines; line++) { - for (int i = 0; i < 3; i++) { - TypeInfo type = sTypes[line * 3 + i]; - Button radio = new Button(grid, SWT.RADIO); - type.setWidget(radio); - radio.setSelection(false); - radio.setText(type.getUiName()); - radio.setToolTipText(type.getTooltip()); - radio.addSelectionListener(radioListener); + int num_lines = (n + NUM_COL/2) / NUM_COL; + for (int line = 0, k = 0; line < num_lines; line++) { + for (int i = 0; i < NUM_COL; i++, k++) { + if (k < n) { + TypeInfo type = sTypes[k]; + Button radio = new Button(grid, SWT.RADIO); + type.setWidget(radio); + radio.setSelection(false); + radio.setText(type.getUiName()); + radio.setToolTipText(type.getTooltip()); + radio.addSelectionListener(radioListener); + } else { + emptyCell(grid); + } } } // label before configuration selector label = new Label(parent, SWT.NONE); label.setText("What type of resource configuration would you like?"); - label.setLayoutData(newGridData(3)); + label.setLayoutData(newGridData(NUM_COL)); // configuration selector emptyCell(parent); @@ -527,6 +585,7 @@ class NewXmlFileCreationPage extends WizardPage { gd.heightHint = ConfigurationSelector.HEIGHT_HINT; mConfigSelector.setLayoutData(gd); mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated()); + emptyCell(parent); // folder name String tooltip = "The folder where the file will be generated, relative to the project."; @@ -542,25 +601,23 @@ class NewXmlFileCreationPage extends WizardPage { onWsFolderPathUpdated(); } }); - - emptyCell(parent); } /** * Creates the root element combo. *

      - * The parent must be a GridLayout with 3 colums. + * The parent must be a GridLayout with NUM_COL colums. */ private void createRootGroup(Composite parent) { // separator Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); - label.setLayoutData(newGridData(3, GridData.GRAB_HORIZONTAL)); + label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); // label before the root combo String tooltip = "The root element to create in the XML file."; label = new Label(parent, SWT.NONE); label.setText("Select the root element for the XML file:"); - label.setLayoutData(newGridData(3)); + label.setLayoutData(newGridData(NUM_COL)); label.setToolTipText(tooltip); // root combo @@ -572,7 +629,7 @@ class NewXmlFileCreationPage extends WizardPage { mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mRootElementCombo.setToolTipText(tooltip); - emptyCell(parent); + padWithEmptyCells(parent, 2); } /** @@ -690,11 +747,13 @@ class NewXmlFileCreationPage extends WizardPage { // get the AndroidTargetData from the project IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); AndroidTargetData data = Sdk.getCurrent().getTargetData(target); - ElementDescriptor descriptor = data.getDescriptorProvider( - (Integer)rootSeed).getDescriptor(); - HashSet visited = new HashSet(); - initRootElementDescriptor(roots, descriptor, visited); + IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); + ElementDescriptor descriptor = provider.getDescriptor(); + if (descriptor != null) { + HashSet visited = new HashSet(); + initRootElementDescriptor(roots, descriptor, visited); + } // Sort alphabetically. Collections.sort(roots); @@ -743,15 +802,7 @@ class NewXmlFileCreationPage extends WizardPage { } if (found != mProject) { - mProject = found; - - // update the Type with the new descriptors. - initializeRootValues(); - - // update the combo - updateRootCombo(getSelectedType()); - - validatePage(); + changeProject(found); } } @@ -761,17 +812,27 @@ class NewXmlFileCreationPage extends WizardPage { private void onProjectBrowse() { IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText()); if (p != null) { - mProject = p.getProject(); + changeProject(p.getProject()); mProjectTextField.setText(mProject.getName()); - - // update the Type with the new descriptors. - initializeRootValues(); - - // update the combo - updateRootCombo(getSelectedType()); - - validatePage(); } + } + + /** + * Changes mProject to the given new project and update the UI accordingly. + */ + private void changeProject(IProject newProject) { + mProject = newProject; + + // enable types based on new API level + enableTypesBasedOnApi(); + + // update the Type with the new descriptors. + initializeRootValues(); + + // update the combo + updateRootCombo(getSelectedType()); + + validatePage(); } /** @@ -985,6 +1046,26 @@ class NewXmlFileCreationPage extends WizardPage { } } + /** + * Helper method to enable the type radio buttons depending on the current API level. + *

      + * A type radio button is enabled either if: + * - if mProject is null, API level 1 is considered valid + * - if mProject is !null, the project->target->API must be >= to the type's API level. + */ + private void enableTypesBasedOnApi() { + + IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null; + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getApiVersionNumber(); + } + + for (TypeInfo type : sTypes) { + type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel); + } + } + /** * Validates the fields, displays errors and warnings. * Enables the finish button if there are no errors. @@ -1017,6 +1098,22 @@ class NewXmlFileCreationPage extends WizardPage { } } + // -- validate type API level + if (error == null) { + IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getApiVersionNumber(); + } + + TypeInfo type = getSelectedType(); + + if (type.getTargetApiLevel() > currentApiLevel) { + error = "The API level of the selected type (e.g. AppWidget, etc.) is not " + + "compatible with the API level of the project."; + } + } + // -- validate folder configuration if (error == null) { ConfigurationState state = mConfigSelector.getState(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java index fa1370f31..144b7ac26 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java @@ -53,6 +53,9 @@ public final class XmlDescriptors implements IDescriptorProvider { /** The root document descriptor for preferences. */ private DocumentDescriptor mPrefDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + /** The root document descriptor for widget provider. */ + private DocumentDescriptor mAppWidgetDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + /** @return the root descriptor for both searchable and preferences. */ public DocumentDescriptor getDescriptor() { return mDescriptor; @@ -72,6 +75,11 @@ public final class XmlDescriptors implements IDescriptorProvider { return mPrefDescriptor; } + /** @return the root descriptor for widget providers. */ + public DocumentDescriptor getAppWidgetDescriptor() { + return mAppWidgetDescriptor; + } + public IDescriptorProvider getSearchableProvider() { return new IDescriptorProvider() { public ElementDescriptor getDescriptor() { @@ -96,6 +104,18 @@ public final class XmlDescriptors implements IDescriptorProvider { }; } + public IDescriptorProvider getAppWidgetProvider() { + return new IDescriptorProvider() { + public ElementDescriptor getDescriptor() { + return mAppWidgetDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mAppWidgetDescriptor.getChildren(); + } + }; + } + /** * Updates the document descriptor. *

      @@ -103,11 +123,13 @@ public final class XmlDescriptors implements IDescriptorProvider { * all at once. * * @param searchableStyleMap The map style=>attributes for from the attrs.xml file + * @param appWidgetStyleMap The map style=>attributes for from the attrs.xml file * @param prefs The list of non-group preference descriptions * @param prefGroups The list of preference group descriptions */ public synchronized void updateDescriptors( Map searchableStyleMap, + Map appWidgetStyleMap, ViewClassInfo[] prefs, ViewClassInfo[] prefGroups) { XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor( @@ -115,12 +137,17 @@ public final class XmlDescriptors implements IDescriptorProvider { SdkConstants.NS_RESOURCES); ElementDescriptor searchable = createSearchable(searchableStyleMap, xmlns); + ElementDescriptor appWidget = createAppWidgetProviderInfo(appWidgetStyleMap, xmlns); ElementDescriptor preferences = createPreference(prefs, prefGroups, xmlns); ArrayList list = new ArrayList(); if (searchable != null) { list.add(searchable); mSearchDescriptor.setChildren(new ElementDescriptor[]{ searchable }); } + if (appWidget != null) { + list.add(appWidget); + mAppWidgetDescriptor.setChildren(new ElementDescriptor[]{ appWidget }); + } if (preferences != null) { list.add(preferences); mPrefDescriptor.setChildren(new ElementDescriptor[]{ preferences }); @@ -161,6 +188,28 @@ public final class XmlDescriptors implements IDescriptorProvider { false /* mandatory */ ); return searchable; } + + /** + * Returns the new ElementDescriptor for + */ + private ElementDescriptor createAppWidgetProviderInfo( + Map appWidgetStyleMap, + XmlnsAttributeDescriptor xmlns) { + + if (appWidgetStyleMap == null) { + return null; + } + + ElementDescriptor appWidget = createElement(appWidgetStyleMap, + "AppWidgetProviderInfo", //$NON-NLS-1$ styleName + "appwidget-provider", //$NON-NLS-1$ xmlName + "AppWidget Provider", // uiName + null, // sdk url + xmlns, // extraAttribute + null, // childrenElements + false /* mandatory */ ); + return appWidget; + } /** * Returns a new ElementDescriptor constructed from the information given here diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/.classpath b/tools/eclipse/plugins/com.android.ide.eclipse.tests/.classpath index 40886832d..6f2a534bb 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/.classpath +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/.classpath @@ -5,6 +5,6 @@ - + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF b/tools/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF index 266008c3b..c7f5ba88e 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF @@ -13,7 +13,7 @@ Require-Bundle: org.eclipse.ui, org.eclipse.jdt.launching, org.eclipse.ui.views, com.android.ide.eclipse.ddms -Eclipse-LazyStart: true +Bundle-ActivationPolicy: lazy Bundle-Vendor: The Android Open Source Project Bundle-ClassPath: kxml2-2.3.0.jar, . diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegateTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegateTest.java new file mode 100644 index 000000000..df3745e68 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/launch/JUnitLaunchConfigDelegateTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.launch; + +import java.io.IOException; +import java.util.Arrays; +import junit.framework.TestCase; + +public class JUnitLaunchConfigDelegateTest extends TestCase { + + public void testAbleToFetchJunitJar() throws IOException { + assertTrue(JUnitLaunchConfigDelegate.getJunitJarLocation().endsWith("junit.jar")); + } + + public void testFixBootpathExtWithAndroidJar() { + String[][] testArray = { + null, + { "android.jar"}, + null, + { "some_other_jar.jar" }, + }; + + String[][] expectedArray = { + null, + null, + null, + { "some_other_jar.jar" }, + }; + + assertEqualsArrays(expectedArray, JUnitLaunchConfigDelegate.fixBootpathExt(testArray)); + } + + public void testFixBootpathExtWithNoAndroidJar() { + String[][] testArray = { + null, + { "somejar.jar"}, + null, + }; + + String[][] expectedArray = { + null, + { "somejar.jar"}, + null, + }; + + assertEqualsArrays(expectedArray, JUnitLaunchConfigDelegate.fixBootpathExt(testArray)); + } + + public void testFixClasspathWithJunitJar() throws IOException { + String[] testArray = { + JUnitLaunchConfigDelegate.getJunitJarLocation(), + }; + + String[] expectedArray = { + JUnitLaunchConfigDelegate.getJunitJarLocation(), + }; + + assertEqualsArrays(expectedArray, + JUnitLaunchConfigDelegate.fixClasspath(testArray, "test")); + } + + public void testFixClasspathWithoutJunitJar() throws IOException { + String[] testArray = { + "random.jar", + }; + + String[] expectedArray = { + "random.jar", + JUnitLaunchConfigDelegate.getJunitJarLocation(), + }; + + assertEqualsArrays(expectedArray, + JUnitLaunchConfigDelegate.fixClasspath(testArray, "test")); + } + + + public void testFixClasspathWithNoJars() throws IOException { + String[] testArray = { + }; + + String[] expectedArray = { + JUnitLaunchConfigDelegate.getJunitJarLocation(), + }; + + assertEqualsArrays(expectedArray, + JUnitLaunchConfigDelegate.fixClasspath(testArray, "test")); + } + + private void assertEqualsArrays(String[][] a1, String[][] a2) { + assertTrue(Arrays.deepEquals(a1, a2)); + } + + private void assertEqualsArrays(String[] a1, String[] a2) { + assertTrue(Arrays.deepEquals(a1, a2)); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java index 29538bb8b..6aaa20918 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java @@ -15,15 +15,14 @@ */ package com.android.ide.eclipse.tests; -import junit.framework.TestCase; -import junit.framework.TestSuite; - import org.eclipse.core.runtime.Plugin; import java.lang.reflect.Modifier; import java.net.URL; import java.util.Enumeration; -import java.util.logging.Logger; + +import junit.framework.TestCase; +import junit.framework.TestSuite; /** * Class for collecting all test cases in an eclipse plugin @@ -31,8 +30,6 @@ import java.util.logging.Logger; */ public class EclipseTestCollector { - private static final Logger sLogger = Logger.getLogger(EclipseTestCollector.class.getName()); - /** * Constructor */ @@ -49,13 +46,13 @@ public class EclipseTestCollector { */ public void addTestCases(TestSuite suite, Plugin plugin, String expectedPackage) { if (plugin != null) { - Enumeration entries = plugin.getBundle().findEntries("/", "*.class", true); + Enumeration entries = plugin.getBundle().findEntries("/", "*.class", true); while (entries.hasMoreElements()) { URL entry = (URL)entries.nextElement(); String filePath = entry.getPath().replace(".class", ""); try { - Class testClass = getClass(filePath, expectedPackage); + Class testClass = getClass(filePath, expectedPackage); if (isTestClass(testClass)) { suite.addTestSuite(testClass); } @@ -69,11 +66,11 @@ public class EclipseTestCollector { } /** - * Returns true if given class shouk\ld be added to suite + * Returns true if given class should be added to suite * @param testClass * @return */ - protected boolean isTestClass(Class testClass) { + protected boolean isTestClass(Class testClass) { return TestCase.class.isAssignableFrom(testClass) && Modifier.isPublic(testClass.getModifiers()) && hasPublicConstructor(testClass); @@ -84,7 +81,7 @@ public class EclipseTestCollector { * @param testClass * @return */ - protected boolean hasPublicConstructor(Class testClass) { + protected boolean hasPublicConstructor(Class testClass) { try { TestSuite.getTestConstructor(testClass); } catch(NoSuchMethodException e) { @@ -100,7 +97,7 @@ public class EclipseTestCollector { * @return * @throws ClassNotFoundException */ - protected Class getClass(String filePath, String expectedPackage) throws ClassNotFoundException { + protected Class getClass(String filePath, String expectedPackage) throws ClassNotFoundException { String dotPath = filePath.replace('/', '.'); // remove the output folders, by finding where package name starts int index = dotPath.indexOf(expectedPackage); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java index ac928db03..67e7cb23b 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java @@ -49,7 +49,7 @@ public class UnitTests { * Override parent class to exclude functional tests */ @Override - protected boolean isTestClass(Class testClass) { + protected boolean isTestClass(Class testClass) { return super.isTestClass(testClass) && !testClass.getPackage().getName().startsWith(FuncTests.FUNC_TEST_PACKAGE); } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java index 5a89d0154..8c52d81cf 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java @@ -22,7 +22,6 @@ import com.android.ide.eclipse.mock.JavaProjectMock; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.launching.JavaRuntime; import junit.framework.TestCase; diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java index 872938bc6..8af7e025a 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java @@ -28,7 +28,7 @@ import java.util.HashMap; import junit.framework.TestCase; /** - * Unit Test for {@link FrameworkClassLoader}. + * Unit Test for {@link AndroidJarLoader}. * * Uses the classes jar.example.Class1/Class2 stored in tests/data/jar_example.jar. */ @@ -36,7 +36,7 @@ public class AndroidJarLoaderTest extends TestCase { private AndroidJarLoader mFrameworkClassLoader; - /** Creates an instance of {@link FrameworkClassLoader} on our test data JAR */ + /** Creates an instance of {@link AndroidJarLoader} on our test data JAR */ @Override public void setUp() throws Exception { String jarfilePath = AdtTestData.getInstance().getTestFilePath("jar_example.jar"); //$NON-NLS-1$ diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java index b66fcd646..cedf4d4ce 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java @@ -35,7 +35,7 @@ import junit.framework.TestCase; * Test the inner private methods of PlatformDataParser. * * Convention: method names that start with an underscore are actually local wrappers - * that call private methods from {@link FrameworkResourceParser} using reflection. + * that call private methods from {@link AndroidTargetParser} using reflection. * This is inspired by the Python coding rule which mandates underscores prefixes for * "private" methods. */ @@ -131,6 +131,7 @@ public class LayoutParamsParserTest extends TestCase { //---- access to private methods /** Calls the private constructor of the parser */ + @SuppressWarnings("unused") private AndroidTargetParser _Constructor(String osJarPath) throws Exception { Constructor constructor = AndroidTargetParser.class.getDeclaredConstructor(String.class); @@ -139,6 +140,7 @@ public class LayoutParamsParserTest extends TestCase { } /** calls the private getLayoutClasses() of the parser */ + @SuppressWarnings("unused") private void _getLayoutClasses() throws Exception { Method method = AndroidTargetParser.class.getDeclaredMethod("getLayoutClasses"); //$NON-NLS-1$ method.setAccessible(true); @@ -146,6 +148,7 @@ public class LayoutParamsParserTest extends TestCase { } /** calls the private addGroup() of the parser */ + @SuppressWarnings("unused") private ViewClassInfo _addGroup(Class groupClass) throws Exception { Method method = LayoutParamsParser.class.getDeclaredMethod("addGroup", //$NON-NLS-1$ IClassDescriptor.class); @@ -154,6 +157,7 @@ public class LayoutParamsParserTest extends TestCase { } /** calls the private addLayoutParams() of the parser */ + @SuppressWarnings("unused") private LayoutParamsInfo _addLayoutParams(Class groupClass) throws Exception { Method method = LayoutParamsParser.class.getDeclaredMethod("addLayoutParams", //$NON-NLS-1$ IClassDescriptor.class); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java deleted file mode 100644 index 66042648b..000000000 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php - * - * 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.ide.eclipse.common.project; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -import junit.framework.TestCase; - -public class AndroidManifestHelperTest extends TestCase { - private File mFile; - private AndroidManifestHelper mManifest; - - @Override - protected void setUp() throws Exception { - super.setUp(); - mFile = File.createTempFile("androidManifest", "xml"); //$NON-NLS-1$ //$NON-NLS-2$ - assertNotNull(mFile); - - FileWriter fw = new FileWriter(mFile); - fw.write("\n"); //$NON-NLS-1$ - fw.write("\n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \"\n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write(" \n"); //$NON-NLS-1$ - fw.write("\n"); //$NON-NLS-1$ - fw.flush(); - fw.close(); - - mManifest = new AndroidManifestHelper(mFile.getAbsolutePath()); - } - - @Override - protected void tearDown() throws Exception { - assertTrue(mFile.delete()); - super.tearDown(); - } - - public void testExists() { - assertTrue(mManifest.exists()); - } - - public void testNotExists() throws IOException { - File f = File.createTempFile("androidManifest2", "xml"); //$NON-NLS-1$ //$NON-NLS-2$ - assertTrue(f.delete()); - AndroidManifestHelper manifest = new AndroidManifestHelper(f.getAbsolutePath()); - assertFalse(manifest.exists()); - } - - public void testGetPackageName() { - assertEquals("com.android.testapp", mManifest.getPackageName()); - } - - public void testGetActivityName() { - assertEquals("", mManifest.getActivityName(0)); //$NON-NLS-1$ - assertEquals(".MainActivity", mManifest.getActivityName(1)); //$NON-NLS-1$ - assertEquals(".OptionsActivity", mManifest.getActivityName(2)); //$NON-NLS-1$ - assertEquals(".InfoActivity", mManifest.getActivityName(3)); //$NON-NLS-1$ - assertEquals("", mManifest.getActivityName(4)); //$NON-NLS-1$ - } - -} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java new file mode 100644 index 000000000..516e448e6 --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestParserTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.common.project; + +import junit.framework.TestCase; + +import com.android.ide.eclipse.mock.FileMock; + +/** + * Tests for {@link AndroidManifestParser} + */ +public class AndroidManifestParserTest extends TestCase { + private AndroidManifestParser mManifest; + + private static final String PACKAGE_NAME = "com.android.testapp"; //$NON-NLS-1$ + private static final String ACTIVITY_NAME = "com.android.testapp.MainActivity"; //$NON-NLS-1$ + private static final String LIBRARY_NAME = "android.test.runner"; //$NON-NLS-1$ + private static final String INSTRUMENTATION_NAME = "android.test.InstrumentationTestRunner"; //$NON-NLS-1$ + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create the test data + StringBuilder sb = new StringBuilder(); + sb.append("\n"); //$NON-NLS-1$ + sb.append("\n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \"\n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" \n"); //$NON-NLS-1$ + sb.append(" "); //$NON-NLS-1$ + sb.append(" \n"); + sb.append("\n"); //$NON-NLS-1$ + + FileMock mockFile = new FileMock("AndroidManifest.xml", sb.toString().getBytes()); + + mManifest = AndroidManifestParser.parseForData(mockFile); + assertNotNull(mManifest); + } + + public void testGetPackage() { + assertEquals("com.android.testapp", mManifest.getPackage()); + } + + public void testGetActivities() { + assertEquals(1, mManifest.getActivities().length); + assertEquals(ACTIVITY_NAME, mManifest.getActivities()[0]); + } + + public void testGetLauncherActivity() { + assertEquals(ACTIVITY_NAME, mManifest.getLauncherActivity()); + } + + public void testGetUsesLibraries() { + assertEquals(1, mManifest.getUsesLibraries().length); + assertEquals(LIBRARY_NAME, mManifest.getUsesLibraries()[0]); + } + + public void testGetInstrumentations() { + assertEquals(1, mManifest.getInstrumentations().length); + assertEquals(INSTRUMENTATION_NAME, mManifest.getInstrumentations()[0]); + } +} diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java index 25a86c3b1..46e60ba9a 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java @@ -31,7 +31,6 @@ import com.android.ide.eclipse.mock.FolderMock; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayList; import junit.framework.TestCase; @@ -47,7 +46,6 @@ public class ConfigMatchTest extends TestCase { private FolderConfiguration config2; private FolderConfiguration config1; - @SuppressWarnings("unchecked") @Override protected void setUp() throws Exception { super.setUp(); diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java index 6a555a440..0920f0004 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java @@ -20,7 +20,6 @@ import com.android.ide.eclipse.editors.resources.configurations.FolderConfigurat import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; import java.lang.reflect.Field; -import java.util.ArrayList; import junit.framework.TestCase; @@ -41,7 +40,6 @@ public class QualifierListTest extends TestCase { mManager = null; } - @SuppressWarnings("unchecked") public void testQualifierList() { try { // get the list of qualifier in the resource manager diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java index a95286c57..987ea9247 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java @@ -37,6 +37,7 @@ import org.eclipse.core.runtime.jobs.ISchedulingRule; import sun.reflect.generics.reflectiveObjects.NotImplementedException; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.net.URI; @@ -44,16 +45,28 @@ import java.util.Map; /** * Mock implementation of {@link IFile}. + * + * Optionally backed by an in-memory byte array + * *

      Supported methods: *

        + *
      • getName()
      • + *
      • getContents()
      • + *
      • getContents(boolean force)
      • *
      */ public class FileMock implements IFile { private String mName; + private byte[] mContentData; public FileMock(String name) { + this(name, new byte[0]); + } + + public FileMock(String name, byte[] fileData) { mName = name; + mContentData = fileData; } // -------- MOCKED METHODS ---------------- @@ -62,6 +75,15 @@ public class FileMock implements IFile { return mName; } + public InputStream getContents() throws CoreException { + return new ByteArrayInputStream(mContentData); + } + + public InputStream getContents(boolean force) throws CoreException { + // ignore force + return getContents(); + } + // -------- UNIMPLEMENTED METHODS ---------------- public void appendContents(InputStream source, int updateFlags, IProgressMonitor monitor) @@ -115,14 +137,6 @@ public class FileMock implements IFile { throw new NotImplementedException(); } - public InputStream getContents() throws CoreException { - throw new NotImplementedException(); - } - - public InputStream getContents(boolean force) throws CoreException { - throw new NotImplementedException(); - } - public int getEncoding() throws CoreException { throw new NotImplementedException(); } @@ -139,7 +153,8 @@ public class FileMock implements IFile { throw new NotImplementedException(); } - public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor) + public void move(IPath destination, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { throw new NotImplementedException(); } @@ -229,7 +244,8 @@ public class FileMock implements IFile { throw new NotImplementedException(); } - public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException { + public void deleteMarkers(String type, boolean includeSubtypes, int depth) + throws CoreException { throw new NotImplementedException(); } @@ -424,24 +440,26 @@ public class FileMock implements IFile { throw new NotImplementedException(); } - public Map getPersistentProperties() throws CoreException { + @SuppressWarnings("unchecked") + public Map getPersistentProperties() throws CoreException { throw new NotImplementedException(); - } + } - public Map getSessionProperties() throws CoreException { + @SuppressWarnings("unchecked") + public Map getSessionProperties() throws CoreException { throw new NotImplementedException(); - } + } - public boolean isDerived(int options) { + public boolean isDerived(int options) { throw new NotImplementedException(); - } + } - public boolean isHidden() { + public boolean isHidden() { throw new NotImplementedException(); - } + } - public void setHidden(boolean isHidden) throws CoreException { + public void setHidden(boolean isHidden) throws CoreException { throw new NotImplementedException(); - } - + } } + diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java index 223deb03f..73a69aad4 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java @@ -428,11 +428,11 @@ public final class FolderMock implements IFolder { throw new NotImplementedException(); } - public Map getPersistentProperties() throws CoreException { + public Map getPersistentProperties() throws CoreException { throw new NotImplementedException(); } - public Map getSessionProperties() throws CoreException { + public Map getSessionProperties() throws CoreException { throw new NotImplementedException(); } diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java index 4c409dceb..0e6fde06d 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java @@ -42,6 +42,7 @@ import sun.reflect.generics.reflectiveObjects.NotImplementedException; import java.net.URI; import java.util.Map; +@SuppressWarnings("deprecation") public class ProjectMock implements IProject { public void build(int kind, IProgressMonitor monitor) throws CoreException { @@ -95,7 +96,6 @@ public class ProjectMock implements IProject { throw new NotImplementedException(); } - @SuppressWarnings("deprecation") public IPath getPluginWorkingLocation(IPluginDescriptor plugin) { throw new NotImplementedException(); } @@ -459,6 +459,8 @@ public class ProjectMock implements IProject { throw new NotImplementedException(); } + + @SuppressWarnings("unchecked") public Object getAdapter(Class adapter) { throw new NotImplementedException(); } @@ -476,11 +478,11 @@ public class ProjectMock implements IProject { throw new NotImplementedException(); } - public Map getPersistentProperties() throws CoreException { + public Map getPersistentProperties() throws CoreException { throw new NotImplementedException(); } - public Map getSessionProperties() throws CoreException { + public Map getSessionProperties() throws CoreException { throw new NotImplementedException(); } diff --git a/tools/eclipse/scripts/collect_sources_for_sdk.sh b/tools/eclipse/scripts/collect_sources_for_sdk.sh new file mode 100644 index 000000000..4824da7f7 --- /dev/null +++ b/tools/eclipse/scripts/collect_sources_for_sdk.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +function usage() { + cat < + + The source and destination directories must already exist. + Use -n for a dry-run. + +EOF +} + +DRY="" +if [ "-n" == "$1" ]; then + DRY="echo" + shift +fi + +DIR="frameworks" +if [ "-s" == "$1" ]; then + shift + DIR="$1" + shift +fi + +SRC="$1" +DST="$2" + +if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -d "$SRC" ] || [ ! -d "$DST" ]; then + usage + exit 1 +fi + +function process() { + echo "Examine" $1 +} + +N=0 +E=0 +for i in `find -L "${SRC}/${DIR}" -name "*.java"`; do + if [ -f "$i" ]; then + # look for ^package (android.view.blah);$ + PACKAGE=`sed -n '/^package [^ ;]\+; */{s/[^ ]* *\([^ ;]*\).*/\1/p;q}' "$i"` + if [ -n "$PACKAGE" ]; then + PACKAGE=${PACKAGE//./\/} # e.g. android.view => android/view + JAVA=`basename "$i"` # e.g. View.java + [ -z $DRY ] && [ ! -d "$DST/$PACKAGE" ] && mkdir -p -v "$DST/$PACKAGE" + $DRY cp -v "$i" "$DST/$PACKAGE/$JAVA" + N=$((N+1)) + else + echo "Warning: $i does not have a Java package." + E=$((E+1)) + fi + fi +done + +echo "$N java files copied" +[ $E -gt 0 ] && echo "$E warnings" + diff --git a/tools/eclipse/sites/external/site.xml b/tools/eclipse/sites/external/site.xml index e98a98881..9fca8d28a 100644 --- a/tools/eclipse/sites/external/site.xml +++ b/tools/eclipse/sites/external/site.xml @@ -6,6 +6,9 @@ + + + Features that add Android support to Eclipse for application developers. diff --git a/tools/eclipse/sites/internal/site.xml b/tools/eclipse/sites/internal/site.xml index a7ef62813..9f2642f97 100644 --- a/tools/eclipse/sites/internal/site.xml +++ b/tools/eclipse/sites/internal/site.xml @@ -6,6 +6,9 @@ + + + diff --git a/tools/findunused/findunusedresources b/tools/findunused/findunusedresources index 963e2f1e6..748139a70 100755 --- a/tools/findunused/findunusedresources +++ b/tools/findunused/findunusedresources @@ -25,9 +25,24 @@ fi for app in $apps do + echo '-----------------------------------------------------------' if [ -d $app/res ] then appname=$(basename $app) + resources= + for res in $(echo $app/res/*) + do + resources="$resources $(echo $res | grep -v '\-mcc\|[a-z]*-[a-z][a-z]$\|[a-z]*-[a-z][a-z]-.*')" + done + sources=$app/src + if [ -d $app/tests ] + then + sources="$sources $app/tests" + fi + if [ -d $app/samples ] + then + sources="$sources $app/samples" + fi # find the R.java file that contains all the generated resource identifiers rDotJava=$(find out/target/common/obj/APPS/${appname}_intermediates/ -name R.java) @@ -40,7 +55,7 @@ do # refer to such constants from java by using an underscore instead of a period, we also # replace all underscores with a pattern that will match periods and underscores. p=$(echo $i | sed 's/_/[\\._]/g') - echo $i $(grep -Rw R\\..*\\.$i\\\|@style/$p\\\|@drawable/$p\\\|@anim/$p\\\|@color/$p\\\|@xml/$p\\\|@layout/$p\\\|@menu/$p\\\|@+id/$p\\\|@array/$p\\\|@string/$p $app | wc -l) + echo $i $(grep -Rw R\\..*\\.$i\\\|@style/$p\\\|@drawable/$p\\\|@anim/$p\\\|@color/$p\\\|@xml/$p\\\|@layout/$p\\\|@menu/$p\\\|@+id/$p\\\|@array/$p\\\|@string/$p\\\|@dimen/$p $resources $sources $app/AndroidManifest.xml | wc -l) done | grep " 0$" | { # this block gets as its input a list of constants which no references were found, one per line if [ "$showall" == "yes" ] diff --git a/tools/hierarchyviewer/etc/hierarchyviewer.bat b/tools/hierarchyviewer/etc/hierarchyviewer.bat index 67e4f80b9..2024a7978 100755 --- a/tools/hierarchyviewer/etc/hierarchyviewer.bat +++ b/tools/hierarchyviewer/etc/hierarchyviewer.bat @@ -20,9 +20,9 @@ rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 -rem Change current directory to where ddms is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=hierarchyviewer.jar set frameworkdir= diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java new file mode 100644 index 000000000..83b911342 --- /dev/null +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java @@ -0,0 +1,77 @@ +/* + * 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.hierarchyviewer.scene; + +import com.android.ddmlib.Device; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.device.DeviceBridge; + +import java.net.Socket; +import java.net.InetSocketAddress; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; + +public class ProfilesLoader { + public static double[] loadProfiles(Device device, Window window, String params) { + Socket socket = null; + BufferedReader in = null; + BufferedWriter out = null; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + out.write("PROFILE " + window.encode() + " " + params); + out.newLine(); + out.flush(); + + String response = in.readLine(); + String[] data = response.split(" "); + + double[] profiles = new double[data.length]; + for (int i = 0; i < data.length; i++) { + profiles[i] = (Long.parseLong(data[i]) / 1000.0) / 1000.0; // convert to ms + } + return profiles; + } catch (IOException e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return null; + } +} diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java index 6efb52d67..1f3e27854 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java @@ -32,6 +32,7 @@ import java.net.Socket; import java.util.Collections; import java.util.Comparator; import java.util.Stack; +import java.util.regex.Pattern; public class ViewHierarchyLoader { @SuppressWarnings("empty-statement") @@ -109,7 +110,9 @@ public class ViewHierarchyLoader { parent.children.add(lastNode); } } - + + updateIndices(scene.getRoot()); + } catch (IOException ex) { Exceptions.printStackTrace(ex); } finally { @@ -127,10 +130,20 @@ public class ViewHierarchyLoader { } System.out.println("==> DONE"); - + return scene; } - + + private static void updateIndices(ViewNode root) { + if (root == null) return; + + root.computeIndex(); + + for (ViewNode node : root.children) { + updateIndices(node); + } + } + private static int countFrontWhitespace(String line) { int count = 0; while (line.charAt(count) == ' ') { diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java index d99a80c45..08dc39590 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java @@ -60,22 +60,25 @@ public class ViewHierarchyScene extends GraphScene { @Override protected Widget attachNodeWidget(ViewNode node) { - Widget widget = createBox(node.name, node.id); + Widget widget = createBox(node, node.name, node.id); widget.getActions().addAction(createSelectAction()); widget.getActions().addAction(moveAction); widgetLayer.addChild(widget); return widget; } - private Widget createBox(String node, String id) { - Widget box = new GradientWidget(this); + private Widget createBox(ViewNode node, String nodeName, String id) { + final String shortName = getShortName(nodeName); + node.setShortName(shortName); + + GradientWidget box = new GradientWidget(this, node); box.setLayout(LayoutFactory.createVerticalFlowLayout()); box.setBorder(BorderFactory.createLineBorder(2, Color.BLACK)); box.setOpaque(true); LabelWidget label = new LabelWidget(this); label.setFont(getDefaultFont().deriveFont(Font.PLAIN, 12.0f)); - label.setLabel(getShortName(node)); + label.setLabel(shortName); label.setBorder(BorderFactory.createEmptyBorder(6, 6, 0, 6)); label.setAlignment(LabelWidget.Alignment.CENTER); @@ -83,9 +86,11 @@ public class ViewHierarchyScene extends GraphScene { label = new LabelWidget(this); label.setFont(getDefaultFont().deriveFont(Font.PLAIN, 10.0f)); - label.setLabel(getAddress(node)); + label.setLabel(getAddress(nodeName)); label.setBorder(BorderFactory.createEmptyBorder(3, 6, 0, 6)); label.setAlignment(LabelWidget.Alignment.CENTER); + + box.addressWidget = label; box.addChild(label); @@ -136,7 +141,7 @@ public class ViewHierarchyScene extends GraphScene { connection.setTargetAnchor(AnchorFactory.createRectangularAnchor(target)); } - private static class GradientWidget extends Widget { + private static class GradientWidget extends Widget implements ViewNode.StateListener { public static final GradientPaint BLUE_EXPERIENCE = new GradientPaint( new Point2D.Double(0, 0), new Color(168, 204, 241), @@ -177,15 +182,28 @@ public class ViewHierarchyScene extends GraphScene { new Color(129, 138, 155), new Point2D.Double(0, 1), new Color(58, 66, 82)); + public static final GradientPaint NIGHT_GRAY_VERY_LIGHT = new GradientPaint( + new Point2D.Double(0, 0), + new Color(129, 138, 155, 60), + new Point2D.Double(0, 1), + new Color(58, 66, 82, 60)); private static Color UNSELECTED = Color.BLACK; private static Color SELECTED = Color.WHITE; - private boolean isSelected = false; - private GradientPaint gradient = MAC_OSX_SELECTED; + private final ViewNode node; - public GradientWidget(ViewHierarchyScene scene) { + private LabelWidget addressWidget; + + private boolean isSelected = false; + private final GradientPaint selectedGradient = MAC_OSX_SELECTED; + private final GradientPaint filteredGradient = RED_XP; + private final GradientPaint focusGradient = NIGHT_GRAY_VERY_LIGHT; + + public GradientWidget(ViewHierarchyScene scene, ViewNode node) { super(scene); + this.node = node; + node.setStateListener(this); } @Override @@ -193,8 +211,12 @@ public class ViewHierarchyScene extends GraphScene { super.notifyStateChanged(previous, state); isSelected = state.isSelected() || state.isFocused() || state.isWidgetFocused(); + pickChildrenColor(); + } + + private void pickChildrenColor() { for (Widget child : getChildren()) { - child.setForeground(isSelected ? SELECTED : UNSELECTED); + child.setForeground(isSelected || node.filtered ? SELECTED : UNSELECTED); } repaint(); @@ -206,14 +228,35 @@ public class ViewHierarchyScene extends GraphScene { Graphics2D g2 = getGraphics(); Rectangle bounds = getBounds(); - + if (!isSelected) { - g2.setColor(Color.WHITE); + if (!node.filtered) { + if (!node.hasFocus) { + g2.setColor(Color.WHITE); + } else { + g2.setPaint(new GradientPaint(bounds.x, bounds.y, + focusGradient.getColor1(), bounds.x, bounds.x + bounds.height, + focusGradient.getColor2())); + } + } else { + g2.setPaint(new GradientPaint(bounds.x, bounds.y, filteredGradient.getColor1(), + bounds.x, bounds.x + bounds.height, filteredGradient.getColor2())); + } } else { - g2.setPaint(new GradientPaint(bounds.x, bounds.y, gradient.getColor1(), - bounds.x, bounds.x + bounds.height, gradient.getColor2())); + g2.setPaint(new GradientPaint(bounds.x, bounds.y, selectedGradient.getColor1(), + bounds.x, bounds.x + bounds.height, selectedGradient.getColor2())); } g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); } + + public void nodeStateChanged(ViewNode node) { + pickChildrenColor(); + } + + public void nodeIndexChanged(ViewNode node) { + if (addressWidget != null) { + addressWidget.setLabel("#" + node.index + addressWidget.getLabel()); + } + } } } diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java index 6b212c0c1..2b7efd636 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java @@ -17,7 +17,6 @@ package com.android.hierarchyviewer.scene; import com.android.ddmlib.Device; -import com.android.hierarchyviewer.device.Configuration; import com.android.hierarchyviewer.device.Window; import com.android.hierarchyviewer.device.DeviceBridge; diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java index 8284df13a..64c070353 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; public class ViewNode { public String id; @@ -52,8 +53,15 @@ public class ViewNode { public boolean willNotDraw; public boolean hasMargins; + boolean hasFocus; + int index; + public boolean decoded; - + public boolean filtered; + + private String shortName; + private StateListener listener; + void decode() { id = namedProperties.get("mID").value; @@ -73,6 +81,7 @@ public class ViewNode { marginBottom = getInt("layout_bottomMargin", Integer.MIN_VALUE); baseline = getInt("getBaseline()", 0); willNotDraw = getBoolean("willNotDraw()", false); + hasFocus = getBoolean("hasFocus()", false); hasMargins = marginLeft != Integer.MIN_VALUE && marginRight != Integer.MIN_VALUE && @@ -101,11 +110,33 @@ public class ViewNode { return Integer.parseInt(p.value); } catch (NumberFormatException e) { return defaultValue; - } + } } return defaultValue; } + public void filter(Pattern pattern) { + if (pattern == null || pattern.pattern().length() == 0) { + filtered = false; + } else { + filtered = pattern.matcher(shortName).find() || pattern.matcher(id).find(); + } + listener.nodeStateChanged(this); + } + + void computeIndex() { + index = parent == null ? 0 : parent.children.indexOf(this); + listener.nodeIndexChanged(this); + } + + void setShortName(String shortName) { + this.shortName = shortName; + } + + void setStateListener(StateListener listener) { + this.listener = listener; + } + @SuppressWarnings({"StringEquality"}) @Override public boolean equals(Object obj) { @@ -164,4 +195,9 @@ public class ViewNode { return hash; } } + + interface StateListener { + void nodeStateChanged(ViewNode node); + void nodeIndexChanged(ViewNode node); + } } diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java index a50905c36..89438340d 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java @@ -25,6 +25,8 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.Set; class LayoutRenderer extends JComponent { @@ -34,14 +36,23 @@ class LayoutRenderer extends JComponent { private boolean showExtras; private ViewHierarchyScene scene; + private JComponent sceneView; - LayoutRenderer(ViewHierarchyScene scene) { + LayoutRenderer(ViewHierarchyScene scene, JComponent sceneView) { this.scene = scene; + this.sceneView = sceneView; setOpaque(true); setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0)); setBackground(Color.BLACK); setForeground(Color.WHITE); + + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + selectChild(event.getX(), event.getY()); + } + }); } @Override @@ -118,4 +129,49 @@ class LayoutRenderer extends JComponent { this.showExtras = showExtras; repaint(); } + + private void selectChild(int x, int y) { + + if (scene == null) { + return; + } + + ViewNode root = scene.getRoot(); + if (root == null) { + return; + } + + Insets insets = getInsets(); + + int xoffset = (getWidth() - insets.left - insets.right - root.width) / 2 + insets.left + 1; + int yoffset = (getHeight() - insets.top - insets.bottom - root.height) / 2 + insets.top + 1; + + x -= xoffset; + y -= yoffset; + if (x >= 0 && x < EMULATED_SCREEN_WIDTH && y >= 0 && y < EMULATED_SCREEN_HEIGHT) { + ViewNode hit = findChild(root, root, x, y); + scene.setFocusedObject(hit); + sceneView.repaint(); + } + } + + private ViewNode findChild(ViewNode root, ViewNode besthit, int x, int y) { + ViewNode hit = besthit; + for (ViewNode node : root.children) { + + if (node.left <= x && x < node.left + node.width && + node.top <= y && y < node.top + node.height) { + if (node.width <= hit.width && node.height <= hit.height) { + hit = node; + } + } + + if (node.children.size() > 0) { + hit = findChild(node, hit, + x - (node.left - node.parent.scrollX), + y - (node.top - node.parent.scrollY)); + } + } + return hit; + } } diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java index 83d926f80..e4144b12c 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java @@ -45,6 +45,8 @@ import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.util.concurrent.ExecutionException; class ScreenViewer extends JPanel implements ActionListener { @@ -69,6 +71,8 @@ class ScreenViewer extends JPanel implements ActionListener { private Timer timer; private ViewNode node; + private JSlider zoomSlider; + ScreenViewer(Workspace workspace, Device device, int spacing) { setLayout(new BorderLayout()); setOpaque(false); @@ -95,6 +99,7 @@ class ScreenViewer extends JPanel implements ActionListener { private JPanel buildLoupePanel(int spacing) { loupe = new LoupeViewer(); + loupe.addMouseWheelListener(new WheelZoomListener()); CrosshairPanel crosshairPanel = new CrosshairPanel(loupe); JPanel loupePanel = new JPanel(new BorderLayout()); @@ -106,9 +111,20 @@ class ScreenViewer extends JPanel implements ActionListener { return loupePanel; } + private class WheelZoomListener implements MouseWheelListener { + public void mouseWheelMoved(MouseWheelEvent e) { + if (zoomSlider != null) { + int val = zoomSlider.getValue(); + val -= e.getWheelRotation() * 2; + zoomSlider.setValue(val); + } + } + } + private JPanel buildViewerAndControls() { JPanel panel = new JPanel(new GridBagLayout()); crosshair = new Crosshair(new ScreenshotViewer()); + crosshair.addMouseWheelListener(new WheelZoomListener()); panel.add(crosshair, new GridBagConstraints(0, y++, 2, 1, 1.0f, 0.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, @@ -131,7 +147,8 @@ class ScreenViewer extends JPanel implements ActionListener { timer.restart(); } }); - buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2).addChangeListener( + zoomSlider = buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2); + zoomSlider.addChangeListener( new ChangeListener() { public void stateChanged(ChangeEvent event) { zoom = ((JSlider) event.getSource()).getValue(); diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java index 0add4e9db..d530c3540 100644 --- a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java @@ -27,6 +27,7 @@ import com.android.hierarchyviewer.scene.ViewHierarchyScene; import com.android.hierarchyviewer.scene.ViewManager; import com.android.hierarchyviewer.scene.ViewNode; import com.android.hierarchyviewer.scene.WindowsLoader; +import com.android.hierarchyviewer.scene.ProfilesLoader; import com.android.hierarchyviewer.util.OS; import com.android.hierarchyviewer.util.WorkerThread; import com.android.hierarchyviewer.ui.action.ShowDevicesAction; @@ -43,6 +44,7 @@ import com.android.hierarchyviewer.ui.util.PngFileFilter; import com.android.hierarchyviewer.ui.util.IconLoader; import com.android.hierarchyviewer.ui.model.PropertiesTableModel; import com.android.hierarchyviewer.ui.model.ViewsTreeModel; +import com.android.hierarchyviewer.ui.model.ProfilesTableModel; import org.jdesktop.swingworker.SwingWorker; import org.netbeans.api.visual.graph.layout.TreeGraphLayout; import org.netbeans.api.visual.model.ObjectSceneEvent; @@ -67,6 +69,7 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JScrollPane; +import javax.swing.JScrollBar; import javax.swing.JSlider; import javax.swing.JSplitPane; import javax.swing.JTable; @@ -76,6 +79,9 @@ import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.JTree; import javax.swing.Box; +import javax.swing.JTextField; +import javax.swing.text.Document; +import javax.swing.text.BadLocationException; import javax.swing.tree.TreePath; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.event.ChangeEvent; @@ -84,6 +90,8 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TreeSelectionListener; import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.DocumentEvent; import javax.swing.table.DefaultTableModel; import java.awt.image.BufferedImage; import java.awt.BorderLayout; @@ -100,11 +108,15 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.concurrent.ExecutionException; public class Workspace extends JFrame { @@ -113,6 +125,7 @@ public class Workspace extends JFrame { private JSplitPane sideSplitter; private JSplitPane mainSplitter; private JTable propertiesTable; + private JTable profilingTable; private JComponent pixelPerfectPanel; private JTree pixelPerfectTree; private ScreenViewer screenViewer; @@ -156,6 +169,8 @@ public class Workspace extends JFrame { private JTable windows; private JLabel minZoomLabel; private JLabel maxZoomLabel; + private JTextField filterText; + private JLabel filterLabel; public Workspace() { super("Hierarchy Viewer"); @@ -164,6 +179,7 @@ public class Workspace extends JFrame { add(buildMainPanel()); setJMenuBar(buildMenuBar()); + devices.changeSelection(0, 0, false, false); currentDeviceChanged(); pack(); @@ -261,11 +277,32 @@ public class Workspace extends JFrame { JScrollPane tableScroller = new JScrollPane(propertiesTable); tableScroller.setBorder(null); + profilingTable = new JTable(); + profilingTable.setModel(new DefaultTableModel(new Object[][] { + { " " , " " }, { " " , " " }, { " " , " " } }, + new String[] { "Operation", "Duration (ms)" })); + profilingTable.setBorder(null); + profilingTable.getTableHeader().setBorder(null); + + JScrollPane firstTableScroller = new JScrollPane(profilingTable); + firstTableScroller.setBorder(null); + + setVisibleRowCount(profilingTable, 5); + firstTableScroller.setMinimumSize(profilingTable.getPreferredScrollableViewportSize()); + + JSplitPane tablesSplitter = new JSplitPane(); + tablesSplitter.setBorder(null); + tablesSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT); + tablesSplitter.setResizeWeight(0); + tablesSplitter.setLeftComponent(firstTableScroller); + tablesSplitter.setBottomComponent(tableScroller); + tablesSplitter.setContinuousLayout(true); + sideSplitter = new JSplitPane(); sideSplitter.setBorder(null); sideSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT); sideSplitter.setResizeWeight(0.5); - sideSplitter.setLeftComponent(tableScroller); + sideSplitter.setLeftComponent(tablesSplitter); sideSplitter.setBottomComponent(null); sideSplitter.setContinuousLayout(true); @@ -313,10 +350,33 @@ public class Workspace extends JFrame { graphViewButton.setSelected(true); + filterText = new JTextField(20); + filterText.putClientProperty("JComponent.sizeVariant", "small"); + filterText.getDocument().addDocumentListener(new DocumentListener() { + public void insertUpdate(DocumentEvent e) { + updateFilter(e); + } + + public void removeUpdate(DocumentEvent e) { + updateFilter(e); + } + + public void changedUpdate(DocumentEvent e) { + updateFilter(e); + } + }); + + filterLabel = new JLabel("Filter by class or id:"); + filterLabel.putClientProperty("JComponent.sizeVariant", "small"); + filterLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6)); + + leftSide.add(filterLabel); + leftSide.add(filterText); + minZoomLabel = new JLabel(); minZoomLabel.setText("20%"); minZoomLabel.putClientProperty("JComponent.sizeVariant", "small"); - minZoomLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 0)); + minZoomLabel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0)); leftSide.add(minZoomLabel); zoomSlider = new JSlider(); @@ -357,12 +417,18 @@ public class Workspace extends JFrame { statusPanel.add(rightSide, BorderLayout.LINE_END); + hideStatusBarComponents(); + + return statusPanel; + } + + private void hideStatusBarComponents() { viewCountLabel.setVisible(false); zoomSlider.setVisible(false); minZoomLabel.setVisible(false); - maxZoomLabel.setVisible(false); - - return statusPanel; + maxZoomLabel.setVisible(false); + filterLabel.setVisible(false); + filterText.setVisible(false); } private JToolBar buildToolBar() { @@ -513,10 +579,7 @@ public class Workspace extends JFrame { } private void toggleGraphView() { - viewCountLabel.setVisible(true); - zoomSlider.setVisible(true); - minZoomLabel.setVisible(true); - maxZoomLabel.setVisible(true); + showStatusBarComponents(); screenViewer.stop(); mainPanel.remove(pixelPerfectPanel); @@ -526,6 +589,15 @@ public class Workspace extends JFrame { repaint(); } + private void showStatusBarComponents() { + viewCountLabel.setVisible(true); + zoomSlider.setVisible(true); + minZoomLabel.setVisible(true); + maxZoomLabel.setVisible(true); + filterLabel.setVisible(true); + filterText.setVisible(true); + } + private void togglePixelPerfectView() { if (pixelPerfectPanel == null) { pixelPerfectPanel = buildPixelPerfectPanel(); @@ -534,10 +606,7 @@ public class Workspace extends JFrame { screenViewer.start(); } - viewCountLabel.setVisible(false); - zoomSlider.setVisible(false); - minZoomLabel.setVisible(false); - maxZoomLabel.setVisible(false); + hideStatusBarComponents(); mainPanel.remove(mainSplitter); mainPanel.add(pixelPerfectPanel, BorderLayout.CENTER); @@ -558,6 +627,22 @@ public class Workspace extends JFrame { propertiesTable.setModel(new PropertiesTableModel(node)); } + private void updateProfiles(double[] profiles) { + profilingTable.setModel(new ProfilesTableModel(profiles)); + setVisibleRowCount(profilingTable, profiles.length + 1); + } + + public static void setVisibleRowCount(JTable table, int rows) { + int height = 0; + for (int row = 0; row < rows; row++) { + height += table.getRowHeight(row); + } + + Dimension size = new Dimension(table.getPreferredScrollableViewportSize().width, height); + table.setPreferredScrollableViewportSize(size); + table.revalidate(); + } + private void showPixelPerfectTree() { if (pixelPerfectTree == null) { return; @@ -602,14 +687,12 @@ public class Workspace extends JFrame { graphViewButton.setEnabled(true); pixelPerfectViewButton.setEnabled(true); - viewCountLabel.setVisible(true); - zoomSlider.setVisible(true); - minZoomLabel.setVisible(true); - maxZoomLabel.setVisible(true); + showStatusBarComponents(); } sceneView = scene.createView(); sceneView.addMouseListener(new NodeClickListener()); + sceneView.addMouseWheelListener(new WheelZoomListener()); sceneScroller.setViewportView(sceneView); if (extrasPanel != null) { @@ -640,7 +723,10 @@ public class Workspace extends JFrame { private JPanel buildExtrasPanel() { extrasPanel = new JPanel(new BorderLayout()); - extrasPanel.add(new JScrollPane(layoutView = new LayoutRenderer(scene))); + JScrollPane p = new JScrollPane(layoutView = new LayoutRenderer(scene, sceneView)); + JScrollBar b = p.getVerticalScrollBar(); + b.setUnitIncrement(10); + extrasPanel.add(p); extrasPanel.add(scene.createSatelliteView(), BorderLayout.SOUTH); extrasPanel.add(buildLayoutViewControlButtons(), BorderLayout.NORTH); return extrasPanel; @@ -776,10 +862,7 @@ public class Workspace extends JFrame { pixelPerfectPanel = mainSplitter = null; graphViewButton.setSelected(true); - viewCountLabel.setVisible(false); - zoomSlider.setVisible(false); - minZoomLabel.setVisible(false); - maxZoomLabel.setVisible(false); + hideStatusBarComponents(); saveMenuItem.setEnabled(false); showDevicesMenuItem.setEnabled(false); @@ -865,6 +948,34 @@ public class Workspace extends JFrame { }); } + private void updateFilter(DocumentEvent e) { + final Document document = e.getDocument(); + try { + updateFilteredNodes(document.getText(0, document.getLength())); + } catch (BadLocationException e1) { + e1.printStackTrace(); + } + } + + private void updateFilteredNodes(String filterText) { + final ViewNode root = scene.getRoot(); + try { + final Pattern pattern = Pattern.compile(filterText, Pattern.CASE_INSENSITIVE); + filterNodes(pattern, root); + } catch (PatternSyntaxException e) { + filterNodes(null, root); + } + repaint(); + } + + private void filterNodes(Pattern pattern, ViewNode root) { + root.filter(pattern); + + for (ViewNode node : root.children) { + filterNodes(pattern, node); + } + } + public void beginTask() { progress.setVisible(true); } @@ -1063,22 +1174,24 @@ public class Workspace extends JFrame { } } - private class LoadGraphTask extends SwingWorker { + private class LoadGraphTask extends SwingWorker { public LoadGraphTask() { beginTask(); } @Override @WorkerThread - protected ViewHierarchyScene doInBackground() { + protected double[] doInBackground() { scene = ViewHierarchyLoader.loadScene(currentDevice, currentWindow); - return scene; + return ProfilesLoader.loadProfiles(currentDevice, currentWindow, + scene.getRoot().toString()); } @Override protected void done() { try { - createGraph(get()); + createGraph(scene); + updateProfiles(get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { @@ -1168,6 +1281,15 @@ public class Workspace extends JFrame { } } + private class WheelZoomListener implements MouseWheelListener { + public void mouseWheelMoved(MouseWheelEvent e) { + if (zoomSlider != null) { + int val = zoomSlider.getValue(); + val -= e.getWheelRotation() * 10; + zoomSlider.setValue(val); + } + } + } private class DevicesTableModel extends DefaultTableModel implements AndroidDebugBridge.IDeviceChangeListener { diff --git a/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java new file mode 100644 index 000000000..fcbe6b57e --- /dev/null +++ b/tools/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java @@ -0,0 +1,68 @@ +/* + * 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.hierarchyviewer.ui.model; + +import javax.swing.table.DefaultTableModel; +import java.text.NumberFormat; + +public class ProfilesTableModel extends DefaultTableModel { + private static final String[] NAMES = { "measure", "layout", "draw" }; + + private final double[] profiles; + private final NumberFormat formatter; + + public ProfilesTableModel(double[] profiles) { + this.profiles = profiles; + formatter = NumberFormat.getNumberInstance(); + } + + @Override + public int getRowCount() { + return profiles == null ? 0 : profiles.length; + } + + @Override + public Object getValueAt(int row, int column) { + if (profiles == null) return ""; + + if (column == 0) { + return NAMES[row]; + } + + + return formatter.format(profiles[row]) + ""; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public String getColumnName(int column) { + return column == 0 ? "Operation" : "Duration (ms)"; + } + + @Override + public boolean isCellEditable(int arg0, int arg1) { + return false; + } + + @Override + public void setValueAt(Object arg0, int arg1, int arg2) { + } +} diff --git a/tools/makedict/Android.mk b/tools/makedict/Android.mk new file mode 100644 index 000000000..b9fc5533d --- /dev/null +++ b/tools/makedict/Android.mk @@ -0,0 +1,24 @@ +# +# 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) +LOCAL_JAR_MANIFEST := etc/manifest.txt +LOCAL_MODULE := makedict + +include $(BUILD_HOST_JAVA_LIBRARY) +include $(LOCAL_PATH)/etc/Android.mk diff --git a/tools/makedict/etc/Android.mk b/tools/makedict/etc/Android.mk new file mode 100644 index 000000000..da162868a --- /dev/null +++ b/tools/makedict/etc/Android.mk @@ -0,0 +1,20 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := makedict +include $(BUILD_HOST_PREBUILT) + diff --git a/tools/makedict/etc/makedict b/tools/makedict/etc/makedict new file mode 100755 index 000000000..8420d6e5e --- /dev/null +++ b/tools/makedict/etc/makedict @@ -0,0 +1,63 @@ +#!/bin/sh +# Copyright 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=makedict.jar +frameworkdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec java -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@" diff --git a/tools/makedict/etc/manifest.txt b/tools/makedict/etc/manifest.txt new file mode 100644 index 000000000..aa3a3e84c --- /dev/null +++ b/tools/makedict/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.tools.dict.MakeBinaryDictionary diff --git a/tools/makedict/src/com/android/tools/dict/MakeBinaryDictionary.java b/tools/makedict/src/com/android/tools/dict/MakeBinaryDictionary.java new file mode 100755 index 000000000..cbe702872 --- /dev/null +++ b/tools/makedict/src/com/android/tools/dict/MakeBinaryDictionary.java @@ -0,0 +1,294 @@ +/* + * 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.tools.dict; + +import org.xml.sax.Attributes; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Compresses a list of words and frequencies into a tree structured binary dictionary. + */ +public class MakeBinaryDictionary { + + public static final int ALPHA_SIZE = 256; + + public static final String TAG_WORD = "w"; + public static final String ATTR_FREQ = "f"; + + public static final CharNode EMPTY_NODE = new CharNode(); + + List roots; + Map mDictionary; + int mWordCount; + + static class CharNode { + char data; + int freq; + boolean terminal; + List children; + static int sNodes; + + public CharNode() { + sNodes++; + } + } + + public static void usage() { + System.err.println("Usage: makedict "); + System.exit(-1); + } + + public static void main(String[] args) { + if (args.length < 2) { + usage(); + } else { + new MakeBinaryDictionary(args[0], args[1]); + } + } + + public MakeBinaryDictionary(String srcFilename, String destFilename) { + populateDictionary(srcFilename); + writeToDict(destFilename); + // Enable the code below to verify that the generated tree is traversable. + if (false) { + traverseDict(0, new char[32], 0); + } + } + + private void populateDictionary(String filename) { + roots = new ArrayList(); + try { + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse(new File(filename), new DefaultHandler() { + boolean inWord; + int freq; + + @Override + public void startElement(String uri, String localName, + String qName, Attributes attributes) { + if (qName.equals("w")) { + inWord = true; + freq = Integer.parseInt(attributes.getValue(0)); + } + } + + @Override + public void characters(char[] data, int offset, int length) { + // Ignore other whitespace + if (!inWord) return; + + // Ignore one letter words + if (length < 2) return; + mWordCount++; + String word = new String(data, offset, length); + addWordTop(word, freq); + } + + @Override + public void endElement(String uri, String localName, + String qName) { + if (qName.equals("w")) inWord = false; + } + }); + } catch (Exception ioe) { + System.err.println("Exception in parsing\n" + ioe); + ioe.printStackTrace(); + } + System.out.println("Nodes = " + CharNode.sNodes); + } + + private int indexOf(List children, char c) { + if (children == null) { + return -1; + } + for (int i = 0; i < children.size(); i++) { + if (children.get(i).data == c) { + return i; + } + } + return -1; + } + + private void addWordTop(String word, int occur) { + if (occur > 255) occur = 255; + + char firstChar = word.charAt(0); + int index = indexOf(roots, firstChar); + if (index == -1) { + CharNode newNode = new CharNode(); + newNode.data = firstChar; + newNode.freq = occur; + index = roots.size(); + roots.add(newNode); + } else { + roots.get(index).freq += occur; + } + if (word.length() > 1) { + addWordRec(roots.get(index), word, 1, occur); + } else { + roots.get(index).terminal = true; + } + } + + private void addWordRec(CharNode parent, String word, int charAt, int occur) { + CharNode child = null; + char data = word.charAt(charAt); + if (parent.children == null) { + parent.children = new ArrayList(); + } else { + for (int i = 0; i < parent.children.size(); i++) { + CharNode node = parent.children.get(i); + if (node.data == data) { + child = node; + break; + } + } + } + if (child == null) { + child = new CharNode(); + parent.children.add(child); + } + child.data = data; + child.freq += occur; + if (word.length() > charAt + 1) { + addWordRec(child, word, charAt + 1, occur); + } else { + child.terminal = true; + child.freq = occur; + } + } + + byte[] dict; + int dictSize; + static final int CHAR_WIDTH = 8; + static final int FLAGS_WIDTH = 1; // Terminal flag (word end) + static final int ADDR_WIDTH = 23; // Offset to children + static final int FREQ_WIDTH_BYTES = 1; + static final int COUNT_WIDTH_BYTES = 1; + static final int NODE_SIZE_BYTES = + (CHAR_WIDTH + FLAGS_WIDTH + ADDR_WIDTH) / 8 + FREQ_WIDTH_BYTES; + + private void addCount(int count) { + dict[dictSize++] = (byte) (0xFF & count); + } + + /* TODO: Allow characters to be beyond the 0-255 range. This is required for some latin + language not currently supported */ + private void addNode(CharNode node) { + int charData = 0xFFFF & node.data; + if (charData > 254) { + System.out.println("WARNING: Non-ASCII character encountered : " + node.data + + ", value = " + charData); + dict[dictSize++] = '@'; + } else { + dict[dictSize++] = (byte) (0xFF & node.data); + } + dictSize += 3; // Space for children address + if ((0xFFFFFF & node.freq) > 255) { + node.freq = (byte) 255; + } + dict[dictSize++] = (byte) (0xFF & node.freq); + } + + private void updateNodeAddress(int nodeAddress, CharNode node, + int childrenAddress) { + childrenAddress = 0x7FFFFF & childrenAddress; + if (node.terminal) { + childrenAddress |= 0x800000; + } + dict[nodeAddress + 1] = (byte) (childrenAddress >> 16); + dict[nodeAddress + 2] = (byte) ((childrenAddress & 0xFF00) >> 8); + dict[nodeAddress + 3] = (byte) ((childrenAddress & 0xFF)); + } + + void writeWordsRec(List children) { + if (children == null || children.size() == 0) { + return; + } + addCount(children.size()); + int childrenStart = dictSize; + for (int j = 0; j < children.size(); j++) { + CharNode node = children.get(j); + addNode(node); + } + for (int j = 0; j < children.size(); j++) { + CharNode node = children.get(j); + // TODO: Fix this when child length becomes variable + int nodeAddress = childrenStart + NODE_SIZE_BYTES * j; + int cacheDictSize = dictSize; + writeWordsRec(node.children); + updateNodeAddress(nodeAddress, node, node.children != null + ? cacheDictSize : 0); + } + } + + void writeToDict(String dictFilename) { + // 2MB max + dict = new byte[2 * 1024 * 1024]; // 2MB upper limit. Actual is probably + // < 1MB in most cases, as there is a limit in the + // resource size in apks. + dictSize = 0; + writeWordsRec(roots); + System.out.println("Dict Size = " + dictSize); + try { + FileOutputStream fos = new FileOutputStream(dictFilename); + fos.write(dict, 0, dictSize); + fos.close(); + } catch (IOException ioe) { + System.err.println("Error writing dict file:" + ioe); + } + } + + void traverseDict(int pos, char[] word, int depth) { + int count = dict[pos++] & 0xFF; + for (int i = 0; i < count; i++) { + char c = (char) (dict[pos] & 0xFF); + word[depth] = c; + if ((dict[pos + 1] & 0x80) > 0) { + showWord(word, depth + 1, dict[pos + 4] & 0xFF); + } + int address = + ((dict[pos + 1] & 0x7F) << 16) + | ((dict[pos + 2] & 0xFF) << 8) + | ((dict[pos + 3] & 0xFF)); + if (address != 0) { + traverseDict(address, word, depth + 1); + } + pos += NODE_SIZE_BYTES; + } + } + + void showWord(char[] word, int size, int freq) { + System.out.print(new String(word, 0, size) + " " + freq + "\n"); + } +} diff --git a/tools/ninepatch/src/com/android/ninepatch/NinePatch.java b/tools/ninepatch/src/com/android/ninepatch/NinePatch.java index 39e05c667..35a1824e9 100644 --- a/tools/ninepatch/src/com/android/ninepatch/NinePatch.java +++ b/tools/ninepatch/src/com/android/ninepatch/NinePatch.java @@ -127,8 +127,7 @@ public class NinePatch { try { - if (mPatches.size() == 0 || mHorizontalPatches.size() == 0 || - mVerticalPatches.size() == 0) { + if (mPatches.size() == 0) { g.drawImage(mImage, x, y, scaledWidth, scaledHeight, null); return; } @@ -254,6 +253,14 @@ public class NinePatch { start = rect.x; } } + } else { + int start = -1; + for (Rectangle rect : mPatches) { + if (rect.x > start) { + mHorizontalPatchesSum += rect.width; + start = rect.x; + } + } } mVerticalPatchesSum = 0; @@ -265,6 +272,14 @@ public class NinePatch { start = rect.y; } } + } else { + int start = -1; + for (Rectangle rect : mPatches) { + if (rect.y > start) { + mVerticalPatchesSum += rect.height; + start = rect.y; + } + } } } @@ -286,28 +301,11 @@ public class NinePatch { boolean[] result = new boolean[1]; Pair>> left = getPatches(column, result); mVerticalStartWithPatch = result[0]; - - // compute the min size, based on the list of fixed sections, which is stored in - // Pair.mFirst - mMinHeight = 0; - List> fixedSections = left.mFirst; - for (Pair section : fixedSections) { - mMinHeight += section.mSecond - section.mFirst; - } result = new boolean[1]; Pair>> top = getPatches(row, result); mHorizontalStartWithPatch = result[0]; - // compute the min size, based on the list of fixed sections, which is stored in - // Pair.mFirst - - mMinWidth = 0; - fixedSections = top.mFirst; - for (Pair section : fixedSections) { - mMinWidth += section.mSecond - section.mFirst; - } - mFixed = getRectangles(left.mFirst, top.mFirst); mPatches = getRectangles(left.mSecond, top.mSecond); @@ -315,7 +313,15 @@ public class NinePatch { mHorizontalPatches = getRectangles(left.mFirst, top.mSecond); mVerticalPatches = getRectangles(left.mSecond, top.mFirst); } else { - mHorizontalPatches = mVerticalPatches = new ArrayList(0); + if (top.mFirst.size() > 0) { + mHorizontalPatches = new ArrayList(0); + mVerticalPatches = getVerticalRectangles(top.mFirst); + } else if (left.mFirst.size() > 0) { + mHorizontalPatches = getHorizontalRectangles(left.mFirst); + mVerticalPatches = new ArrayList(0); + } else { + mHorizontalPatches = mVerticalPatches = new ArrayList(0); + } } row = GraphicsUtilities.getPixels(mImage, 0, height - 1, width, 1, row); @@ -326,31 +332,30 @@ public class NinePatch { left = getPatches(column, result); mVerticalPadding = getPadding(left.mFirst); - - mHorizontalPatchesSum = 0; - if (mHorizontalPatches.size() > 0) { - int start = -1; - for (Rectangle rect : mHorizontalPatches) { - if (rect.x > start) { - mHorizontalPatchesSum += rect.width; - start = rect.x; - } - } - } - - mVerticalPatchesSum = 0; - if (mVerticalPatches.size() > 0) { - int start = -1; - for (Rectangle rect : mVerticalPatches) { - if (rect.y > start) { - mVerticalPatchesSum += rect.height; - start = rect.y; - } - } - } - } - + + private List getVerticalRectangles(List> topPairs) { + List rectangles = new ArrayList(); + for (Pair top : topPairs) { + int x = top.mFirst; + int width = top.mSecond - top.mFirst; + + rectangles.add(new Rectangle(x, 1, width, mImage.getHeight() - 2)); + } + return rectangles; + } + + private List getHorizontalRectangles(List> leftPairs) { + List rectangles = new ArrayList(); + for (Pair left : leftPairs) { + int y = left.mFirst; + int height = left.mSecond - left.mFirst; + + rectangles.add(new Rectangle(1, y, mImage.getWidth() - 2, height)); + } + return rectangles; + } + private Pair getPadding(List> pairs) { if (pairs.size() == 0) { return new Pair(0, 0); @@ -366,14 +371,14 @@ public class NinePatch { pairs.get(index).mSecond - pairs.get(index).mFirst); } } - + private List getRectangles(List> leftPairs, List> topPairs) { List rectangles = new ArrayList(); for (Pair left : leftPairs) { int y = left.mFirst; int height = left.mSecond - left.mFirst; - for (Pair top: topPairs) { + for (Pair top : topPairs) { int x = top.mFirst; int width = top.mSecond - top.mFirst; @@ -382,7 +387,7 @@ public class NinePatch { } return rectangles; } - + private Pair>> getPatches(int[] pixels, boolean[] startWithPatch) { int lastIndex = 1; int lastPixel = pixels[1]; @@ -390,7 +395,7 @@ public class NinePatch { List> fixed = new ArrayList>(); List> patches = new ArrayList>(); - + for (int i = 1; i < pixels.length - 1; i++) { int pixel = pixels[i]; if (pixel != lastPixel) { @@ -418,6 +423,7 @@ public class NinePatch { startWithPatch[0] = true; fixed.clear(); } + return new Pair>>(fixed, patches); } diff --git a/tools/runtest b/tools/runtest index 9978504d9..62809100f 100755 --- a/tools/runtest +++ b/tools/runtest @@ -84,7 +84,7 @@ function showUsage() { # tests by creating a file named "~/.android/runtest.rc" and adding them to that # file. (No array needed, just plain lines of text). # -# Rests are defined by entries with the following format: +# Tests are defined by entries with the following format: # # # @@ -99,8 +99,8 @@ function showUsage() { # # In order to define the most common cases simply, "#" can be used for some of # the fields, with the following default values: -# = "#": test class is fully qualified with package # = "#": skip build/sync step +# = "#": test class is fully qualified with package # = "#": omit "-e class" section # = "#": use same value as test-package # = "#": use "android.test.InstrumentationTestRunner" @@ -117,7 +117,8 @@ knownTests=( "smoke frameworks/base/tests/SmokeTest com.android.smoketest # com.android.smoketest.tests #" "core frameworks/base/tests/CoreTests # android.core.CoreTests android.core #" "libcore frameworks/base/tests/CoreTests # android.core.JavaTests android.core #" - "apidemos samples/ApiDemos com.example.android.apis # com.example.android.apis.tests #" + "apidemos development/samples/ApiDemos com.example.android.apis # com.example.android.apis.tests #" + "launchperf development/apps/launchperf com.android.launchperf # # .SimpleActivityLaunchPerformance" # targeted framework tests "heap frameworks/base/tests/AndroidTests com.android.unit_tests HeapTest # #" @@ -130,7 +131,7 @@ knownTests=( "browser packages/apps/Browser com.android.browser # # .BrowserTestRunner" "browserfunc packages/apps/Browser com.android.browser # # .BrowserFunctionalTestRunner" "calendar packages/apps/Calendar/tests com.android.calendar.tests # # #" - "calprov content/providers/calendar com.android.providers.calendar.tests # # #" + "calprov packages/providers/CalendarProvider com.android.providers.calendar # com.android.providers.calendar.tests #" "camera tests/Camera com.android.cameratests # # CameraInstrumentationTestRunner" "contactsprov packages/providers/GoogleContactsProvider/tests com.android.providers.contacts # com.android.providers.contactstests #" "email packages/apps/Email com.android.email # com.android.email.tests #" @@ -150,11 +151,7 @@ knownTests=( # the list of known tests. # function readConfigFile () { - pathToRc=$1 - if [[ -z ${pathToRc} ]]; then - exit 0 - fi - rcFile=${pathToRc}/runtest.rc + rcFile=$1 if [[ -f ${rcFile} ]] ; then declare -a lines exec 3<${rcFile} || exit @@ -261,7 +258,7 @@ shift $((OPTIND-1)) readConfigFile $optUserTests # TODO: Read from *any* vendor/*/runtest.rc -readConfigFile $(gettop)/vendor/google +readConfigFile $(gettop)/vendor/google/runtest.rc # if requested, list all tests and halt if [[ ${optListTests} -ne 0 ]] ; then @@ -338,6 +335,10 @@ if [[ ${#testInfo[@]} -eq 5 ]] ; then debugOptions="-r "${debugOptions} fi + # "prevent" a race condition where we try to run the tests before they're + # actually installed + sleep 2 + # now run the command if [[ $optVerbose -ne 0 || ${optPreview} -ne 0 ]] ; then echo adb ${optAdbTarget} shell am instrument -w \ diff --git a/tools/scripts/alias_rules.xml b/tools/scripts/alias_rules.xml index 0443193cc..bc7de7cdc 100644 --- a/tools/scripts/alias_rules.xml +++ b/tools/scripts/alias_rules.xml @@ -1,4 +1,4 @@ - + diff --git a/tools/scripts/android_rules.xml b/tools/scripts/android_rules.xml index ce34e2873..aad9dbd6e 100644 --- a/tools/scripts/android_rules.xml +++ b/tools/scripts/android_rules.xml @@ -1,24 +1,46 @@ - + + + + + + + + + + + - - - + - - + + @@ -26,9 +48,6 @@ - - - @@ -42,30 +61,19 @@ - - - - - - - - - - - - - - + Creating output directories if needed... + + @@ -77,7 +85,7 @@ - + @@ -93,6 +101,7 @@ + @@ -100,13 +109,14 @@ - + + + - + @@ -119,85 +129,56 @@ - + - - - Packaging resources and assets... - - - - - - - - - - - - - - - - - - - Packaging resources... - - - - - - - - - - - - - + + + Packaging resources + - - Packaging ${out-debug-package}, and signing it with a debug key... - - - - - - - - - - - - - + + + + + + + - - Packaging ${out-unsigned-package} for release... - - - - - - - - - - - - - - - It will need to be signed with jarsigner before it is published. + + + + + + + + All generated packages need to be signed with jarsigner before they are published. diff --git a/tools/scripts/app_engine_server/LICENSE b/tools/scripts/app_engine_server/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/tools/scripts/app_engine_server/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/tools/scripts/app_engine_server/app.yaml b/tools/scripts/app_engine_server/app.yaml new file mode 100755 index 000000000..1fb50c7df --- /dev/null +++ b/tools/scripts/app_engine_server/app.yaml @@ -0,0 +1,16 @@ +application: androidappdocs-staging +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /gae_shell/static + static_dir: gae_shell/static + expiration: 1d + +- url: /gae_shell/.* + script: /gae_shell/shell.py + login: admin + +- url: .* + script: main.py diff --git a/tools/scripts/app_engine_server/gae_shell/README b/tools/scripts/app_engine_server/gae_shell/README new file mode 100644 index 000000000..5b0089fef --- /dev/null +++ b/tools/scripts/app_engine_server/gae_shell/README @@ -0,0 +1,17 @@ +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. diff --git a/tools/scripts/app_engine_server/gae_shell/__init__.py b/tools/scripts/app_engine_server/gae_shell/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/scripts/app_engine_server/gae_shell/__init__.pyc b/tools/scripts/app_engine_server/gae_shell/__init__.pyc new file mode 100644 index 000000000..84951e922 Binary files /dev/null and b/tools/scripts/app_engine_server/gae_shell/__init__.pyc differ diff --git a/tools/scripts/app_engine_server/gae_shell/shell.py b/tools/scripts/app_engine_server/gae_shell/shell.py new file mode 100755 index 000000000..df2fb1708 --- /dev/null +++ b/tools/scripts/app_engine_server/gae_shell/shell.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# +# Copyright 2007 Google Inc. +# +# 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. + +""" +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. + +TODO: unit tests! +""" + +import logging +import new +import os +import pickle +import sys +import traceback +import types +import wsgiref.handlers + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + + +# Set to True if stack traces should be shown in the browser, etc. +_DEBUG = True + +# The entity kind for shell sessions. Feel free to rename to suit your app. +_SESSION_KIND = '_Shell_Session' + +# Types that can't be pickled. +UNPICKLABLE_TYPES = ( + types.ModuleType, + types.TypeType, + types.ClassType, + types.FunctionType, + ) + +# Unpicklable statements to seed new sessions with. +INITIAL_UNPICKLABLES = [ + 'import logging', + 'import os', + 'import sys', + 'from google.appengine.ext import db', + 'from google.appengine.api import users', + ] + + +class Session(db.Model): + """A shell session. Stores the session's globals. + + Each session globals is stored in one of two places: + + If the global is picklable, it's stored in the parallel globals and + global_names list properties. (They're parallel lists to work around the + unfortunate fact that the datastore can't store dictionaries natively.) + + If the global is not picklable (e.g. modules, classes, and functions), or if + it was created by the same statement that created an unpicklable global, + it's not stored directly. Instead, the statement is stored in the + unpicklables list property. On each request, before executing the current + statement, the unpicklable statements are evaluated to recreate the + unpicklable globals. + + The unpicklable_names property stores all of the names of globals that were + added by unpicklable statements. When we pickle and store the globals after + executing a statement, we skip the ones in unpicklable_names. + + Using Text instead of string is an optimization. We don't query on any of + these properties, so they don't need to be indexed. + """ + global_names = db.ListProperty(db.Text) + globals = db.ListProperty(db.Blob) + unpicklable_names = db.ListProperty(db.Text) + unpicklables = db.ListProperty(db.Text) + + def set_global(self, name, value): + """Adds a global, or updates it if it already exists. + + Also removes the global from the list of unpicklable names. + + Args: + name: the name of the global to remove + value: any picklable value + """ + blob = db.Blob(pickle.dumps(value)) + + if name in self.global_names: + index = self.global_names.index(name) + self.globals[index] = blob + else: + self.global_names.append(db.Text(name)) + self.globals.append(blob) + + self.remove_unpicklable_name(name) + + def remove_global(self, name): + """Removes a global, if it exists. + + Args: + name: string, the name of the global to remove + """ + if name in self.global_names: + index = self.global_names.index(name) + del self.global_names[index] + del self.globals[index] + + def globals_dict(self): + """Returns a dictionary view of the globals. + """ + return dict((name, pickle.loads(val)) + for name, val in zip(self.global_names, self.globals)) + + def add_unpicklable(self, statement, names): + """Adds a statement and list of names to the unpicklables. + + Also removes the names from the globals. + + Args: + statement: string, the statement that created new unpicklable global(s). + names: list of strings; the names of the globals created by the statement. + """ + self.unpicklables.append(db.Text(statement)) + + for name in names: + self.remove_global(name) + if name not in self.unpicklable_names: + self.unpicklable_names.append(db.Text(name)) + + def remove_unpicklable_name(self, name): + """Removes a name from the list of unpicklable names, if it exists. + + Args: + name: string, the name of the unpicklable global to remove + """ + if name in self.unpicklable_names: + self.unpicklable_names.remove(name) + + +class FrontPageHandler(webapp.RequestHandler): + """Creates a new session and renders the shell.html template. + """ + + def get(self): + # set up the session. TODO: garbage collect old shell sessions + session_key = self.request.get('session') + if session_key: + session = Session.get(session_key) + else: + # create a new session + session = Session() + session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES] + session_key = session.put() + + template_file = os.path.join(os.path.dirname(__file__), 'templates', + 'shell.html') + session_url = '/?session=%s' % session_key + vars = { 'server_software': os.environ['SERVER_SOFTWARE'], + 'python_version': sys.version, + 'session': str(session_key), + 'user': users.get_current_user(), + 'login_url': users.create_login_url(session_url), + 'logout_url': users.create_logout_url(session_url), + } + rendered = webapp.template.render(template_file, vars, debug=_DEBUG) + self.response.out.write(rendered) + + +class StatementHandler(webapp.RequestHandler): + """Evaluates a python statement in a given session and returns the result. + """ + + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + + # extract the statement to be run + statement = self.request.get('statement') + if not statement: + return + + # the python compiler doesn't like network line endings + statement = statement.replace('\r\n', '\n') + + # add a couple newlines at the end of the statement. this makes + # single-line expressions such as 'class Foo: pass' evaluate happily. + statement += '\n\n' + + # log and compile the statement up front + try: + logging.info('Compiling and evaluating:\n%s' % statement) + compiled = compile(statement, '', 'single') + except: + self.response.out.write(traceback.format_exc()) + return + + # create a dedicated module to be used as this statement's __main__ + statement_module = new.module('__main__') + + # use this request's __builtin__, since it changes on each request. + # this is needed for import statements, among other things. + import __builtin__ + statement_module.__builtins__ = __builtin__ + + # load the session from the datastore + session = Session.get(self.request.get('session')) + + # swap in our custom module for __main__. then unpickle the session + # globals, run the statement, and re-pickle the session globals, all + # inside it. + old_main = sys.modules.get('__main__') + try: + sys.modules['__main__'] = statement_module + statement_module.__name__ = '__main__' + + # re-evaluate the unpicklables + for code in session.unpicklables: + exec code in statement_module.__dict__ + + # re-initialize the globals + for name, val in session.globals_dict().items(): + try: + statement_module.__dict__[name] = val + except: + msg = 'Dropping %s since it could not be unpickled.\n' % name + self.response.out.write(msg) + logging.warning(msg + traceback.format_exc()) + session.remove_global(name) + + # run! + old_globals = dict(statement_module.__dict__) + try: + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = self.response.out + sys.stderr = self.response.out + exec compiled in statement_module.__dict__ + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + except: + self.response.out.write(traceback.format_exc()) + return + + # extract the new globals that this statement added + new_globals = {} + for name, val in statement_module.__dict__.items(): + if name not in old_globals or val != old_globals[name]: + new_globals[name] = val + + if True in [isinstance(val, UNPICKLABLE_TYPES) + for val in new_globals.values()]: + # this statement added an unpicklable global. store the statement and + # the names of all of the globals it added in the unpicklables. + session.add_unpicklable(statement, new_globals.keys()) + logging.debug('Storing this statement as an unpicklable.') + + else: + # this statement didn't add any unpicklables. pickle and store the + # new globals back into the datastore. + for name, val in new_globals.items(): + if not name.startswith('__'): + session.set_global(name, val) + + finally: + sys.modules['__main__'] = old_main + + session.put() + + +def main(): + application = webapp.WSGIApplication( + [('/gae_shell/', FrontPageHandler), + ('/gae_shell/shell.do', StatementHandler)], debug=_DEBUG) + wsgiref.handlers.CGIHandler().run(application) + + +if __name__ == '__main__': + main() diff --git a/tools/scripts/app_engine_server/gae_shell/shell.py~ b/tools/scripts/app_engine_server/gae_shell/shell.py~ new file mode 100755 index 000000000..dee9fdb09 --- /dev/null +++ b/tools/scripts/app_engine_server/gae_shell/shell.py~ @@ -0,0 +1,308 @@ +#!/usr/bin/python +# +# Copyright 2007 Google Inc. +# +# 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. + +""" +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. + +TODO: unit tests! +""" + +import logging +import new +import os +import pickle +import sys +import traceback +import types +import wsgiref.handlers + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + + +# Set to True if stack traces should be shown in the browser, etc. +_DEBUG = True + +# The entity kind for shell sessions. Feel free to rename to suit your app. +_SESSION_KIND = '_Shell_Session' + +# Types that can't be pickled. +UNPICKLABLE_TYPES = ( + types.ModuleType, + types.TypeType, + types.ClassType, + types.FunctionType, + ) + +# Unpicklable statements to seed new sessions with. +INITIAL_UNPICKLABLES = [ + 'import logging', + 'import os', + 'import sys', + 'from google.appengine.ext import db', + 'from google.appengine.api import users', + ] + + +class Session(db.Model): + """A shell session. Stores the session's globals. + + Each session globals is stored in one of two places: + + If the global is picklable, it's stored in the parallel globals and + global_names list properties. (They're parallel lists to work around the + unfortunate fact that the datastore can't store dictionaries natively.) + + If the global is not picklable (e.g. modules, classes, and functions), or if + it was created by the same statement that created an unpicklable global, + it's not stored directly. Instead, the statement is stored in the + unpicklables list property. On each request, before executing the current + statement, the unpicklable statements are evaluated to recreate the + unpicklable globals. + + The unpicklable_names property stores all of the names of globals that were + added by unpicklable statements. When we pickle and store the globals after + executing a statement, we skip the ones in unpicklable_names. + + Using Text instead of string is an optimization. We don't query on any of + these properties, so they don't need to be indexed. + """ + global_names = db.ListProperty(db.Text) + globals = db.ListProperty(db.Blob) + unpicklable_names = db.ListProperty(db.Text) + unpicklables = db.ListProperty(db.Text) + + def set_global(self, name, value): + """Adds a global, or updates it if it already exists. + + Also removes the global from the list of unpicklable names. + + Args: + name: the name of the global to remove + value: any picklable value + """ + blob = db.Blob(pickle.dumps(value)) + + if name in self.global_names: + index = self.global_names.index(name) + self.globals[index] = blob + else: + self.global_names.append(db.Text(name)) + self.globals.append(blob) + + self.remove_unpicklable_name(name) + + def remove_global(self, name): + """Removes a global, if it exists. + + Args: + name: string, the name of the global to remove + """ + if name in self.global_names: + index = self.global_names.index(name) + del self.global_names[index] + del self.globals[index] + + def globals_dict(self): + """Returns a dictionary view of the globals. + """ + return dict((name, pickle.loads(val)) + for name, val in zip(self.global_names, self.globals)) + + def add_unpicklable(self, statement, names): + """Adds a statement and list of names to the unpicklables. + + Also removes the names from the globals. + + Args: + statement: string, the statement that created new unpicklable global(s). + names: list of strings; the names of the globals created by the statement. + """ + self.unpicklables.append(db.Text(statement)) + + for name in names: + self.remove_global(name) + if name not in self.unpicklable_names: + self.unpicklable_names.append(db.Text(name)) + + def remove_unpicklable_name(self, name): + """Removes a name from the list of unpicklable names, if it exists. + + Args: + name: string, the name of the unpicklable global to remove + """ + if name in self.unpicklable_names: + self.unpicklable_names.remove(name) + + +class FrontPageHandler(webapp.RequestHandler): + """Creates a new session and renders the shell.html template. + """ + + def get(self): + # set up the session. TODO: garbage collect old shell sessions + session_key = self.request.get('session') + if session_key: + session = Session.get(session_key) + else: + # create a new session + session = Session() + session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES] + session_key = session.put() + + template_file = os.path.join(os.path.dirname(__file__), 'templates', + 'shell.html') + session_url = '/?session=%s' % session_key + vars = { 'server_software': os.environ['SERVER_SOFTWARE'], + 'python_version': sys.version, + 'session': str(session_key), + 'user': users.get_current_user(), + 'login_url': users.create_login_url(session_url), + 'logout_url': users.create_logout_url(session_url), + } + rendered = webapp.template.render(template_file, vars, debug=_DEBUG) + self.response.out.write(rendered) + + +class StatementHandler(webapp.RequestHandler): + """Evaluates a python statement in a given session and returns the result. + """ + + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + + # extract the statement to be run + statement = self.request.get('statement') + if not statement: + return + + # the python compiler doesn't like network line endings + statement = statement.replace('\r\n', '\n') + + # add a couple newlines at the end of the statement. this makes + # single-line expressions such as 'class Foo: pass' evaluate happily. + statement += '\n\n' + + # log and compile the statement up front + try: + logging.info('Compiling and evaluating:\n%s' % statement) + compiled = compile(statement, '', 'single') + except: + self.response.out.write(traceback.format_exc()) + return + + # create a dedicated module to be used as this statement's __main__ + statement_module = new.module('__main__') + + # use this request's __builtin__, since it changes on each request. + # this is needed for import statements, among other things. + import __builtin__ + statement_module.__builtins__ = __builtin__ + + # load the session from the datastore + session = Session.get(self.request.get('session')) + + # swap in our custom module for __main__. then unpickle the session + # globals, run the statement, and re-pickle the session globals, all + # inside it. + old_main = sys.modules.get('__main__') + try: + sys.modules['__main__'] = statement_module + statement_module.__name__ = '__main__' + + # re-evaluate the unpicklables + for code in session.unpicklables: + exec code in statement_module.__dict__ + + # re-initialize the globals + for name, val in session.globals_dict().items(): + try: + statement_module.__dict__[name] = val + except: + msg = 'Dropping %s since it could not be unpickled.\n' % name + self.response.out.write(msg) + logging.warning(msg + traceback.format_exc()) + session.remove_global(name) + + # run! + old_globals = dict(statement_module.__dict__) + try: + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = self.response.out + sys.stderr = self.response.out + exec compiled in statement_module.__dict__ + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + except: + self.response.out.write(traceback.format_exc()) + return + + # extract the new globals that this statement added + new_globals = {} + for name, val in statement_module.__dict__.items(): + if name not in old_globals or val != old_globals[name]: + new_globals[name] = val + + if True in [isinstance(val, UNPICKLABLE_TYPES) + for val in new_globals.values()]: + # this statement added an unpicklable global. store the statement and + # the names of all of the globals it added in the unpicklables. + session.add_unpicklable(statement, new_globals.keys()) + logging.debug('Storing this statement as an unpicklable.') + + else: + # this statement didn't add any unpicklables. pickle and store the + # new globals back into the datastore. + for name, val in new_globals.items(): + if not name.startswith('__'): + session.set_global(name, val) + + finally: + sys.modules['__main__'] = old_main + + session.put() + + +def main(): + application = webapp.WSGIApplication( + [('/', FrontPageHandler), + ('/shell.do', StatementHandler)], debug=_DEBUG) + wsgiref.handlers.CGIHandler().run(application) + + +if __name__ == '__main__': + main() diff --git a/tools/scripts/app_engine_server/gae_shell/static/shell.js b/tools/scripts/app_engine_server/gae_shell/static/shell.js new file mode 100644 index 000000000..4aa15838a --- /dev/null +++ b/tools/scripts/app_engine_server/gae_shell/static/shell.js @@ -0,0 +1,195 @@ +// Copyright 2007 Google Inc. +// +// 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. + +/** + * @fileoverview + * Javascript code for the interactive AJAX shell. + * + * Part of http://code.google.com/p/google-app-engine-samples/. + * + * Includes a function (shell.runStatement) that sends the current python + * statement in the shell prompt text box to the server, and a callback + * (shell.done) that displays the results when the XmlHttpRequest returns. + * + * Also includes cross-browser code (shell.getXmlHttpRequest) to get an + * XmlHttpRequest. + */ + +/** + * Shell namespace. + * @type {Object} + */ +var shell = {} + +/** + * The shell history. history is an array of strings, ordered oldest to + * newest. historyCursor is the current history element that the user is on. + * + * The last history element is the statement that the user is currently + * typing. When a statement is run, it's frozen in the history, a new history + * element is added to the end of the array for the new statement, and + * historyCursor is updated to point to the new element. + * + * @type {Array} + */ +shell.history = ['']; + +/** + * See {shell.history} + * @type {number} + */ +shell.historyCursor = 0; + +/** + * A constant for the XmlHttpRequest 'done' state. + * @type Number + */ +shell.DONE_STATE = 4; + +/** + * A cross-browser function to get an XmlHttpRequest object. + * + * @return {XmlHttpRequest?} a new XmlHttpRequest + */ +shell.getXmlHttpRequest = function() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } else if (window.ActiveXObject) { + try { + return new ActiveXObject('Msxml2.XMLHTTP'); + } catch(e) { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + } + + return null; +}; + +/** + * This is the prompt textarea's onkeypress handler. Depending on the key that + * was pressed, it will run the statement, navigate the history, or update the + * current statement in the history. + * + * @param {Event} event the keypress event + * @return {Boolean} false to tell the browser not to submit the form. + */ +shell.onPromptKeyPress = function(event) { + var statement = document.getElementById('statement'); + + if (this.historyCursor == this.history.length - 1) { + // we're on the current statement. update it in the history before doing + // anything. + this.history[this.historyCursor] = statement.value; + } + + // should we pull something from the history? + if (event.ctrlKey && event.keyCode == 38 /* up arrow */) { + if (this.historyCursor > 0) { + statement.value = this.history[--this.historyCursor]; + } + return false; + } else if (event.ctrlKey && event.keyCode == 40 /* down arrow */) { + if (this.historyCursor < this.history.length - 1) { + statement.value = this.history[++this.historyCursor]; + } + return false; + } else if (!event.altKey) { + // probably changing the statement. update it in the history. + this.historyCursor = this.history.length - 1; + this.history[this.historyCursor] = statement.value; + } + + // should we submit? + var ctrlEnter = (document.getElementById('submit_key').value == 'ctrl-enter'); + if (event.keyCode == 13 /* enter */ && !event.altKey && !event.shiftKey && + event.ctrlKey == ctrlEnter) { + return this.runStatement(); + } +}; + +/** + * The XmlHttpRequest callback. If the request succeeds, it adds the command + * and its resulting output to the shell history div. + * + * @param {XmlHttpRequest} req the XmlHttpRequest we used to send the current + * statement to the server + */ +shell.done = function(req) { + if (req.readyState == this.DONE_STATE) { + var statement = document.getElementById('statement') + statement.className = 'prompt'; + + // add the command to the shell output + var output = document.getElementById('output'); + + output.value += '\n>>> ' + statement.value; + statement.value = ''; + + // add a new history element + this.history.push(''); + this.historyCursor = this.history.length - 1; + + // add the command's result + var result = req.responseText.replace(/^\s*|\s*$/g, ''); // trim whitespace + if (result != '') + output.value += '\n' + result; + + // scroll to the bottom + output.scrollTop = output.scrollHeight; + if (output.createTextRange) { + var range = output.createTextRange(); + range.collapse(false); + range.select(); + } + } +}; + +/** + * This is the form's onsubmit handler. It sends the python statement to the + * server, and registers shell.done() as the callback to run when it returns. + * + * @return {Boolean} false to tell the browser not to submit the form. + */ +shell.runStatement = function() { + var form = document.getElementById('form'); + + // build a XmlHttpRequest + var req = this.getXmlHttpRequest(); + if (!req) { + document.getElementById('ajax-status').innerHTML = + "Your browser doesn't support AJAX. :("; + return false; + } + + req.onreadystatechange = function() { shell.done(req); }; + + // build the query parameter string + var params = ''; + for (i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type != 'submit' && elem.type != 'button' && elem.id != 'caret') { + var value = escape(elem.value).replace(/\+/g, '%2B'); // escape ignores + + params += '&' + elem.name + '=' + value; + } + } + + // send the request and tell the user. + document.getElementById('statement').className = 'prompt processing'; + req.open(form.method, form.action + '?' + params, true); + req.setRequestHeader('Content-type', + 'application/x-www-form-urlencoded;charset=UTF-8'); + req.send(null); + + return false; +}; diff --git a/tools/scripts/app_engine_server/gae_shell/static/spinner.gif b/tools/scripts/app_engine_server/gae_shell/static/spinner.gif new file mode 100644 index 000000000..3e58d6e83 Binary files /dev/null and b/tools/scripts/app_engine_server/gae_shell/static/spinner.gif differ diff --git a/tools/scripts/app_engine_server/gae_shell/templates/shell.html b/tools/scripts/app_engine_server/gae_shell/templates/shell.html new file mode 100644 index 000000000..123b2009f --- /dev/null +++ b/tools/scripts/app_engine_server/gae_shell/templates/shell.html @@ -0,0 +1,122 @@ + + + + + Interactive Shell + + + + + + +

      Interactive server-side Python shell for +Google App Engine. +(source) +

      + + + +
      + + + + + + +
      + +

      + +

      +{% if user %} + {{ user.nickname }} + (log out) +{% else %} + log in +{% endif %} + | Ctrl-Up/Down for history | + + +

      + + + + + + diff --git a/tools/scripts/app_engine_server/index.yaml b/tools/scripts/app_engine_server/index.yaml new file mode 100644 index 000000000..8e6046de9 --- /dev/null +++ b/tools/scripts/app_engine_server/index.yaml @@ -0,0 +1,12 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + diff --git a/tools/scripts/app_engine_server/memcache_zipserve.py b/tools/scripts/app_engine_server/memcache_zipserve.py new file mode 100644 index 000000000..e11cfc544 --- /dev/null +++ b/tools/scripts/app_engine_server/memcache_zipserve.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# Copyright 2009 Google Inc. +# +# 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. +# + +"""A class to serve pages from zip files and use memcache for performance. + +This contains a class and a function to create an anonymous instance of the +class to serve HTTP GET requests. Memcache is used to increase response speed +and lower processing cycles used in serving. Credit to Guido van Rossum and +his implementation of zipserve which served as a reference as I wrote this. + + MemcachedZipHandler: Class that serves request + create_handler: method to create instance of MemcachedZipHandler +""" + +__author__ = 'jmatt@google.com (Justin Mattson)' + +import email.Utils +import logging +import mimetypes +import time +import zipfile + +from google.appengine.api import memcache +from google.appengine.ext import webapp +from google.appengine.ext.webapp import util + + +def create_handler(zip_files, max_age=None, public=None): + """Factory method to create a MemcachedZipHandler instance. + + Args: + zip_files: A list of file names, or a list of lists of file name, first + member of file mappings. See MemcachedZipHandler documentation for + more information about using the list of lists format + max_age: The maximum client-side cache lifetime + public: Whether this should be declared public in the client-side cache + Returns: + A MemcachedZipHandler wrapped in a pretty, anonymous bow for use with App + Engine + + Raises: + ValueError: if the zip_files argument is not a list + """ + # verify argument integrity. If the argument is passed in list format, + # convert it to list of lists format + + if zip_files and type(zip_files).__name__ == 'list': + num_items = len(zip_files) + while num_items > 0: + if type(zip_files[num_items - 1]).__name__ != 'list': + zip_files[num_items - 1] = [zip_files[num_items-1]] + num_items -= 1 + else: + raise ValueError('File name arguments must be a list') + + class HandlerWrapper(MemcachedZipHandler): + """Simple wrapper for an instance of MemcachedZipHandler. + + I'm still not sure why this is needed + """ + + def get(self, name): + self.zipfilenames = zip_files + self.TrueGet(name) + if max_age is not None: + MAX_AGE = max_age + if public is not None: + PUBLIC = public + + return HandlerWrapper + + +class MemcachedZipHandler(webapp.RequestHandler): + """Handles get requests for a given URL. + + Serves a GET request from a series of zip files. As files are served they are + put into memcache, which is much faster than retreiving them from the zip + source file again. It also uses considerably fewer CPU cycles. + """ + zipfile_cache = {} # class cache of source zip files + MAX_AGE = 600 # max client-side cache lifetime + PUBLIC = True # public cache setting + CACHE_PREFIX = 'cache://' # memcache key prefix for actual URLs + NEG_CACHE_PREFIX = 'noncache://' # memcache key prefix for non-existant URL + + def TrueGet(self, name): + """The top-level entry point to serving requests. + + Called 'True' get because it does the work when called from the wrapper + class' get method + + Args: + name: URL requested + + Returns: + None + """ + name = self.PreprocessUrl(name) + + # see if we have the page in the memcache + resp_data = self.GetFromCache(name) + if resp_data is None: + logging.info('Cache miss for %s', name) + resp_data = self.GetFromNegativeCache(name) + if resp_data is None: + resp_data = self.GetFromStore(name) + + # IF we have the file, put it in the memcache + # ELSE put it in the negative cache + if resp_data is not None: + self.StoreOrUpdateInCache(name, resp_data) + else: + logging.info('Adding %s to negative cache, serving 404', name) + self.StoreInNegativeCache(name) + self.Write404Error() + return + else: + self.Write404Error() + return + + content_type, encoding = mimetypes.guess_type(name) + if content_type: + self.response.headers['Content-Type'] = content_type + self.SetCachingHeaders() + self.response.out.write(resp_data) + + def PreprocessUrl(self, name): + """Any preprocessing work on the URL when it comes it. + + Put any work related to interpretting the incoming URL here. For example, + this is used to redirect requests for a directory to the index.html file + in that directory. Subclasses should override this method to do different + preprocessing. + + Args: + name: The incoming URL + + Returns: + The processed URL + """ + # handle special case of requesting the domain itself + if not name: + name = 'index.html' + + # determine if this is a request for a directory + final_path_segment = name + final_slash_offset = name.rfind('/') + if final_slash_offset != len(name) - 1: + final_path_segment = name[final_slash_offset + 1:] + if final_path_segment.find('.') == -1: + name = ''.join([name, '/']) + + # if this is a directory, redirect to index.html + if name[len(name) - 1:] == '/': + return '%s%s' % (name, 'index.html') + else: + return name + + def GetFromStore(self, file_path): + """Retrieve file from zip files. + + Get the file from the source, it must not have been in the memcache. If + possible, we'll use the zip file index to quickly locate where the file + should be found. (See MapToFileArchive documentation for assumptions about + file ordering.) If we don't have an index or don't find the file where the + index says we should, look through all the zip files to find it. + + Args: + file_path: the file that we're looking for + + Returns: + The contents of the requested file + """ + resp_data = None + file_itr = iter(self.zipfilenames) + + # check the index, if we have one, to see what archive the file is in + archive_name = self.MapFileToArchive(file_path) + if not archive_name: + archive_name = file_itr.next()[0] + + while resp_data is None and archive_name: + zip_archive = self.LoadZipFile(archive_name) + if zip_archive: + + # we expect some lookups will fail, and that's okay, 404s will deal + # with that + try: + resp_data = zip_archive.read(file_path) + except (KeyError, RuntimeError), err: + # no op + x = False + if resp_data is not None: + logging.info('%s read from %s', file_path, archive_name) + + try: + archive_name = file_itr.next()[0] + except (StopIteration), err: + archive_name = False + + return resp_data + + def LoadZipFile(self, zipfilename): + """Convenience method to load zip file. + + Just a convenience method to load the zip file from the data store. This is + useful if we ever want to change data stores and also as a means of + dependency injection for testing. This method will look at our file cache + first, and then load and cache the file if there's a cache miss + + Args: + zipfilename: the name of the zip file to load + + Returns: + The zip file requested, or None if there is an I/O error + """ + zip_archive = None + zip_archive = self.zipfile_cache.get(zipfilename) + if zip_archive is None: + try: + zip_archive = zipfile.ZipFile(zipfilename) + self.zipfile_cache[zipfilename] = zip_archive + except (IOError, RuntimeError), err: + logging.error('Can\'t open zipfile %s, cause: %s' % (zipfilename, + err)) + return zip_archive + + def MapFileToArchive(self, file_path): + """Given a file name, determine what archive it should be in. + + This method makes two critical assumptions. + (1) The zip files passed as an argument to the handler, if concatenated + in that same order, would result in a total ordering + of all the files. See (2) for ordering type. + (2) Upper case letters before lower case letters. The traversal of a + directory tree is depth first. A parent directory's files are added + before the files of any child directories + + Args: + file_path: the file to be mapped to an archive + + Returns: + The name of the archive where we expect the file to be + """ + num_archives = len(self.zipfilenames) + while num_archives > 0: + target = self.zipfilenames[num_archives - 1] + if len(target) > 1: + if self.CompareFilenames(target[1], file_path) >= 0: + return target[0] + num_archives -= 1 + + return None + + def CompareFilenames(self, file1, file2): + """Determines whether file1 is lexigraphically 'before' file2. + + WARNING: This method assumes that paths are output in a depth-first, + with parent directories' files stored before childs' + + We say that file1 is lexigraphically before file2 if the last non-matching + path segment of file1 is alphabetically before file2. + + Args: + file1: the first file path + file2: the second file path + + Returns: + A positive number if file1 is before file2 + A negative number if file2 is before file1 + 0 if filenames are the same + """ + f1_segments = file1.split('/') + f2_segments = file2.split('/') + + segment_ptr = 0 + while (segment_ptr < len(f1_segments) and + segment_ptr < len(f2_segments) and + f1_segments[segment_ptr] == f2_segments[segment_ptr]): + segment_ptr += 1 + + if len(f1_segments) == len(f2_segments): + + # we fell off the end, the paths much be the same + if segment_ptr == len(f1_segments): + return 0 + + # we didn't fall of the end, compare the segments where they differ + if f1_segments[segment_ptr] < f2_segments[segment_ptr]: + return 1 + elif f1_segments[segment_ptr] > f2_segments[segment_ptr]: + return -1 + else: + return 0 + + # the number of segments differs, we either mismatched comparing + # directories, or comparing a file to a directory + else: + + # IF we were looking at the last segment of one of the paths, + # the one with fewer segments is first because files come before + # directories + # ELSE we just need to compare directory names + if (segment_ptr + 1 == len(f1_segments) or + segment_ptr + 1 == len(f2_segments)): + return len(f2_segments) - len(f1_segments) + else: + if f1_segments[segment_ptr] < f2_segments[segment_ptr]: + return 1 + elif f1_segments[segment_ptr] > f2_segments[segment_ptr]: + return -1 + else: + return 0 + + def SetCachingHeaders(self): + """Set caching headers for the request.""" + max_age = self.MAX_AGE + self.response.headers['Expires'] = email.Utils.formatdate( + time.time() + max_age, usegmt=True) + cache_control = [] + if self.PUBLIC: + cache_control.append('public') + cache_control.append('max-age=%d' % max_age) + self.response.headers['Cache-Control'] = ', '.join(cache_control) + + def GetFromCache(self, filename): + """Get file from memcache, if available. + + Args: + filename: The URL of the file to return + + Returns: + The content of the file + """ + return memcache.get('%s%s' % (self.CACHE_PREFIX, filename)) + + def StoreOrUpdateInCache(self, filename, data): + """Store data in the cache. + + Store a piece of data in the memcache. Memcache has a maximum item size of + 1*10^6 bytes. If the data is too large, fail, but log the failure. Future + work will consider compressing the data before storing or chunking it + + Args: + filename: the name of the file to store + data: the data of the file + + Returns: + None + """ + try: + if not memcache.add('%s%s' % (self.CACHE_PREFIX, filename), data): + memcache.replace('%s%s' % (self.CACHE_PREFIX, filename), data) + except (ValueError), err: + logging.warning('Data size too large to cache\n%s' % err) + + def Write404Error(self): + """Ouptut a simple 404 response.""" + self.error(404) + self.response.out.write( + ''.join(['404: Not Found', + '

      Error 404


      ', + 'File not found
      '])) + + def StoreInNegativeCache(self, filename): + """If a non-existant URL is accessed, cache this result as well. + + Future work should consider setting a maximum negative cache size to + prevent it from from negatively impacting the real cache. + + Args: + filename: URL to add ot negative cache + + Returns: + None + """ + memcache.add('%s%s' % (self.NEG_CACHE_PREFIX, filename), -1) + + def GetFromNegativeCache(self, filename): + """Retrieve from negative cache. + + Args: + filename: URL to retreive + + Returns: + The file contents if present in the negative cache. + """ + return memcache.get('%s%s' % (self.NEG_CACHE_PREFIX, filename)) + + +def main(): + application = webapp.WSGIApplication([('/([^/]+)/(.*)', + MemcachedZipHandler)]) + util.run_wsgi_app(application) + + +if __name__ == '__main__': + main() diff --git a/tools/scripts/build.alias.template b/tools/scripts/build.alias.template index f7de2e889..b60529599 100644 --- a/tools/scripts/build.alias.template +++ b/tools/scripts/build.alias.template @@ -1,4 +1,4 @@ - + - + + diff --git a/tools/scripts/combine_sdks.sh b/tools/scripts/combine_sdks.sh index 89a1141aa..ebaa1c6ee 100755 --- a/tools/scripts/combine_sdks.sh +++ b/tools/scripts/combine_sdks.sh @@ -2,22 +2,36 @@ function replace() { - echo replacing $1 - rm -rf $UNZIPPED_BASE_DIR/$1 - cp -rf $UNZIPPED_IMAGE_DIR/$1 $UNZIPPED_BASE_DIR/$1 + echo replacing $1 + rm $V -rf "$UNZIPPED_BASE_DIR"/$1 + cp $V -rf "$UNZIPPED_IMAGE_DIR"/$1 "$UNZIPPED_BASE_DIR"/$1 } -BASE=$1 -IMAGES=$2 -OUTPUT=$3 +V="" +Q="-q" +if [ "$1" == "-v" ]; then + V="-v" + Q="" + shift +fi -if [[ -z $BASE || -z $IMAGES || -z $OUTPUT ]] ; then - echo "usage: combine_sdks.sh BASE IMAGES OUTPUT" +NOZIP="" +if [ "$1" == "-nozip" ]; then + NOZIP="1" + shift +fi + +BASE="$1" +IMAGES="$2" +OUTPUT="$3" + +if [[ -z "$BASE" || -z "$IMAGES" || -z "$OUTPUT" ]] ; then + echo "usage: combine_sdks.sh [-v] [-nozip] BASE IMAGES OUTPUT" echo echo " BASE and IMAGES should be sdk zip files. The system image files," echo " emulator and other runtime files will be copied from IMAGES and" echo " everything else will be copied from BASE. All of this will be" - echo " bundled into OUTPUT and zipped up again." + echo " bundled into OUTPUT and zipped up again (unless -nozip is specified)." echo exit 1 fi @@ -26,15 +40,25 @@ TMP=$(mktemp -d) TMP_ZIP=tmp.zip -BASE_DIR=$TMP/base -IMAGES_DIR=$TMP/images -OUTPUT_TMP_ZIP=$BASE_DIR/$TMP_ZIP +# determine executable extension +case `uname -s` in + *_NT-*) # for Windows + EXE=.exe + ;; + *) + EXE= + ;; +esac -unzip -q $BASE -d $BASE_DIR -unzip -q $IMAGES -d $IMAGES_DIR +BASE_DIR="$TMP"/base +IMAGES_DIR="$TMP"/images +OUTPUT_TMP_ZIP="$BASE_DIR/$TMP_ZIP" -UNZIPPED_BASE_DIR=$(echo $BASE_DIR/*) -UNZIPPED_IMAGE_DIR=$(echo $IMAGES_DIR/*) +unzip $Q "$BASE" -d "$BASE_DIR" +unzip $Q "$IMAGES" -d "$IMAGES_DIR" + +UNZIPPED_BASE_DIR=$(echo "$BASE_DIR"/*) +UNZIPPED_IMAGE_DIR=$(echo "$IMAGES_DIR"/*) # # The commands to copy over the files that we want @@ -42,21 +66,40 @@ UNZIPPED_IMAGE_DIR=$(echo $IMAGES_DIR/*) # replace tools/emulator # at this time we do not want the exe from SDK1.x replace tools/lib/images +replace tools/lib/res +replace tools/lib/fonts +replace tools/lib/layoutlib.jar replace docs replace android.jar +for i in widgets categories broadcast_actions service_actions; do + replace tools/lib/$i.txt +done + +if [ -d "$UNZIPPED_BASE_DIR"/usb_driver ]; then + replace usb_driver +fi + # # end # -pushd $BASE_DIR &> /dev/null - # rename the directory to the leaf minus the .zip of OUTPUT - LEAF=$(echo $OUTPUT | sed -e "s:.*\.zip/::" | sed -e "s:.zip$::") - mv * $LEAF - # zip it - zip -qr $TMP_ZIP $LEAF -popd &> /dev/null +if [ -z "$NOZIP" ]; then + pushd "$BASE_DIR" &> /dev/null + # rename the directory to the leaf minus the .zip of OUTPUT + LEAF=$(echo "$OUTPUT" | sed -e "s:.*\.zip/::" | sed -e "s:.zip$::") + mv * "$LEAF" + # zip it + zip $V -qr "$TMP_ZIP" "$LEAF" + popd &> /dev/null -cp $OUTPUT_TMP_ZIP $OUTPUT + cp $V "$OUTPUT_TMP_ZIP" "$OUTPUT" + echo "Combined SDK available at $OUTPUT" +else + OUT_DIR="${OUTPUT//.zip/}" + mv $V "$BASE_DIR"/* "$OUT_DIR" + echo "Unzipped combined SDK available at $OUT_DIR" +fi + +rm $V -rf "$TMP" -rm -rf $TMP diff --git a/tools/sdkmanager/app/etc/android.bat b/tools/sdkmanager/app/etc/android.bat index 1af1e4788..de950edd3 100755 --- a/tools/sdkmanager/app/etc/android.bat +++ b/tools/sdkmanager/app/etc/android.bat @@ -23,9 +23,9 @@ set prog=%~f0 rem Grab current directory before we change it set workdir=%cd% -rem Change current directory to where ddms is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=sdkmanager.jar set frameworkdir= diff --git a/tools/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java b/tools/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java index 2db668ef5..9f3fb996e 100644 --- a/tools/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java +++ b/tools/sdkmanager/app/src/com/android/sdkmanager/CommandLineProcessor.java @@ -31,52 +31,67 @@ import java.util.Map.Entry; *
    • define flags for your actions. *
    *

    - * To use, call {@link #parseArgs(String[])} and then call {@link #getValue(String, String)}. + * To use, call {@link #parseArgs(String[])} and then + * call {@link #getValue(String, String, String)}. */ public class CommandLineProcessor { - - /** Internal action name for all global flags. */ - public final static String GLOBAL_FLAG = "global"; - /** Internal action name for internally hidden flags. - * This is currently used to store the requested action name. */ - public final static String INTERNAL_FLAG = "internal"; + /** Internal verb name for internally hidden flags. */ + public final static String GLOBAL_FLAG_VERB = "@@internal@@"; + + /** String to use when the verb doesn't need any object. */ + public final static String NO_VERB_OBJECT = ""; + /** The global help flag. */ public static final String KEY_HELP = "help"; /** The global verbose flag. */ public static final String KEY_VERBOSE = "verbose"; /** The global silent flag. */ public static final String KEY_SILENT = "silent"; - /** The internal action flag. */ - public static final String KEY_ACTION = "action"; + + /** Verb requested by the user. Null if none specified, which will be an error. */ + private String mVerbRequested; + /** Direct object requested by the user. Can be null. */ + private String mDirectObjectRequested; - /** List of available actions. + /** + * Action definitions. *

    - * Each entry must be a 2-string array with first the action name and then - * a description. + * Each entry is a string array with: + *

      + *
    • the verb. + *
    • a direct object (use #NO_VERB_OBJECT if there's no object). + *
    • a description. + *
    • an alternate form for the object (e.g. plural). + *
    */ private final String[][] mActions; - /** The hash of all defined arguments. + + private static final int ACTION_VERB_INDEX = 0; + private static final int ACTION_OBJECT_INDEX = 1; + private static final int ACTION_DESC_INDEX = 2; + private static final int ACTION_ALT_OBJECT_INDEX = 3; + + /** + * The map of all defined arguments. *

    - * The key is a string "action/longName". + * The key is a string "verb/directObject/longName". */ private final HashMap mArguments = new HashMap(); + /** Logger */ private final ISdkLog mLog; public CommandLineProcessor(ISdkLog logger, String[][] actions) { mLog = logger; mActions = actions; - define(MODE.STRING, false, INTERNAL_FLAG, null, KEY_ACTION, - "Selected Action", null); - - define(MODE.BOOLEAN, false, GLOBAL_FLAG, "v", KEY_VERBOSE, + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE, "Verbose mode: errors, warnings and informational messages are printed.", false); - define(MODE.BOOLEAN, false, GLOBAL_FLAG, "s", KEY_SILENT, + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT, "Silent mode: only errors are printed out.", false); - define(MODE.BOOLEAN, false, GLOBAL_FLAG, "h", KEY_HELP, + define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP, "This help.", false); } @@ -86,47 +101,88 @@ public class CommandLineProcessor { /** Helper that returns true if --verbose was requested. */ public boolean isVerbose() { - return ((Boolean) getValue(GLOBAL_FLAG, KEY_VERBOSE)).booleanValue(); + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue(); } /** Helper that returns true if --silent was requested. */ public boolean isSilent() { - return ((Boolean) getValue(GLOBAL_FLAG, KEY_SILENT)).booleanValue(); + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue(); } /** Helper that returns true if --help was requested. */ public boolean isHelpRequested() { - return ((Boolean) getValue(GLOBAL_FLAG, KEY_HELP)).booleanValue(); + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue(); + } + + /** Returns the verb name from the command-line. Can be null. */ + public String getVerb() { + return mVerbRequested; } - /** Helper that returns the requested action name. */ - public String getActionRequested() { - return (String) getValue(INTERNAL_FLAG, KEY_ACTION); + /** Returns the direct object name from the command-line. Can be null. */ + public String getDirectObject() { + return mDirectObjectRequested; } //------------------ /** * Raw access to parsed parameter values. - * @param action The action name, including {@link #GLOBAL_FLAG} and {@link #INTERNAL_FLAG} - * @param longFlagName The long flag name for the given action. + *

    + * The default is to scan all parameters. Parameters that have been explicitly set on the + * command line are returned first. Otherwise one with a non-null value is returned. + *

    + * Both a verb and a direct object filter can be specified. When they are non-null they limit + * the scope of the search. + *

    + * If nothing has been found, return the last default value seen matching the filter. + * + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible + * verbs that match the direct object condition will be examined and the first + * value set will be used. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null, + * all possible direct objects that match the verb condition will be examined and + * the first value set will be used. + * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null. * @return The current value object stored in the parameter, which depends on the argument mode. */ - public Object getValue(String action, String longFlagName) { - String key = action + "/" + longFlagName; - Arg arg = mArguments.get(key); - return arg.getCurrentValue(); + public Object getValue(String verb, String directObject, String longFlagName) { + + if (verb != null && directObject != null) { + String key = verb + "/" + directObject + "/" + longFlagName; + Arg arg = mArguments.get(key); + return arg.getCurrentValue(); + } + + Object lastDefault = null; + for (Arg arg : mArguments.values()) { + if (arg.getLongArg().equals(longFlagName)) { + if (verb == null || arg.getVerb().equals(verb)) { + if (directObject == null || arg.getDirectObject().equals(directObject)) { + if (arg.isInCommandLine()) { + return arg.getCurrentValue(); + } + if (arg.getCurrentValue() != null) { + lastDefault = arg.getCurrentValue(); + } + } + } + } + } + + return lastDefault; } /** * Internal setter for raw parameter value. - * @param action The action name, including {@link #GLOBAL_FLAG} and {@link #INTERNAL_FLAG} + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. * @param longFlagName The long flag name for the given action. * @param value The new current value object stored in the parameter, which depends on the * argument mode. */ - protected void setValue(String action, String longFlagName, Object value) { - String key = action + "/" + longFlagName; + protected void setValue(String verb, String directObject, String longFlagName, Object value) { + String key = verb + "/" + directObject + "/" + longFlagName; Arg arg = mArguments.get(key); arg.setCurrentValue(value); } @@ -140,114 +196,175 @@ public class CommandLineProcessor { */ public void parseArgs(String[] args) { String needsHelp = null; - String action = null; - - int n = args.length; - for (int i = 0; i < n; i++) { - Arg arg = null; - String a = args[i]; - if (a.startsWith("--")) { - arg = findLongArg(action, a.substring(2)); - } else if (a.startsWith("-")) { - arg = findShortArg(action, a.substring(1)); - } - - // Not a keyword and we don't have an action yet, this should be an action - if (arg == null && action == null) { + String verb = null; + String directObject = null; - if (a.startsWith("-")) { - // Got a keyword but not valid for global flags - needsHelp = String.format( - "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the action name?", - a, action); - break; - } - - for (String[] actionDesc : mActions) { - if (actionDesc[0].equals(a)) { - action = a; - break; - } + try { + int n = args.length; + for (int i = 0; i < n; i++) { + Arg arg = null; + String a = args[i]; + if (a.startsWith("--")) { + arg = findLongArg(verb, directObject, a.substring(2)); + } else if (a.startsWith("-")) { + arg = findShortArg(verb, directObject, a.substring(1)); } - if (action == null) { - needsHelp = String.format( - "Expected action name after global parameters but found %1$s instead.", - a); - break; - } - } else if (arg == null && action != null) { - // Got a keyword but not valid for the current action - needsHelp = String.format( - "Flag '%1$s' is not valid for action '%2$s'.", - a, action); - break; - - } else if (arg != null) { - // Process keyword - String error = null; - if (arg.getMode().needsExtra()) { - if (++i >= n) { - needsHelp = String.format("Missing argument for flag %1$s.", a); - break; + // No matching argument name found + if (arg == null) { + // Does it looks like a dashed parameter? + if (a.startsWith("-")) { + if (verb == null || directObject == null) { + // It looks like a dashed parameter and we don't have a a verb/object + // set yet, the parameter was just given too early. + + needsHelp = String.format( + "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?", + a); + return; + } else { + // It looks like a dashed parameter and but it is unknown by this + // verb-object combination + + needsHelp = String.format( + "Flag '%1$s' is not valid for '%2$s %3$s'.", + a, verb, directObject); + return; + } } - error = arg.getMode().process(arg, args[i]); - } else { - error = arg.getMode().process(arg, null); - - // If we just toggled help, we want to exit now without printing any error. - // We do this test here only when a Boolean flag is toggled since booleans - // are the only flags that don't take parameters and help is a boolean. - if (isHelpRequested()) { - printHelpAndExit(null); - // The call above should terminate however in unit tests we override - // it so we still need to return here. + if (verb == null) { + // Fill verb first. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(a)) { + verb = a; + break; + } + } + + // Error if it was not a valid verb + if (verb == null) { + needsHelp = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + } + + } else if (directObject == null) { + // Then fill the direct object. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb)) { + if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) { + directObject = a; + break; + } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX && + actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) { + // if the alternate form exist and is used, we internally + // only memorize the default direct object form. + directObject = actionDesc[ACTION_OBJECT_INDEX]; + break; + } + } + } + + // Error if it was not a valid object for that verb + if (directObject == null) { + needsHelp = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + + } + } + } else if (arg != null) { + // This argument was present on the command line + arg.setInCommandLine(true); + + // Process keyword + String error = null; + if (arg.getMode().needsExtra()) { + if (++i >= n) { + needsHelp = String.format("Missing argument for flag %1$s.", a); + return; + } + + error = arg.getMode().process(arg, args[i]); + } else { + error = arg.getMode().process(arg, null); + + // If we just toggled help, we want to exit now without printing any error. + // We do this test here only when a Boolean flag is toggled since booleans + // are the only flags that don't take parameters and help is a boolean. + if (isHelpRequested()) { + printHelpAndExit(null); + // The call above should terminate however in unit tests we override + // it so we still need to return here. + return; + } + } + + if (error != null) { + needsHelp = String.format("Invalid usage for flag %1$s: %2$s.", a, error); return; } } - - if (error != null) { - needsHelp = String.format("Invalid usage for flag %1$s: %2$s.", a, error); - break; - } } - } - if (needsHelp == null) { - if (action == null) { - needsHelp = "Missing action name."; - } else { - // Validate that all mandatory arguments are non-null for this action - String missing = null; - boolean plural = false; - for (Entry entry : mArguments.entrySet()) { - Arg arg = entry.getValue(); - if (arg.getAction().equals(action)) { - if (arg.isMandatory() && arg.getCurrentValue() == null) { - if (missing == null) { - missing = "--" + arg.getLongArg(); - } else { - missing += ", --" + arg.getLongArg(); - plural = true; + if (needsHelp == null) { + if (verb == null) { + needsHelp = "Missing verb name."; + } else { + if (directObject == null) { + // Make sure this verb has an optional direct object + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb) && + actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) { + directObject = NO_VERB_OBJECT; + break; + } + } + + if (directObject == null) { + needsHelp = String.format("Missing object name for verb '%1$s'.", verb); + return; + } + } + + // Validate that all mandatory arguments are non-null for this action + String missing = null; + boolean plural = false; + for (Entry entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && + arg.getDirectObject().equals(directObject)) { + if (arg.isMandatory() && arg.getCurrentValue() == null) { + if (missing == null) { + missing = "--" + arg.getLongArg(); + } else { + missing += ", --" + arg.getLongArg(); + plural = true; + } } } } - } + + if (missing != null) { + needsHelp = String.format( + "The %1$s %2$s must be defined for action '%3$s %4$s'", + plural ? "parameters" : "parameter", + missing, + verb, + directObject); + } - if (missing != null) { - needsHelp = String.format("The %1$s %2$s must be defined for action '%3$s'", - plural ? "parameters" : "parameter", - missing, - action); + mVerbRequested = verb; + mDirectObjectRequested = directObject; } - - setValue(INTERNAL_FLAG, KEY_ACTION, action); } - } - - if (needsHelp != null) { - printHelpAndExitForAction(action, needsHelp); + } finally { + if (needsHelp != null) { + printHelpAndExitForAction(verb, directObject, needsHelp); + } } } @@ -255,11 +372,14 @@ public class CommandLineProcessor { * Finds an {@link Arg} given an action name and a long flag name. * @return The {@link Arg} found or null. */ - protected Arg findLongArg(String action, String longName) { - if (action == null) { - action = GLOBAL_FLAG; + protected Arg findLongArg(String verb, String directObject, String longName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; } - String key = action + "/" + longName; + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + String key = verb + "/" + directObject + "/" + longName; return mArguments.get(key); } @@ -267,14 +387,17 @@ public class CommandLineProcessor { * Finds an {@link Arg} given an action name and a short flag name. * @return The {@link Arg} found or null. */ - protected Arg findShortArg(String action, String shortName) { - if (action == null) { - action = GLOBAL_FLAG; + protected Arg findShortArg(String verb, String directObject, String shortName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; + } + if (directObject == null) { + directObject = NO_VERB_OBJECT; } for (Entry entry : mArguments.entrySet()) { Arg arg = entry.getValue(); - if (arg.getAction().equals(action)) { + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { if (shortName.equals(arg.getShortArg())) { return arg; } @@ -291,18 +414,22 @@ public class CommandLineProcessor { * @param args Arguments for String.format */ public void printHelpAndExit(String errorFormat, Object... args) { - printHelpAndExitForAction(null /*actionFilter*/, errorFormat, args); + printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args); } /** * Prints the help/usage and exits. * - * @param actionFilter If null, displays help for all actions. If not null, display help only - * for that specific action. In all cases also display general usage and action list. + * @param verb If null, displays help for all verbs. If not null, display help only + * for that specific verb. In all cases also displays general usage and action list. + * @param directObject If null, displays help for all verb objects. + * If not null, displays help only for that specific action + * In all cases also display general usage and action list. * @param errorFormat Optional error message to print prior to usage using String.format * @param args Arguments for String.format */ - public void printHelpAndExitForAction(String actionFilter, String errorFormat, Object... args) { + public void printHelpAndExitForAction(String verb, String directObject, + String errorFormat, Object... args) { if (errorFormat != null) { stderr(errorFormat, args); } @@ -316,25 +443,29 @@ public class CommandLineProcessor { " android [global options] action [action options]\n" + "\n" + "Global options:"); - listOptions(GLOBAL_FLAG); + listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT); - stdout("\nValid actions:"); - for (String[] action : mActions) { - String filler = ""; - int len = action[0].length(); - if (len < 10) { - filler = " ".substring(len); + if (verb == null || directObject == null) { + stdout("\nValid actions are composed of a verb and an optional direct object:"); + for (String[] action : mActions) { + + stdout("- %1$6s %2$-7s: %3$s", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX], + action[ACTION_DESC_INDEX]); } - - stdout("- %1$s:%2$s %3$s", action[0], filler, action[1]); } for (String[] action : mActions) { - if (actionFilter == null || actionFilter.equals(action[0])) { - stdout("\nAction \"%1$s\":", action[0]); - stdout(" %1$s", action[1]); - stdout("Options:"); - listOptions(action[0]); + if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { + if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) { + stdout("\nAction \"%1$s %2$s\":", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX]); + stdout(" %1$s", action[ACTION_DESC_INDEX]); + stdout("Options:"); + listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]); + } } } @@ -344,11 +475,11 @@ public class CommandLineProcessor { /** * Internal helper to print all the option flags for a given action name. */ - protected void listOptions(String action) { + protected void listOptions(String verb, String directObject) { int numOptions = 0; for (Entry entry : mArguments.entrySet()) { Arg arg = entry.getValue(); - if (arg.getAction().equals(action)) { + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { String value = ""; if (arg.getDefaultValue() instanceof String[]) { @@ -483,21 +614,33 @@ public class CommandLineProcessor { * or a String array (in which case the first item is the current by default.) */ static class Arg { - private final String mAction; + /** Verb for that argument. Never null. */ + private final String mVerb; + /** Direct Object for that argument. Never null, but can be empty string. */ + private final String mDirectObject; + /** The 1-letter short name of the argument, e.g. -v. */ private final String mShortName; + /** The long name of the argument, e.g. --verbose. */ private final String mLongName; + /** A description. Never null. */ private final String mDescription; + /** A default value. Can be null. */ private final Object mDefaultValue; - private Object mCurrentValue; + /** The argument mode (type + process method). Never null. */ private final MODE mMode; + /** True if this argument is mandatory for this verb/directobject. */ private final boolean mMandatory; + /** Current value. Initially set to the default value. */ + private Object mCurrentValue; + /** True if the argument has been used on the command line. */ + private boolean mInCommandLine; /** * Creates a new argument flag description. * * @param mode The {@link MODE} for the argument. * @param mandatory True if this argument is mandatory for this action. - * @param action The action name. Can be #GLOBAL_FLAG or #INTERNAL_FLAG. + * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG. * @param shortName The one-letter short argument name. Cannot be empty nor null. * @param longName The long argument name. Cannot be empty nor null. * @param description The description. Cannot be null. @@ -505,18 +648,21 @@ public class CommandLineProcessor { */ public Arg(MODE mode, boolean mandatory, - String action, + String verb, + String directObject, String shortName, String longName, String description, Object defaultValue) { mMode = mode; mMandatory = mandatory; - mAction = action; + mVerb = verb; + mDirectObject = directObject; mShortName = shortName; mLongName = longName; mDescription = description; mDefaultValue = defaultValue; + mInCommandLine = false; if (defaultValue instanceof String[]) { mCurrentValue = ((String[])defaultValue)[0]; } else { @@ -524,48 +670,73 @@ public class CommandLineProcessor { } } + /** Return true if this argument is mandatory for this verb/directobject. */ public boolean isMandatory() { return mMandatory; } + /** Returns the 1-letter short name of the argument, e.g. -v. */ public String getShortArg() { return mShortName; } + /** Returns the long name of the argument, e.g. --verbose. */ public String getLongArg() { return mLongName; } + /** Returns the description. Never null. */ public String getDescription() { return mDescription; } - public String getAction() { - return mAction; + /** Returns the verb for that argument. Never null. */ + public String getVerb() { + return mVerb; + } + + /** Returns the direct Object for that argument. Never null, but can be empty string. */ + public String getDirectObject() { + return mDirectObject; } + /** Returns the default value. Can be null. */ public Object getDefaultValue() { return mDefaultValue; } + /** Returns the current value. Initially set to the default value. Can be null. */ public Object getCurrentValue() { return mCurrentValue; } + /** Sets the current value. Can be null. */ public void setCurrentValue(Object currentValue) { mCurrentValue = currentValue; } + /** Returns the argument mode (type + process method). Never null. */ public MODE getMode() { return mMode; } + + /** Returns true if the argument has been used on the command line. */ + public boolean isInCommandLine() { + return mInCommandLine; + } + + /** Sets if the argument has been used on the command line. */ + public void setInCommandLine(boolean inCommandLine) { + mInCommandLine = inCommandLine; + } } /** * Internal helper to define a new argument for a give action. * * @param mode The {@link MODE} for the argument. - * @param action The action name. Can be #GLOBAL_FLAG or #INTERNAL_FLAG. + * @param verb The verb name. Can be #INTERNAL_VERB. + * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG. * @param shortName The one-letter short argument name. Cannot be empty nor null. * @param longName The long argument name. Cannot be empty nor null. * @param description The description. Cannot be null. @@ -573,14 +744,19 @@ public class CommandLineProcessor { */ protected void define(MODE mode, boolean mandatory, - String action, + String verb, + String directObject, String shortName, String longName, String description, Object defaultValue) { assert(mandatory || mode == MODE.BOOLEAN); // a boolean mode cannot be mandatory - String key = action + "/" + longName; + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + + String key = verb + "/" + directObject + "/" + longName; mArguments.put(key, new Arg(mode, mandatory, - action, shortName, longName, description, defaultValue)); + verb, directObject, shortName, longName, description, defaultValue)); } /** diff --git a/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java b/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java index 1544f5bff..154788ee1 100644 --- a/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java +++ b/tools/sdkmanager/app/src/com/android/sdkmanager/Main.java @@ -23,12 +23,12 @@ import com.android.sdklib.ISdkLog; import com.android.sdklib.SdkConstants; import com.android.sdklib.SdkManager; import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.HardwareProperties; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdklib.avd.HardwareProperties.HardwareProperty; import com.android.sdklib.project.ProjectCreator; import com.android.sdklib.project.ProjectCreator.OutputLevel; -import com.android.sdklib.vm.HardwareProperties; -import com.android.sdklib.vm.VmManager; -import com.android.sdklib.vm.HardwareProperties.HardwareProperty; -import com.android.sdklib.vm.VmManager.VmInfo; import java.io.File; import java.io.IOException; @@ -56,8 +56,6 @@ class Main { private ISdkLog mSdkLog; /** The SDK manager parses the SDK folder and gives access to the content. */ private SdkManager mSdkManager; - /** Virtual Machine manager to access the list of VMs or create new ones. */ - private VmManager mVmManager; /** Command-line processor with options specific to SdkManager. */ private SdkCommandLine mSdkCommandLine; /** The working directory, either null or set to an existing absolute canonical directory. */ @@ -183,73 +181,102 @@ class Main { * Actually do an action... */ private void doAction() { - String action = mSdkCommandLine.getActionRequested(); + String verb = mSdkCommandLine.getVerb(); + String directObject = mSdkCommandLine.getDirectObject(); - if (SdkCommandLine.ACTION_LIST.equals(action)) { + if (SdkCommandLine.VERB_LIST.equals(verb)) { // list action. - if (SdkCommandLine.ARG_TARGET.equals(mSdkCommandLine.getListFilter())) { + if (SdkCommandLine.OBJECT_TARGET.equals(directObject)) { displayTargetList(); - } else if (SdkCommandLine.ARG_VM.equals(mSdkCommandLine.getListFilter())) { - displayVmList(); + } else if (SdkCommandLine.OBJECT_AVD.equals(directObject)) { + displayAvdList(); } else { displayTargetList(); - displayVmList(); + displayAvdList(); } - } else if (SdkCommandLine.ACTION_NEW_VM.equals(action)) { - createVm(); - } else if (SdkCommandLine.ACTION_NEW_PROJECT.equals(action)) { - // get the target and try to resolve it. - int targetId = mSdkCommandLine.getNewProjectTargetId(); - IAndroidTarget[] targets = mSdkManager.getTargets(); - if (targetId < 1 || targetId > targets.length) { - errorAndExit("Target id is not valid. Use '%s list -f target' to get the target Ids.", - SdkConstants.androidCmdName()); - } - IAndroidTarget target = targets[targetId - 1]; - - ProjectCreator creator = new ProjectCreator(mSdkFolder, - mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : - mSdkCommandLine.isSilent() ? OutputLevel.SILENT : - OutputLevel.NORMAL, - mSdkLog); - String projectDir = getProjectLocation(mSdkCommandLine.getNewProjectLocation()); - - creator.createProject(projectDir, - mSdkCommandLine.getNewProjectName(), - mSdkCommandLine.getNewProjectPackage(), - mSdkCommandLine.getNewProjectActivity(), - target, - false /* isTestProject*/); - } else if (SdkCommandLine.ACTION_UPDATE_PROJECT.equals(action)) { - // get the target and try to resolve it. - IAndroidTarget target = null; - int targetId = mSdkCommandLine.getUpdateProjectTargetId(); - if (targetId >= 0) { - IAndroidTarget[] targets = mSdkManager.getTargets(); - if (targetId < 1 || targetId > targets.length) { - errorAndExit("Target id is not valid. Use '%s list -f target' to get the target Ids.", - SdkConstants.androidCmdName()); - } - target = targets[targetId - 1]; - } - - ProjectCreator creator = new ProjectCreator(mSdkFolder, - mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : - mSdkCommandLine.isSilent() ? OutputLevel.SILENT : - OutputLevel.NORMAL, - mSdkLog); + } else if (SdkCommandLine.VERB_CREATE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + createAvd(); - String projectDir = getProjectLocation(mSdkCommandLine.getUpdateProjectLocation()); - - creator.updateProject(projectDir, - target, - mSdkCommandLine.getUpdateProjectName()); + } else if (SdkCommandLine.VERB_DELETE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + deleteAvd(); + + } else if (SdkCommandLine.VERB_MOVE.equals(verb) && + SdkCommandLine.OBJECT_AVD.equals(directObject)) { + moveAvd(); + + } else if (SdkCommandLine.VERB_CREATE.equals(verb) && + SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { + createProject(); + + } else if (SdkCommandLine.VERB_UPDATE.equals(verb) && + SdkCommandLine.OBJECT_PROJECT.equals(directObject)) { + updateProject(); } else { mSdkCommandLine.printHelpAndExit(null); } } + /** + * Creates a new Android project based on command-line parameters + */ + private void createProject() { + // get the target and try to resolve it. + int targetId = mSdkCommandLine.getParamTargetId(); + IAndroidTarget[] targets = mSdkManager.getTargets(); + if (targetId < 1 || targetId > targets.length) { + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", + SdkConstants.androidCmdName()); + } + IAndroidTarget target = targets[targetId - 1]; + + ProjectCreator creator = new ProjectCreator(mSdkFolder, + mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : + mSdkCommandLine.isSilent() ? OutputLevel.SILENT : + OutputLevel.NORMAL, + mSdkLog); + + String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); + + creator.createProject(projectDir, + mSdkCommandLine.getParamName(), + mSdkCommandLine.getParamProjectPackage(), + mSdkCommandLine.getParamProjectActivity(), + target, + false /* isTestProject*/); + } + + /** + * Updates an existing Android project based on command-line parameters + */ + private void updateProject() { + // get the target and try to resolve it. + IAndroidTarget target = null; + int targetId = mSdkCommandLine.getParamTargetId(); + if (targetId >= 0) { + IAndroidTarget[] targets = mSdkManager.getTargets(); + if (targetId < 1 || targetId > targets.length) { + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", + SdkConstants.androidCmdName()); + } + target = targets[targetId - 1]; + } + + ProjectCreator creator = new ProjectCreator(mSdkFolder, + mSdkCommandLine.isVerbose() ? OutputLevel.VERBOSE : + mSdkCommandLine.isSilent() ? OutputLevel.SILENT : + OutputLevel.NORMAL, + mSdkLog); + + String projectDir = getProjectLocation(mSdkCommandLine.getParamLocationPath()); + + creator.updateProject(projectDir, + target, + mSdkCommandLine.getParamName()); + } + /** * Adjusts the project location to make it absolute & canonical relative to the * working directory, if any. @@ -318,54 +345,82 @@ class Main { } // get the target skins - String[] skins = target.getSkins(); - mSdkLog.printf(" Skins: "); - if (skins != null) { - boolean first = true; - for (String skin : skins) { - if (first == false) { - mSdkLog.printf(", "); - } else { - first = false; - } - mSdkLog.printf(skin); - } - mSdkLog.printf("\n"); - } else { - mSdkLog.printf("no skins.\n"); - } + displaySkinList(target, " Skins: "); index++; } } + + /** + * Displays the skins valid for the given target. + */ + private void displaySkinList(IAndroidTarget target, String message) { + String[] skins = target.getSkins(); + String defaultSkin = target.getDefaultSkin(); + mSdkLog.printf(message); + if (skins != null) { + boolean first = true; + for (String skin : skins) { + if (first == false) { + mSdkLog.printf(", "); + } else { + first = false; + } + mSdkLog.printf(skin); + + if (skin.equals(defaultSkin)) { + mSdkLog.printf(" (default)"); + } + } + mSdkLog.printf("\n"); + } else { + mSdkLog.printf("no skins.\n"); + } + } /** - * Displays the list of available VMs. + * Displays the list of available AVDs. */ - private void displayVmList() { + private void displayAvdList() { try { - mVmManager = new VmManager(mSdkManager, null /* sdklog */); + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); - mSdkLog.printf("Available Android VMs:\n"); + mSdkLog.printf("Available Android Virtual Devices:\n"); - int index = 1; - for (VmInfo info : mVmManager.getVms()) { - mSdkLog.printf("[%d] %s\n", index, info.getName()); + AvdInfo[] avds = avdManager.getAvds(); + for (int index = 0 ; index < avds.length ; index++) { + AvdInfo info = avds[index]; + if (index > 0) { + mSdkLog.printf("---------\n"); + } + mSdkLog.printf(" Name: %s\n", info.getName()); mSdkLog.printf(" Path: %s\n", info.getPath()); - // get the target of the Vm + // get the target of the AVD IAndroidTarget target = info.getTarget(); if (target.isPlatform()) { - mSdkLog.printf(" Target: %s (API level %d)\n", target.getName(), + mSdkLog.printf(" Target: %s (API level %d)\n", target.getName(), target.getApiVersionNumber()); } else { - mSdkLog.printf(" Target: %s (%s)\n", target.getName(), target + mSdkLog.printf(" Target: %s (%s)\n", target.getName(), target .getVendor()); - mSdkLog.printf(" Based on Android %s (API level %d)\n", target + mSdkLog.printf(" Based on Android %s (API level %d)\n", target .getApiVersionName(), target.getApiVersionNumber()); } - - index++; + + // display some extra values. + Map properties = info.getProperties(); + String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); + if (skin != null) { + mSdkLog.printf(" Skin: %s\n", skin); + } + String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard == null) { + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); + } + if (sdcard != null) { + mSdkLog.printf(" Sdcard: %s\n", sdcard); + } } } catch (AndroidLocationException e) { errorAndExit(e.getMessage()); @@ -373,57 +428,237 @@ class Main { } /** - * Creates a new VM. This is a text based creation with command line prompt. + * Creates a new AVD. This is a text based creation with command line prompt. */ - private void createVm() { + private void createAvd() { // find a matching target - int targetId = mSdkCommandLine.getNewVmTargetId(); + int targetId = mSdkCommandLine.getParamTargetId(); IAndroidTarget target = null; if (targetId >= 1 && targetId <= mSdkManager.getTargets().length) { target = mSdkManager.getTargets()[targetId-1]; // target it is 1-based } else { - errorAndExit("Target id is not valid. Use '%s list -f target' to get the target Ids.", + errorAndExit("Target id is not valid. Use '%s list targets' to get the target ids.", SdkConstants.androidCmdName()); } try { - mVmManager = new VmManager(mSdkManager, mSdkLog); + boolean removePrevious = false; + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); - String vmName = mSdkCommandLine.getNewVmName(); - VmInfo info = mVmManager.getVm(vmName); + String avdName = mSdkCommandLine.getParamName(); + AvdInfo info = avdManager.getAvd(avdName); if (info != null) { - errorAndExit("VM %s already exists.", vmName); - } else { - String vmParentFolder = mSdkCommandLine.getNewVmLocation(); - if (vmParentFolder == null) { - vmParentFolder = AndroidLocation.getFolder() + AndroidLocation.FOLDER_VMS; + if (mSdkCommandLine.getFlagForce()) { + removePrevious = true; + mSdkLog.warning( + "Android Virtual Device '%s' already exists and will be replaced.", + avdName); + } else { + errorAndExit("Android Virtual Device '%s' already exists.", avdName); + return; } + } - Map hardwareConfig = null; - if (target.isPlatform()) { - try { - hardwareConfig = promptForHardware(target); - } catch (IOException e) { - errorAndExit(e.getMessage()); + String paramFolderPath = mSdkCommandLine.getParamLocationPath(); + File avdFolder = null; + if (paramFolderPath != null) { + avdFolder = new File(paramFolderPath); + } else { + avdFolder = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + avdName + AvdManager.AVD_FOLDER_EXTENSION); + } + + Map hardwareConfig = null; + if (target.isPlatform()) { + try { + hardwareConfig = promptForHardware(target); + } catch (IOException e) { + errorAndExit(e.getMessage()); + } + } + + AvdInfo oldAvdInfo = null; + if (removePrevious) { + oldAvdInfo = avdManager.getAvd(avdName); + } + + // Validate skin is either default (empty) or NNNxMMM or a valid skin name. + String skin = mSdkCommandLine.getParamSkin(); + if (skin != null && skin.length() == 0) { + skin = null; + } + if (skin != null) { + boolean valid = false; + // Is it a know skin name for this target? + for (String s : target.getSkins()) { + if (skin.equalsIgnoreCase(s)) { + skin = s; // Make skin names case-insensitive. + valid = true; + break; } } + + // Is it NNNxMMM? + if (!valid) { + valid = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin).matches(); + } - mVmManager.createVm(vmParentFolder, - mSdkCommandLine.getNewVmName(), - target, - mSdkCommandLine.getNewVmSkin(), - mSdkCommandLine.getNewVmSdCard(), - hardwareConfig, - mSdkLog); + if (!valid) { + displaySkinList(target, "Valid skins: "); + errorAndExit("'%s' is not a valid skin name or size (NNNxMMM)", skin); + return; + } } + + AvdInfo newAvdInfo = avdManager.createAvd(avdFolder, + avdName, + target, + skin, + mSdkCommandLine.getParamSdCard(), + hardwareConfig, + removePrevious, + mSdkLog); + + if (newAvdInfo != null && + oldAvdInfo != null && + !oldAvdInfo.getPath().equals(newAvdInfo.getPath())) { + mSdkLog.warning("Removing previous AVD directory at %s", oldAvdInfo.getPath()); + // Remove the old data directory + File dir = new File(oldAvdInfo.getPath()); + avdManager.recursiveDelete(dir); + dir.delete(); + // Remove old avd info from manager + avdManager.removeAvd(oldAvdInfo); + } + } catch (AndroidLocationException e) { errorAndExit(e.getMessage()); } } /** - * Prompts the user to setup a hardware config for a Platform-based VM. + * Delete an AVD. + */ + private void deleteAvd() { + try { + String avdName = mSdkCommandLine.getParamName(); + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + AvdInfo info = avdManager.getAvd(avdName); + + if (info == null) { + errorAndExit("There is no Android Virtual Device named '%s'.", avdName); + return; + } + + avdManager.deleteAvd(info, mSdkLog); + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Move an AVD. + */ + private void moveAvd() { + try { + String avdName = mSdkCommandLine.getParamName(); + AvdManager avdManager = new AvdManager(mSdkManager, mSdkLog); + AvdInfo info = avdManager.getAvd(avdName); + + if (info == null) { + errorAndExit("There is no Android Virtual Device named '%s'.", avdName); + return; + } + + // This is a rename if there's a new name for the AVD + String newName = mSdkCommandLine.getParamMoveNewName(); + if (newName != null && newName.equals(info.getName())) { + // same name, not actually a rename operation + newName = null; + } + + // This is a move (of the data files) if there's a new location path + String paramFolderPath = mSdkCommandLine.getParamLocationPath(); + if (paramFolderPath != null) { + // check if paths are the same. Use File methods to account for OS idiosyncrasies. + try { + File f1 = new File(paramFolderPath).getCanonicalFile(); + File f2 = new File(info.getPath()).getCanonicalFile(); + if (f1.equals(f2)) { + // same canonical path, so not actually a move + paramFolderPath = null; + } + } catch (IOException e) { + // Fail to resolve canonical path. Fail now since a move operation might fail + // later and be harder to recover from. + errorAndExit(e.getMessage()); + return; + } + } + + if (newName == null && paramFolderPath == null) { + mSdkLog.warning("Move operation aborted: same AVD name, same canonical data path"); + return; + } + + // If a rename was requested and no data move was requested, check if the original + // data path is our default constructed from the AVD name. In this case we still want + // to rename that folder too. + if (newName != null && paramFolderPath == null) { + // Compute the original data path + File originalFolder = new File( + AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + info.getName() + AvdManager.AVD_FOLDER_EXTENSION); + if (originalFolder.equals(info.getPath())) { + try { + // The AVD is using the default data folder path based on the AVD name. + // That folder needs to be adjusted to use the new name. + File f = new File(AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD, + newName + AvdManager.AVD_FOLDER_EXTENSION); + paramFolderPath = f.getCanonicalPath(); + } catch (IOException e) { + // Fail to resolve canonical path. Fail now rather than later. + errorAndExit(e.getMessage()); + } + } + } + + // Check for conflicts + + if (newName != null && avdManager.getAvd(newName) != null) { + errorAndExit("There is already an AVD named '%s'.", newName); + return; + } + if (newName != null) { + if (avdManager.getAvd(newName) != null) { + errorAndExit("There is already an AVD named '%s'.", newName); + return; + } + + File ini = info.getIniFile(); + if (ini.equals(AvdInfo.getIniFile(newName))) { + errorAndExit("The AVD file '%s' is in the way.", ini.getCanonicalPath()); + return; + } + } + + if (paramFolderPath != null && new File(paramFolderPath).exists()) { + errorAndExit( + "There is already a file or directory at '%s'.\nUse --path to specify a different data folder.", + paramFolderPath); + } + + avdManager.moveAvd(info, newName, paramFolderPath, mSdkLog); + } catch (AndroidLocationException e) { + errorAndExit(e.getMessage()); + } catch (IOException e) { + errorAndExit(e.getMessage()); + } + } + + /** + * Prompts the user to setup a hardware config for a Platform-based AVD. * @throws IOException */ private Map promptForHardware(IAndroidTarget createTarget) throws IOException { diff --git a/tools/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java b/tools/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java index 08626a833..34a69bd86 100644 --- a/tools/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java +++ b/tools/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java @@ -25,152 +25,196 @@ import com.android.sdklib.SdkManager; */ public class SdkCommandLine extends CommandLineProcessor { - public static final String ARG_ALIAS = "alias"; - public static final String ARG_ACTIVITY = "activity"; - public static final String ARG_VM = "vm"; - public static final String ARG_TARGET = "target"; - public static final String ARG_ALL = "all"; - - public static final String KEY_ACTIVITY = ARG_ACTIVITY; - public static final String KEY_PACKAGE = "package"; - public static final String KEY_MODE = "mode"; - public static final String KEY_TARGET_ID = ARG_TARGET; - public static final String KEY_NAME = "name"; - public static final String KEY_OUT = "out"; - public static final String KEY_FILTER = "filter"; - public static final String KEY_SKIN = "skin"; - public static final String KEY_SDCARD = "sdcard"; + public final static String VERB_LIST = "list"; + public final static String VERB_CREATE = "create"; + public final static String VERB_MOVE = "move"; + public final static String VERB_DELETE = "delete"; + public final static String VERB_UPDATE = "update"; - public final static String ACTION_LIST = "list"; - public final static String ACTION_NEW_VM = ARG_VM; - public final static String ACTION_NEW_PROJECT = "project"; - public final static String ACTION_UPDATE_PROJECT = "update"; - + public static final String OBJECT_AVD = "avd"; + public static final String OBJECT_AVDS = "avds"; + public static final String OBJECT_TARGET = "target"; + public static final String OBJECT_TARGETS = "targets"; + public static final String OBJECT_PROJECT = "project"; + + public static final String ARG_ALIAS = "alias"; + public static final String ARG_ACTIVITY = "activity"; + + public static final String KEY_ACTIVITY = ARG_ACTIVITY; + public static final String KEY_PACKAGE = "package"; + public static final String KEY_MODE = "mode"; + public static final String KEY_TARGET_ID = OBJECT_TARGET; + public static final String KEY_NAME = "name"; + public static final String KEY_PATH = "path"; + public static final String KEY_FILTER = "filter"; + public static final String KEY_SKIN = "skin"; + public static final String KEY_SDCARD = "sdcard"; + public static final String KEY_FORCE = "force"; + public static final String KEY_RENAME = "rename"; + + /** + * Action definitions for SdkManager command line. + *

    + * Each entry is a string array with: + *

      + *
    • the verb. + *
    • an object (use #NO_VERB_OBJECT if there's no object). + *
    • a description. + *
    • an alternate form for the object (e.g. plural). + *
    + */ private final static String[][] ACTIONS = { - { ACTION_LIST, - "Lists existing targets or VMs." }, - { ACTION_NEW_VM, - "Creates a new VM." }, - { ACTION_NEW_PROJECT, - "Creates a new project using a template." }, - { ACTION_UPDATE_PROJECT, - "Updates a project from existing source (must have an AndroidManifest.xml)." }, + { VERB_LIST, NO_VERB_OBJECT, + "Lists existing targets or virtual devices." }, + { VERB_LIST, OBJECT_AVD, + "Lists existing Android Virtual Devices.", + OBJECT_AVDS }, + { VERB_LIST, OBJECT_TARGET, + "Lists existing targets.", + OBJECT_TARGETS }, + + { VERB_CREATE, OBJECT_AVD, + "Creates a new Android Virtual Device." }, + { VERB_MOVE, OBJECT_AVD, + "Moves or renames an Android Virtual Device." }, + { VERB_DELETE, OBJECT_AVD, + "Deletes an Android Virtual Device." }, + + { VERB_CREATE, OBJECT_PROJECT, + "Creates a new Android Project." }, + { VERB_UPDATE, OBJECT_PROJECT, + "Updates an Android Project (must have an AndroidManifest.xml)." }, }; public SdkCommandLine(ISdkLog logger) { super(logger, ACTIONS); - define(MODE.ENUM, false, ACTION_LIST, "f", KEY_FILTER, - "List filter", new String[] { ARG_ALL, ARG_TARGET, ARG_VM }); + // --- create avd --- + + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "p", KEY_PATH, + "Location path of the directory where the new AVD will be created", null); + define(MODE.STRING, true, + VERB_CREATE, OBJECT_AVD, "n", KEY_NAME, + "Name of the new AVD", null); + define(MODE.INTEGER, true, + VERB_CREATE, OBJECT_AVD, "t", KEY_TARGET_ID, + "Target id of the new AVD", null); + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "s", KEY_SKIN, + "Skin of the new AVD", null); + define(MODE.STRING, false, + VERB_CREATE, OBJECT_AVD, "c", KEY_SDCARD, + "Path to a shared SD card image, or size of a new sdcard for the new AVD", null); + define(MODE.BOOLEAN, false, + VERB_CREATE, OBJECT_AVD, "f", KEY_FORCE, + "Force creation (override an existing AVD)", false); - define(MODE.STRING, false, ACTION_NEW_VM, "o", KEY_OUT, - "Location path of new VM", null); - define(MODE.STRING, true, ACTION_NEW_VM, "n", KEY_NAME, - "Name of the new VM", null); - define(MODE.INTEGER, true, ACTION_NEW_VM, "t", KEY_TARGET_ID, - "Target id of the new VM", null); - define(MODE.STRING, true, ACTION_NEW_VM, "s", KEY_SKIN, - "Skin of the new VM", null); - define(MODE.STRING, false, ACTION_NEW_VM, "c", KEY_SDCARD, - "Path to a shared SD card image, or size of a new sdcard for the new VM", null); + // --- delete avd --- + + define(MODE.STRING, true, + VERB_DELETE, OBJECT_AVD, "n", KEY_NAME, + "Name of the AVD to delete", null); - define(MODE.ENUM, true, ACTION_NEW_PROJECT, "m", KEY_MODE, + // --- move avd --- + + define(MODE.STRING, true, + VERB_MOVE, OBJECT_AVD, "n", KEY_NAME, + "Name of the AVD to move or rename", null); + define(MODE.STRING, false, + VERB_MOVE, OBJECT_AVD, "r", KEY_RENAME, + "New name of the AVD to rename", null); + define(MODE.STRING, false, + VERB_MOVE, OBJECT_AVD, "p", KEY_PATH, + "New location path of the directory where to move the AVD", null); + + // --- create project --- + + define(MODE.ENUM, true, + VERB_CREATE, OBJECT_PROJECT, "m", KEY_MODE, "Project mode", new String[] { ARG_ACTIVITY, ARG_ALIAS }); - define(MODE.STRING, true, ACTION_NEW_PROJECT, "o", KEY_OUT, + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, + "p", KEY_PATH, "Location path of new project", null); - define(MODE.INTEGER, true, ACTION_NEW_PROJECT, "t", KEY_TARGET_ID, + define(MODE.INTEGER, true, + VERB_CREATE, OBJECT_PROJECT, "t", KEY_TARGET_ID, "Target id of the new project", null); - define(MODE.STRING, true, ACTION_NEW_PROJECT, "p", KEY_PACKAGE, + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, "k", KEY_PACKAGE, "Package name", null); - define(MODE.STRING, true, ACTION_NEW_PROJECT, "a", KEY_ACTIVITY, + define(MODE.STRING, true, + VERB_CREATE, OBJECT_PROJECT, "a", KEY_ACTIVITY, "Activity name", null); - define(MODE.STRING, false, ACTION_NEW_PROJECT, "n", KEY_NAME, + define(MODE.STRING, false, + VERB_CREATE, OBJECT_PROJECT, "n", KEY_NAME, "Project name", null); - define(MODE.STRING, true, ACTION_UPDATE_PROJECT, "o", KEY_OUT, + // --- update project --- + + define(MODE.STRING, true, + VERB_UPDATE, OBJECT_PROJECT, + "p", KEY_PATH, "Location path of the project", null); - define(MODE.INTEGER, true, ACTION_UPDATE_PROJECT, "t", KEY_TARGET_ID, + define(MODE.INTEGER, true, + VERB_UPDATE, OBJECT_PROJECT, + "t", KEY_TARGET_ID, "Target id to set for the project", -1); - define(MODE.STRING, false, ACTION_UPDATE_PROJECT, "n", KEY_NAME, + define(MODE.STRING, false, + VERB_UPDATE, OBJECT_PROJECT, + "n", KEY_NAME, "Project name", null); } - // -- some helpers for list action flags + // -- some helpers for generic action flags - /** Helper to retrieve the --filter for the list action. */ - public String getListFilter() { - return ((String) getValue(ACTION_LIST, KEY_FILTER)); + /** Helper to retrieve the --path value. */ + public String getParamLocationPath() { + return ((String) getValue(null, null, KEY_PATH)); + } + + /** Helper to retrieve the --target id value. */ + public int getParamTargetId() { + return ((Integer) getValue(null, null, KEY_TARGET_ID)).intValue(); } - // -- some helpers for vm action flags - - /** Helper to retrieve the --out location for the new vm action. */ - public String getNewVmLocation() { - return ((String) getValue(ACTION_NEW_VM, KEY_OUT)); + /** Helper to retrieve the --name value. */ + public String getParamName() { + return ((String) getValue(null, null, KEY_NAME)); } - /** Helper to retrieve the --target id for the new vm action. */ - public int getNewVmTargetId() { - return ((Integer) getValue(ACTION_NEW_VM, KEY_TARGET_ID)).intValue(); + /** Helper to retrieve the --skin value. */ + public String getParamSkin() { + return ((String) getValue(null, null, KEY_SKIN)); } - /** Helper to retrieve the --name for the new vm action. */ - public String getNewVmName() { - return ((String) getValue(ACTION_NEW_VM, KEY_NAME)); + /** Helper to retrieve the --sdcard value. */ + public String getParamSdCard() { + return ((String) getValue(null, null, KEY_SDCARD)); } - /** Helper to retrieve the --skin name for the new vm action. */ - public String getNewVmSkin() { - return ((String) getValue(ACTION_NEW_VM, KEY_SKIN)); + /** Helper to retrieve the --force flag. */ + public boolean getFlagForce() { + return ((Boolean) getValue(null, null, KEY_FORCE)).booleanValue(); } - /** Helper to retrieve the --sdcard data for the new vm action. */ - public String getNewVmSdCard() { - return ((String) getValue(ACTION_NEW_VM, KEY_SDCARD)); + // -- some helpers for avd action flags + + /** Helper to retrieve the --rename value for a move verb. */ + public String getParamMoveNewName() { + return ((String) getValue(VERB_MOVE, null, KEY_RENAME)); } // -- some helpers for project action flags - /** Helper to retrieve the --out location for the new project action. */ - public String getNewProjectLocation() { - return ((String) getValue(ACTION_NEW_PROJECT, KEY_OUT)); - } - - /** Helper to retrieve the --target id for the new project action. */ - public int getNewProjectTargetId() { - return ((Integer) getValue(ACTION_NEW_PROJECT, KEY_TARGET_ID)).intValue(); - } - - /** Helper to retrieve the --name for the new project action. */ - public String getNewProjectName() { - return ((String) getValue(ACTION_NEW_PROJECT, KEY_NAME)); - } - - /** Helper to retrieve the --package for the new project action. */ - public String getNewProjectPackage() { - return ((String) getValue(ACTION_NEW_PROJECT, KEY_PACKAGE)); + /** Helper to retrieve the --package value. */ + public String getParamProjectPackage() { + return ((String) getValue(null, OBJECT_PROJECT, KEY_PACKAGE)); } /** Helper to retrieve the --activity for the new project action. */ - public String getNewProjectActivity() { - return ((String) getValue(ACTION_NEW_PROJECT, KEY_ACTIVITY)); - } - - // -- some helpers for update action flags - - /** Helper to retrieve the --out location for the update project action. */ - public String getUpdateProjectLocation() { - return ((String) getValue(ACTION_UPDATE_PROJECT, KEY_OUT)); - } - - /** Helper to retrieve the --target id for the update project action. */ - public int getUpdateProjectTargetId() { - return ((Integer) getValue(ACTION_UPDATE_PROJECT, KEY_TARGET_ID)).intValue(); - } - - /** Helper to retrieve the --name for the update project action. */ - public String getUpdateProjectName() { - return ((String) getValue(ACTION_UPDATE_PROJECT, KEY_NAME)); + public String getParamProjectActivity() { + return ((String) getValue(null, OBJECT_PROJECT, KEY_ACTIVITY)); } } diff --git a/tools/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java b/tools/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java index 1a8215121..918591b53 100644 --- a/tools/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java +++ b/tools/sdkmanager/app/tests/com/android/sdkmanager/CommandLineProcessorTest.java @@ -38,20 +38,20 @@ public class CommandLineProcessorTest extends TestCase { public MockCommandLineProcessor(ISdkLog logger) { super(logger, new String[][] { - { "action1", "Some action" }, - { "action2", "Another action" }, + { "verb1", "action1", "Some action" }, + { "verb1", "action2", "Another action" }, }); define(MODE.STRING, false /*mandatory*/, - "action1", "1", "first", "non-mandatory flag", null); + "verb1", "action1", "1", "first", "non-mandatory flag", null); define(MODE.STRING, true /*mandatory*/, - "action1", "2", "second", "mandatory flag", null); + "verb1", "action1", "2", "second", "mandatory flag", null); } @Override - public void printHelpAndExitForAction(String actionFilter, + public void printHelpAndExitForAction(String verb, String directObject, String errorFormat, Object... args) { mHelpCalled = true; - super.printHelpAndExitForAction(actionFilter, errorFormat, args); + super.printHelpAndExitForAction(verb, directObject, errorFormat, args); } @Override @@ -132,14 +132,14 @@ public class CommandLineProcessorTest extends TestCase { assertTrue(c.isVerbose()); assertTrue(c.wasExitCalled()); assertTrue(c.wasHelpCalled()); - assertTrue(c.getStdErr().indexOf("Missing action name.") != -1); + assertTrue(c.getStdErr().indexOf("Missing verb name.") != -1); c = new MockCommandLineProcessor(mLog); c.parseArgs(new String[] { "--verbose" }); assertTrue(c.isVerbose()); assertTrue(c.wasExitCalled()); assertTrue(c.wasHelpCalled()); - assertTrue(c.getStdErr().indexOf("Missing action name.") != -1); + assertTrue(c.getStdErr().indexOf("Missing verb name.") != -1); } public final void testHelp() { @@ -148,39 +148,39 @@ public class CommandLineProcessorTest extends TestCase { c.parseArgs(new String[] { "-h" }); assertTrue(c.wasExitCalled()); assertTrue(c.wasHelpCalled()); - assertTrue(c.getStdErr().indexOf("Missing action name.") == -1); + assertTrue(c.getStdErr().indexOf("Missing verb name.") == -1); c = new MockCommandLineProcessor(mLog); c.parseArgs(new String[] { "--help" }); assertTrue(c.wasExitCalled()); assertTrue(c.wasHelpCalled()); - assertTrue(c.getStdErr().indexOf("Missing action name.") == -1); + assertTrue(c.getStdErr().indexOf("Missing verb name.") == -1); } public final void testMandatory() { MockCommandLineProcessor c = new MockCommandLineProcessor(mLog); - c.parseArgs(new String[] { "action1", "-1", "value1", "-2", "value2" }); + c.parseArgs(new String[] { "verb1", "action1", "-1", "value1", "-2", "value2" }); assertFalse(c.wasExitCalled()); assertFalse(c.wasHelpCalled()); assertEquals("", c.getStdErr()); - assertEquals("value1", c.getValue("action1", "first")); - assertEquals("value2", c.getValue("action1", "second")); + assertEquals("value1", c.getValue("verb1", "action1", "first")); + assertEquals("value2", c.getValue("verb1", "action1", "second")); c = new MockCommandLineProcessor(mLog); - c.parseArgs(new String[] { "action1", "-2", "value2" }); + c.parseArgs(new String[] { "verb1", "action1", "-2", "value2" }); assertFalse(c.wasExitCalled()); assertFalse(c.wasHelpCalled()); assertEquals("", c.getStdErr()); - assertEquals(null, c.getValue("action1", "first")); - assertEquals("value2", c.getValue("action1", "second")); + assertEquals(null, c.getValue("verb1", "action1", "first")); + assertEquals("value2", c.getValue("verb1", "action1", "second")); c = new MockCommandLineProcessor(mLog); - c.parseArgs(new String[] { "action1" }); + c.parseArgs(new String[] { "verb1", "action1" }); assertTrue(c.wasExitCalled()); assertTrue(c.wasHelpCalled()); assertTrue(c.getStdErr().indexOf("must be defined") != -1); - assertEquals(null, c.getValue("action1", "first")); - assertEquals(null, c.getValue("action1", "second")); + assertEquals(null, c.getValue("verb1", "action1", "first")); + assertEquals(null, c.getValue("verb1", "action1", "second")); } } diff --git a/tools/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java b/tools/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java index 5a2c8e1ef..07a32e04e 100644 --- a/tools/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java +++ b/tools/sdkmanager/app/tests/com/android/sdkmanager/SdkCommandLineTest.java @@ -37,10 +37,10 @@ public class SdkCommandLineTest extends TestCase { } @Override - public void printHelpAndExitForAction(String actionFilter, + public void printHelpAndExitForAction(String verb, String directObject, String errorFormat, Object... args) { mHelpCalled = true; - super.printHelpAndExitForAction(actionFilter, errorFormat, args); + super.printHelpAndExitForAction(verb, directObject, errorFormat, args); } @Override @@ -78,34 +78,64 @@ public class SdkCommandLineTest extends TestCase { super.tearDown(); } - /** Test list with long name and verbose */ - public final void testList_Long_Verbose() { + /** Test list */ + public final void testList_Avd_Verbose() { MockSdkCommandLine c = new MockSdkCommandLine(mLog); - assertEquals("all", c.getListFilter()); - c.parseArgs(new String[] { "-v", "list", "--filter", "vm" }); + c.parseArgs(new String[] { "-v", "list", "avd" }); assertFalse(c.wasHelpCalled()); assertFalse(c.wasExitCalled()); - assertEquals("vm", c.getListFilter()); + assertEquals("list", c.getVerb()); + assertEquals("avd", c.getDirectObject()); assertTrue(c.isVerbose()); } - /** Test list with short name and no verbose */ - public final void testList_Short() { + public final void testList_Target() { MockSdkCommandLine c = new MockSdkCommandLine(mLog); - assertEquals("all", c.getListFilter()); - c.parseArgs(new String[] { "list", "-f", "vm" }); + c.parseArgs(new String[] { "list", "target" }); assertFalse(c.wasHelpCalled()); assertFalse(c.wasExitCalled()); - assertEquals("vm", c.getListFilter()); + assertEquals("list", c.getVerb()); + assertEquals("target", c.getDirectObject()); + assertFalse(c.isVerbose()); } - - /** Test list with long name and missing parameter */ - public final void testList_Long_MissingParam() { + + public final void testList_None() { MockSdkCommandLine c = new MockSdkCommandLine(mLog); - assertEquals("all", c.getListFilter()); - c.parseArgs(new String[] { "list", "--filter" }); + c.parseArgs(new String[] { "list" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + assertEquals("", c.getDirectObject()); + assertFalse(c.isVerbose()); + } + + public final void testList_Invalid() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "unknown" }); assertTrue(c.wasHelpCalled()); assertTrue(c.wasExitCalled()); - assertEquals("all", c.getListFilter()); + assertEquals(null, c.getVerb()); + assertEquals(null, c.getDirectObject()); + assertFalse(c.isVerbose()); + } + + public final void testList_Plural() { + MockSdkCommandLine c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "avds" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + // we get the non-plural form + assertEquals("avd", c.getDirectObject()); + assertFalse(c.isVerbose()); + + c = new MockSdkCommandLine(mLog); + c.parseArgs(new String[] { "list", "targets" }); + assertFalse(c.wasHelpCalled()); + assertFalse(c.wasExitCalled()); + assertEquals("list", c.getVerb()); + // we get the non-plural form + assertEquals("target", c.getDirectObject()); + assertFalse(c.isVerbose()); } } diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java index ada61f78d..0a5910734 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AddOnTarget.java @@ -69,6 +69,7 @@ final class AddOnTarget implements IAndroidTarget { private final String mVendor; private final String mDescription; private String[] mSkins; + private String mDefaultSkin; private IOptionalLibrary[] mLibraries; /** @@ -141,6 +142,10 @@ final class AddOnTarget implements IAndroidTarget { return false; } + public IAndroidTarget getParent() { + return mBasePlatform; + } + public String getPath(int pathId) { switch (pathId) { case IMAGES: @@ -157,6 +162,10 @@ final class AddOnTarget implements IAndroidTarget { public String[] getSkins() { return mSkins; } + + public String getDefaultSkin() { + return mDefaultSkin; + } public IOptionalLibrary[] getOptionalLibraries() { return mLibraries; @@ -236,7 +245,9 @@ final class AddOnTarget implements IAndroidTarget { // ---- local methods. - public void setSkins(String[] skins) { + public void setSkins(String[] skins, String defaultSkin) { + mDefaultSkin = defaultSkin; + // we mix the add-on and base platform skins HashSet skinSet = new HashSet(); skinSet.addAll(Arrays.asList(skins)); diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java index acf1187e0..fa462bd6e 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/IAndroidTarget.java @@ -60,6 +60,14 @@ public interface IAndroidTarget extends Comparable { public static int SOURCES = 18; /** OS Path to the target specific docs */ public static int DOCS = 19; + /** OS Path to the target's version of the aapt tool. */ + public static int AAPT = 20; + /** OS Path to the target's version of the aidl tool. */ + public static int AIDL = 21; + /** OS Path to the target's version of the dx too. */ + public static int DX = 22; + /** OS Path to the target's version of the dx.jar file. */ + public static int DX_JAR = 23; public interface IOptionalLibrary { String getName(); @@ -108,6 +116,12 @@ public interface IAndroidTarget extends Comparable { */ boolean isPlatform(); + /** + * Returns the parent target. This is likely to only be non null if + * {@link #isPlatform()} returns false + */ + IAndroidTarget getParent(); + /** * Returns the path of a platform component. * @param pathId the id representing the path to return. Any of the constants defined in the @@ -120,6 +134,11 @@ public interface IAndroidTarget extends Comparable { */ String[] getSkins(); + /** + * Returns the default skin for this target. + */ + String getDefaultSkin(); + /** * Returns the available optional libraries for this target. * @return an array of optional libraries or null if there is none. diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java index 8cbe44a7e..489451746 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/ISdkLog.java @@ -16,11 +16,48 @@ package com.android.sdklib; +import java.util.Formatter; + /** * Interface used to display warnings/errors while parsing the SDK content. */ public interface ISdkLog { + + /** + * Prints a warning message on stdout. + *

    + * Implementations should only display warnings in verbose mode. + * The message should be prefixed with "Warning:". + * + * @param warningFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for warningFormat. + */ void warning(String warningFormat, Object... args); + + /** + * Prints an error message on stderr. + *

    + * Implementation should always display errors, independent of verbose mode. + * The message should be prefixed with "Error:". + * + * @param t is an optional {@link Throwable} or {@link Exception}. If non-null, it's + * message will be printed out. + * @param errorFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for errorFormat. + */ void error(Throwable t, String errorFormat, Object... args); + + /** + * Prints a message as-is on stdout. + *

    + * Implementation should always display errors, independent of verbose mode. + * No prefix is used, the message is printed as-is after formatting. + * + * @param msgFormat is an optional error format. If non-null, it will be printed + * using a {@link Formatter} with the provided arguments. + * @param args provides the arguments for msgFormat. + */ void printf(String msgFormat, Object... args); } diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java index 59fa81c85..a3da70e63 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/PlatformTarget.java @@ -75,8 +75,13 @@ final class PlatformTarget implements IAndroidTarget { SdkConstants.FN_INTENT_ACTIONS_SERVICE); mPaths.put(CATEGORIES, mLocation + SdkConstants.OS_PLATFORM_DATA_FOLDER + SdkConstants.FN_INTENT_CATEGORIES); + mPaths.put(AAPT, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT); + mPaths.put(AIDL, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL); + mPaths.put(DX, mLocation + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX); + mPaths.put(DX_JAR, mLocation + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + + SdkConstants.FN_DX_JAR); } - + public String getLocation() { return mLocation; } @@ -123,6 +128,10 @@ final class PlatformTarget implements IAndroidTarget { return true; } + public IAndroidTarget getParent() { + return null; + } + public String getPath(int pathId) { return mPaths.get(pathId); } @@ -130,6 +139,11 @@ final class PlatformTarget implements IAndroidTarget { public String[] getSkins() { return mSkins; } + + public String getDefaultSkin() { + // at this time, this is the default skin for all the platform. + return "HVGA"; + } /* * Always returns null, as a standard platforms have no optional libraries. diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java index b79dedbe3..00594d12f 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java @@ -30,7 +30,18 @@ import java.io.File; * */ public final class SdkConstants { + public final static int PLATFORM_UNKNOWN = 0; + public final static int PLATFORM_LINUX = 1; + public final static int PLATFORM_WINDOWS = 2; + public final static int PLATFORM_DARWIN = 3; + /** + * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + public final static int CURRENT_PLATFORM = currentPlatform(); + + /** An SDK Project's AndroidManifest.xml file */ public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml"; /** An SDK Project's build.xml file */ @@ -69,6 +80,21 @@ public final class SdkConstants { /** Skin layout file */ public final static String FN_SKIN_LAYOUT = "layout";//$NON-NLS-1$ + /** dex.jar file */ + public static final String FN_DX_JAR = "dx.jar"; //$NON-NLS-1$ + + /** dx executable */ + public final static String FN_DX = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "dx.bat" : "dx"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** aapt executable */ + public final static String FN_AAPT = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "aapt.exe" : "aapt"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** aidl executable */ + public final static String FN_AIDL = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ? + "aidl.exe" : "aidl"; //$NON-NLS-1$ //$NON-NLS-2$ + /* Folder Names for Android Projects . */ /** Resources folder name, i.e. "res". */ @@ -77,6 +103,8 @@ public final class SdkConstants { public final static String FD_ASSETS = "assets"; //$NON-NLS-1$ /** Default source folder name, i.e. "src" */ public final static String FD_SOURCES = "src"; //$NON-NLS-1$ + /** Default generated source folder name, i.e. "gen" */ + public final static String FD_GEN_SOURCES = "gen"; //$NON-NLS-1$ /** Default native library folder name inside the project, i.e. "libs" * While the folder inside the .apk is "lib", we call that one libs because * that's what we use in ant for both .jar and .so and we need to make the 2 development ways @@ -142,11 +170,11 @@ public final class SdkConstants { * This is an OS path, ending with a separator. */ public final static String OS_SDK_DOCS_FOLDER = FD_DOCS + File.separator; - /** Path of the tools directory relative to the sdk folder. + /** Path of the tools directory relative to the sdk folder, or to a platform folder. * This is an OS path, ending with a separator. */ public final static String OS_SDK_TOOLS_FOLDER = FD_TOOLS + File.separator; - /** Path of the lib directory relative to the sdk folder. + /** Path of the lib directory relative to the sdk folder, or to a platform folder. * This is an OS path, ending with a separator. */ public final static String OS_SDK_TOOLS_LIB_FOLDER = OS_SDK_TOOLS_FOLDER + FD_LIB + File.separator; @@ -233,4 +261,22 @@ public final class SdkConstants { return cmd; } + /** + * Returns current platform + * + * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN}, + * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}. + */ + private static int currentPlatform() { + String os = System.getProperty("os.name"); //$NON-NLS-1$ + if (os.startsWith("Mac OS")) { //$NON-NLS-1$ + return PLATFORM_DARWIN; + } else if (os.startsWith("Windows")) { //$NON-NLS-1$ + return PLATFORM_WINDOWS; + } else if (os.startsWith("Linux")) { //$NON-NLS-1$ + return PLATFORM_LINUX; + } + + return PLATFORM_UNKNOWN; + } } diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java index 83a90e6c7..28227c6d2 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java @@ -44,6 +44,7 @@ public final class SdkManager { private final static String ADDON_API = "api"; private final static String ADDON_DESCRIPTION = "description"; private final static String ADDON_LIBRARIES = "libraries"; + private final static String ADDON_DEFAULT_SKIN = "skin"; private final static Pattern PATTERN_PROP = Pattern.compile( "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); @@ -51,6 +52,17 @@ public final class SdkManager { private final static Pattern PATTERN_LIB_DATA = Pattern.compile( "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); + /** List of items in the platform to check when parsing it. These paths are relative to the + * platform root folder. */ + private final static String[] sPlatformContentList = new String[] { + SdkConstants.FN_FRAMEWORK_LIBRARY, + SdkConstants.FN_FRAMEWORK_AIDL, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL, + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX, + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR, + }; + /** the location of the SDK */ private final String mSdkLocation; private IAndroidTarget[] mTargets; @@ -176,6 +188,10 @@ public final class SdkManager { String apiNumber = map.get(PROP_VERSION_SDK); String apiName = map.get(PROP_VERSION_RELEASE); if (apiNumber != null && apiName != null) { + // api number and name looks valid, perform a few more checks + if (checkPlatformContent(platform, log) == false) { + return null; + } // create the target. PlatformTarget target = new PlatformTarget( platform.getAbsolutePath(), @@ -351,7 +367,19 @@ public final class SdkManager { // need to parse the skins. String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS)); - target.setSkins(skins); + + // get the default skin, or take it from the base platform if needed. + String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN); + + if (defaultSkin == null) { + if (skins.length == 1) { + defaultSkin = skins[1]; + } else { + defaultSkin = baseTarget.getDefaultSkin(); + } + } + + target.setSkins(skins, defaultSkin); return target; } @@ -369,7 +397,28 @@ public final class SdkManager { addonName, valueName, SdkConstants.FN_MANIFEST_INI); } } + + /** + * Checks the given platform has all the required files, and returns true if they are all + * present. + *

    This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), + * aidl(.exe), dx(.bat), and dx.jar + */ + private boolean checkPlatformContent(File platform, ISdkLog log) { + for (String relativePath : sPlatformContentList) { + File f = new File(platform, relativePath); + if (f.exists() == false) { + log.error(null, + "Ignoring platform '%1$s': %2$s is missing.", + platform.getName(), relativePath); + return false; + } + + } + return true; + } + /** * Parses a property file and returns * @param buildProp the property file to parse @@ -377,9 +426,11 @@ public final class SdkManager { * @return the map of (key,value) pairs, or null if the parsing failed. */ public static Map parsePropertyFile(File buildProp, ISdkLog log) { + FileInputStream fis = null; + BufferedReader reader = null; try { - FileInputStream fis = new FileInputStream(buildProp); - BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); + fis = new FileInputStream(buildProp); + reader = new BufferedReader(new InputStreamReader(fis)); String line = null; Map map = new HashMap(); @@ -407,6 +458,21 @@ public final class SdkManager { log.warning("Error parsing '%1$s': %2$s.", buildProp.getAbsolutePath(), e.getMessage()); } + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // pass + } + } + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // pass + } + } } return null; diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java new file mode 100644 index 000000000..65cbbe356 --- /dev/null +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/AvdManager.java @@ -0,0 +1,844 @@ +/* + * Copyright (C) 2008 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.sdklib.avd; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.SdkManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Android Virtual Device Manager to manage AVDs. + */ +public final class AvdManager { + + public static final String AVD_FOLDER_EXTENSION = ".avd"; + + private final static String AVD_INFO_PATH = "path"; + private final static String AVD_INFO_TARGET = "target"; + + /** + * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any, + * or a 320x480 like constant for a numeric skin size. + * + * @see #NUMERIC_SKIN_SIZE + */ + public final static String AVD_INI_SKIN_PATH = "skin.path"; + /** + * AVD/config.ini key name representing an UI name for the skin. + * This config key is ignored by the emulator. It is only used by the SDK manager or + * tools to give a friendlier name to the skin. + * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead. + */ + public final static String AVD_INI_SKIN_NAME = "skin.name"; + /** + * AVD/config.ini key name representing the path to the sdcard file. + * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such + * a file. + * + * @see #SDCARD_IMG + */ + public final static String AVD_INI_SDCARD_PATH = "sdcard.path"; + /** + * AVD/config.ini key name representing the size of the SD card. + * This property is for UI purposes only. It is not used by the emulator. + * + * @see #SDCARD_SIZE_PATTERN + */ + public final static String AVD_INI_SDCARD_SIZE = "sdcard.size"; + /** + * AVD/config.ini key name representing the first path where the emulator looks + * for system images. Typically this is the path to the add-on system image or + * the path to the platform system image if there's no add-on. + *

    + * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}. + */ + public final static String AVD_INI_IMAGES_1 = "image.sysdir.1"; + /** + * AVD/config.ini key name representing the second path where the emulator looks + * for system images. Typically this is the path to the platform system image. + * + * @see #AVD_INI_IMAGES_1 + */ + public final static String AVD_INI_IMAGES_2 = "image.sysdir.2"; + + /** + * Pattern to match pixel-sized skin "names", e.g. "320x480". + */ + public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("[0-9]{2,}x[0-9]{2,}"); + + + private final static String USERDATA_IMG = "userdata.img"; + private final static String CONFIG_INI = "config.ini"; + private final static String SDCARD_IMG = "sdcard.img"; + + private final static String INI_EXTENSION = ".ini"; + private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + INI_EXTENSION + "$", + Pattern.CASE_INSENSITIVE); + + /** + * Pattern for matching SD Card sizes, e.g. "4K" or "16M". + */ + private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("\\d+[MK]?"); + + /** An immutable structure describing an Android Virtual Device. */ + public static final class AvdInfo { + private final String mName; + private final String mPath; + private final IAndroidTarget mTarget; + private final Map mProperties; + + /** Creates a new AVD info. Values are immutable. + * @param properties */ + public AvdInfo(String name, String path, IAndroidTarget target, + Map properties) { + mName = name; + mPath = path; + mTarget = target; + mProperties = properties; + } + + /** Returns the name of the AVD. */ + public String getName() { + return mName; + } + + /** Returns the path of the AVD data directory. */ + public String getPath() { + return mPath; + } + + /** Returns the target of the AVD. */ + public IAndroidTarget getTarget() { + return mTarget; + } + + /** + * Helper method that returns the .ini {@link File} for a given AVD name. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + public static File getIniFile(String name) throws AndroidLocationException { + String avdRoot; + avdRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; + return new File(avdRoot, name + INI_EXTENSION); + } + + /** + * Returns the .ini {@link File} for this AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + public File getIniFile() throws AndroidLocationException { + return getIniFile(mName); + } + + /** + * Returns a map of properties for the AVD. + */ + public Map getProperties() { + return mProperties; + } + } + + private final ArrayList mAvdList = new ArrayList(); + private ISdkLog mSdkLog; + private final SdkManager mSdk; + + public AvdManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException { + mSdk = sdk; + mSdkLog = sdkLog; + buildAvdList(); + } + + /** + * Returns the existing AVDs. + * @return a newly allocated array containing all the AVDs. + */ + public AvdInfo[] getAvds() { + return mAvdList.toArray(new AvdInfo[mAvdList.size()]); + } + + /** + * Returns the {@link AvdInfo} matching the given name. + * @return the matching AvdInfo or null if none were found. + */ + public AvdInfo getAvd(String name) { + for (AvdInfo info : mAvdList) { + if (info.getName().equals(name)) { + return info; + } + } + + return null; + } + + /** + * Creates a new AVD. It is expected that there is no existing AVD with this name already. + * @param avdFolder the data folder for the AVD. It will be created as needed. + * @param name the name of the AVD + * @param target the target of the AVD + * @param skinName the name of the skin. Can be null. Must have been verified by caller. + * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to + * an existing sdcard image or a sdcard size (\d+, \d+K, \dM). + * @param hardwareConfig the hardware setup for the AVD + * @param removePrevious If true remove any previous files. + */ + public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target, + String skinName, String sdcard, Map hardwareConfig, + boolean removePrevious, ISdkLog log) { + + File iniFile = null; + boolean needCleanup = false; + try { + if (avdFolder.exists()) { + if (removePrevious) { + // AVD already exists and removePrevious is set, try to remove the + // directory's content first (but not the directory itself). + recursiveDelete(avdFolder); + } else { + // AVD shouldn't already exist if removePrevious is false. + if (log != null) { + log.error(null, + "Folder %s is in the way. Use --force if you want to overwrite.", + avdFolder.getAbsolutePath()); + } + return null; + } + } else { + // create the AVD folder. + avdFolder.mkdir(); + } + + // actually write the ini file + iniFile = createAvdIniFile(name, avdFolder, target); + + // writes the userdata.img in it. + String imagePath = target.getPath(IAndroidTarget.IMAGES); + File userdataSrc = new File(imagePath, USERDATA_IMG); + + if (userdataSrc.exists() == false && target.isPlatform() == false) { + imagePath = target.getParent().getPath(IAndroidTarget.IMAGES); + userdataSrc = new File(imagePath, USERDATA_IMG); + } + + if (userdataSrc.exists() == false) { + log.error(null, "Unable to find a '%1$s' file to copy into the AVD folder.", + USERDATA_IMG); + needCleanup = true; + return null; + } + + FileInputStream fis = new FileInputStream(userdataSrc); + + File userdataDest = new File(avdFolder, USERDATA_IMG); + FileOutputStream fos = new FileOutputStream(userdataDest); + + byte[] buffer = new byte[4096]; + int count; + while ((count = fis.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + + fos.close(); + fis.close(); + + // Config file. + HashMap values = new HashMap(); + + // First the image folders of the target itself + imagePath = getImageRelativePath(target, log); + if (imagePath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_IMAGES_1, imagePath); + + // If the target is an add-on we need to add the Platform image as a backup. + IAndroidTarget parent = target.getParent(); + if (parent != null) { + imagePath = getImageRelativePath(parent, log); + if (imagePath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_IMAGES_2, imagePath); + } + + + // Now the skin. + if (skinName == null) { + skinName = target.getDefaultSkin(); + } + + if (NUMERIC_SKIN_SIZE.matcher(skinName).matches()) { + // Skin name is an actual screen resolution. + // Set skin.name for display purposes in the AVD manager and + // set skin.path for use by the emulator. + values.put(AVD_INI_SKIN_NAME, skinName); + values.put(AVD_INI_SKIN_PATH, skinName); + } else { + // get the path of the skin (relative to the SDK) + // assume skin name is valid + String skinPath = getSkinRelativePath(skinName, target, log); + if (skinPath == null) { + needCleanup = true; + return null; + } + + values.put(AVD_INI_SKIN_PATH, skinPath); + values.put(AVD_INI_SKIN_NAME, skinName); + } + + if (sdcard != null) { + File sdcardFile = new File(sdcard); + if (sdcardFile.isFile()) { + // sdcard value is an external sdcard, so we put its path into the config.ini + values.put(AVD_INI_SDCARD_PATH, sdcard); + } else { + // Sdcard is possibly a size. In that case we create a file called 'sdcard.img' + // in the AVD folder, and do not put any value in config.ini. + + // First, check that it matches the pattern for sdcard size + Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); + if (m.matches()) { + // create the sdcard. + sdcardFile = new File(avdFolder, SDCARD_IMG); + String path = sdcardFile.getAbsolutePath(); + + // execute mksdcard with the proper parameters. + File toolsFolder = new File(mSdk.getLocation(), SdkConstants.FD_TOOLS); + File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName()); + + if (mkSdCard.isFile() == false) { + log.error(null, "'%1$s' is missing from the SDK tools folder.", + mkSdCard.getName()); + needCleanup = true; + return null; + } + + if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) { + needCleanup = true; + return null; // mksdcard output has already been displayed, no need to + // output anything else. + } + + // add a property containing the size of the sdcard for display purpose + // only when the dev does 'android list avd' + values.put(AVD_INI_SDCARD_SIZE, sdcard); + } else { + log.error(null, + "'%1$s' is not recognized as a valid sdcard value.\n" + + "Value should be:\n" + + "1. path to an sdcard.\n" + + "2. size of the sdcard to create: [K|M]", + sdcard); + needCleanup = true; + return null; + } + } + } + + if (hardwareConfig != null) { + values.putAll(hardwareConfig); + } + + File configIniFile = new File(avdFolder, CONFIG_INI); + createConfigIni(configIniFile, values); + + if (log != null) { + if (target.isPlatform()) { + log.printf("Created AVD '%s' based on %s\n", name, target.getName()); + } else { + log.printf("Created AVD '%s' based on %s (%s)\n", name, target.getName(), + target.getVendor()); + } + } + + // create the AvdInfo object, and add it to the list + AvdInfo avdInfo = new AvdInfo(name, avdFolder.getAbsolutePath(), target, values); + + mAvdList.add(avdInfo); + + return avdInfo; + } catch (AndroidLocationException e) { + if (log != null) { + log.error(e, null); + } + } catch (IOException e) { + if (log != null) { + log.error(e, null); + } + } finally { + if (needCleanup) { + if (iniFile != null && iniFile.exists()) { + iniFile.delete(); + } + + recursiveDelete(avdFolder); + avdFolder.delete(); + } + } + + return null; + } + + /** + * Returns the path to the target images folder as a relative path to the SDK. + */ + private String getImageRelativePath(IAndroidTarget target, ISdkLog log) { + String imageFullPath = target.getPath(IAndroidTarget.IMAGES); + + // make this path relative to the SDK location + String sdkLocation = mSdk.getLocation(); + if (imageFullPath.startsWith(sdkLocation) == false) { + // this really really should not happen. + log.error(null, "Target location is not inside the SDK."); + assert false; + return null; + } + + imageFullPath = imageFullPath.substring(sdkLocation.length()); + if (imageFullPath.charAt(0) == File.separatorChar) { + imageFullPath = imageFullPath.substring(1); + } + return imageFullPath; + } + + /** + * Returns the path to the skin, as a relative path to the SDK. + */ + private String getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log) { + // first look to see if the skin is in the target + + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + + if (skin.exists() == false && target.isPlatform() == false) { + target = target.getParent(); + + path = target.getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + // skin really does not exist! + if (skin.exists() == false) { + log.error(null, "Skin '%1$s' does not exist.", skinName); + return null; + } + + // get the skin path + path = skin.getAbsolutePath(); + + // make this path relative to the SDK location + String sdkLocation = mSdk.getLocation(); + if (path.startsWith(sdkLocation) == false) { + // this really really should not happen. + log.error(null, "Target location is not inside the SDK."); + assert false; + return null; + } + + path = path.substring(sdkLocation.length()); + if (path.charAt(0) == File.separatorChar) { + path = path.substring(1); + } + return path; + } + + /** + * Creates the ini file for an AVD. + * + * @param name of the AVD. + * @param avdFolder path for the data folder of the AVD. + * @param target of the AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(String name, File avdFolder, IAndroidTarget target) + throws AndroidLocationException, IOException { + HashMap values = new HashMap(); + File iniFile = AvdInfo.getIniFile(name); + values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath()); + values.put(AVD_INFO_TARGET, target.hashString()); + createConfigIni(iniFile, values); + + return iniFile; + } + + /** + * Creates the ini file for an AVD. + * + * @param info of the AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(AvdInfo info) throws AndroidLocationException, IOException { + return createAvdIniFile(info.getName(), new File(info.getPath()), info.getTarget()); + } + + /** + * Actually deletes the files of an existing AVD. + *

    + * This also remove it from the manager's list, The caller does not need to + * call {@link #removeAvd(AvdInfo)} afterwards. + * + * @param avdInfo the information on the AVD to delete + */ + public void deleteAvd(AvdInfo avdInfo, ISdkLog log) { + try { + File f = avdInfo.getIniFile(); + if (f.exists()) { + log.warning("Deleting file %s", f.getCanonicalPath()); + if (!f.delete()) { + log.error(null, "Failed to delete %s", f.getCanonicalPath()); + } + } + + f = new File(avdInfo.getPath()); + if (f.exists()) { + log.warning("Deleting folder %s", f.getCanonicalPath()); + recursiveDelete(f); + if (!f.delete()) { + log.error(null, "Failed to delete %s", f.getCanonicalPath()); + } + } + + removeAvd(avdInfo); + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } + } + + /** + * Moves and/or rename an existing AVD and its files. + * This also change it in the manager's list. + *

    + * The caller should make sure the name or path given are valid, do not exist and are + * actually different than current values. + * + * @param avdInfo the information on the AVD to move. + * @param newName the new name of the AVD if non null. + * @param paramFolderPath the new data folder if non null. + * @return True if the move succeeded or there was nothing to do. + * If false, this method will have had already output error in the log. + */ + public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) { + + try { + if (paramFolderPath != null) { + File f = new File(avdInfo.getPath()); + log.warning("Moving '%s' to '%s'.", avdInfo.getPath(), paramFolderPath); + if (!f.renameTo(new File(paramFolderPath))) { + log.error(null, "Failed to move '%s' to '%s'.", + avdInfo.getPath(), paramFolderPath); + return false; + } + + // update avd info + AvdInfo info = new AvdInfo(avdInfo.getName(), paramFolderPath, avdInfo.getTarget(), + avdInfo.getProperties()); + mAvdList.remove(avdInfo); + mAvdList.add(info); + avdInfo = info; + + // update the ini file + createAvdIniFile(avdInfo); + } + + if (newName != null) { + File oldIniFile = avdInfo.getIniFile(); + File newIniFile = AvdInfo.getIniFile(newName); + + log.warning("Moving '%s' to '%s'.", oldIniFile.getPath(), newIniFile.getPath()); + if (!oldIniFile.renameTo(newIniFile)) { + log.error(null, "Failed to move '%s' to '%s'.", + oldIniFile.getPath(), newIniFile.getPath()); + return false; + } + + // update avd info + AvdInfo info = new AvdInfo(newName, avdInfo.getPath(), avdInfo.getTarget(), + avdInfo.getProperties()); + mAvdList.remove(avdInfo); + mAvdList.add(info); + } + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } + + // nothing to do or succeeded + return true; + } + + /** + * Helper method to recursively delete a folder's content (but not the folder itself). + * + * @throws SecurityException like {@link File#delete()} does if file/folder is not writable. + */ + public void recursiveDelete(File folder) { + for (File f : folder.listFiles()) { + if (f.isDirectory()) { + recursiveDelete(folder); + } + f.delete(); + } + } + + private void buildAvdList() throws AndroidLocationException { + // get the Android prefs location. + String avdRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; + + final boolean avdListDebug = System.getenv("AVD_LIST_DEBUG") != null; + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root: '%s'\n", avdRoot); + } + + // ensure folder validity. + File folder = new File(avdRoot); + if (folder.isFile()) { + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root is a file.\n"); + } + throw new AndroidLocationException(String.format("%s is not a valid folder.", avdRoot)); + } else if (folder.exists() == false) { + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] AVD root folder doesn't exist, creating.\n"); + } + // folder is not there, we create it and return + folder.mkdirs(); + return; + } + + File[] avds = folder.listFiles(new FilenameFilter() { + public boolean accept(File parent, String name) { + if (INI_NAME_PATTERN.matcher(name).matches()) { + // check it's a file and not a folder + boolean isFile = new File(parent, name).isFile(); + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Item '%s': %s\n", + name, isFile ? "accepted file" : "rejected"); + } + return isFile; + } + + return false; + } + }); + + for (File avd : avds) { + AvdInfo info = parseAvdInfo(avd); + if (info != null) { + mAvdList.add(info); + if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Added AVD '%s'\n", info.getPath()); + } + } else if (avdListDebug) { + mSdkLog.printf("[AVD LIST DEBUG] Failed to parse AVD '%s'\n", avd.getPath()); + } + } + } + + private AvdInfo parseAvdInfo(File path) { + Map map = SdkManager.parsePropertyFile(path, mSdkLog); + + String avdPath = map.get(AVD_INFO_PATH); + if (avdPath == null) { + return null; + } + + String targetHash = map.get(AVD_INFO_TARGET); + if (targetHash == null) { + return null; + } + + IAndroidTarget target = mSdk.getTargetFromHashString(targetHash); + if (target == null) { + return null; + } + + // load the avd properties. + File configIniFile = new File(avdPath, CONFIG_INI); + Map properties = SdkManager.parsePropertyFile(configIniFile, mSdkLog); + + Matcher matcher = INI_NAME_PATTERN.matcher(path.getName()); + + AvdInfo info = new AvdInfo( + matcher.matches() ? matcher.group(1) : path.getName(), // should not happen + avdPath, + target, + properties); + + return info; + } + + private static void createConfigIni(File iniFile, Map values) + throws IOException { + FileWriter writer = new FileWriter(iniFile); + + for (Entry entry : values.entrySet()) { + writer.write(String.format("%s=%s\n", entry.getKey(), entry.getValue())); + } + writer.close(); + + } + + private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) { + try { + String[] command = new String[3]; + command[0] = toolLocation; + command[1] = size; + command[2] = location; + Process process = Runtime.getRuntime().exec(command); + + ArrayList errorOutput = new ArrayList(); + ArrayList stdOutput = new ArrayList(); + int status = grabProcessOutput(process, errorOutput, stdOutput, + true /* waitForReaders */); + + if (status != 0) { + log.error(null, "Failed to create the SD card."); + for (String error : errorOutput) { + log.error(null, error); + } + + return false; + } + + return true; + } catch (InterruptedException e) { + log.error(null, "Failed to create the SD card."); + } catch (IOException e) { + log.error(null, "Failed to create the SD card."); + } + + return false; + } + + /** + * Gets the stderr/stdout outputs of a process and returns when the process is done. + * Both must be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList errorOutput, + final ArrayList stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Removes an {@link AvdInfo} from the internal list. + * + * @param avdInfo The {@link AvdInfo} to remove. + * @return true if this {@link AvdInfo} was present and has been removed. + */ + public boolean removeAvd(AvdInfo avdInfo) { + return mAvdList.remove(avdInfo); + } + +} diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/HardwareProperties.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java similarity index 99% rename from tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/HardwareProperties.java rename to tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java index 98e97fe12..ed5b94926 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/HardwareProperties.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/avd/HardwareProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.sdklib.vm; +package com.android.sdklib.avd; import com.android.sdklib.ISdkLog; diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java new file mode 100644 index 000000000..b89d3bd31 --- /dev/null +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ApkConfigurationHelper.java @@ -0,0 +1,106 @@ +/* + * 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.sdklib.project; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * Helper class to read and write Apk Configuration into a {@link ProjectProperties} file. + */ +public class ApkConfigurationHelper { + /** Prefix for property names for config definition. This prevents having config named + * after other valid properties such as "target". */ + final static String CONFIG_PREFIX = "apk-config-"; + + /** + * Reads the Apk Configurations from a {@link ProjectProperties} file and returns them as a map. + *

    If there are no defined configurations, the returned map will be empty. + * @return a map of apk configurations. The map contains (name, filter) where name is + * the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c) + */ + public static Map getConfigs(ProjectProperties properties) { + HashMap configMap = new HashMap(); + + // get the list of configs. + String configList = properties.getProperty(ProjectProperties.PROPERTY_APK_CONFIGS); + if (configList != null) { + // this is a comma separated list + String[] configs = configList.split(","); //$NON-NLS-1$ + + // read the value of each config and store it in a map + for (String config : configs) { + config = config.trim(); + String configValue = properties.getProperty(CONFIG_PREFIX + config); + if (configValue != null) { + configMap.put(config, configValue); + } + } + } + + return configMap; + } + + /** + * Writes the Apk Configurations from a given map into a {@link ProjectProperties}. + * @param properties the {@link ProjectProperties} in which to store the apk configurations. + * @param configMap a map of apk configurations. The map contains (name, filter) where name is + * the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c) + * @return true if the {@link ProjectProperties} contained Apk Configuration that were not + * present in the map. + */ + public static boolean setConfigs(ProjectProperties properties, Map configMap) { + // load the current configs, in order to remove the value properties for each of them + // in case a config was removed. + + // get the list of configs. + String configList = properties.getProperty(ProjectProperties.PROPERTY_APK_CONFIGS); + + boolean hasRemovedConfig = false; + + if (configList != null) { + // this is a comma separated list + String[] configs = configList.split(","); //$NON-NLS-1$ + + for (String config : configs) { + config = config.trim(); + if (configMap.containsKey(config) == false) { + hasRemovedConfig = true; + properties.removeProperty(CONFIG_PREFIX + config); + } + } + } + + // now add the properties. + Set> entrySet = configMap.entrySet(); + StringBuilder sb = new StringBuilder(); + for (Entry entry : entrySet) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(entry.getKey()); + properties.setProperty(CONFIG_PREFIX + entry.getKey(), entry.getValue()); + } + properties.setProperty(ProjectProperties.PROPERTY_APK_CONFIGS, sb.toString()); + + return hasRemovedConfig; + } +} diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java index 1cff43c88..7489b65d6 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectCreator.java @@ -209,7 +209,7 @@ public class ProjectCreator { } // create the source folder and the java package folders. - final String srcFolderPath = SdkConstants.FD_SOURCES + File.separator + packagePath; + String srcFolderPath = SdkConstants.FD_SOURCES + File.separator + packagePath; File sourceFolder = createDirs(projectFolder, srcFolderPath); String javaTemplate = "java_file.template"; String activityFileName = activityName + ".java"; @@ -220,6 +220,10 @@ public class ProjectCreator { installTemplate(javaTemplate, new File(sourceFolder, activityFileName), keywords, target); + // create the generate source folder + srcFolderPath = SdkConstants.FD_GEN_SOURCES + File.separator + packagePath; + sourceFolder = createDirs(projectFolder, srcFolderPath); + // create other useful folders File resourceFodler = createDirs(projectFolder, SdkConstants.FD_RESOURCES); createDirs(projectFolder, SdkConstants.FD_OUTPUT); @@ -314,9 +318,14 @@ public class ProjectCreator { } } - // Update default.prop iif --target was specified + // Update default.prop if --target was specified if (target != null) { - props = ProjectProperties.create(folderPath, PropertyType.DEFAULT); + // we already attempted to load the file earlier, if that failed, create it. + if (props == null) { + props = ProjectProperties.create(folderPath, PropertyType.DEFAULT); + } + + // set or replace the target props.setAndroidTarget(target); try { props.save(); @@ -330,7 +339,14 @@ public class ProjectCreator { } // Refresh/create "sdk" in local.properties - props = ProjectProperties.create(folderPath, PropertyType.LOCAL); + // because the file may already exists and contain other values (like apk config), + // we first try to load it. + props = ProjectProperties.load(folderPath, PropertyType.LOCAL); + if (props == null) { + props = ProjectProperties.create(folderPath, PropertyType.LOCAL); + } + + // set or replace the sdk location. props.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); try { props.save(); diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java index 4e1c27ff0..69a16bee8 100644 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/project/ProjectProperties.java @@ -33,6 +33,7 @@ import java.util.Map.Entry; public final class ProjectProperties { /** The property name for the project target */ public final static String PROPERTY_TARGET = "target"; + public final static String PROPERTY_APK_CONFIGS = "apk-configurations"; public final static String PROPERTY_SDK = "sdk-location"; public static enum PropertyType { @@ -97,7 +98,19 @@ public final class ProjectProperties { // 1-------10--------20--------30--------40--------50--------60--------70--------80 COMMENT_MAP.put(PROPERTY_TARGET, "# Project target.\n"); - COMMENT_MAP.put(PROPERTY_SDK, "# location of the SDK. This is only used by Ant\n" + + COMMENT_MAP.put(PROPERTY_APK_CONFIGS, + "# apk configurations. This property allows creation of APK files with limited\n" + + "# resources. For example, if your application contains many locales and\n" + + "# you wish to release multiple smaller apks instead of a large one, you can\n" + + "# define configuration to create apks with limited language sets.\n" + + "# Format is a comma separated list of configuration names. For each\n" + + "# configuration, a property will declare the resource configurations to\n" + + "# include. Example:\n" + + "# " + PROPERTY_APK_CONFIGS +"=european,northamerica\n" + + "# " + ApkConfigurationHelper.CONFIG_PREFIX + "european=en,fr,it,de,es\n" + + "# " + ApkConfigurationHelper.CONFIG_PREFIX + "northamerica=en,es\n"); + COMMENT_MAP.put(PROPERTY_SDK, + "# location of the SDK. This is only used by Ant\n" + "# For customization when using a Version Control System, please read the\n" + "# header note.\n"); } @@ -201,6 +214,14 @@ public final class ProjectProperties { public String getProperty(String name) { return mProperties.get(name); } + + /** + * Removes a property and returns its previous value (or null if the property did not exist). + * @param name the name of the property to remove. + */ + public String removeProperty(String name) { + return mProperties.remove(name); + } /** * Saves the property file. diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/VmManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/VmManager.java deleted file mode 100644 index 39316d2b0..000000000 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/vm/VmManager.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2008 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.sdklib.vm; - -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISdkLog; -import com.android.sdklib.SdkConstants; -import com.android.sdklib.SdkManager; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Virtual Machine manager to access the list of VMs or create new ones. - */ -public final class VmManager { - - private final static String VM_INFO_PATH = "path"; - private final static String VM_INFO_TARGET = "target"; - - private final static String IMAGE_USERDATA = "userdata.img"; - private final static String CONFIG_INI = "config.ini"; - - private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\.ini$", - Pattern.CASE_INSENSITIVE); - - private final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("\\d+[MK]?"); - - public static final class VmInfo { - String name; - String path; - IAndroidTarget target; - - public String getName() { - return name; - } - - public String getPath() { - return path; - } - - public IAndroidTarget getTarget() { - return target; - } - } - - private final ArrayList mVmList = new ArrayList(); - private ISdkLog mSdkLog; - private final SdkManager mSdk; - - public VmManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException { - mSdk = sdk; - mSdkLog = sdkLog; - buildVmList(); - } - - /** - * Returns the existing VMs. - * @return a newly allocated arrays containing all the VMs. - */ - public VmInfo[] getVms() { - return mVmList.toArray(new VmInfo[mVmList.size()]); - } - - /** - * Returns the {@link VmInfo} matching the given name. - * @return the matching VmInfo or null if none were found. - */ - public VmInfo getVm(String name) { - for (VmInfo info : mVmList) { - if (info.name.equals(name)) { - return info; - } - } - - return null; - } - - /** - * Creates a new VM. It is expected that there is no existing VM with this name already. - * @param parentFolder the folder to contain the VM. A new folder will be created in this - * folder with the name of the VM - * @param name the name of the VM - * @param target the target of the VM - * @param skinName the name of the skin. Can be null. - * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to - * an existing sdcard image or a sdcard size (\d+, \d+K, \dM). - * @param hardwareConfig the hardware setup for the VM - */ - public VmInfo createVm(String parentFolder, String name, IAndroidTarget target, - String skinName, String sdcard, Map hardwareConfig, - ISdkLog log) { - - try { - File rootDirectory = new File(parentFolder); - if (rootDirectory.isDirectory() == false) { - if (log != null) { - log.error(null, "Folder %s does not exist.", parentFolder); - } - return null; - } - - File vmFolder = new File(parentFolder, name + ".avm"); - if (vmFolder.exists()) { - if (log != null) { - log.error(null, "Folder %s is in the way.", vmFolder.getAbsolutePath()); - } - return null; - } - - // create the vm folder. - vmFolder.mkdir(); - - HashMap values = new HashMap(); - - // prepare the ini file. - String vmRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_VMS; - File iniFile = new File(vmRoot, name + ".ini"); - values.put(VM_INFO_PATH, vmFolder.getAbsolutePath()); - values.put(VM_INFO_TARGET, target.hashString()); - createConfigIni(iniFile, values); - - // writes the userdata.img in it. - String imagePath = target.getPath(IAndroidTarget.IMAGES); - File userdataSrc = new File(imagePath, IMAGE_USERDATA); - FileInputStream fis = new FileInputStream(userdataSrc); - - File userdataDest = new File(vmFolder, IMAGE_USERDATA); - FileOutputStream fos = new FileOutputStream(userdataDest); - - byte[] buffer = new byte[4096]; - int count; - while ((count = fis.read(buffer)) != -1) { - fos.write(buffer, 0, count); - } - - fos.close(); - fis.close(); - - // Config file - values.clear(); - if (skinName != null) { - // check that the skin name is valid - String[] skinNames = target.getSkins(); - boolean found = false; - for (String n : skinNames) { - if (n.equals(skinName)) { - values.put("skin", skinName); - found = true; - break; - } - } - - if (found == false && log != null) { - log.warning("Skin '%1$s' does not exists, using default skin.", skinName); - } - } - - if (sdcard != null) { - File sdcardFile = new File(sdcard); - if (sdcardFile.isFile()) { - values.put("sdcard", sdcard); - } else { - // check that it matches the pattern for sdcard size - Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); - if (m.matches()) { - // create the sdcard. - sdcardFile = new File(vmFolder, "sdcard.img"); - String path = sdcardFile.getAbsolutePath(); - - // execute mksdcard with the proper parameters. - File toolsFolder = new File(mSdk.getLocation(), SdkConstants.FD_TOOLS); - File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName()); - - if (mkSdCard.isFile() == false) { - log.error(null, "'%1$s' is missing from the SDK tools folder.", - mkSdCard.getName()); - return null; - } - - if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) { - return null; // mksdcard output has already been displayed, no need to - // output anything else. - } - - // add its path to the values. - values.put("sdcard", path); - } else { - log.error(null, "'%1$s' is not recognized as a valid sdcard value", sdcard); - return null; - } - } - } - - if (hardwareConfig != null) { - values.putAll(hardwareConfig); - } - - File configIniFile = new File(vmFolder, CONFIG_INI); - createConfigIni(configIniFile, values); - - if (log != null) { - if (target.isPlatform()) { - log.printf("Created VM '%s' based on %s\n", name, target.getName()); - } else { - log.printf( - "Created VM '%s' based on %s (%s)\n", name, target.getName(), - target.getVendor()); - } - } - - // create the VmInfo object, and add it to the list - VmInfo vmInfo = new VmInfo(); - vmInfo.name = name; - vmInfo.path = vmFolder.getAbsolutePath(); - vmInfo.target = target; - - mVmList.add(vmInfo); - - return vmInfo; - } catch (AndroidLocationException e) { - if (log != null) { - log.error(e, null); - } - } catch (IOException e) { - if (log != null) { - log.error(e, null); - } - } - - return null; - } - - private void buildVmList() throws AndroidLocationException { - // get the Android prefs location. - String vmRoot = AndroidLocation.getFolder() + AndroidLocation.FOLDER_VMS; - - // ensure folder validity. - File folder = new File(vmRoot); - if (folder.isFile()) { - throw new AndroidLocationException(String.format("%s is not a valid folder.", vmRoot)); - } else if (folder.exists() == false) { - // folder is not there, we create it and return - folder.mkdirs(); - return; - } - - File[] vms = folder.listFiles(new FilenameFilter() { - public boolean accept(File parent, String name) { - if (INI_NAME_PATTERN.matcher(name).matches()) { - // check it's a file and not a folder - return new File(parent, name).isFile(); - } - - return false; - } - }); - - for (File vm : vms) { - VmInfo info = parseVmInfo(vm); - if (info != null) { - mVmList.add(info); - } - } - } - - private VmInfo parseVmInfo(File path) { - Map map = SdkManager.parsePropertyFile(path, mSdkLog); - - String vmPath = map.get(VM_INFO_PATH); - if (vmPath == null) { - return null; - } - - String targetHash = map.get(VM_INFO_TARGET); - if (targetHash == null) { - return null; - } - - IAndroidTarget target = mSdk.getTargetFromHashString(targetHash); - if (target == null) { - return null; - } - - VmInfo info = new VmInfo(); - Matcher matcher = INI_NAME_PATTERN.matcher(path.getName()); - if (matcher.matches()) { - info.name = matcher.group(1); - } else { - info.name = path.getName(); // really this should not happen. - } - info.path = vmPath; - info.target = target; - - return info; - } - - private static void createConfigIni(File iniFile, Map values) - throws IOException { - FileWriter writer = new FileWriter(iniFile); - - for (Entry entry : values.entrySet()) { - writer.write(String.format("%s=%s\n", entry.getKey(), entry.getValue())); - } - writer.close(); - - } - - private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) { - try { - String[] command = new String[3]; - command[0] = toolLocation; - command[1] = size; - command[2] = location; - Process process = Runtime.getRuntime().exec(command); - - ArrayList errorOutput = new ArrayList(); - ArrayList stdOutput = new ArrayList(); - int status = grabProcessOutput(process, errorOutput, stdOutput, - true /* waitForReaders */); - - if (status != 0) { - log.error(null, "Failed to create the SD card."); - for (String error : errorOutput) { - log.error(null, error); - } - - return false; - } - - return true; - } catch (InterruptedException e) { - log.error(null, "Failed to create the SD card."); - } catch (IOException e) { - log.error(null, "Failed to create the SD card."); - } - - return false; - } - - /** - * Gets the stderr/stdout outputs of a process and returns when the process is done. - * Both must be read or the process will block on windows. - * @param process The process to get the ouput from - * @param errorOutput The array to store the stderr output. cannot be null. - * @param stdOutput The array to store the stdout output. cannot be null. - * @param waitforReaders if true, this will wait for the reader threads. - * @return the process return code. - * @throws InterruptedException - */ - private int grabProcessOutput(final Process process, final ArrayList errorOutput, - final ArrayList stdOutput, boolean waitforReaders) - throws InterruptedException { - assert errorOutput != null; - assert stdOutput != null; - // read the lines as they come. if null is returned, it's - // because the process finished - Thread t1 = new Thread("") { //$NON-NLS-1$ - @Override - public void run() { - // create a buffer to read the stderr output - InputStreamReader is = new InputStreamReader(process.getErrorStream()); - BufferedReader errReader = new BufferedReader(is); - - try { - while (true) { - String line = errReader.readLine(); - if (line != null) { - errorOutput.add(line); - } else { - break; - } - } - } catch (IOException e) { - // do nothing. - } - } - }; - - Thread t2 = new Thread("") { //$NON-NLS-1$ - @Override - public void run() { - InputStreamReader is = new InputStreamReader(process.getInputStream()); - BufferedReader outReader = new BufferedReader(is); - - try { - while (true) { - String line = outReader.readLine(); - if (line != null) { - stdOutput.add(line); - } else { - break; - } - } - } catch (IOException e) { - // do nothing. - } - } - }; - - t1.start(); - t2.start(); - - // it looks like on windows process#waitFor() can return - // before the thread have filled the arrays, so we wait for both threads and the - // process itself. - if (waitforReaders) { - try { - t1.join(); - } catch (InterruptedException e) { - } - try { - t2.join(); - } catch (InterruptedException e) { - } - } - - // get the return code from the process - return process.waitFor(); - } - -} diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java new file mode 100644 index 000000000..1460fd7c8 --- /dev/null +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigEditDialog.java @@ -0,0 +1,177 @@ +/* + * 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.sdkuilib; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Edit dialog to create/edit APK configuration. The dialog displays 2 text fields for the config + * name and its filter. + */ +class ApkConfigEditDialog extends Dialog implements ModifyListener, VerifyListener { + + private String mName; + private String mFilter; + private Text mNameField; + private Text mFilterField; + private Button mOkButton; + + /** + * Creates an edit dialog with optional initial values for the name and filter. + * @param name optional value for the name. Can be null. + * @param filter optional value for the filter. Can be null. + * @param parentShell the parent shell. + */ + protected ApkConfigEditDialog(String name, String filter, Shell parentShell) { + super(parentShell); + mName = name; + mFilter = filter; + } + + /** + * Returns the name of the config. This is only valid if the user clicked OK and {@link #open()} + * returned {@link Window#OK} + */ + public String getName() { + return mName; + } + + /** + * Returns the filter for the config. This is only valid if the user clicked OK and + * {@link #open()} returned {@link Window#OK} + */ + public String getFilter() { + return mFilter; + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + + mOkButton = getButton(IDialogConstants.OK_ID); + validateButtons(); + + return control; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridLayout layout; + composite.setLayout(layout = new GridLayout(2, false)); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + layout.horizontalSpacing = convertHorizontalDLUsToPixels( + IDialogConstants.HORIZONTAL_SPACING); + + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Label l = new Label(composite, SWT.NONE); + l.setText("Name"); + + mNameField = new Text(composite, SWT.BORDER); + mNameField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNameField.addVerifyListener(this); + if (mName != null) { + mNameField.setText(mName); + } + mNameField.addModifyListener(this); + + l = new Label(composite, SWT.NONE); + l.setText("Filter"); + + mFilterField = new Text(composite, SWT.BORDER); + mFilterField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (mFilter != null) { + mFilterField.setText(mFilter); + } + mFilterField.addVerifyListener(this); + mFilterField.addModifyListener(this); + + applyDialogFont(composite); + return composite; + } + + /** + * Validates the OK button based on the content of the 2 text fields. + */ + private void validateButtons() { + mOkButton.setEnabled(mNameField.getText().trim().length() > 0 && + mFilterField.getText().trim().length() > 0); + } + + @Override + protected void okPressed() { + mName = mNameField.getText(); + mFilter = mFilterField.getText().trim(); + super.okPressed(); + } + + /** + * Callback for text modification in the 2 text fields. + */ + public void modifyText(ModifyEvent e) { + validateButtons(); + } + + /** + * Callback to ensure the content of the text field are proper. + */ + public void verifyText(VerifyEvent e) { + Text source = ((Text)e.getSource()); + if (source == mNameField) { + // check for a-zA-Z0-9. + final String text = e.text; + final int len = text.length(); + for (int i = 0 ; i < len; i++) { + char letter = text.charAt(i); + if (letter > 255 || Character.isLetterOrDigit(letter) == false) { + e.doit = false; + return; + } + } + } else if (source == mFilterField) { + // we can't validate the content as its typed, but we can at least ensure the characters + // are valid. Same as mNameFiled + the comma. + final String text = e.text; + final int len = text.length(); + for (int i = 0 ; i < len; i++) { + char letter = text.charAt(i); + if (letter > 255 || (Character.isLetterOrDigit(letter) == false && letter != ',')) { + e.doit = false; + return; + } + } + } + } +} diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java new file mode 100644 index 000000000..825be93b1 --- /dev/null +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ApkConfigWidget.java @@ -0,0 +1,211 @@ +/* + * 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.sdkuilib; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * The APK Configuration widget is a table that is added to the given parent composite. + *

    + * To use, create it using {@link #ApkConfigWidget(Composite)} then + * call {@link #fillTable(Map)} to set the initial list of configurations. + */ +public class ApkConfigWidget { + private final static int INDEX_NAME = 0; + private final static int INDEX_FILTER = 1; + + private Table mApkConfigTable; + private Button mEditButton; + private Button mDelButton; + + public ApkConfigWidget(final Composite parent) { + final Composite apkConfigComp = new Composite(parent, SWT.NONE); + apkConfigComp.setLayoutData(new GridData(GridData.FILL_BOTH)); + apkConfigComp.setLayout(new GridLayout(2, false)); + + mApkConfigTable = new Table(apkConfigComp, SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + mApkConfigTable.setHeaderVisible(true); + mApkConfigTable.setLinesVisible(true); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mApkConfigTable.setLayoutData(data); + + // create the table columns + final TableColumn column0 = new TableColumn(mApkConfigTable, SWT.NONE); + column0.setText("Name"); + column0.setWidth(100); + final TableColumn column1 = new TableColumn(mApkConfigTable, SWT.NONE); + column1.setText("Configuration"); + column1.setWidth(100); + + Composite buttonComp = new Composite(apkConfigComp, SWT.NONE); + buttonComp.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + + Button newButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + newButton.setText("New..."); + newButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mEditButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + mEditButton.setText("Edit..."); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDelButton = new Button(buttonComp, SWT.PUSH | SWT.FLAT); + mDelButton.setText("Delete"); + mDelButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + newButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + ApkConfigEditDialog dlg = new ApkConfigEditDialog(null /*name*/, null /*filter*/, + apkConfigComp.getShell()); + if (dlg.open() == Dialog.OK) { + TableItem item = new TableItem(mApkConfigTable, SWT.NONE); + item.setText(INDEX_NAME, dlg.getName()); + item.setText(INDEX_FILTER, dlg.getFilter()); + + onSelectionChanged(); + } + } + }); + + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the current selection (single mode so we don't care about any item beyond + // index 0). + TableItem[] items = mApkConfigTable.getSelection(); + if (items.length != 0) { + ApkConfigEditDialog dlg = new ApkConfigEditDialog( + items[0].getText(INDEX_NAME), items[0].getText(INDEX_FILTER), + apkConfigComp.getShell()); + if (dlg.open() == Dialog.OK) { + items[0].setText(INDEX_NAME, dlg.getName()); + items[0].setText(INDEX_FILTER, dlg.getFilter()); + } + } + } + }); + + mDelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the current selection (single mode so we don't care about any item beyond + // index 0). + int[] indices = mApkConfigTable.getSelectionIndices(); + if (indices.length != 0) { + TableItem item = mApkConfigTable.getItem(indices[0]); + if (MessageDialog.openQuestion(parent.getShell(), + "Apk Configuration deletion", + String.format( + "Are you sure you want to delete configuration '%1$s'?", + item.getText(INDEX_NAME)))) { + // delete the item. + mApkConfigTable.remove(indices[0]); + + onSelectionChanged(); + } + } + } + }); + + // Add a listener to resize the column to the full width of the table + mApkConfigTable.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = mApkConfigTable.getClientArea(); + column0.setWidth(r.width * 30 / 100); // 30% + column1.setWidth(r.width * 70 / 100); // 70% + } + }); + + // add a selection listener on the table, to enable/disable buttons. + mApkConfigTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onSelectionChanged(); + } + }); + } + + public void fillTable(Map apkConfigMap) { + // get the names in a list so that we can sort them. + if (apkConfigMap != null) { + Set keys = apkConfigMap.keySet(); + String[] keyArray = keys.toArray(new String[keys.size()]); + Arrays.sort(keyArray); + + for (String key : keyArray) { + TableItem item = new TableItem(mApkConfigTable, SWT.NONE); + item.setText(INDEX_NAME, key); + item.setText(INDEX_FILTER, apkConfigMap.get(key)); + } + } + + onSelectionChanged(); + } + + public Map getApkConfigs() { + // go through all the items from the table and fill a new map + HashMap map = new HashMap(); + + TableItem[] items = mApkConfigTable.getItems(); + for (TableItem item : items) { + map.put(item.getText(INDEX_NAME), item.getText(INDEX_FILTER)); + } + + return map; + } + + /** + * Handles table selection changes. + */ + private void onSelectionChanged() { + if (mApkConfigTable.getSelectionCount() > 0) { + mEditButton.setEnabled(true); + mDelButton.setEnabled(true); + } else { + mEditButton.setEnabled(false); + mDelButton.setEnabled(false); + } + } +} diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/VmSelector.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java similarity index 73% rename from tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/VmSelector.java rename to tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java index dcc0b9ea1..67c70a676 100644 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/VmSelector.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/AvdSelector.java @@ -17,7 +17,7 @@ package com.android.sdkuilib; import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.vm.VmManager.VmInfo; +import com.android.sdklib.avd.AvdManager.AvdInfo; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlAdapter; @@ -36,35 +36,32 @@ import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; -import java.util.ArrayList; - /** - * The VM selector is a table that is added to the given parent composite. + * The AVD selector is a table that is added to the given parent composite. *

    - * To use, create it using {@link #VmSelector(Composite, VmInfo[], boolean)} then - * call {@link #setSelection(VmInfo)}, {@link #setSelectionListener(SelectionListener)} - * and finally use {@link #getFirstSelected()} or {@link #getAllSelected()} to retrieve the - * selection. + * To use, create it using {@link #AvdSelector(Composite, AvdInfo[])} then + * call {@link #setSelection(AvdInfo)}, {@link #setSelectionListener(SelectionListener)} + * and finally use {@link #getFirstSelected()} to retrieve the selection. */ -public final class VmSelector { +public final class AvdSelector { - private VmInfo[] mVms; - private final boolean mAllowMultipleSelection; + private AvdInfo[] mAvds; private SelectionListener mSelectionListener; private Table mTable; private Label mDescription; /** - * Creates a new SDK Target Selector. + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by a {@link IAndroidTarget}. + *

    Only the {@link AvdInfo} able to run application developed for the given + * {@link IAndroidTarget} will be displayed. * * @param parent The parent composite where the selector will be added. - * @param vms The list of vms. This is not copied, the caller must not modify. - * @param allowMultipleSelection True if more than one SDK target can be selected at the same - * time. + * @param avds The list of AVDs. This is not copied, the caller must not modify. */ - public VmSelector(Composite parent, VmInfo[] vms, boolean allowMultipleSelection) { - mVms = vms; + public AvdSelector(Composite parent, AvdInfo[] avds, IAndroidTarget filter) { + mAvds = avds; // Layout has 1 column Composite group = new Composite(parent, SWT.NONE); @@ -72,7 +69,6 @@ public final class VmSelector { group.setLayoutData(new GridData(GridData.FILL_BOTH)); group.setFont(parent.getFont()); - mAllowMultipleSelection = allowMultipleSelection; mTable = new Table(group, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); mTable.setHeaderVisible(true); mTable.setLinesVisible(false); @@ -89,7 +85,7 @@ public final class VmSelector { // create the table columns final TableColumn column0 = new TableColumn(mTable, SWT.NONE); - column0.setText("VM Name"); + column0.setText("AVD Name"); final TableColumn column1 = new TableColumn(mTable, SWT.NONE); column1.setText("Target Name"); final TableColumn column2 = new TableColumn(mTable, SWT.NONE); @@ -99,30 +95,51 @@ public final class VmSelector { adjustColumnsWidth(mTable, column0, column1, column2, column3); setupSelectionListener(mTable); - fillTable(mTable, null /* target filter */); + fillTable(mTable, filter); setupTooltip(mTable); } /** - * Sets a new set of VM, with an optional filter. + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}. + * + * @param parent The parent composite where the selector will be added. + * @param avds The list of AVDs. This is not copied, the caller must not modify. + */ + public AvdSelector(Composite parent, AvdInfo[] avds) { + this(parent, avds, null /* filter */); + } + + + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + data.heightHint = heightHint; + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + /** + * Sets a new set of AVD, with an optional filter. *

    This must be called from the UI thread. * - * @param vms The list of vms. This is not copied, the caller must not modify. - * @param filter An IAndroidTarget. If non-null, only VM whose target are compatible with the + * @param avds The list of AVDs. This is not copied, the caller must not modify. + * @param filter An IAndroidTarget. If non-null, only AVD whose target are compatible with the * filter target will displayed an available for selection. */ - public void setVms(VmInfo[] vms, IAndroidTarget filter) { - mVms = vms; + public void setAvds(AvdInfo[] avds, IAndroidTarget filter) { + mAvds = avds; fillTable(mTable, filter); } /** - * Returns the list of known Vms. + * Returns the list of known AVDs. *

    * This is not a copy. Callers must not modify this array. */ - public VmInfo[] getVms() { - return mVms; + public AvdInfo[] getAvds() { + return mAvds; } /** @@ -133,8 +150,7 @@ public final class VmSelector { * The event's item contains a {@link TableItem}. * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. *

    - * It is recommended that the caller uses the {@link #getFirstSelected()} and - * {@link #getAllSelected()} methods instead. + * It is recommended that the caller uses the {@link #getFirstSelected()} method instead. * * @param selectionListener The new listener or null to remove it. */ @@ -151,11 +167,11 @@ public final class VmSelector { * @param target the target to be selection * @return true if the target could be selected, false otherwise. */ - public boolean setSelection(VmInfo target) { + public boolean setSelection(AvdInfo target) { boolean found = false; boolean modified = false; for (TableItem i : mTable.getItems()) { - if ((VmInfo) i.getData() == target) { + if ((AvdInfo) i.getData() == target) { found = true; if (!i.getChecked()) { modified = true; @@ -174,39 +190,33 @@ public final class VmSelector { return found; } - /** - * Returns all selected items. - * This is useful when the table is in multiple-selection mode. - * - * @see #getFirstSelected() - * @return An array of selected items. The list can be empty but not null. - */ - public VmInfo[] getAllSelected() { - ArrayList list = new ArrayList(); - for (TableItem i : mTable.getItems()) { - if (i.getChecked()) { - list.add((IAndroidTarget) i.getData()); - } - } - return list.toArray(new VmInfo[list.size()]); - } - /** * Returns the first selected item. * This is useful when the table is in single-selection mode. * - * @see #getAllSelected() * @return The first selected item or null. */ - public VmInfo getFirstSelected() { + public AvdInfo getFirstSelected() { for (TableItem i : mTable.getItems()) { if (i.getChecked()) { - return (VmInfo) i.getData(); + return (AvdInfo) i.getData(); } } return null; } + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + mTable.setEnabled(enabled); + mDescription.setEnabled(enabled); + } + /** * Adds a listener to adjust the columns width when the parent is resized. *

    @@ -239,20 +249,11 @@ public final class VmSelector { private void setupSelectionListener(final Table table) { // Add a selection listener that will check/uncheck items when they are double-clicked table.addSelectionListener(new SelectionListener() { - /** Default selection means double-click on "most" platforms */ - public void widgetDefaultSelected(SelectionEvent e) { - if (e.item instanceof TableItem) { - TableItem i = (TableItem) e.item; - i.setChecked(!i.getChecked()); - enforceSingleSelection(i); - updateDescription(i); - } - - if (mSelectionListener != null) { - mSelectionListener.widgetDefaultSelected(e); - } - } + /** + * Handles single-click selection on the table. + * {@inheritDoc} + */ public void widgetSelected(SelectionEvent e) { if (e.item instanceof TableItem) { TableItem i = (TableItem) e.item; @@ -266,11 +267,32 @@ public final class VmSelector { } /** - * If we're not in multiple selection mode, uncheck all other - * items when this one is selected. + * Handles double-click selection on the table. + * Note that the single-click handler will probably already have been called. + * + * On double-click, always check the table item. + * + * {@inheritDoc} + */ + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(true); + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + } + } + + /** + * To ensure single selection, uncheck all other items when this one is selected. + * This makes the chekboxes act as radio buttons. */ private void enforceSingleSelection(TableItem item) { - if (!mAllowMultipleSelection && item.getChecked()) { + if (item.getChecked()) { Table parentTable = item.getParent(); for (TableItem i2 : parentTable.getItems()) { if (i2 != item && i2.getChecked()) { @@ -283,7 +305,7 @@ public final class VmSelector { } /** - * Fills the table with all VM. + * Fills the table with all AVD. * The table columns are: *

      *
    • column 0: sdk name @@ -294,14 +316,14 @@ public final class VmSelector { */ private void fillTable(final Table table, IAndroidTarget filter) { table.removeAll(); - if (mVms != null && mVms.length > 0) { + if (mAvds != null && mAvds.length > 0) { table.setEnabled(true); - for (VmInfo vm : mVms) { - if (filter == null || filter.isCompatibleBaseFor(vm.getTarget())) { + for (AvdInfo avd : mAvds) { + if (filter == null || filter.isCompatibleBaseFor(avd.getTarget())) { TableItem item = new TableItem(table, SWT.NONE); - item.setData(vm); - item.setText(0, vm.getName()); - IAndroidTarget target = vm.getTarget(); + item.setData(avd); + item.setText(0, avd.getName()); + IAndroidTarget target = avd.getTarget(); item.setText(1, target.getFullName()); item.setText(2, target.getApiVersionName()); item.setText(3, Integer.toString(target.getApiVersionNumber())); @@ -314,7 +336,7 @@ public final class VmSelector { TableItem item = new TableItem(table, SWT.NONE); item.setData(null); item.setText(0, "--"); - item.setText(1, "No VM available"); + item.setText(1, "No AVD available"); item.setText(2, "--"); item.setText(3, "--"); } @@ -365,13 +387,13 @@ public final class VmSelector { } /** - * Updates the description label with the path of the item's VM, if any. + * Updates the description label with the path of the item's AVD, if any. */ private void updateDescription(TableItem item) { if (item != null) { Object data = item.getData(); - if (data instanceof VmInfo) { - String newTooltip = ((VmInfo) data).getPath(); + if (data instanceof AvdInfo) { + String newTooltip = ((AvdInfo) data).getPath(); mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ } } diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java index fc951f278..5f9e9c23a 100644 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/SdkTargetSelector.java @@ -48,32 +48,57 @@ import java.util.ArrayList; */ public class SdkTargetSelector { - private final IAndroidTarget[] mTargets; + private IAndroidTarget[] mTargets; + private final boolean mAllowSelection; private final boolean mAllowMultipleSelection; private SelectionListener mSelectionListener; private Table mTable; private Label mDescription; + private Composite mInnerGroup; + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowMultipleSelection True if more than one SDK target can be selected at the same + * time. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, + boolean allowMultipleSelection) { + this(parent, targets, true /*allowSelection*/, allowMultipleSelection); + } /** * Creates a new SDK Target Selector. * * @param parent The parent composite where the selector will be added. * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowSelection True if selection is enabled. * @param allowMultipleSelection True if more than one SDK target can be selected at the same - * time. + * time. Used only if allowSelection is true. */ public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, + boolean allowSelection, boolean allowMultipleSelection) { - mTargets = targets; - // Layout has 1 column - Composite group = new Composite(parent, SWT.NONE); - group.setLayout(new GridLayout()); - group.setLayoutData(new GridData(GridData.FILL_BOTH)); - group.setFont(parent.getFont()); + mInnerGroup = new Composite(parent, SWT.NONE); + mInnerGroup.setLayout(new GridLayout()); + mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + mInnerGroup.setFont(parent.getFont()); + mAllowSelection = allowSelection; mAllowMultipleSelection = allowMultipleSelection; - mTable = new Table(group, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + int style = SWT.BORDER; + if (allowSelection) { + style |= SWT.CHECK | SWT.FULL_SELECTION; + } + if (!mAllowMultipleSelection) { + style |= SWT.SINGLE; + } + mTable = new Table(mInnerGroup, style); mTable.setHeaderVisible(true); mTable.setLinesVisible(false); @@ -84,7 +109,7 @@ public class SdkTargetSelector { data.verticalAlignment = GridData.FILL; mTable.setLayoutData(data); - mDescription = new Label(group, SWT.WRAP); + mDescription = new Label(mInnerGroup, SWT.WRAP); mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // create the table columns @@ -93,15 +118,26 @@ public class SdkTargetSelector { final TableColumn column1 = new TableColumn(mTable, SWT.NONE); column1.setText("Vendor"); final TableColumn column2 = new TableColumn(mTable, SWT.NONE); - column2.setText("API Level"); + column2.setText("Version"); final TableColumn column3 = new TableColumn(mTable, SWT.NONE); - column3.setText("SDK"); + column3.setText("API Level"); adjustColumnsWidth(mTable, column0, column1, column2, column3); setupSelectionListener(mTable); - fillTable(mTable); + setTargets(targets); setupTooltip(mTable); } + + /** + * Returns the layout data of the inner composite widget that contains the target selector. + * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH} + * mode. + *

      + * This can be useful if you want to change the {@link GridData#horizontalSpan} for example. + */ + public Object getLayoutData() { + return mInnerGroup.getLayoutData(); + } /** * Returns the list of known targets. @@ -112,6 +148,16 @@ public class SdkTargetSelector { return mTargets; } + /** + * Changes the targets of the SDK Target Selector. + * + * @param targets The list of targets. This is not copied, the caller must not modify. + */ + public void setTargets(IAndroidTarget[] targets) { + mTargets = targets; + fillTable(mTable); + } + /** * Sets a selection listener. Set it to null to remove it. * The listener will be called after this table processed its selection @@ -139,6 +185,10 @@ public class SdkTargetSelector { * @return true if the target could be selected, false otherwise. */ public boolean setSelection(IAndroidTarget target) { + if (!mAllowSelection) { + return false; + } + boolean found = false; boolean modified = false; for (TableItem i : mTable.getItems()) { @@ -224,6 +274,10 @@ public class SdkTargetSelector { * double-clicked (aka "the default selection"). */ private void setupSelectionListener(final Table table) { + if (!mAllowSelection) { + return; + } + // Add a selection listener that will check/uncheck items when they are double-clicked table.addSelectionListener(new SelectionListener() { /** Default selection means double-click on "most" platforms */ @@ -281,6 +335,9 @@ public class SdkTargetSelector { *

    */ private void fillTable(final Table table) { + + table.removeAll(); + if (mTargets != null && mTargets.length > 0) { table.setEnabled(true); for (IAndroidTarget target : mTargets) { diff --git a/tools/traceview/etc/traceview.bat b/tools/traceview/etc/traceview.bat index d074f428a..a9b573dcd 100755 --- a/tools/traceview/etc/traceview.bat +++ b/tools/traceview/etc/traceview.bat @@ -20,9 +20,9 @@ rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 -rem Change current directory to where traceview is, to avoid issues with directories -rem containing whitespaces. -cd %~dp0 +rem Change current directory and drive to where traceview.bat is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 set jarfile=traceview.jar set frameworkdir=