Merge pull request #31 from rthakohov/authentication

Authentication on demand
This commit is contained in:
Ruslan Tkhakokhov 2017-08-09 09:06:45 -07:00 committed by GitHub
commit cde6c2ae83
11 changed files with 407 additions and 62 deletions

View file

@ -48,6 +48,7 @@
</intent-filter>
</provider>
<activity android:name=".auth.AuthActivity" />
</application>
</manifest>

View file

@ -45,6 +45,7 @@ public class ShareManager implements Iterable<String> {
// JSON value
private static final String URI_KEY = "uri";
private static final String MOUNT_KEY = "mount";
private static final String CREDENTIAL_TUPLE_KEY = "credentialTuple";
private static final String WORKGROUP_KEY = "workgroup";
private static final String USERNAME_KEY = "username";
@ -52,6 +53,7 @@ public class ShareManager implements Iterable<String> {
private final SharedPreferences mPref;
private final Set<String> mServerStringSet;
private final Set<String> mMountedServerSet = new HashSet<>();
private final Map<String, String> mServerStringMap = new HashMap<>();
private final CredentialCache mCredentialCache;
@ -61,13 +63,13 @@ public class ShareManager implements Iterable<String> {
mCredentialCache = credentialCache;
mPref = context.getSharedPreferences(SERVER_CACHE_PREF_KEY, Context.MODE_PRIVATE);
// Loading mounted servers
// Loading saved servers.
final Set<String> serverStringSet =
mPref.getStringSet(SERVER_STRING_SET_KEY, Collections.<String> emptySet());
final Map<String, CredentialTuple> credentialMap = new HashMap<>(serverStringSet.size());
final Map<String, ShareTuple> shareMap = new HashMap<>(serverStringSet.size());
for (String serverString : serverStringSet) {
// TODO: Add decryption
String uri = decode(serverString, credentialMap);
String uri = decode(serverString, shareMap);
if (uri != null) {
mServerStringMap.put(uri, serverString);
}
@ -75,24 +77,89 @@ public class ShareManager implements Iterable<String> {
mServerStringSet = new HashSet<>(serverStringSet);
for (Map.Entry<String, CredentialTuple> server : credentialMap.entrySet()) {
final CredentialTuple tuple = server.getValue();
for (Map.Entry<String, ShareTuple> server : shareMap.entrySet()) {
final ShareTuple tuple = server.getValue();
if (tuple.mIsMounted) {
mMountedServerSet.add(server.getKey());
}
mCredentialCache.putCredential(
server.getKey(), tuple.mWorkgroup, tuple.mUsername, tuple.mPassword);
}
}
public synchronized void mountServer(
String uri, String workgroup, String username, String password, ShareMountChecker checker)
throws IOException {
if (mServerStringMap.containsKey(uri)) {
throw new IllegalStateException("Uri " + uri + " is already mounted.");
/**
* Save the server and credentials to permanent storage.
* Throw an exception if a server with such a uri is already present.
*/
public synchronized void addServer(
String uri, String workgroup, String username, String password,
ShareMountChecker checker, boolean mount) throws IOException {
if (mMountedServerSet.contains(uri)) {
throw new IllegalStateException("Uri " + uri + " is already stored.");
}
saveServerInfo(uri, workgroup, username, password, checker, mount);
}
/**
* Update the server info. If a server with such a uri doesn't exist, create it.
*/
public synchronized void addOrUpdateServer(
String uri, String workgroup, String username, String password,
ShareMountChecker checker, boolean mount) throws IOException {
saveServerInfo(uri, workgroup, username, password, checker, mount);
}
private void saveServerInfo(
String uri, String workgroup, String username, String password,
ShareMountChecker checker, boolean mount) throws IOException {
checkServerCredentials(uri, workgroup, username, password, checker);
final boolean hasPassword = !TextUtils.isEmpty(username) && !TextUtils.isEmpty(password);
if (hasPassword) {
final ShareTuple tuple = hasPassword
? new ShareTuple(workgroup, username, password, mount)
: ShareTuple.EMPTY_TUPLE;
updateServersData(uri, tuple, mount);
}
private void updateServersData(
String uri, ShareTuple tuple, boolean shouldNotify) {
final String serverString = encode(uri, tuple);
if (serverString == null) {
throw new IllegalStateException("Failed to encode credential tuple.");
}
// TODO: Add encryption
mServerStringSet.add(serverString);
if (tuple.mIsMounted) {
mMountedServerSet.add(uri);
} else {
mMountedServerSet.remove(uri);
}
mPref.edit().putStringSet(SERVER_STRING_SET_KEY, mServerStringSet).apply();
mServerStringMap.put(uri, serverString);
if (shouldNotify) {
notifyServerChange();
}
}
private void checkServerCredentials(
String uri, String workgroup, String username, String password, ShareMountChecker checker)
throws IOException {
if (!username.isEmpty() && !password.isEmpty()) {
mCredentialCache.putCredential(uri, workgroup, username, password);
}
runMountChecker(uri, checker);
}
private void runMountChecker(String uri, ShareMountChecker checker) throws IOException {
try {
checker.checkShareMounting();
} catch (Exception e) {
@ -100,20 +167,6 @@ public class ShareManager implements Iterable<String> {
mCredentialCache.removeCredential(uri);
throw e;
}
final CredentialTuple tuple = hasPassword
? new CredentialTuple(workgroup, username, password)
: CredentialTuple.EMPTY_TUPLE;
final String serverString = encode(uri, tuple);
if (serverString == null) {
throw new IllegalStateException("Failed to encode credential tuple.");
}
// TODO: Add encryption
mServerStringSet.add(serverString);
mPref.edit().putStringSet(SERVER_STRING_SET_KEY, mServerStringSet).apply();
mServerStringMap.put(uri, serverString);
notifyServerChange();
}
public synchronized boolean unmountServer(String uri) {
@ -149,6 +202,10 @@ public class ShareManager implements Iterable<String> {
return mServerStringMap.containsKey(uri);
}
public synchronized boolean isShareMounted(String uri) {
return mMountedServerSet.contains(uri);
}
private void notifyServerChange() {
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onMountedServerChange();
@ -163,7 +220,7 @@ public class ShareManager implements Iterable<String> {
mListeners.remove(listener);
}
private static String encode(String uri, CredentialTuple tuple) {
private static String encode(String uri, ShareTuple tuple) {
final StringWriter stringWriter = new StringWriter();
try (final JsonWriter jsonWriter = new JsonWriter(stringWriter)) {
jsonWriter.beginObject();
@ -180,25 +237,26 @@ public class ShareManager implements Iterable<String> {
return stringWriter.toString();
}
private static void encodeTuple(JsonWriter writer, CredentialTuple tuple) throws IOException {
if (tuple == CredentialTuple.EMPTY_TUPLE) {
private static void encodeTuple(JsonWriter writer, ShareTuple tuple) throws IOException {
if (tuple == ShareTuple.EMPTY_TUPLE) {
writer.nullValue();
} else {
writer.beginObject();
writer.name(WORKGROUP_KEY).value(tuple.mWorkgroup);
writer.name(USERNAME_KEY).value(tuple.mUsername);
writer.name(PASSWORD_KEY).value(tuple.mPassword);
writer.name(MOUNT_KEY).value(tuple.mIsMounted);
writer.endObject();
}
}
private static String decode(String content, Map<String, CredentialTuple> credentialMap) {
private static String decode(String content, Map<String, ShareTuple> shareMap) {
final StringReader stringReader = new StringReader(content);
try (final JsonReader jsonReader = new JsonReader(stringReader)) {
jsonReader.beginObject();
String uri = null;
CredentialTuple tuple = null;
ShareTuple tuple = null;
while (jsonReader.hasNext()) {
final String name = jsonReader.nextName();
switch (name) {
@ -217,7 +275,7 @@ public class ShareManager implements Iterable<String> {
if (uri == null || tuple == null) {
throw new IllegalStateException("Either uri or tuple is null.");
}
credentialMap.put(uri, tuple);
shareMap.put(uri, tuple);
return uri;
} catch (IOException e) {
@ -226,48 +284,51 @@ public class ShareManager implements Iterable<String> {
}
}
private static CredentialTuple decodeTuple(JsonReader reader) throws IOException {
private static ShareTuple decodeTuple(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return CredentialTuple.EMPTY_TUPLE;
return ShareTuple.EMPTY_TUPLE;
}
String workgroup = null;
String username = null;
String password = null;
boolean mounted = true;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
String value = reader.nextString();
switch (name) {
case WORKGROUP_KEY:
workgroup = value;
workgroup = reader.nextString();
break;
case USERNAME_KEY:
username = value;
username = reader.nextString();
break;
case PASSWORD_KEY:
password = value;
password = reader.nextString();
break;
case MOUNT_KEY:
mounted = reader.nextBoolean();
default:
Log.w(TAG, "Ignoring unknown key " + name);
}
}
reader.endObject();
return new CredentialTuple(workgroup, username, password);
return new ShareTuple(workgroup, username, password, mounted);
}
private static class CredentialTuple {
private static final CredentialTuple EMPTY_TUPLE = new CredentialTuple("", "", "");
private static class ShareTuple {
private static final ShareTuple EMPTY_TUPLE = new ShareTuple("", "", "", true);
private final String mWorkgroup;
private final String mUsername;
private final String mPassword;
private boolean mIsMounted;
private CredentialTuple(String workgroup, String username, String password) {
private ShareTuple(String workgroup, String username, String password, boolean isMounted) {
if (workgroup == null) {
throw new IllegalArgumentException("workgroup is null.");
}
@ -280,6 +341,7 @@ public class ShareManager implements Iterable<String> {
mWorkgroup = workgroup;
mUsername = username;
mPassword = password;
mIsMounted = isMounted;
}
}

View file

@ -0,0 +1,165 @@
/*
* Copyright 2017 Google Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.google.android.sambadocumentsprovider.auth;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.sambadocumentsprovider.R;
import com.google.android.sambadocumentsprovider.SambaProviderApplication;
import com.google.android.sambadocumentsprovider.ShareManager;
import com.google.android.sambadocumentsprovider.base.AuthFailedException;
import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback;
import com.google.android.sambadocumentsprovider.nativefacade.SmbClient;
public class AuthActivity extends AppCompatActivity {
private static final String TAG = "AuthActivity";
private static final String SHARE_URI_KEY = "shareUri";
private EditText mSharePathEditText;
private EditText mDomainEditText;
private EditText mUsernameEditText;
private EditText mPasswordEditText;
private CheckBox mPinShareCheckbox;
private ProgressDialog progressDialog;
private ShareManager mShareManager;
private SmbClient mClient;
private final View.OnClickListener mLoginListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
tryAuth();
}
};
private final OnTaskFinishedCallback<Void> callback = new OnTaskFinishedCallback<Void>() {
@Override
public void onTaskFinished(@Status int status, @Nullable Void item, @Nullable Exception e) {
progressDialog.dismiss();
if (status == SUCCEEDED) {
setResult(RESULT_OK);
finish();
} else {
Log.i(TAG, "Authentication failed: ", e);
showMessage(R.string.credential_error);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Context context = getApplicationContext();
mShareManager = SambaProviderApplication.getServerManager(context);
mClient = SambaProviderApplication.getSambaClient(context);
Intent authIntent = getIntent();
String shareUri = authIntent.getStringExtra(SHARE_URI_KEY);
prepareUI(shareUri);
}
private void prepareUI(String shareUri) {
mSharePathEditText = (EditText) findViewById(R.id.share_path);
mUsernameEditText = (EditText) findViewById(R.id.username);
mDomainEditText = (EditText) findViewById(R.id.domain);
mPasswordEditText = (EditText) findViewById(R.id.password);
CheckBox passwordCheckbox = (CheckBox) findViewById(R.id.needs_password);
mPinShareCheckbox = (CheckBox) findViewById(R.id.pin_share);
mSharePathEditText.setText(shareUri);
mSharePathEditText.setEnabled(false);
passwordCheckbox.setVisibility(View.GONE);
mPinShareCheckbox.setVisibility(View.VISIBLE);
Button mLoginButton = (Button) findViewById(R.id.mount);
mLoginButton.setText(getResources().getString(R.string.login));
mLoginButton.setOnClickListener(mLoginListener);
final Button cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
private void tryAuth() {
progressDialog = ProgressDialog.show(
this, null, getResources().getString(R.string.authenticating), true);
final String username = mUsernameEditText.getText().toString();
final String password = mPasswordEditText.getText().toString();
if (username.isEmpty() || password.isEmpty()) {
showMessage(R.string.empty_credentials);
return;
}
new AuthorizationTask(
mSharePathEditText.getText().toString(),
username,
password,
mDomainEditText.getText().toString(),
mPinShareCheckbox.isChecked(),
mShareManager,
mClient,
callback).execute();
}
private void showMessage(@StringRes int id) {
Snackbar.make(mPinShareCheckbox, id, Snackbar.LENGTH_SHORT).show();
}
public static PendingIntent createAuthIntent(Context context, String shareUri) {
Intent authIntent = new Intent();
authIntent.setComponent(new ComponentName(
context.getPackageName(),
AuthActivity.class.getName()));
authIntent.putExtra(SHARE_URI_KEY, shareUri);
return PendingIntent.getActivity(
context, 0, authIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright 2017 Google Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.google.android.sambadocumentsprovider.auth;
import android.net.Uri;
import com.google.android.sambadocumentsprovider.ShareManager;
import com.google.android.sambadocumentsprovider.base.BiResultTask;
import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback;
import com.google.android.sambadocumentsprovider.document.DocumentMetadata;
import com.google.android.sambadocumentsprovider.nativefacade.SmbClient;
import java.io.IOException;
class AuthorizationTask extends BiResultTask<Void, Void, Void> {
private static final String TAG = "AuthorizationTask";
private final String mUri;
private final String mUser;
private final String mPassword;
private final String mDomain;
private final boolean mShouldPin;
private final ShareManager mShareManager;
private final SmbClient mClient;
private final OnTaskFinishedCallback<Void> mCallback;
AuthorizationTask(String uri, String user, String password, String domain, boolean shouldPin,
ShareManager shareManager, SmbClient client,
OnTaskFinishedCallback<Void> callback) {
mUri = uri;
mUser = user;
mPassword = password;
mDomain = domain;
mShouldPin = shouldPin;
mShareManager = shareManager;
mClient = client;
mCallback = callback;
}
@Override
public Void run(Void... voids) throws Exception {
final DocumentMetadata shareMetadata = DocumentMetadata.createShare(Uri.parse(mUri));
final ShareManager.ShareMountChecker mountChecker = new ShareManager.ShareMountChecker() {
@Override
public void checkShareMounting() throws IOException {
shareMetadata.loadChildren(mClient);
}
};
mShareManager.addOrUpdateServer(mUri, mDomain, mUser, mPassword, mountChecker, mShouldPin);
return null;
}
@Override
public void onSucceeded(Void aVoid) {
mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, null, null);
}
@Override
public void onFailed(Exception e) {
mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, null, e);
}
}

View file

@ -137,7 +137,7 @@ class BroadcastUtils {
final int type = buffer.get();
if (type == FILE_SERVER_NODE_TYPE) {
servers.add(serverName);
servers.add(serverName.trim());
}
skipBytes(buffer, 2);

View file

@ -310,6 +310,10 @@ public class DocumentMetadata {
return uri.getPathSegments().isEmpty() && !uri.getAuthority().isEmpty();
}
public static boolean isShareUri(Uri uri) {
return uri.getPathSegments().size() == 1;
}
public static DocumentMetadata fromUri(Uri uri, SmbClient client) throws IOException {
final List<String> pathSegments = uri.getPathSegments();
if (pathSegments.isEmpty()) {

View file

@ -70,6 +70,8 @@ public class MountServerActivity extends AppCompatActivity {
private static final String DOMAIN_KEY = "domain";
private static final String USERNAME_KEY = "username";
private static final String PASSWORD_KEY = "password";
private static final String AUTH_LAUNCH_KEY = "authLaunch";
private final OnClickListener mPasswordStateChangeListener = new OnClickListener() {
@Override
@ -136,8 +138,8 @@ public class MountServerActivity extends AppCompatActivity {
mPasswordEditText = (EditText) findViewById(R.id.password);
mPasswordEditText.setOnKeyListener(mMountKeyListener);
final Button mount = (Button) findViewById(R.id.mount);
mount.setOnClickListener(mMountListener);
final Button mMountShareButton = (Button) findViewById(R.id.mount);
mMountShareButton.setOnClickListener(mMountListener);
final Button cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new OnClickListener() {
@ -147,6 +149,8 @@ public class MountServerActivity extends AppCompatActivity {
}
});
setNeedsPasswordState(false);
// Set MovementMethod to make it respond to clicks on hyperlinks
final TextView gplv3Link = (TextView) findViewById(R.id.gplv3_link);
gplv3Link.setMovementMethod(LinkMovementMethod.getInstance());
@ -162,6 +166,7 @@ public class MountServerActivity extends AppCompatActivity {
}
mSharePathEditText.setText(savedInstanceState.getString(SHARE_PATH_KEY, ""));
final boolean needsPassword = savedInstanceState.getBoolean(NEEDS_PASSWORD_KEY);
setNeedsPasswordState(needsPassword);
if (needsPassword) {
@ -236,7 +241,7 @@ public class MountServerActivity extends AppCompatActivity {
final DocumentMetadata metadata = DocumentMetadata.createShare(host, share);
if (mShareManager.containsShare(metadata.getUri().toString())) {
if (mShareManager.isShareMounted(metadata.getUri().toString())) {
showMessage(R.string.share_already_mounted);
return;
}

View file

@ -70,8 +70,8 @@ class MountServerTask extends BiResultTask<Void, Void, Void> {
@Override
public Void run(Void... args) throws IOException {
mShareManager.mountServer(
mMetadata.getUri().toString(), mDomain, mUsername, mPassword, mChecker);
mShareManager.addServer(
mMetadata.getUri().toString(), mDomain, mUsername, mPassword, mChecker, true);
return null;
}

View file

@ -22,6 +22,7 @@ import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.to
import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toUri;
import static com.google.android.sambadocumentsprovider.base.DocumentIdHelper.toUriString;
import android.app.AuthenticationRequiredException;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
@ -46,6 +47,7 @@ import com.google.android.sambadocumentsprovider.SambaProviderApplication;
import com.google.android.sambadocumentsprovider.ShareManager;
import com.google.android.sambadocumentsprovider.ShareManager.MountedShareChangeListener;
import com.google.android.sambadocumentsprovider.TaskManager;
import com.google.android.sambadocumentsprovider.auth.AuthActivity;
import com.google.android.sambadocumentsprovider.base.AuthFailedException;
import com.google.android.sambadocumentsprovider.base.DirectoryEntry;
import com.google.android.sambadocumentsprovider.base.DocumentCursor;
@ -57,11 +59,11 @@ import com.google.android.sambadocumentsprovider.document.LoadChildrenTask;
import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback;
import com.google.android.sambadocumentsprovider.document.LoadDocumentTask;
import com.google.android.sambadocumentsprovider.document.LoadStatTask;
import com.google.android.sambadocumentsprovider.mount.MountServerActivity;
import com.google.android.sambadocumentsprovider.nativefacade.SmbFacade;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -186,7 +188,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
if(BuildConfig.DEBUG) Log.d(TAG, "Querying roots.");
projection = (projection == null) ? DEFAULT_ROOT_PROJECTION : projection;
MatrixCursor cursor = new MatrixCursor(projection, mShareManager.size());
MatrixCursor cursor = new MatrixCursor(projection);
cursor.addRow(new Object[] {
NetworkBrowser.SMB_BROWSING_URI.toString(),
@ -197,6 +199,10 @@ public class SambaDocumentsProvider extends DocumentsProvider {
});
for (String uri : mShareManager) {
if (!mShareManager.isShareMounted(uri)) {
continue;
}
final String name;
final Uri parsedUri = Uri.parse(uri);
try(CacheResult result = mCache.get(parsedUri)) {
@ -273,7 +279,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
@Override
public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder)
throws FileNotFoundException {
throws FileNotFoundException, AuthenticationRequiredException {
if (BuildConfig.DEBUG) Log.d(TAG, "Querying children documents under " + documentId);
projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection;
@ -360,7 +366,12 @@ public class SambaDocumentsProvider extends DocumentsProvider {
return cursor;
}
} catch (AuthFailedException e) {
return buildErrorCursor(projection, R.string.view_folder_denied);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && DocumentMetadata.isShareUri(uri)) {
throw new AuthenticationRequiredException(
e, AuthActivity.createAuthIntent(getContext(), uri.toString()));
} else {
return buildErrorCursor(projection, R.string.view_folder_denied);
}
} catch (FileNotFoundException|RuntimeException e) {
throw e;
} catch (Exception e) {

View file

@ -54,43 +54,53 @@
android:layout_width="match_parent"
android:layout_height="@dimen/clickable_height"
android:layout_below="@id/share_path"
android:checked="false"
android:text="@string/needs_password"/>
<LinearLayout
android:id="@+id/password_hide_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<EditText
android:id="@+id/domain"
android:layout_width="match_parent"
android:layout_height="@dimen/clickable_height"
android:minLines="1"
android:maxLines="1"
android:hint="@string/domain"
android:inputType="text"
android:hint="@string/domain"/>
android:maxLines="1"
android:minLines="1"/>
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="@dimen/clickable_height"
android:minLines="1"
android:maxLines="1"
android:hint="@string/username"
android:inputType="text"
android:hint="@string/username"/>
android:maxLines="1"
android:minLines="1"/>
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="@dimen/clickable_height"
android:minLines="1"
android:maxLines="1"
android:inputType="textPassword"
android:fontFamily="sans-serif"
android:hint="@string/password"/>
android:hint="@string/password"
android:inputType="textPassword"
android:maxLines="1"
android:minLines="1"/>
</LinearLayout>
<CheckBox
android:id="@+id/pin_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/pin_this_share"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View file

@ -49,4 +49,9 @@
<string name="no_web_browser">It needs a web browser to send feedback.</string>
<string name="browsing_root_name">Samba Shares</string>
<string name="pin_this_share">Pin this share</string>
<string name="login">Login</string>
<string name="authenticating">Authenticating...</string>
<string name="empty_credentials">Username and password can not be empty!</string>
</resources>