From 924958e21bbe1d3c918235d9ca78a6dc64c7879b Mon Sep 17 00:00:00 2001 From: Pablo Leite Date: Mon, 30 May 2011 15:14:14 -0300 Subject: Add support for authenticated add-on servers. Displays a login prompt if necessary. Note: This code depends on Apache http-client libraries. Build files had been modified in order to add this dependency. http-client libraries had also be added on prebuilt project. prebuilt changeID=I084d78dd09a431bc3a2d77e77810b84c693bdcb7 GerritLink=https://review.source.android.com/#change,23387 Change-Id: Icada9b41a21fe3aacef9a1eff209a3fe5591a4e0 --- .../plugins/com.android.ide.eclipse.adt/.classpath | 5 + .../META-INF/MANIFEST.MF | 8 +- .../com.android.ide.eclipse.adt/build.properties | 7 +- sdkmanager/app/etc/android.bat | 5 + sdkmanager/libs/sdklib/.classpath | 5 + sdkmanager/libs/sdklib/Android.mk | 7 +- sdkmanager/libs/sdklib/manifest.txt | 2 +- .../internal/repository/AddonsListFetcher.java | 27 ++- .../internal/repository/ArchiveInstaller.java | 6 +- .../sdklib/internal/repository/ITaskMonitor.java | 17 ++ .../internal/repository/NullTaskMonitor.java | 7 + .../sdklib/internal/repository/SdkSource.java | 32 ++-- .../sdklib/internal/repository/UrlOpener.java | 193 +++++++++++++++++++++ .../sdklib/internal/repository/MockMonitor.java | 5 + .../internal/repository/SdkUpdaterNoWindow.java | 74 ++++++++ .../internal/tasks/IProgressUiProvider.java | 17 ++ .../internal/tasks/ProgressTaskDialog.java | 45 ++++- .../sdkuilib/internal/tasks/ProgressView.java | 57 ++++-- .../sdkuilib/internal/tasks/TaskMonitorImpl.java | 24 ++- .../android/sdkuilib/ui/AuthenticationDialog.java | 132 ++++++++++++++ 20 files changed, 620 insertions(+), 55 deletions(-) create mode 100644 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java create mode 100644 sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ui/AuthenticationDialog.java diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath index a77b6b6..72a5c8b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath @@ -1,6 +1,11 @@ + + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index 49f73e6..ebcf44f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -14,7 +14,13 @@ Bundle-ClassPath: ., libs/layoutlib_api.jar, libs/ide_common.jar, libs/common.jar, - libs/assetstudio.jar + libs/assetstudio.jar, + libs/httpclient-4.1.1.jar, + libs/httpcore-4.1.jar, + libs/httpmime-4.1.1.jar, + libs/httpclient-4.1.1.jar, + libs/commons-logging-1.1.1.jar, + libs/commons-codec-1.4.jar Bundle-Activator: com.android.ide.eclipse.adt.AdtPlugin Bundle-Vendor: The Android Open Source Project Require-Bundle: com.android.ide.eclipse.ddms, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/build.properties b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties index 96d191e..a82e779 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/build.properties +++ b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties @@ -8,7 +8,12 @@ bin.includes = plugin.xml,\ about.properties,\ NOTICE,\ about.html,\ - libs/common.jar + libs/common.jar,\ + libs/httpclient-4.1.1.jar,\ + libs/httpcore-4.1.jar,\ + libs/httpmime-4.1.1.jar,\ + libs/commons-logging-1.1.1.jar,\ + libs/commons-codec-1.4.jar source.. = src/ output.. = bin/ bin.excludes = libs/.gitignore diff --git a/sdkmanager/app/etc/android.bat b/sdkmanager/app/etc/android.bat index f616dd7..3233ebd 100755 --- a/sdkmanager/app/etc/android.bat +++ b/sdkmanager/app/etc/android.bat @@ -61,6 +61,11 @@ if not "%1"=="" goto EndTempCopy copy /B /D /Y lib\common.jar %tmp_dir%\lib\ > nul copy /B /D /Y lib\commons-compress* %tmp_dir%\lib\ > nul copy /B /D /Y lib\swtmenubar.jar %tmp_dir%\lib\ > nul + copy /B /D /Y lib\commons-logging* %tmp_dir%\lib\ > nul + copy /B /D /Y lib\commons-codec* %tmp_dir%\lib\ > nul + copy /B /D /Y lib\httpclient* %tmp_dir%\lib\ > nul + copy /B /D /Y lib\httpcode* %tmp_dir%\lib\ > nul + copy /B /D /Y lib\httpmime* %tmp_dir%\lib\ > nul rem jar_path and swt_path are relative to PWD so we don't need to adjust them, just change dirs. set tools_dir=%cd% diff --git a/sdkmanager/libs/sdklib/.classpath b/sdkmanager/libs/sdklib/.classpath index 7cabaa0..c48e0bf 100644 --- a/sdkmanager/libs/sdklib/.classpath +++ b/sdkmanager/libs/sdklib/.classpath @@ -7,5 +7,10 @@ + + + + + \ No newline at end of file diff --git a/sdkmanager/libs/sdklib/Android.mk b/sdkmanager/libs/sdklib/Android.mk index 7ed009c..ab800da 100644 --- a/sdkmanager/libs/sdklib/Android.mk +++ b/sdkmanager/libs/sdklib/Android.mk @@ -28,7 +28,12 @@ LOCAL_JAR_MANIFEST := manifest.txt LOCAL_JAVA_LIBRARIES := \ androidprefs \ common \ - commons-compress-1.0 + commons-compress-1.0 \ + httpclient-4.1.1 \ + httpcore-4.1 \ + httpmime-4.1.1 \ + commons-logging-1.1.1 \ + commons-codec-1.4 LOCAL_MODULE := sdklib diff --git a/sdkmanager/libs/sdklib/manifest.txt b/sdkmanager/libs/sdklib/manifest.txt index 0ead4fc..0efb701 100644 --- a/sdkmanager/libs/sdklib/manifest.txt +++ b/sdkmanager/libs/sdklib/manifest.txt @@ -1 +1 @@ -Class-Path: androidprefs.jar common.jar commons-compress-1.0.jar +Class-Path: androidprefs.jar common.jar commons-compress-1.0.jar httpclient-4.1.1.jar httpcore-4.1.jar httpmime-4.1.1.jar commons-logging-1.1.1.jar commons-codec-1.4.jar diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java index 11dad83..93a8a98 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonsListFetcher.java @@ -31,7 +31,6 @@ import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.regex.Matcher; @@ -91,7 +90,7 @@ public class AddonsListFetcher { url = url == null ? "" : url.trim(); - monitor.setProgressMax(4); + monitor.setProgressMax(5); monitor.setDescription("Fetching %1$s", url); monitor.incProgress(1); @@ -101,7 +100,7 @@ public class AddonsListFetcher { Document validatedDoc = null; String validatedUri = null; - ByteArrayInputStream xml = fetchUrl(url, exception); + ByteArrayInputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception); if (xml != null) { monitor.setDescription("Validate XML"); @@ -176,21 +175,19 @@ public class AddonsListFetcher { } /** - * Fetches the document at the given URL and returns it as a stream. - * Returns null if anything wrong happens. - * - * References:
- * Java URL Connection: http://java.sun.com/docs/books/tutorial/networking/urls/readingWriting.html
- * Java URL Reader: http://java.sun.com/docs/books/tutorial/networking/urls/readingURL.html
- * Java set Proxy: http://java.sun.com/docs/books/tutorial/networking/urls/_setProxy.html
+ * Fetches the document at the given URL and returns it as a stream. Returns + * null if anything wrong happens. References:
+ * URL Connection: * + * @see {@link UrlOpener} which handles all URL logic. * @param urlString The URL to load, as a string. - * @param outException If non null, where to store any exception that happens during the fetch. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. */ - private ByteArrayInputStream fetchUrl(String urlString, Exception[] outException) { - URL url; + private ByteArrayInputStream fetchUrl(String urlString, ITaskMonitor monitor, + Exception[] outException) { try { - url = new URL(urlString); InputStream is = null; @@ -199,7 +196,7 @@ public class AddonsListFetcher { byte[] result = new byte[inc]; try { - is = url.openStream(); + is = UrlOpener.openURL(urlString, monitor); int n; while ((n = is.read(result, curr, result.length - curr)) != -1) { diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java index 552c7bb..06e64df 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java @@ -30,7 +30,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Enumeration; @@ -38,7 +37,6 @@ import java.util.HashSet; import java.util.Properties; import java.util.Set; - /** * Performs the work of installing a given {@link Archive}. */ @@ -267,15 +265,13 @@ public class ArchiveInstaller { String urlString, String description, ITaskMonitor monitor) { - URL url; description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)"; FileOutputStream os = null; InputStream is = null; try { - url = new URL(urlString); - is = url.openStream(); + is = UrlOpener.openURL(urlString, monitor); os = new FileOutputStream(tmpFile); MessageDigest digester = archive.getChecksumType().getMessageDigest(); diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java index 313f36e..4a1a1e0 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java @@ -17,6 +17,7 @@ package com.android.sdklib.internal.repository; import com.android.sdklib.ISdkLog; +import com.android.util.Pair; /** @@ -131,4 +132,20 @@ public interface ITaskMonitor extends ISdkLog { * @return true if YES was clicked. */ public boolean displayPrompt(final String title, final String message); + + /** + * Launch an interface which asks for login and password. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. + If operation is canceled by user the return value must be null. + */ + public Pair displayLoginPasswordPrompt(String title, String message); } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/NullTaskMonitor.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/NullTaskMonitor.java index afe291f..8cd52d6 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/NullTaskMonitor.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/NullTaskMonitor.java @@ -18,6 +18,7 @@ package com.android.sdklib.internal.repository; import com.android.sdklib.ISdkLog; import com.android.sdklib.NullSdkLog; +import com.android.util.Pair; /** @@ -92,6 +93,11 @@ public class NullTaskMonitor implements ITaskMonitor { return false; } + /** Always return null. */ + public Pair displayLoginPasswordPrompt(String title, String message) { + return null; + } + // --- ISdkLog --- public void error(Throwable t, String errorFormat, Object... args) { @@ -105,4 +111,5 @@ public class NullTaskMonitor implements ITaskMonitor { public void printf(String msgFormat, Object... args) { mLog.printf(msgFormat, args); } + } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java index 22b9c42..a5b16c3 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java @@ -249,7 +249,7 @@ public abstract class SdkSource implements IDescription, Comparable { */ public void load(ITaskMonitor monitor, boolean forceHttp) { - monitor.setProgressMax(4); + monitor.setProgressMax(6); setDefaultDescription(); @@ -265,7 +265,7 @@ public abstract class SdkSource implements IDescription, Comparable { Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; String[] validationError = new String[] { null }; Exception[] exception = new Exception[] { null }; - InputStream xml = fetchUrl(url, exception); + InputStream xml = fetchUrl(url, monitor.createSubMonitor(1), exception); Document validatedDoc = null; boolean usingAlternateXml = false; boolean usingAlternateUrl = false; @@ -279,13 +279,15 @@ public abstract class SdkSource implements IDescription, Comparable { } url += getUrlDefaultXmlFile(); - xml = fetchUrl(url, exception); + xml = fetchUrl(url, monitor.createSubMonitor(1), exception); usingAlternateUrl = true; } if (xml != null) { monitor.setDescription(String.format("Validate XML: %1$s", url)); + ITaskMonitor subMonitor = monitor.createSubMonitor(2); + subMonitor.setProgressMax(2); for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) { // Explore the XML to find the potential XML schema version int version = getXmlSchemaVersion(xml); @@ -347,8 +349,8 @@ public abstract class SdkSource implements IDescription, Comparable { } url += getUrlDefaultXmlFile(); - xml = fetchUrl(url, null /*outException*/); - + xml = fetchUrl(url, subMonitor.createSubMonitor(1), null /* outException */); + subMonitor.incProgress(1); // Loop to try the alternative document if (xml != null) { usingAlternateUrl = true; @@ -465,21 +467,19 @@ public abstract class SdkSource implements IDescription, Comparable { } /** - * Fetches the document at the given URL and returns it as a string. - * Returns null if anything wrong happens and write errors to the monitor. - * + * Fetches the document at the given URL and returns it as a string. Returns + * null if anything wrong happens and write errors to the monitor. * References:
- * Java URL Connection: http://java.sun.com/docs/books/tutorial/networking/urls/readingWriting.html
- * Java URL Reader: http://java.sun.com/docs/books/tutorial/networking/urls/readingURL.html
- * Java set Proxy: http://java.sun.com/docs/books/tutorial/networking/urls/_setProxy.html
+ * URL Connection: * + * @see {@link UrlOpener} which handles all URL logic. * @param urlString The URL to load, as a string. - * @param outException If non null, where to store any exception that happens during the fetch. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. */ - private InputStream fetchUrl(String urlString, Exception[] outException) { - URL url; + private InputStream fetchUrl(String urlString, ITaskMonitor monitor, Exception[] outException) { try { - url = new URL(urlString); InputStream is = null; @@ -488,7 +488,7 @@ public abstract class SdkSource implements IDescription, Comparable { byte[] result = new byte[inc]; try { - is = url.openStream(); + is = UrlOpener.openURL(urlString, monitor); int n; while ((n = is.read(result, curr, result.length - curr)) != -1) { diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java new file mode 100644 index 0000000..a99baf6 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/UrlOpener.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdklib.internal.repository; + +import com.android.util.Pair; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.AuthState; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.ProxySelectorRoutePlanner; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ProxySelector; +import java.util.HashMap; +import java.util.Map; + +/** + * This class holds methods for adding URLs management. + */ +public class UrlOpener { + + private static Map> sRealmCache = + new HashMap>(); + + /** + * Open a URL. It can be a simple URL or one which requires basic + * authentication.
Description + *

+ * Tries to access the given URL. If http response is either + * {@link HttpStatus.SC_UNAUTHORIZED} or + * {@link HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED} asks for + * login/password and tries to authenticate into proxy server and/or URL. + *

+ *

+ * This implementation relies on the Apache Http Client due to its + * capabilities of proxy/http authentication.
Proxy configuration is + * determined by {@link ProxySelectorRoutePlanner} using the JVM proxy + * settings by default. For more information see: + * http://hc.apache.org/httpcomponents-client-ga/ + * http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html + *

+ *

+ * There's a very simplecache implementation. Login/Password for each + * realm are stored on a static {@link Map}. Before asking the user the + * method verifies if the information is already available in cache + *

+ * + * @param url the URL string to be opened. + * @param monitor {@link ITaskMonitor} which is related to this URL + * fetching. + * @return Returns an {@link InputStream} holding the URL content. + * @throws IOException Exception thrown when there are problems retrieving + * the URL or its content. + */ + static InputStream openURL(String url, ITaskMonitor monitor) throws IOException { + + InputStream stream = null; + HttpEntity entity = null; + Pair result = null; + String realm = null; + + // use the simple one + DefaultHttpClient httpclient = new DefaultHttpClient(); + + // create local execution context + HttpContext localContext = new BasicHttpContext(); + HttpGet httpget = new HttpGet(url); + + // retrieve local java configured network in case there is the need to + // authenticate a proxy + ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(httpclient + .getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault()); + httpclient.setRoutePlanner(routePlanner); + + boolean trying = true; + // loop while the response is being fetched + while (trying) { + // connect and get status code + HttpResponse response = httpclient.execute(httpget, localContext); + int statusCode = response.getStatusLine().getStatusCode(); + + // check whether any authentication is required + AuthState authenticationState = null; + if (statusCode == HttpStatus.SC_UNAUTHORIZED) { + // Target host authentication required + authenticationState = (AuthState) localContext + .getAttribute(ClientContext.TARGET_AUTH_STATE); + } + if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { + // Proxy authentication required + authenticationState = (AuthState) localContext + .getAttribute(ClientContext.PROXY_AUTH_STATE); + } + if (statusCode == HttpStatus.SC_OK) { + // in case the status is OK and there is a realm and result, + // cache it + if (realm != null && result != null) { + sRealmCache.put(realm, result); + } + } + + // there is the need for authentication + if (authenticationState != null) { + + // get scope and realm + AuthScope authScope = authenticationState.getAuthScope(); + + // If the current realm is different from the last one it means + // a pass was performed successfully to the last URL, therefore + // cache the last realm + if (realm != null && !realm.equals(authScope.getRealm())) { + sRealmCache.put(realm, result); + } + + realm = authScope.getRealm(); + + // in case there is cache for this Realm, use it to authenticate + if (sRealmCache.containsKey(realm)) { + result = sRealmCache.get(realm); + } else { + // since there is no cache, request for login and password + result = monitor.displayLoginPasswordPrompt("Site Authentication", + "Please login to the following domain: " + realm + + "\n\nServer requiring authentication:\n" + authScope.getHost()); + if (result == null) { + throw new IOException("User canceled login dialog."); + } + } + + // retrieve authentication data + String user = result.getFirst(); + String password = result.getSecond(); + + // proceed in case there is indeed a user + if (user != null && user.length() > 0) { + Credentials credentials = new UsernamePasswordCredentials(user, password); + httpclient.getCredentialsProvider().setCredentials(authScope, credentials); + trying = true; + } else { + trying = false; + } + } else { + trying = false; + } + entity = response.getEntity(); + if (entity != null) { + // in case another pass to the Http Client will be performed, + // consume the entity + if (trying) { + entity.consumeContent(); + } + // since no pass to the Http Client is needed, retrieve the + // entity's content + else { + // Use a buffered entity because the stream in which it will + // be transfered, will not be closed later, unexpectedly + BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity); + stream = bufferedEntity.getContent(); + } + } + } + + // since Http Client is no longer needed, close it + httpclient.getConnectionManager().shutdown(); + + return stream; + } +} diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockMonitor.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockMonitor.java index c006707..e61184d 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockMonitor.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockMonitor.java @@ -16,6 +16,7 @@ package com.android.sdklib.internal.repository; +import com.android.util.Pair; /** * Mock implementation of {@link ITaskMonitor} that simply captures @@ -95,4 +96,8 @@ public class MockMonitor implements ITaskMonitor { public void warning(String warningFormat, Object... args) { } + + public Pair displayLoginPasswordPrompt(String title, String message) { + return null; + } } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterNoWindow.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterNoWindow.java index ef4864b..10f4074 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterNoWindow.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/SdkUpdaterNoWindow.java @@ -23,7 +23,9 @@ import com.android.sdklib.internal.repository.ITaskFactory; import com.android.sdklib.internal.repository.ITaskMonitor; import com.android.sdklib.internal.repository.NullTaskMonitor; import com.android.sdklib.repository.SdkRepoConstants; +import com.android.util.Pair; +import java.io.IOException; import java.util.ArrayList; import java.util.Properties; @@ -336,6 +338,74 @@ public class SdkUpdaterNoWindow { } /** + * Displays a prompt message to the user and read two values, + * login/password. + *

+ * Asks user for login/password information. + *

+ * This method shows a question in the standard output, asking for login + * and password.
+ * Method Output:
+ * Title
+ * Message
+ * Login: (Wait for user input)
+ * Password: (Wait for user input)
+ *

+ * + * @param title The title of the iteration. + * @param message The message to be displayed. + * @return A {@link Pair} holding the entered login and password. The + * first element is always the Login, and the + * second element is always the Password. This + * method will never return null, in case of error the pair will + * be filled with empty strings. + * @see ITaskMonitor#displayLoginPasswordPrompt(String, String) + */ + public Pair displayLoginPasswordPrompt(String title, String message) { + String login = ""; //$NON-NLS-1$ + String password = ""; //$NON-NLS-1$ + mSdkLog.printf("\n%1$s\n%2$s", title, message); + byte[] readBuffer = new byte[2048]; + try { + mSdkLog.printf("\nLogin: "); + login = readLine(readBuffer); + mSdkLog.printf("\nPassword: "); + password = readLine(readBuffer); + /* + * TODO: Implement a way to don't echo the typed password On + * Java 5 there's no simple way to do this. There's just a + * workaround which is output backspaces on each keystroke. + * A good alternative is to use Java 6 java.io.Console + */ + } catch (IOException e) { + // Reset login/pass to empty Strings. + login = ""; //$NON-NLS-1$ + password = ""; //$NON-NLS-1$ + //Just print the error to console. + mSdkLog.printf("\nError occurred during login/pass query: %s\n", e.getMessage()); + } + + return Pair.of(login, password); + } + + private String readLine(byte[] buffer) throws IOException { + int count = System.in.read(buffer); + + // is the input longer than the buffer? + if (count == buffer.length && buffer[count-1] != 10) { + throw new IOException(String.format( + "Input is longer than the buffer size, (%1$s) bytes", buffer.length)); + } + + // ignore end whitespace + while (count > 0 && (buffer[count-1] == '\r' || buffer[count-1] == '\n')) { + count--; + } + + return new String(buffer, 0, count); + } + + /** * Creates a sub-monitor that will use up to tickCount on the progress bar. * tickCount must be 1 or more. */ @@ -433,6 +503,10 @@ public class SdkUpdaterNoWindow { return mRoot.displayPrompt(title, message); } + public Pair displayLoginPasswordPrompt(String title, String message) { + return mRoot.displayLoginPasswordPrompt(title, message); + } + public ITaskMonitor createSubMonitor(int tickCount) { assert mSubCoef > 0; assert tickCount > 0; diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java index 4a7922d..f6e13c6 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java @@ -17,6 +17,7 @@ package com.android.sdkuilib.internal.tasks; import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.util.Pair; import org.eclipse.swt.widgets.ProgressBar; @@ -90,4 +91,20 @@ interface IProgressUiProvider { */ public abstract boolean displayPrompt(String title, String message); + /** + * Launch an interface which asks for login and password. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. This method should never return a null pair. + * It's elements however can be empty Strings + */ + public abstract Pair displayLoginPasswordPrompt(String title, String message); + } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java index a4236fa..dbc871f 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java @@ -18,6 +18,9 @@ package com.android.sdkuilib.internal.tasks; import com.android.sdklib.SdkConstants; import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; +import com.android.util.Pair; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; @@ -154,7 +157,8 @@ final class ProgressTaskDialog extends Dialog implements IProgressUiProvider { }); mResultText = new Text(mRootComposite, - SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); + SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | + SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); mResultText.setEditable(true); mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); } @@ -393,6 +397,45 @@ final class ProgressTaskDialog extends Dialog implements IProgressUiProvider { } /** + * This method opens a pop-up window which requests for User Login and + * password. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginPasswordPrompt(String, String) + */ + public Pair displayLoginPasswordPrompt( + final String title, final String message) { + final String[] resultArray = new String[2]; + Display display = mDialogShell.getDisplay(); + + // open dialog and request login and password + display.syncExec(new Runnable() { + public void run() { + AuthenticationDialog authenticationDialog = new AuthenticationDialog(mDialogShell, + title, + message); + int dlgResult= authenticationDialog.open(); + if(dlgResult == GridDialog.OK) { + resultArray[0] = authenticationDialog.getLogin(); + resultArray[1] = authenticationDialog.getPassword(); + } else { + resultArray[0] = null; + resultArray[1] = null; + } + } + }); + + return resultArray[0] == null ? null : Pair.of(resultArray[0], resultArray[1]); + } + + /** * Starts the thread that runs the task. * This is deferred till the UI is created. */ diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressView.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressView.java index d90eaed..9485885 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressView.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressView.java @@ -19,11 +19,13 @@ package com.android.sdkuilib.internal.tasks; import com.android.sdklib.ISdkLog; import com.android.sdklib.internal.repository.ITask; import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; +import com.android.util.Pair; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; 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; @@ -325,20 +327,53 @@ public final class ProgressView implements IProgressUiProvider { public boolean displayPrompt(final String title, final String message) { final boolean[] result = new boolean[] { false }; - if (!mProgressBar.isDisposed()) { - final Shell shell = mProgressBar.getShell(); - Display display = shell.getDisplay(); - - display.syncExec(new Runnable() { - public void run() { - result[0] = MessageDialog.openQuestion(shell, title, message); - } - }); - } + syncExec(mProgressBar, new Runnable() { + public void run() { + Shell shell = mProgressBar.getShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); return result[0]; } + /** + * This method opens a pop-up window which requests for User Login and + * password. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginPasswordPrompt(String, String) + */ + public Pair + displayLoginPasswordPrompt(final String title, final String message) { + final String[] resultArray = new String[] {"", ""}; + // open dialog and request login and password + syncExec(mProgressBar, new Runnable() { + public void run() { + Shell shell = mProgressBar.getShell(); + AuthenticationDialog authenticationDialog = new AuthenticationDialog(shell, + title, + message); + int dlgResult = authenticationDialog.open(); + if (dlgResult == GridDialog.OK) { + resultArray[0] = authenticationDialog.getLogin(); + resultArray[1] = authenticationDialog.getPassword(); + } else { + resultArray[0] = null; + resultArray[1] = null; + } + } + }); + return resultArray[0] == null ? null : Pair.of(resultArray[0], resultArray[1]); + } + // ---- /** diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java index 5286df5..f3dff37 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java @@ -17,8 +17,7 @@ package com.android.sdkuilib.internal.tasks; import com.android.sdklib.internal.repository.ITaskMonitor; - -import org.eclipse.swt.widgets.ProgressBar; +import com.android.util.Pair; /** * Internal class that implements the logic of an {@link ITaskMonitor}. @@ -156,7 +155,7 @@ class TaskMonitorImpl implements ITaskMonitor { } /** - * Display a yes/no question dialog box. + * Displays a yes/no question dialog box. * * This implementation allow this to be called from any thread, it * makes sure the dialog is opened synchronously in the ui thread. @@ -170,6 +169,21 @@ class TaskMonitorImpl implements ITaskMonitor { } /** + * Displays a Login/Password dialog. This implementation allows this method to be + * called from any thread, it makes sure the dialog is opened synchronously + * in the ui thread. + * + * @param title The title of the dialog box + * @param message Message to be displayed + * @return Pair with entered login/password. Login is always the first + * element and Password is always the second. If any error occurs a + * pair with empty strings is returned. + */ + public Pair displayLoginPasswordPrompt(String title, String message) { + return mUi.displayLoginPasswordPrompt(title, message); + } + + /** * Creates a sub-monitor that will use up to tickCount on the progress bar. * tickCount must be 1 or more. */ @@ -284,6 +298,10 @@ class TaskMonitorImpl implements ITaskMonitor { return mRoot.displayPrompt(title, message); } + public Pair displayLoginPasswordPrompt(String title, String message) { + return mRoot.displayLoginPasswordPrompt(title, message); + } + public ITaskMonitor createSubMonitor(int tickCount) { assert mSubCoef > 0; assert tickCount > 0; diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ui/AuthenticationDialog.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ui/AuthenticationDialog.java new file mode 100644 index 0000000..497c752 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/ui/AuthenticationDialog.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkuilib.ui; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Dialog which collects from the user his/her login and password. + */ +public class AuthenticationDialog extends GridDialog { + + private Text mTxtLogin; + + private Text mTxtPassword; + + private String mTitle; + + private String mMessage; + + protected String mLogin; + + protected String mPassword; + + /** + * Constructor which retrieves the parent {@link Shell} and the message to + * be displayed in this dialog. + * + * @param parentShell Parent Shell + * @param title Title of the window. + * @param message Message the be displayed in this dialog. + */ + public AuthenticationDialog(Shell parentShell, String title, String message) { + super(parentShell, 1, false); + // assign fields + mTitle = title; + mMessage = message; + } + + @Override + public void createDialogContent(Composite parent) { + // Configure Dialog + getShell().setText(mTitle); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + parent.setLayoutData(data); + + // Upper Composite + Composite upperComposite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.verticalSpacing = 10; + upperComposite.setLayout(layout); + data = new GridData(SWT.FILL, SWT.CENTER, true, true); + upperComposite.setLayoutData(data); + + // add message label + Label lblMessage = new Label(upperComposite, SWT.WRAP); + lblMessage.setText(mMessage); + data = new GridData(SWT.FILL, SWT.CENTER, true, true, 2, 1); + data.widthHint = 500; + lblMessage.setLayoutData(data); + + // add user name label and text field + Label lblUserName = new Label(upperComposite, SWT.NONE); + lblUserName.setText("Login:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblUserName.setLayoutData(data); + + mTxtLogin = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtLogin.setLayoutData(data); + mTxtLogin.setFocus(); + mTxtLogin.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + mLogin = mTxtLogin.getText(); + } + }); + + // add password label and text field + Label lblPassword = new Label(upperComposite, SWT.NONE); + lblPassword.setText("Password:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblPassword.setLayoutData(data); + + mTxtPassword = new Text(upperComposite, SWT.SINGLE | SWT.PASSWORD | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtPassword.setLayoutData(data); + mTxtPassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent arg0) { + mPassword = mTxtPassword.getText(); + } + }); + } + + /** + * Retrieves the Login field information + * + * @return Login field value or empty String. Return value is never null + */ + public String getLogin() { + return mLogin != null ? mLogin : ""; //$NON-NLS-1$ + } + + /** + * Retrieves the Password field information + * + * @return Password field value or empty String. Return value is never null + */ + public String getPassword() { + return mPassword != null ? mPassword : ""; //$NON-NLS-1$ + } +} -- cgit v1.1