CIFS в Android, или как я файлы с разбитого телефона доставал

от автора

Так получилось, что я разбил экран у своего любимого Nexus 4. Первой мыслью было «Чёрт! Теперь я буду как один из этих нищебродов, с разбитым экраном!». Но, видимо, создатели Nexus 4 были ярыми противниками нищебродства, так как вместе с разбитым экраном, полностью отказал сенсорный экран. В общем, ничего страшного, отнести телефон в ремонт и все. Однако, на телефоне были файлы, которые нужны были мне прямо сейчас, а не через пару недель. Но вот получить их представлялось возможным только при разблокированном экране, телефон категорически не хотел показывать содержимое SD карты без разблокировки экрана “супер секретным” жестом.

Немного покопавшись с adb я плюнул на попытки разблокировать экран через консоль. Все советы по взлому экрана блокировки требовали наличие рута, а мой телефон не из этих. Решено было действовать изнутри. Выбор пал на библиотеку JCIFS, так как раньше мне уже приходилось работать с ней и проблем в её использовании не возникало.

Нужно было написать приложение, которое бы самостоятельно скопировало файлы с телефона на расшаренную по Wi-Fi папку. Обязательные условия для такого трюка: включенная отладка через USB, а также наличие Wi-Fi сети, к которой телефон подключится как только ее увидит (у меня это домашний Wi-Fi).

Подготовительные работы

Создадим проект с одной Activity. Хоть она и не увидит белого света из-за экрана блокировки, но для запуска сервиса, который сделает основную работу, она будет нужна.

MainActivity.java

public class MainActivity extends Activity {     @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        startService(new Intent(this, SynchronizeService.class));    } }

Копированием файлов будет заниматься отдельный сервис. Так как Activity не видна, на ее жизнеспособность расчитывать не стоит, а вот сервис, запущенный в Foreground, прекрасно справится с этой задачей.

SynchronizeService.java

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 у меня выглядел вот так.

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 ничем не отличается от работы с обычным файлом, поэтому, процесс переноса данных с телефона на компьютер не отличается оригинальностью. Я даже спрячу этот код по спойлер, дабы не засорять статью еще большим количеством очевидного кода.

Методы copyFiles и copySingleFile

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *