Compare commits

..

9 Commits
v0.33 ... v0.34

Author SHA1 Message Date
Fredrik Fornwall
746dc750df Bump version to 0.34 2016-04-22 03:48:28 +02:00
Fredrik Fornwall
7db1f6c5a1 Add explicit handling of switch constant 2016-04-22 03:30:09 +02:00
Fredrik Fornwall
b7f3fdf528 Update android-annotations version 2016-04-22 03:28:50 +02:00
Fredrik Fornwall
6e7f777d04 Remove support for ACTION_GET_CONTENT
As we have a document provider now we can remove ACTION_GET_CONTENT.

"The ACTION_OPEN_DOCUMENT intent is only available on devices running
Android 4.4 and higher. If you want your application to support
ACTION_GET_CONTENT to accommodate devices that are running Android 4.3
and lower, you should disable the ACTION_GET_CONTENT intent filter in
your manifest for devices running Android 4.4 or higher. A document
provider and ACTION_GET_CONTENT should be considered mutually exclusive.
If you support both of them simultaneously, your app will appear twice
in the system picker UI, offering two different ways of accessing your
stored data. This would be confusing for users."

- http://developer.android.com/guide/topics/providers/document-provider.html#43
2016-04-22 03:26:23 +02:00
Fredrik Fornwall
fc15bd2355 Documents provider: Remove FLAG_SUPPORTS_RECENTS 2016-04-22 02:51:13 +02:00
Fredrik Fornwall
a87cbdd70c Make VolumeUp+x send Alt+x 2016-04-22 02:20:10 +02:00
Fredrik Fornwall
fb7f7d249e Ignore warnings about setExecutable(true) failing 2016-04-22 02:17:41 +02:00
Fredrik Fornwall
026d0b495e Expose files through the Storage Access Framework
This allows e.g. external editors to edit files in the Termux home
folder. Fixes #79.
2016-04-22 02:13:50 +02:00
Fredrik Fornwall
533fa60516 Remove unused imports 2016-04-22 02:00:36 +02:00
10 changed files with 308 additions and 265 deletions

View File

@@ -5,7 +5,7 @@ android {
buildToolsVersion "23.0.2"
dependencies {
compile 'com.android.support:support-annotations:23.1.1'
compile 'com.android.support:support-annotations:23.3.0'
}
sourceSets {
@@ -18,8 +18,8 @@ android {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 23
versionCode 33
versionName "0.33"
versionCode 34
versionName "0.34"
}
buildTypes {

View File

@@ -73,30 +73,16 @@
</intent-filter>
</activity>
<activity
android:name="com.termux.filepicker.TermuxFilePickerActivity"
android:label="@string/application_name"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:noHistory="true">
<intent-filter>
<!--
http://stackoverflow.com/questions/6486716/using-intent-action-pick-for-specific-path
"That said, you should consider ACTION_PICK deprecated. The modern action is ACTION_GET_CONTENT
which is much better supported; you will find support of ACTION_PICK spotty and inconsistent.
Unfortunately ACTION_GET_CONTENT also does not let you specify a directory."
-->
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<provider android:authorities="com.termux.filepicker.provider"
android:readPermission="com.termux.filepicker.READ"
android:exported="true"
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="com.termux.documents"
android:grantUriPermissions="true"
android:name="com.termux.filepicker.TermuxFilePickerProvider" />
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<service
android:name="com.termux.app.TermuxService"

View File

@@ -17,7 +17,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
@@ -34,7 +33,6 @@ import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
@@ -391,6 +389,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
case TermuxPreferences.BELL_VIBRATE:
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
break;
case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character.
break;
}
}

View File

@@ -0,0 +1,242 @@
package com.termux.filepicker;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.termux.R;
import com.termux.app.TermuxService;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps.
* <p>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI,
* offering two different ways of accessing your stored data. This would be confusing for users."
* - http://developer.android.com/guide/topics/providers/document-provider.html#43
*/
public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
} catch (IOException e) {
isInsideHome = true;
}
final boolean isHidden = file.getName().startsWith(".");
if (isInsideHome && !isHidden) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
* <p>
* The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/**
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
*/
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
}
}

View File

