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



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

Представленная, совсем не давно, новая версия Android, возможно, закроет эту дыру, так как для доступа к необходимым разрешениям, потребуется подтверждение пользователя, что невозможно при заблокированном экране. А пока, Android разработчик, будь на стороже, если не хочешь, чтобы твои ню фоточки увидел кто-то другой. И помни, разрешая любому компьютеру отладку по USB ты создаешь еще одну лазейку для взлома собственного телефона.

Спасибо за внимание. Буду рад увидеть ваши мысли в комментариях.
Исходный код можно найти по следующей ссылке: github.com/KamiSempai/WiFiFolderSynchronizer

UPD: Как я и предполагал, есть более простое решение моей проблемы. А именно воспользоваться командой adb pull. К сожалению редкое использование adb и узкий взгляд на проблему не позволили мне к нему прийти. Я пытался разблокировать экран, а не скачать файлы.
Спасибо EvgenT за удачное замечание.

Комментарии (27)


  1. Georg93
    02.07.2015 13:14

    Была аналогичная проблема, отказавшийся жить тачскрин, разблокировка жестом.
    Спасла программа myphoneexplorer.
    Суть предельно проста: программа установлена на пк, телефон подключён через USB, главное, чтобы на аппарате был включён режим отладки по USB.
    После этого можно копировать файлы, контакты, sms, даже больше — полностью управлять телефоном, используя мышку, а картинка с телефона выводится в отдельном окне на компьютер.
    Таким образом можно при желании разблокировать экран, зная жест.


    1. JerleShannara
      02.07.2015 18:27
      +1

      Я в таком случае воспользовался VNC сервером, залив его на телефон по adb. Кроме косяков со шрифтами всё вышло наура. Включая разблокировку.


      1. bazilxp
        03.07.2015 11:20

        я разбил экран в нокию 1020 (виндоус),

        Перед отнести в ремонт с помощью родного программного обеспечения достал фотографии…

        Ситуация была похожая что описано с андроидом… в данном случае повезло что родной софт прост в использовании…


  1. grossws
    02.07.2015 13:27
    +1

    В аналогичной ситуации тупо перебрал телефон. После очередного падения покрошился экран и чуток перекосился шлейф touchscreen'а. Оказалось достаточно пошевелить его, чтобы он снова начал отзываться на ласковое поглаживание для разблокировки. То без этого не очень реально включить режим отладки. Или, если говорить про 2 андроид, то touchscreen бывает нужен для подключения телефона как MSD (на 4 не актуально, т. к. используется MTP).

    А насчёт того, что телефон при разрешенной отладке является легкой добычей выводится предупреждение при включении режима отладки. Не спроста оно там ,)


  1. DROS
    02.07.2015 14:00
    +4

    Хм, а если бы у Вас телефон сперли к примеру? Тогда нужные файлы канули бы в лету. Странная привычка хранить важные данные в одном месте и без бэкапа. Или я не прав?


    1. KamiSempai Автор
      02.07.2015 14:22
      +2

      Забавно то, что мне нужны были именно бэкапы, которые автоматически создавались одним моим приложением. И в приложении даже есть функционал по выгрузке этих бэкапов на Google Drive. Но так как я использовал этот телефон для разработки, иногда мне приходилось удалять приложение и ставить его заново. Из за этого, данная настройка оказалась сброшена и на Google Drive находился не самый свежий бэкап.


  1. k1b0rg
    02.07.2015 14:09
    +2

    Можно еще через OTG подключить мышь


    1. GRascm
      02.07.2015 14:13
      +4

      Nexus 4 умеет OTG только со специальным ядром, и внешним питанием. То есть, проще сказать, не умеет :)


  1. EvgenT
    02.07.2015 14:45
    +10

    Стоп, если Вы «Немного покопавшись с adb я плюнул на попытки разблокировать экран через консоль», то что мешало использовать adb pull?


    1. easyman
      02.07.2015 14:55
      +3

      Я ждал этот коммент!


      1. EvgenT
        02.07.2015 15:01
        +2

        Ну, так каков ответ то?


        1. easyman
          02.07.2015 15:01
          +5

          Подождите, автору ещё плохо.


        1. easyman
          02.07.2015 15:05
          +5

          Я в Nexus4 из adb с помошью dd и netcat бакапил разделами и не парился.

          Upd: forum.xda-developers.com/showthread.php?t=1818321


    1. petrovichtim
      02.07.2015 15:07
      +1

      adb pull /sdcard/ c:\backup


      1. easyman
        02.07.2015 15:13

        Я пробовал.
        У меня рвалось или зависало соединение (не помню уже), поэтому использовал netcat. Возможно, у автора тоже были какие-то проблемы (у меня они проявлялись, только если много копировал)


        1. petrovichtim
          02.07.2015 15:15

          Только что проверил на Nexus 5 все скопировалось без проблем


    1. KamiSempai Автор
      02.07.2015 15:22
      +2

      А вот это справедливое замечание. Обновил статью. Добавил тэги «велосипед» и «нестандартные решения».


  1. petrovichtim
    02.07.2015 14:57
    +2

    Это значит, что при одном только включенном режиме отладки не трудно получить доступ к содержимому SD карты, даже не зная пароля.

    Нет не значит, нужно ещё и на устройстве разрешить отладку для конкретного компьютера. У Вас она очевидно была разрешена ранее.


    1. KamiSempai Автор
      02.07.2015 15:57

      Все верно. Видимо, это так давно было, что я и за был про него. Немного подкорректировал статью.


  1. sanzstez
    02.07.2015 21:27

    Тоже нерабочий тачскрин. Победил так: прошил CWM Recovery, через build.prop активировал отладку по USB и командами adb уже дальше управлял смартфоном. На самом деле через adb рулить девайсом весьма забавно и интересно. А с рут правами уже можно найти применение разбитому девайсу.


  1. BreakHeart
    03.07.2015 09:58

    А нельзя было просто вытащить sd-карточку и засунуть картридер? Или вы все храните во внутренней памяти?


    1. orosz
      03.07.2015 10:54

      LG Nexus 4 не поддерживает карты памяти.

      Разработчики решили, что пользователю будет достаточно 8 или 16 ГБ Flash без возможности расширения картами памяти microSD. (С) mobile-review.com


      1. BreakHeart
        03.07.2015 10:57
        +1

        Я точно не знаю, но в статье написано, что нужно было получить доступ к содержимому SD карточки.
        Первый абзац:

        телефон категорически не хотел показывать содержимое SD карты без разблокировки экрана “супер секретным” жестом.


        1. orosz
          03.07.2015 11:05

          Действительно, в первом абзаце текста фигурирует фраза «содержимое SD карты», но nexus 4 не поддерживает карты памяти, о чем кстати пишут во многих обзорах, один из которых размещен на geektimes.ru
          Разрешите процитировать фразу из обзора Shirixae:
          «Минусы:
          Мало памяти / нет слота для MicroSD (хотя кому-то и половины этого объёма хватает, покупает же кто-то восьмигиговые версии?).»


        1. KamiSempai Автор
          03.07.2015 11:10

          С точки зрения программного кода, физическая память разделяется на External и Internal. Приложения и их данные хранятся в Internal, к ним нет доступа если нет Root-а. Все остальное External. Так получилось, что в моих кругах эту часть памяти называют SD. Видимо стоит переформулировать это понятие. Только я ни как не могу подобрать подходящее. «Внешняя память» все равно вызывает ассоциацию с SD.


        1. KamiSempai Автор
          03.07.2015 11:46

          Немного переформулировал текст до ката. Надеюсь это уберет неоднозначность.


  1. orosz
    03.07.2015 11:18

    Извините, ошибся веткой.

    KamiSempai, вставьте текст своего коммента в пост, этого будет достаточно, чтобы понять, что подразумевается под выражением «содержимое SD карты»