CIFS в Android, или как я файлы с разбитого телефона доставал
Так получилось, что я разбил экран у своего любимого Nexus 4. Первой мыслью было «Чёрт! Теперь я буду как один из этих нищебродов, с разбитым экраном!». Но, видимо, создатели Nexus 4 были ярыми противниками нищебродства, так как вместе с разбитым экраном, полностью отказал сенсорный экран. В общем, ничего страшного, отнести телефон в ремонт и все. Однако, на телефоне были файлы, которые нужны были мне прямо сейчас, а не через пару недель. Но вот получить их представлялось возможным только при разблокированном экране, телефон категорически не хотел показывать содержимое SD карты без разблокировки экрана «супер секретным» жестом.
Немного покопавшись с adb я плюнул на попытки разблокировать экран через консоль. Все советы по взлому экрана блокировки требовали наличие рута, а мой телефон не из этих. Решено было действовать изнутри. Выбор пал на библиотеку JCIFS, так как раньше мне уже приходилось работать с ней и проблем в её использовании не возникало.
Нужно было написать приложение, которое бы самостоятельно скопировало файлы с телефона на расшаренную по Wi-Fi папку. Обязательные условия для такого трюка: включенная отладка через USB, а также наличие Wi-Fi сети, к которой телефон подключится как только ее увидит (у меня это домашний Wi-Fi).
Подготовительные работы
Создадим проект с одной Activity. Хоть она и не увидит белого света из-за экрана блокировки, но для запуска сервиса, который сделает основную работу, она будет нужна.
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startService(new Intent(this, SynchronizeService.class));
}
}
Копированием файлов будет заниматься отдельный сервис. Так как Activity не видна, на ее жизнеспособность расчитывать не стоит, а вот сервис, запущенный в Foreground, прекрасно справится с этой задачей.
public class SynchronizeService extends Service {
private static final int FOREGROUND_NOTIFY_ID = 1;
@Override
public void onCreate() {
super.onCreate();
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.synchronize_service_message))
.setContentIntent(getDummyContentIntent())
.setColor(Color.BLUE)
.setProgress(1000, 0, true);
startForeground(FOREGROUND_NOTIFY_ID, builder.build());
// Это поможет удерживать CPU в бодром состоянии
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SynchronizeWakelockTag");
wakeLock.acquire();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
Перед тем, как двигаться дальше, добавим зависимость, файл build.gradle, которая добавит в проект библиотеку JCIFS.
dependencies {
...
compile 'jcifs:jcifs:1.3.17'
}
Также нужно добавить кое-какие разрешения в манифест и не забыть написать там про наш сервис. В конечном счете, AndroidManifest.xml у меня выглядел вот так.
// Нужно для удерживания телефона от сна.
// Потребуется для работы с сетью.
// Для чтения с SD карты.
Копирование файлов
Итак, все приготовления закончены. Теперь, если запустить приложение, в списке нотификаций появится сообщения сервиса (начиная с Android 5, можно настроить показ сообщений на экране блокировки. Если версия Android меньше, вы этого сообщения не увидите), а это значит, приложение работает как надо и можно приступать к самому вкусному — перекачке файлов.
Дабы не совершать сетевые операции в главном потоке, вынесем все это дело в AsyncTask.
public class CopyFilesToSharedFolderTask extends AsyncTask {
private final File mFolderToCopy;
private final String mSharedFolderUrl;
private final NtlmPasswordAuthentication mAuth;
private FileFilter mFileFilter;
public CopyFilesToSharedFolderTask(File folderToCopy, String sharedFolderUrl, String user, String password, FileFilter fileFilter) {
super();
mFolderToCopy = folderToCopy; // Папка, которая должна быть скопирована.
mSharedFolderUrl = sharedFolderUrl; // Url к сетевой папке, в которую будет скопированы файлы с телефона.
mAuth = (user != null && password != null)
? new NtlmPasswordAuthentication(user + ":" + password)
: NtlmPasswordAuthentication.ANONYMOUS;
mFileFilter = fileFilter;
}
}
Особое внимание стоит обратить на параметры user и password. Это логин и пароль к сетевой папке, которые будут использованы для создания NtlmPasswordAuthentication. Если для доступа к папке пароль не требуется, в качестве аутентификации нужно использовать NtlmPasswordAuthentication.ANONYMOUS. Выглядит просто, однако, аутентификация это самая большая проблема, с которой вы можете столкнуться при работе с сетевыми папками. Обычно, большинство проблем скрываются в не правильной настройке политики безопасности на компьютере. Самый лучший способ проверить правильность настроек — это попробовать открыть сетевую папку на телефоне, через любой другой файловый менеджер, поддерживающий работу через сеть.
SmbFile — это файл для работы с сетевыми файлами. Удивительно, но в JCIFS очень легко работать с файлами. Вы не почувствуете практически никакой разницы между SmbFile и обычным File. Единственное, что бросается в глаза, это наличие управляемых исключений практически во всех методах класса. А еще для создания объекта SmbFile потребуется данные для аутентификации, которые мы создали ранее.
private double mMaxProgress;
private double mProgress;
...
@Override
protected String doInBackground(Void... voids) {
mMaxProgress = getFilesSize(mFolderToCopy);
mProgress = 0;
publishProgress(0d);
try {
SmbFile sharedFolder = new SmbFile(mSharedFolderUrl, mAuth);
if (sharedFolder.exists() && sharedFolder.isDirectory()) {
copyFiles(mFolderToCopy, sharedFolder);
}
} catch (MalformedURLException e) {
return "Invalid URL.";
} catch (IOException e) {
e.printStackTrace();
return e.getMessage();
}
return null;
}
Метод doInBackground возвращает сообщение об ошибке. Если возвращается null, значит все прошло гладко и без ошибок.
Файлов может быть много… Нет, не так. Их может быть ОЧЕНЬ много! Поэтому, показывать прогресс — жизненно необходимая функция. Рекурсивный метод getFilesSize вычисляет общий объем файлов, который потребуется для вычисления общего прогресса.
private double getFilesSize(File file) {
if (!checkFilter(file))
return 0;
if (file.isDirectory()) {
int size = 0;
File[] filesList = file.listFiles();
for (File innerFile : filesList)
size += getFilesSize(innerFile);
return size;
}
return (double) file.length();
}
private boolean checkFilter(File file) {
return mFileFilter == null || mFileFilter.accept(file);
}
Переданный в конструктор фильтр помогает исключить ненужные файлы и папки. Например, можно исключить все папки начинающиеся с точки или добавить в черный список папку «Android».
Как я уже говорил ранее, работа с SmbFile ничем не отличается от работы с обычным файлом, поэтому, процесс переноса данных с телефона на компьютер не отличается оригинальностью. Я даже спрячу этот код под спойлер, дабы не засорять статью еще большим количеством очевидного кода.
private static final String LOG_TAG = "WiFiSynchronizer";
private void copyFiles(File fileToCopy, SmbFile sharedFolder) throws IOException {
if (!checkFilter(fileToCopy))
return; // Если файл или папка не проходят фильтр, не копируем ее.
if (fileToCopy.exists()) {
if (fileToCopy.isDirectory()) {
File[] filesList = fileToCopy.listFiles();
// При создании директории в конце ставится "/".
SmbFile newSharedFolder = new SmbFile(sharedFolder, fileToCopy.getName() + "/");
if (!newSharedFolder.exists()) {
newSharedFolder.mkdir();
Log.d(LOG_TAG, "Folder created:" + newSharedFolder.getPath());
}
else
Log.d(LOG_TAG, "Folder already exist:" + newSharedFolder.getPath());
for (File file : filesList)
copyFiles(file, newSharedFolder); // Рекурсивный вызов
} else {
SmbFile newSharedFile = new SmbFile(sharedFolder, fileToCopy.getName());
// Если файл уже создан, не будем его копировать.
// Конечно, в другой ситуации, стоило бы добавить проверку по хэшу, но в моем случае это будет лишним.
if (!newSharedFile.exists()) {
copySingleFile(fileToCopy, newSharedFile);
Log.d(LOG_TAG, "File copied:" + newSharedFile.getPath());
}
else
Log.d(LOG_TAG, "File already exist:" + newSharedFile.getPath());
// Обновляем прогресс.
mProgress += (double) fileToCopy.length();
publishProgress(mProgress / mMaxProgress * 100d);
}
}
}
// Ничем не примечательный метод по копированию файлов.
private void copySingleFile(File file, SmbFile sharedFile) throws IOException {
IOException exception = null;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
outputStream = new SmbFileOutputStream(sharedFile);
inputStream = new FileInputStream(file);
byte[] bytesBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(bytesBuffer)) > 0) {
outputStream.write(bytesBuffer, 0, bytesRead);
}
} catch (IOException e) {
exception = e;
} finally {
if (inputStream != null)
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null)
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (exception != null)
throw exception;
}
Код очевиден, однако в нем есть один, совсем не очевидный момент — это добавление символа »/» к концу имени папки при создании нового SmbFile. Дело в том, что JCIFS воспринимает все файлы, которые не заканчиваются на символ »/» только как файл, не как директорию. Поэтому, если Url сетевой папки будет выглядеть так: «file://MY-PC/shared/some_foldel», возникнут казусы при создании новой папки в папке «some_foldel». А именно, «some_foldel» будет отброшено, и новая папка будет иметь Url: «file://MY-PC/shared/new_folder», вместо ожидаемого «file://MY-PC/shared/some_foldel/new_folder». При этом, для таких папок, методы isDirectory, mkdir или listFiles будут работать корректно.
Почти готово. Теперь запустим выполнение этой задачи в onCreate сервиса.
private static final int FOREGROUND_NOTIFY_ID = 1;
private static final int MESSAGE_NOTIFY_ID = 2;
private static final String SHARED_FOLDER_URL = "file://192.168.0.5/shared/";
...
final File folderToCopy = getFolderToCopy();
CopyFilesToSharedFolderTask task = new CopyFilesToSharedFolderTask(folderToCopy, SHARED_FOLDER_URL, null, null, null) {
@Override
protected void onProgressUpdate(Double... values) {
builder.setProgress(100, values[0].intValue(), false)
.setContentText(String.format("%s %.3f", getString(R.string.synchronize_service_progress), values[0]) + "%");
mNotifyManager.notify(FOREGROUND_NOTIFY_ID, builder.build());
}
@Override
protected void onPostExecute(String errorMessage) {
stopForeground(true);
if (errorMessage == null)
showNotification(getString(R.string.synchronize_service_success), Color.GREEN);
else
showNotification(errorMessage, Color.RED);
stopSelf();
wakeLock.release(); // Не забываем освободить wakeLock
}
@Override
protected void onCancelled(String errorMessage) {
// Этот код никогда не выполнится. Но мало ли, вдруг мне захочется что-то поменять.
// Тогда сервис никогда не остановится при закрытии таска.
stopSelf();
wakeLock.release();
}
};
task.execute();
В моем случае логин и пароль не требуются, фильтр я тоже указывать не стал. Метод onProgressUpdate переопределен для показа состояния прогресса, а onPostExecute показывает сообщение об окончании загрузки, либо о возникновении ошибки, после чего завершает работу сервиса.
Запустим приложение. Появилось сообщение от запущенного сервиса. Пока идет вычисление общего объема файлов, показывается неопределенное состояние прогресса. Но вот индикатор показывает 0%, после чего полоска постепенно, маленькими, чуть заметными шажками, начинает двигаться к 100%.
Когда работа была завершена, на экране высветилось сообщение об удачном результате, и у меня на компьютере были все необходимые файлы, ранее заточённые на разбитом телефоне.
Неожиданные выводы
То, что было нужно я получил. В самое время заварить чайку, развалиться на диване и включить какой-нибудь сериальчик. Но, постойте! Несмотря на то, что телефон был мой и доступ к файлам на нем не противоречит российскому законодательству, я достал их без использования пароля! При этом, на телефоне не стоял Root. Это значит, что при одном только включенном режиме отладки не трудно получить доступ к содержимому SD карты, даже не зная пароля. А уж если ваш телефон ведет разнузданную сетевую жизнь, не брезгуя беспорядочными Wi-Fi связями, сложность доступа к его содержимому сводится к минимуму. Более чем уверен, Wi-Fi можно заменить на USB. Это немного усложнит суть работы, но не сильно.
Представленная, совсем не давно, новая версия Android, возможно, закроет эту дыру, так как для доступа к необходимым разрешениям, потребуется подтверждение пользователя, что невозможно при заблокированном экране. А пока, Android разработчик, будь на стороже, если не хочешь, чтобы твои ню фоточки увидел кто-то другой.
Спасибо за внимание. Буду рад увидеть ваши мысли в комментариях.
Исходный код можно найти по следующей ссылке: github.com/KamiSempai/WiFiFolderSynchronizer