@@ -1,103 +0,0 @@
package com.termux.filepicker;
import android.app.Activity;
import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.termux.R;
import com.termux.app.TermuxService;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Activity allowing picking files from the $HOME folder. */
public class TermuxFilePickerActivity extends ListActivity {
private File mCurrentDirectory;
private final List<File> mFiles = new ArrayList<>();
private final List<String> mFileNames = new ArrayList<>();
private ArrayAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.file_picker);
mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mFileNames);
enterDirectory(new File(TermuxService.HOME_PATH));
setListAdapter(mAdapter);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
enterDirectory(mCurrentDirectory.getParentFile());
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
File requestFile = mFiles.get(position);
if (requestFile.isDirectory()) {
enterDirectory(requestFile);
} else {
Uri returnUri = Uri.withAppendedPath(Uri.parse("content://com.termux.filepicker.provider/"), requestFile.getAbsolutePath());
Intent returnIntent = new Intent().setData(returnUri);
returnIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(Activity.RESULT_OK, returnIntent);
finish();
}
}
void enterDirectory(File directory) {
getActionBar().setDisplayHomeAsUpEnabled(!directory.getAbsolutePath().equals(TermuxService.HOME_PATH));
String title = directory.getAbsolutePath() + "/";
if (title.startsWith(TermuxService.HOME_PATH)) {
title = "~" + title.substring(TermuxService.HOME_PATH.length(), title.length());
}
setTitle(title);
mCurrentDirectory = directory;
mFiles.clear();
mFileNames.clear();
mFiles.addAll(Arrays.asList(mCurrentDirectory.listFiles()));
Collections.sort(mFiles, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
final String n1 = f1.getName();
final String n2 = f2.getName();
// Display dot folders last:
if (n1.startsWith(".") && !n2.startsWith(".")) {
return 1;
} else if (n2.startsWith(".") && !n1.startsWith(".")) {
return -1;
}
return n1.compareToIgnoreCase(n2);
}
});
for (File file : mFiles) {
mFileNames.add(file.getName() + (file.isDirectory() ? "/" : ""));
}
mAdapter.notifyDataSetChanged();
}
}

View File

@@ -1,65 +0,0 @@
package com.termux.filepicker;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
/** Provider of files content uris picked from {@link com.termux.filepicker.TermuxFilePickerActivity}. */
public class TermuxFilePickerProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(@NonNull Uri uri) {
String contentType = null;
String path = uri.getPath();
int lastDotIndex = path.lastIndexOf('.');
String possibleFileExtension = path.substring(lastDotIndex + 1, path.length());
if (possibleFileExtension.contains("/")) {
// The dot was in the path, so not a file extension.
} else {
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG":
contentType = mimeTypes.getMimeTypeFromExtension(possibleFileExtension.toLowerCase());
}
if (contentType == null) contentType = "application/octet-stream";
return contentType;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = new File(uri.getPath());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}

View File

@@ -133,6 +133,7 @@ public class TermuxFileReceiverActivity extends Activity {
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
editorProgramFile.setExecutable(true);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
@@ -201,6 +202,7 @@ public class TermuxFileReceiverActivity extends Activity {
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();

View File

@@ -652,52 +652,55 @@ public final class TerminalView extends View {
resultingKeyCode = KeyEvent.KEYCODE_F12;
}
} else if (mVirtualFnKeyDown) {
if (codePoint == 'w' || codePoint == 'W') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (codePoint == 'a' || codePoint == 'A') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
} else if (codePoint == 's' || codePoint == 'S') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
} else if (codePoint == 'd' || codePoint == 'D') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
} else if (codePoint == 'p' || codePoint == 'P') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
} else if (codePoint == 'n' || codePoint == 'N') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else if (codePoint == 't' || codePoint == 'T') {
resultingKeyCode = KeyEvent.KEYCODE_TAB;
} else if (codePoint == 'l' || codePoint == 'L') {
codePoint = '|';
} else if (codePoint == 'u' || codePoint == 'U') {
codePoint = '_';
} else if (codePoint == 'e' || codePoint == 'E') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '.') {
codePoint = 28; // ^\
} else if (codePoint > '0' && codePoint <= '9') {
// F1-F9
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F10;
} else if (codePoint == 'i' || codePoint == 'I') {
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
} else if (codePoint == 'x' || codePoint == 'X') {
resultingKeyCode = KeyEvent.KEYCODE_FORWARD_DEL;
} else if (codePoint == 'h' || codePoint == 'H') {
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
} else if (codePoint == 'f' || codePoint == 'F') {
// As left alt+f, jumping forward in readline:
codePoint = 'f';
leftAltDownFromEvent = true;
} else if (codePoint == 'b' || codePoint == 'B') {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
} else if (codePoint == 'v' || codePoint == 'V') {
codePoint = -1;
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
}
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w': resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; break;
case 'a': resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; break;
case 's': resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; break;
case 'd': resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; break;
// Page up and down.
case 'p': resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; break;
case 'n': resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; break;
// Some special keys:
case 't': resultingKeyCode = KeyEvent.KEYCODE_TAB; break;
case 'i': resultingKeyCode = KeyEvent.KEYCODE_INSERT; break;
case 'h': resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME; break;
// Special characters to input.
case 'u': codePoint = '_'; break;
case 'l': codePoint = '|'; break;
// Function keys.
case '1': case '2': case '3':
case '4': case '5': case '6':
case '7': case '8': case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e': codePoint = /*Escape*/ 27; break;
case '.': codePoint = /*^.*/ 28; break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
codePoint = lowerCase;
leftAltDownFromEvent = true;
break;
// Volume control.
case 'v':
codePoint = -1;
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
}
}
if (codePoint > -1) {

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
<TextView android:id="@android:id/empty"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/empty_folder"/>
</LinearLayout>

View File

@@ -53,8 +53,6 @@
<string name="notification_action_wakelock">Wake</string>
<string name="notification_action_wifilock">Wifi</string>
<string name="empty_folder">Empty folder.</string>
<string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string>
<string name="file_received_open_folder_button">Open folder</string>