Так получилось, что я разбил экран у своего любимого 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 у меня выглядел вот так.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ru.kamisempai.wifisynchronizer"> // Нужно для удерживания телефона от сна. <uses-permission android:name="android.permission.WAKE_LOCK"/> // Потребуется для работы с сетью. <uses-permission android:name="android.permission.INTERNET"/> // Для чтения с SD карты. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:label="@string/app_name" android:icon="@mipmap/ic_launcher"> <activity android:name=".activities.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".services.SynchronizeService"/> </application> </manifest>
Копирование файлов
Итак, все приготовления закончены. Теперь, если запустить приложение, в списке нотификаций появится сообщения сервиса (начиная с Android 5, можно настроить показ сообщений на экране блокировки. Если версия Android меньше, вы этого сообщения не увидите), а это значит, приложение работает как надо и можно приступать к самому вкусному — перекачке файлов.
Дабы не совершать сетевые операции в главном потоке, вынесем все это дело в AsyncTask.
public class CopyFilesToSharedFolderTask extends AsyncTask<Void, Double, String> { 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
ссылка на оригинал статьи http://habrahabr.ru/post/261419/
Добавить комментарий